diff --git a/.env.example b/.env.example index 35ea12a885..df63e08e0e 100644 --- a/.env.example +++ b/.env.example @@ -19,4 +19,13 @@ FORWARDED_ALLOW_IPS='*' # DO NOT TRACK SCARF_NO_ANALYTICS=true DO_NOT_TRACK=true -ANONYMIZED_TELEMETRY=false \ No newline at end of file +ANONYMIZED_TELEMETRY=false + +# Valkey Vector Store (requires VECTOR_DB=valkey) +# VALKEY_URL='valkey://localhost:6379' +# VALKEY_COLLECTION_PREFIX='open_webui' +# VALKEY_INDEX_TYPE='HNSW' +# VALKEY_DISTANCE_METRIC='COSINE' +# VALKEY_HNSW_M='16' +# VALKEY_HNSW_EF_CONSTRUCTION='200' +# VALKEY_HNSW_EF_RUNTIME='10' diff --git a/.github/ISSUE_TEMPLATE/feature_request.yaml b/.github/ISSUE_TEMPLATE/feature_request.yaml index 05dc6cfa94..d5f3a86132 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.yaml +++ b/.github/ISSUE_TEMPLATE/feature_request.yaml @@ -1,82 +1,76 @@ name: Feature Request -description: Suggest an idea for this project +description: Suggest a new feature or improvement title: 'feat: ' labels: ['triage'] body: - type: markdown attributes: value: | - ## Important Notes - ### Before submitting - - Please check the **open AND closed** [Issues](https://github.com/open-webui/open-webui/issues) AND [Discussions](https://github.com/open-webui/open-webui/discussions) to see if a similar request has been posted. - It's likely we're already tracking it! If you’re unsure, start a discussion post first. + ## Before Submitting - #### Scope + Please check **open AND closed** [Issues](https://github.com/open-webui/open-webui/issues) and [Discussions](https://github.com/open-webui/open-webui/discussions) for similar requests. If you find one, add your input there instead. - If your feature request is likely to take more than a quick coding session to implement, test and verify, then open it in the **Ideas** section of the [Discussions](https://github.com/open-webui/open-webui/discussions) instead. - **We will close and force move your feature request to the Ideas section, if we believe your feature request is not trivial/quick to implement.** - This is to ensure the issues tab is used only for issues, quickly addressable feature requests and tracking tickets by the maintainers. - Other feature requests belong in the **Ideas** section of the [Discussions](https://github.com/open-webui/open-webui/discussions). - - If your feature request might impact others in the community, definitely open a discussion instead and evaluate whether and how to implement it. - - This will help us efficiently focus on improving the project. - - ### Collaborate respectfully - We value a **constructive attitude**, so please be mindful of your communication. If negativity is part of your approach, our capacity to engage may be limited. We're here to help if you're **open to learning** and **communicating positively**. + ### Scope Guidelines - Remember: - - Open WebUI is a **volunteer-driven project** - - It's managed by a **single maintainer** - - It's supported by contributors who also have **full-time jobs** + Feature requests that require significant implementation effort should be posted in the **Ideas** section of [Discussions](https://github.com/open-webui/open-webui/discussions) instead. We will move oversized feature requests to Discussions to keep the Issues tab focused on actionable items. - We appreciate your time and ask that you **respect ours**. + If your request might impact the broader community, please open a Discussion first so others can weigh in on the design. + + ### Be Respectful + + Open WebUI is a volunteer-driven project maintained by a small team. We value constructive, positive communication. Please be mindful of maintainers' time and energy. ### Contributing - If you encounter an issue, we highly encourage you to submit a pull request or fork the project. We actively work to prevent contributor burnout to maintain the quality and continuity of Open WebUI. - ### Bug reproducibility - If a bug cannot be reproduced with a `:main` or `:dev` Docker setup, or a `pip install` with Python 3.11, it may require additional help from the community. In such cases, we will move it to the "[issues](https://github.com/open-webui/open-webui/discussions/categories/issues)" Discussions section due to our limited resources. We encourage the community to assist with these issues. Remember, it’s not that the issue doesn’t exist; we need your help! + If you encounter an issue, we encourage you to submit a pull request or fork the project. We actively work to prevent contributor burnout and maintain project quality. + + ### Reproducibility + + If a bug cannot be reproduced with a `:main` or `:dev` Docker setup, or a `pip install` with Python 3.11, it may be moved to the "Issues" section in Discussions for community assistance. - type: checkboxes id: existing-issue attributes: label: Check Existing Issues - description: Please confirm that you've checked for existing similar requests + description: Confirm you have searched for similar requests. options: - - label: I have searched for all existing **open AND closed** issues and discussions for similar requests. I have found none that is comparable to my request. + - label: I have searched all existing **open AND closed** issues and discussions and found none comparable to my request. required: true + - type: checkboxes id: feature-scope attributes: label: Verify Feature Scope - description: Please confirm the feature's scope is within the described scope + description: Confirm this request belongs in Issues rather than Discussions. options: - - label: I have read through and understood the scope definition for feature requests in the Issues section. I believe my feature request meets the definition and belongs in the Issues section instead of the Discussions. + - label: I believe this feature request is appropriately scoped for the Issues section as described above. required: true + - type: textarea id: problem-description attributes: label: Problem Description - description: Is your feature request related to a problem? Please provide a clear and concise description of what the problem is. - placeholder: "Ex. I'm always frustrated when... / Not related to a problem" + description: Is this related to a problem? Describe the pain point clearly. + placeholder: "e.g., I'm frustrated when..." validations: required: true + - type: textarea id: solution-description attributes: - label: Desired Solution you'd like - description: Clearly describe what you want to happen. + label: Proposed Solution + description: Describe what you would like to happen. validations: required: true + - type: textarea id: alternatives-considered attributes: label: Alternatives Considered - description: A clear and concise description of any alternative solutions or features you've considered. + description: Describe any alternative solutions or workarounds you have considered. + - type: textarea id: additional-context attributes: label: Additional Context - description: Add any other context or screenshots about the feature request here. + description: Add any other context, mockups, or screenshots about the feature request. diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 1c83fd305b..5998b3dc96 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -4,17 +4,22 @@ updates: directory: '/' schedule: interval: monthly - target-branch: 'dev' + target-branch: dev - package-ecosystem: pip directory: '/backend' schedule: interval: monthly - target-branch: 'dev' + target-branch: dev - - package-ecosystem: 'github-actions' + - package-ecosystem: github-actions directory: '/' schedule: - # Check for updates to GitHub Actions every week interval: monthly - target-branch: 'dev' + target-branch: dev + + - package-ecosystem: npm + directory: '/' + schedule: + interval: monthly + target-branch: dev diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 2f44750fe0..957f5a3768 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -19,75 +19,74 @@ The most impactful way to contribute to Open WebUI is through well-written bug r **Before submitting, make sure you've checked the following:** - [ ] **Linked Issue/Discussion:** This PR references an existing [Issue](https://github.com/open-webui/open-webui/issues) or [Discussion](https://github.com/open-webui/open-webui/discussions) — `Closes #___` / `Relates to #___`. If one does not exist, create one first. PRs without a linked issue or discussion may be closed without review. -- [ ] **Target branch:** Verify that the pull request targets the `dev` branch. **PRs targeting `main` will be immediately closed.** -- [ ] **Description:** Provide a concise description of the changes made in this pull request down below. -- [ ] **Changelog:** Ensure a changelog entry following the format of [Keep a Changelog](https://keepachangelog.com/) is added at the bottom of the PR description. -- [ ] **Documentation:** Add docs in [Open WebUI Docs Repository](https://github.com/open-webui/docs). Document user-facing behavior, environment variables, public APIs/interfaces, or deployment steps. -- [ ] **Dependencies:** Are there any new or upgraded dependencies? If so, explain why, update the changelog/docs, and include any compatibility notes. Actually run the code/function that uses updated library to ensure it doesn't crash. -- [ ] **Testing:** Perform manual tests to **verify the implemented fix/feature works as intended AND does not break any other functionality**. Include reproducible steps to demonstrate the issue before the fix. Test edge cases (URL encoding, HTML entities, types). Take this as an opportunity to **make screenshots of the feature/fix and include them in the PR description**. -- [ ] **Agentic AI Code:** Confirm this Pull Request is **not written by any AI Agent** or has at least **gone through additional human review AND manual testing**. If any AI Agent is the co-author of this PR, it may lead to immediate closure of the PR. -- [ ] **Code review:** Have you performed a self-review of your code, addressing any coding standard issues and ensuring adherence to the project's coding standards? -- [ ] **Design & Architecture:** Prefer smart defaults over adding new settings; use local state for ephemeral UI logic. Open a Discussion for major architectural or UX changes. -- [ ] **Git Hygiene:** Keep PRs atomic (one logical change). Clean up commits and rebase on `dev` to ensure no unrelated commits (e.g. from `main`) are included. Push updates to the existing PR branch instead of closing and reopening. -- [ ] **Title Prefix:** To clearly categorize this pull request, prefix the pull request title using one of the following: - - **BREAKING CHANGE**: Significant changes that may affect compatibility - - **build**: Changes that affect the build system or external dependencies - - **ci**: Changes to our continuous integration processes or workflows - - **chore**: Refactor, cleanup, or other non-functional code changes - - **docs**: Documentation update or addition - - **feat**: Introduces a new feature or enhancement to the codebase - - **fix**: Bug fix or error correction +- [ ] **Target branch:** The pull request targets the `dev` branch. **PRs targeting `main` will be immediately closed.** +- [ ] **Description:** A concise description of the changes is provided below. +- [ ] **Changelog:** A changelog entry following [Keep a Changelog](https://keepachangelog.com/) format is included at the bottom. +- [ ] **Documentation:** Relevant documentation has been added or updated in the [Open WebUI Docs Repository](https://github.com/open-webui/docs). +- [ ] **Dependencies:** Any new or updated dependencies are explained, tested, and documented. +- [ ] **Testing:** Manual tests have been performed to verify the fix/feature works correctly and does not introduce regressions. Screenshots or recordings are included where applicable. +- [ ] **No Unchecked AI Code:** This PR is either human-written or has undergone thorough human review AND manual testing. Unreviewed AI-generated PRs may be closed immediately. +- [ ] **Self-Review:** A self-review of the code has been performed, ensuring adherence to project coding standards. +- [ ] **Architecture:** Smart defaults are preferred over new settings. Local state is used for ephemeral UI logic. Major architectural or UX changes have been discussed first. +- [ ] **Git Hygiene:** The PR is atomic (one logical change), rebased on `dev`, and contains no unrelated commits. +- [ ] **Title Prefix:** The PR title uses one of the following prefixes: + - **BREAKING CHANGE**: Changes affecting backward compatibility + - **build**: Build system or dependency changes + - **ci**: CI/CD workflow changes + - **chore**: Refactoring, cleanup, or non-functional changes + - **docs**: Documentation additions or updates + - **feat**: New features or enhancements + - **fix**: Bug fixes or corrections - **i18n**: Internationalization or localization changes - - **perf**: Performance improvement - - **refactor**: Code restructuring for better maintainability, readability, or scalability - - **style**: Changes that do not affect the meaning of the code (white space, formatting, missing semi-colons, etc.) - - **test**: Adding missing tests or correcting existing tests - - **WIP**: Work in progress, a temporary label for incomplete or ongoing work + - **perf**: Performance improvements + - **refactor**: Code restructuring + - **style**: Formatting changes (whitespace, semicolons, etc.) + - **test**: Test additions or corrections + - **WIP**: Work in progress # Changelog Entry ### Description -- [Concisely describe the changes made in this pull request, including any relevant motivation and impact (e.g., fixing a bug, adding a feature, or improving performance)] +- [Describe the changes, including motivation and impact] ### Added -- [List any new features, functionalities, or additions] +- [New features, functionalities, or additions] ### Changed -- [List any changes, updates, refactorings, or optimizations] +- [Changes, updates, refactorings, or optimizations] ### Deprecated -- [List any deprecated functionality or features that have been removed] +- [Deprecated functionality or features] ### Removed -- [List any removed features, files, or functionalities] +- [Removed features, files, or functionalities] ### Fixed -- [List any fixes, corrections, or bug fixes] +- [Bug fixes or corrections] ### Security -- [List any new or updated security-related changes, including vulnerability fixes] +- [Security-related changes or vulnerability fixes] ### Breaking Changes -- **BREAKING CHANGE**: [List any breaking changes affecting compatibility or functionality] +- **BREAKING CHANGE**: [Changes affecting compatibility or functionality] --- ### Additional Information -- [Insert any additional context, notes, or explanations for the changes] - - [Reference any related issues, commits, or other relevant information] +- [Any additional context, notes, or references to related issues/commits] ### Screenshots or Videos -- [Attach any relevant screenshots or videos demonstrating the changes] +- [Attach relevant screenshots or videos demonstrating the changes] ### Contributor License Agreement diff --git a/.github/workflows/backend.yaml b/.github/workflows/backend.yaml new file mode 100644 index 0000000000..877a6b0ecc --- /dev/null +++ b/.github/workflows/backend.yaml @@ -0,0 +1,40 @@ +# ───────────────────────────────────────────────────────────────────────────── +# Backend CI — Python formatting checks via Ruff +# Runs on pushes and PRs to main/dev when backend files change +# ───────────────────────────────────────────────────────────────────────────── +name: Python CI + +on: + push: + branches: [main, dev] + paths: ['backend/**', 'pyproject.toml', 'uv.lock'] + pull_request: + branches: [main, dev] + paths: ['backend/**', 'pyproject.toml', 'uv.lock'] + +concurrency: + group: backend-${{ github.ref }} + cancel-in-progress: true + +jobs: + # ── Ruff format check across supported Python versions ─────────────────── + format-check: + name: Ruff Format (${{ matrix.python-version }}) + runs-on: ubuntu-latest + timeout-minutes: 10 + strategy: + fail-fast: false + matrix: + python-version: ['3.11', '3.12'] + steps: + - uses: actions/checkout@v5 + + - uses: actions/setup-python@v6 + with: + python-version: ${{ matrix.python-version }} + + - name: Install formatter + run: pip install "ruff>=0.15.5" + + - name: Verify formatting + run: ruff format --check . --exclude .venv --exclude venv diff --git a/.github/workflows/build-release.yml b/.github/workflows/build-release.yml deleted file mode 100644 index 2ecb013e0b..0000000000 --- a/.github/workflows/build-release.yml +++ /dev/null @@ -1,61 +0,0 @@ -name: Release - -on: - push: - branches: - - main # or whatever branch you want to use - -jobs: - release: - runs-on: ubuntu-latest - - steps: - - name: Checkout repository - uses: actions/checkout@v5 - - - name: Check for changes in package.json - run: | - git diff --cached --diff-filter=d package.json || { - echo "No changes to package.json" - exit 1 - } - - - name: Get version number from package.json - id: get_version - run: | - VERSION=$(jq -r '.version' package.json) - echo "::set-output name=version::$VERSION" - - - name: Extract latest CHANGELOG entry - run: | - VERSION="${{ steps.get_version.outputs.version }}" - awk "/^## \[${VERSION}\]/{found=1; next} /^## \[/{if(found) exit} found{print}" CHANGELOG.md > /tmp/release-notes.md - - - name: Create GitHub release - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - gh release create "v${{ steps.get_version.outputs.version }}" \ - --title "v${{ steps.get_version.outputs.version }}" \ - --notes-file /tmp/release-notes.md - - - name: Upload package to GitHub release - uses: actions/upload-artifact@v4 - with: - name: package - path: | - . - !.git - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - - name: Trigger Docker build workflow - uses: actions/github-script@v8 - with: - script: | - github.rest.actions.createWorkflowDispatch({ - owner: context.repo.owner, - repo: context.repo.repo, - workflow_id: 'docker-build.yaml', - ref: 'v${{ steps.get_version.outputs.version }}', - }) diff --git a/.github/workflows/docker-build.yaml b/.github/workflows/docker-build.yaml deleted file mode 100644 index 0307593476..0000000000 --- a/.github/workflows/docker-build.yaml +++ /dev/null @@ -1,917 +0,0 @@ -name: Create and publish Docker images with specific build args - -on: - workflow_dispatch: - push: - branches: - - main - - dev - tags: - - v* - -env: - REGISTRY: ghcr.io - -jobs: - build-main-image: - runs-on: ${{ matrix.runner }} - permissions: - contents: read - packages: write - strategy: - fail-fast: false - matrix: - include: - - platform: linux/amd64 - runner: ubuntu-latest - - platform: linux/arm64 - runner: ubuntu-24.04-arm - - steps: - # GitHub Packages requires the entire repository name to be in lowercase - # although the repository owner has a lowercase username, this prevents some people from running actions after forking - - name: Set repository and image name to lowercase - run: | - echo "IMAGE_NAME=${IMAGE_NAME,,}" >>${GITHUB_ENV} - echo "FULL_IMAGE_NAME=ghcr.io/${IMAGE_NAME,,}" >>${GITHUB_ENV} - env: - IMAGE_NAME: '${{ github.repository }}' - - - name: Prepare - run: | - platform=${{ matrix.platform }} - echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV - - - name: Checkout repository - uses: actions/checkout@v5 - - - name: Set up QEMU - uses: docker/setup-qemu-action@v3 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: Log in to the Container registry - uses: docker/login-action@v3 - with: - registry: ${{ env.REGISTRY }} - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Extract metadata for Docker images (default latest tag) - id: meta - uses: docker/metadata-action@v5 - with: - images: ${{ env.FULL_IMAGE_NAME }} - tags: | - type=ref,event=branch - type=ref,event=tag - type=sha,prefix=git- - type=semver,pattern={{version}} - type=semver,pattern={{major}}.{{minor}} - flavor: | - latest=${{ github.ref == 'refs/heads/main' }} - - - name: Extract metadata for Docker cache - id: cache-meta - uses: docker/metadata-action@v5 - with: - images: ${{ env.FULL_IMAGE_NAME }} - tags: | - type=ref,event=branch - ${{ github.ref_type == 'tag' && 'type=raw,value=main' || '' }} - flavor: | - prefix=cache-${{ matrix.platform }}- - latest=false - - - name: Build Docker image (latest) - uses: docker/build-push-action@v5 - id: build - with: - context: . - push: true - platforms: ${{ matrix.platform }} - labels: ${{ steps.meta.outputs.labels }} - outputs: type=image,name=${{ env.FULL_IMAGE_NAME }},push-by-digest=true,name-canonical=true,push=true - cache-from: type=registry,ref=${{ steps.cache-meta.outputs.tags }} - cache-to: type=registry,ref=${{ steps.cache-meta.outputs.tags }},mode=max - sbom: true - build-args: | - BUILD_HASH=${{ github.sha }} - - - name: Export digest - run: | - mkdir -p /tmp/digests - digest="${{ steps.build.outputs.digest }}" - touch "/tmp/digests/${digest#sha256:}" - - - name: Upload digest - uses: actions/upload-artifact@v4 - with: - name: digests-main-${{ env.PLATFORM_PAIR }} - path: /tmp/digests/* - if-no-files-found: error - retention-days: 1 - - build-cuda-image: - runs-on: ${{ matrix.runner }} - permissions: - contents: read - packages: write - strategy: - fail-fast: false - matrix: - include: - - platform: linux/amd64 - runner: ubuntu-latest - - platform: linux/arm64 - runner: ubuntu-24.04-arm - - steps: - # GitHub Packages requires the entire repository name to be in lowercase - # although the repository owner has a lowercase username, this prevents some people from running actions after forking - - name: Set repository and image name to lowercase - run: | - echo "IMAGE_NAME=${IMAGE_NAME,,}" >>${GITHUB_ENV} - echo "FULL_IMAGE_NAME=ghcr.io/${IMAGE_NAME,,}" >>${GITHUB_ENV} - env: - IMAGE_NAME: '${{ github.repository }}' - - - name: Prepare - run: | - platform=${{ matrix.platform }} - echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV - - - name: Delete huge unnecessary tools folder - run: rm -rf /opt/hostedtoolcache - - - name: Checkout repository - uses: actions/checkout@v5 - - - name: Set up QEMU - uses: docker/setup-qemu-action@v3 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: Log in to the Container registry - uses: docker/login-action@v3 - with: - registry: ${{ env.REGISTRY }} - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Extract metadata for Docker images (cuda tag) - id: meta - uses: docker/metadata-action@v5 - with: - images: ${{ env.FULL_IMAGE_NAME }} - tags: | - type=ref,event=branch - type=ref,event=tag - type=sha,prefix=git- - type=semver,pattern={{version}} - type=semver,pattern={{major}}.{{minor}} - type=raw,enable=${{ github.ref == 'refs/heads/main' }},prefix=,suffix=,value=cuda - flavor: | - latest=${{ github.ref == 'refs/heads/main' }} - suffix=-cuda,onlatest=true - - - name: Extract metadata for Docker cache - id: cache-meta - uses: docker/metadata-action@v5 - with: - images: ${{ env.FULL_IMAGE_NAME }} - tags: | - type=ref,event=branch - ${{ github.ref_type == 'tag' && 'type=raw,value=main' || '' }} - flavor: | - prefix=cache-cuda-${{ matrix.platform }}- - latest=false - - - name: Build Docker image (cuda) - uses: docker/build-push-action@v5 - id: build - with: - context: . - push: true - platforms: ${{ matrix.platform }} - labels: ${{ steps.meta.outputs.labels }} - outputs: type=image,name=${{ env.FULL_IMAGE_NAME }},push-by-digest=true,name-canonical=true,push=true - cache-from: type=registry,ref=${{ steps.cache-meta.outputs.tags }} - cache-to: type=registry,ref=${{ steps.cache-meta.outputs.tags }},mode=max - sbom: true - build-args: | - BUILD_HASH=${{ github.sha }} - USE_CUDA=true - - - name: Export digest - run: | - mkdir -p /tmp/digests - digest="${{ steps.build.outputs.digest }}" - touch "/tmp/digests/${digest#sha256:}" - - - name: Upload digest - uses: actions/upload-artifact@v4 - with: - name: digests-cuda-${{ env.PLATFORM_PAIR }} - path: /tmp/digests/* - if-no-files-found: error - retention-days: 1 - - build-cuda126-image: - runs-on: ${{ matrix.runner }} - permissions: - contents: read - packages: write - strategy: - fail-fast: false - matrix: - include: - - platform: linux/amd64 - runner: ubuntu-latest - - platform: linux/arm64 - runner: ubuntu-24.04-arm - - steps: - # GitHub Packages requires the entire repository name to be in lowercase - # although the repository owner has a lowercase username, this prevents some people from running actions after forking - - name: Set repository and image name to lowercase - run: | - echo "IMAGE_NAME=${IMAGE_NAME,,}" >>${GITHUB_ENV} - echo "FULL_IMAGE_NAME=ghcr.io/${IMAGE_NAME,,}" >>${GITHUB_ENV} - env: - IMAGE_NAME: '${{ github.repository }}' - - - name: Prepare - run: | - platform=${{ matrix.platform }} - echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV - - - name: Delete huge unnecessary tools folder - run: rm -rf /opt/hostedtoolcache - - - name: Checkout repository - uses: actions/checkout@v5 - - - name: Set up QEMU - uses: docker/setup-qemu-action@v3 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: Log in to the Container registry - uses: docker/login-action@v3 - with: - registry: ${{ env.REGISTRY }} - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Extract metadata for Docker images (cuda126 tag) - id: meta - uses: docker/metadata-action@v5 - with: - images: ${{ env.FULL_IMAGE_NAME }} - tags: | - type=ref,event=branch - type=ref,event=tag - type=sha,prefix=git- - type=semver,pattern={{version}} - type=semver,pattern={{major}}.{{minor}} - type=raw,enable=${{ github.ref == 'refs/heads/main' }},prefix=,suffix=,value=cuda126 - flavor: | - latest=${{ github.ref == 'refs/heads/main' }} - suffix=-cuda126,onlatest=true - - - name: Extract metadata for Docker cache - id: cache-meta - uses: docker/metadata-action@v5 - with: - images: ${{ env.FULL_IMAGE_NAME }} - tags: | - type=ref,event=branch - ${{ github.ref_type == 'tag' && 'type=raw,value=main' || '' }} - flavor: | - prefix=cache-cuda126-${{ matrix.platform }}- - latest=false - - - name: Build Docker image (cuda126) - uses: docker/build-push-action@v5 - id: build - with: - context: . - push: true - platforms: ${{ matrix.platform }} - labels: ${{ steps.meta.outputs.labels }} - outputs: type=image,name=${{ env.FULL_IMAGE_NAME }},push-by-digest=true,name-canonical=true,push=true - cache-from: type=registry,ref=${{ steps.cache-meta.outputs.tags }} - cache-to: type=registry,ref=${{ steps.cache-meta.outputs.tags }},mode=max - sbom: true - build-args: | - BUILD_HASH=${{ github.sha }} - USE_CUDA=true - USE_CUDA_VER=cu126 - - - name: Export digest - run: | - mkdir -p /tmp/digests - digest="${{ steps.build.outputs.digest }}" - touch "/tmp/digests/${digest#sha256:}" - - - name: Upload digest - uses: actions/upload-artifact@v4 - with: - name: digests-cuda126-${{ env.PLATFORM_PAIR }} - path: /tmp/digests/* - if-no-files-found: error - retention-days: 1 - - build-ollama-image: - runs-on: ${{ matrix.runner }} - permissions: - contents: read - packages: write - strategy: - fail-fast: false - matrix: - include: - - platform: linux/amd64 - runner: ubuntu-latest - - platform: linux/arm64 - runner: ubuntu-24.04-arm - - steps: - # GitHub Packages requires the entire repository name to be in lowercase - # although the repository owner has a lowercase username, this prevents some people from running actions after forking - - name: Set repository and image name to lowercase - run: | - echo "IMAGE_NAME=${IMAGE_NAME,,}" >>${GITHUB_ENV} - echo "FULL_IMAGE_NAME=ghcr.io/${IMAGE_NAME,,}" >>${GITHUB_ENV} - env: - IMAGE_NAME: '${{ github.repository }}' - - - name: Prepare - run: | - platform=${{ matrix.platform }} - echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV - - - name: Checkout repository - uses: actions/checkout@v5 - - - name: Set up QEMU - uses: docker/setup-qemu-action@v3 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: Log in to the Container registry - uses: docker/login-action@v3 - with: - registry: ${{ env.REGISTRY }} - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Extract metadata for Docker images (ollama tag) - id: meta - uses: docker/metadata-action@v5 - with: - images: ${{ env.FULL_IMAGE_NAME }} - tags: | - type=ref,event=branch - type=ref,event=tag - type=sha,prefix=git- - type=semver,pattern={{version}} - type=semver,pattern={{major}}.{{minor}} - type=raw,enable=${{ github.ref == 'refs/heads/main' }},prefix=,suffix=,value=ollama - flavor: | - latest=${{ github.ref == 'refs/heads/main' }} - suffix=-ollama,onlatest=true - - - name: Extract metadata for Docker cache - id: cache-meta - uses: docker/metadata-action@v5 - with: - images: ${{ env.FULL_IMAGE_NAME }} - tags: | - type=ref,event=branch - ${{ github.ref_type == 'tag' && 'type=raw,value=main' || '' }} - flavor: | - prefix=cache-ollama-${{ matrix.platform }}- - latest=false - - - name: Build Docker image (ollama) - uses: docker/build-push-action@v5 - id: build - with: - context: . - push: true - platforms: ${{ matrix.platform }} - labels: ${{ steps.meta.outputs.labels }} - outputs: type=image,name=${{ env.FULL_IMAGE_NAME }},push-by-digest=true,name-canonical=true,push=true - cache-from: type=registry,ref=${{ steps.cache-meta.outputs.tags }} - cache-to: type=registry,ref=${{ steps.cache-meta.outputs.tags }},mode=max - sbom: true - build-args: | - BUILD_HASH=${{ github.sha }} - USE_OLLAMA=true - - - name: Export digest - run: | - mkdir -p /tmp/digests - digest="${{ steps.build.outputs.digest }}" - touch "/tmp/digests/${digest#sha256:}" - - - name: Upload digest - uses: actions/upload-artifact@v4 - with: - name: digests-ollama-${{ env.PLATFORM_PAIR }} - path: /tmp/digests/* - if-no-files-found: error - retention-days: 1 - - build-slim-image: - runs-on: ${{ matrix.runner }} - permissions: - contents: read - packages: write - strategy: - fail-fast: false - matrix: - include: - - platform: linux/amd64 - runner: ubuntu-latest - - platform: linux/arm64 - runner: ubuntu-24.04-arm - - steps: - # GitHub Packages requires the entire repository name to be in lowercase - # although the repository owner has a lowercase username, this prevents some people from running actions after forking - - name: Set repository and image name to lowercase - run: | - echo "IMAGE_NAME=${IMAGE_NAME,,}" >>${GITHUB_ENV} - echo "FULL_IMAGE_NAME=ghcr.io/${IMAGE_NAME,,}" >>${GITHUB_ENV} - env: - IMAGE_NAME: '${{ github.repository }}' - - - name: Prepare - run: | - platform=${{ matrix.platform }} - echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV - - - name: Checkout repository - uses: actions/checkout@v5 - - - name: Set up QEMU - uses: docker/setup-qemu-action@v3 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: Log in to the Container registry - uses: docker/login-action@v3 - with: - registry: ${{ env.REGISTRY }} - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Extract metadata for Docker images (slim tag) - id: meta - uses: docker/metadata-action@v5 - with: - images: ${{ env.FULL_IMAGE_NAME }} - tags: | - type=ref,event=branch - type=ref,event=tag - type=sha,prefix=git- - type=semver,pattern={{version}} - type=semver,pattern={{major}}.{{minor}} - type=raw,enable=${{ github.ref == 'refs/heads/main' }},prefix=,suffix=,value=slim - flavor: | - latest=${{ github.ref == 'refs/heads/main' }} - suffix=-slim,onlatest=true - - - name: Extract metadata for Docker cache - id: cache-meta - uses: docker/metadata-action@v5 - with: - images: ${{ env.FULL_IMAGE_NAME }} - tags: | - type=ref,event=branch - ${{ github.ref_type == 'tag' && 'type=raw,value=main' || '' }} - flavor: | - prefix=cache-slim-${{ matrix.platform }}- - latest=false - - - name: Build Docker image (slim) - uses: docker/build-push-action@v5 - id: build - with: - context: . - push: true - platforms: ${{ matrix.platform }} - labels: ${{ steps.meta.outputs.labels }} - outputs: type=image,name=${{ env.FULL_IMAGE_NAME }},push-by-digest=true,name-canonical=true,push=true - cache-from: type=registry,ref=${{ steps.cache-meta.outputs.tags }} - cache-to: type=registry,ref=${{ steps.cache-meta.outputs.tags }},mode=max - sbom: true - build-args: | - BUILD_HASH=${{ github.sha }} - USE_SLIM=true - - - name: Export digest - run: | - mkdir -p /tmp/digests - digest="${{ steps.build.outputs.digest }}" - touch "/tmp/digests/${digest#sha256:}" - - - name: Upload digest - uses: actions/upload-artifact@v4 - with: - name: digests-slim-${{ env.PLATFORM_PAIR }} - path: /tmp/digests/* - if-no-files-found: error - retention-days: 1 - - merge-main-images: - runs-on: ubuntu-latest - needs: [build-main-image] - steps: - # GitHub Packages requires the entire repository name to be in lowercase - # although the repository owner has a lowercase username, this prevents some people from running actions after forking - - name: Set repository and image name to lowercase - run: | - echo "IMAGE_NAME=${IMAGE_NAME,,}" >>${GITHUB_ENV} - echo "FULL_IMAGE_NAME=ghcr.io/${IMAGE_NAME,,}" >>${GITHUB_ENV} - env: - IMAGE_NAME: '${{ github.repository }}' - - - name: Download digests - uses: actions/download-artifact@v5 - with: - pattern: digests-main-* - path: /tmp/digests - merge-multiple: true - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: Log in to the Container registry - uses: docker/login-action@v3 - with: - registry: ${{ env.REGISTRY }} - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Extract metadata for Docker images (default latest tag) - id: meta - uses: docker/metadata-action@v5 - with: - images: ${{ env.FULL_IMAGE_NAME }} - tags: | - type=ref,event=branch - type=ref,event=tag - type=sha,prefix=git- - type=semver,pattern={{version}} - type=semver,pattern={{major}}.{{minor}} - flavor: | - latest=${{ github.ref == 'refs/heads/main' }} - - - name: Create manifest list and push - working-directory: /tmp/digests - run: | - docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \ - $(printf '${{ env.FULL_IMAGE_NAME }}@sha256:%s ' *) - - - name: Inspect image - run: | - docker buildx imagetools inspect ${{ env.FULL_IMAGE_NAME }}:${{ steps.meta.outputs.version }} - - merge-cuda-images: - runs-on: ubuntu-latest - needs: [build-cuda-image] - steps: - # GitHub Packages requires the entire repository name to be in lowercase - # although the repository owner has a lowercase username, this prevents some people from running actions after forking - - name: Set repository and image name to lowercase - run: | - echo "IMAGE_NAME=${IMAGE_NAME,,}" >>${GITHUB_ENV} - echo "FULL_IMAGE_NAME=ghcr.io/${IMAGE_NAME,,}" >>${GITHUB_ENV} - env: - IMAGE_NAME: '${{ github.repository }}' - - - name: Download digests - uses: actions/download-artifact@v5 - with: - pattern: digests-cuda-* - path: /tmp/digests - merge-multiple: true - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: Log in to the Container registry - uses: docker/login-action@v3 - with: - registry: ${{ env.REGISTRY }} - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Extract metadata for Docker images (default latest tag) - id: meta - uses: docker/metadata-action@v5 - with: - images: ${{ env.FULL_IMAGE_NAME }} - tags: | - type=ref,event=branch - type=ref,event=tag - type=sha,prefix=git- - type=semver,pattern={{version}} - type=semver,pattern={{major}}.{{minor}} - type=raw,enable=${{ github.ref == 'refs/heads/main' }},prefix=,suffix=,value=cuda - flavor: | - latest=${{ github.ref == 'refs/heads/main' }} - suffix=-cuda,onlatest=true - - - name: Create manifest list and push - working-directory: /tmp/digests - run: | - docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \ - $(printf '${{ env.FULL_IMAGE_NAME }}@sha256:%s ' *) - - - name: Inspect image - run: | - docker buildx imagetools inspect ${{ env.FULL_IMAGE_NAME }}:${{ steps.meta.outputs.version }} - - merge-cuda126-images: - runs-on: ubuntu-latest - needs: [build-cuda126-image] - steps: - # GitHub Packages requires the entire repository name to be in lowercase - # although the repository owner has a lowercase username, this prevents some people from running actions after forking - - name: Set repository and image name to lowercase - run: | - echo "IMAGE_NAME=${IMAGE_NAME,,}" >>${GITHUB_ENV} - echo "FULL_IMAGE_NAME=ghcr.io/${IMAGE_NAME,,}" >>${GITHUB_ENV} - env: - IMAGE_NAME: '${{ github.repository }}' - - - name: Download digests - uses: actions/download-artifact@v5 - with: - pattern: digests-cuda126-* - path: /tmp/digests - merge-multiple: true - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: Log in to the Container registry - uses: docker/login-action@v3 - with: - registry: ${{ env.REGISTRY }} - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Extract metadata for Docker images (default latest tag) - id: meta - uses: docker/metadata-action@v5 - with: - images: ${{ env.FULL_IMAGE_NAME }} - tags: | - type=ref,event=branch - type=ref,event=tag - type=sha,prefix=git- - type=semver,pattern={{version}} - type=semver,pattern={{major}}.{{minor}} - type=raw,enable=${{ github.ref == 'refs/heads/main' }},prefix=,suffix=,value=cuda126 - flavor: | - latest=${{ github.ref == 'refs/heads/main' }} - suffix=-cuda126,onlatest=true - - - name: Create manifest list and push - working-directory: /tmp/digests - run: | - docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \ - $(printf '${{ env.FULL_IMAGE_NAME }}@sha256:%s ' *) - - - name: Inspect image - run: | - docker buildx imagetools inspect ${{ env.FULL_IMAGE_NAME }}:${{ steps.meta.outputs.version }} - - merge-ollama-images: - runs-on: ubuntu-latest - needs: [build-ollama-image] - steps: - # GitHub Packages requires the entire repository name to be in lowercase - # although the repository owner has a lowercase username, this prevents some people from running actions after forking - - name: Set repository and image name to lowercase - run: | - echo "IMAGE_NAME=${IMAGE_NAME,,}" >>${GITHUB_ENV} - echo "FULL_IMAGE_NAME=ghcr.io/${IMAGE_NAME,,}" >>${GITHUB_ENV} - env: - IMAGE_NAME: '${{ github.repository }}' - - - name: Download digests - uses: actions/download-artifact@v5 - with: - pattern: digests-ollama-* - path: /tmp/digests - merge-multiple: true - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: Log in to the Container registry - uses: docker/login-action@v3 - with: - registry: ${{ env.REGISTRY }} - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Extract metadata for Docker images (default ollama tag) - id: meta - uses: docker/metadata-action@v5 - with: - images: ${{ env.FULL_IMAGE_NAME }} - tags: | - type=ref,event=branch - type=ref,event=tag - type=sha,prefix=git- - type=semver,pattern={{version}} - type=semver,pattern={{major}}.{{minor}} - type=raw,enable=${{ github.ref == 'refs/heads/main' }},prefix=,suffix=,value=ollama - flavor: | - latest=${{ github.ref == 'refs/heads/main' }} - suffix=-ollama,onlatest=true - - - name: Create manifest list and push - working-directory: /tmp/digests - run: | - docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \ - $(printf '${{ env.FULL_IMAGE_NAME }}@sha256:%s ' *) - - - name: Inspect image - run: | - docker buildx imagetools inspect ${{ env.FULL_IMAGE_NAME }}:${{ steps.meta.outputs.version }} - - merge-slim-images: - runs-on: ubuntu-latest - needs: [build-slim-image] - steps: - # GitHub Packages requires the entire repository name to be in lowercase - # although the repository owner has a lowercase username, this prevents some people from running actions after forking - - name: Set repository and image name to lowercase - run: | - echo "IMAGE_NAME=${IMAGE_NAME,,}" >>${GITHUB_ENV} - echo "FULL_IMAGE_NAME=ghcr.io/${IMAGE_NAME,,}" >>${GITHUB_ENV} - env: - IMAGE_NAME: '${{ github.repository }}' - - - name: Download digests - uses: actions/download-artifact@v5 - with: - pattern: digests-slim-* - path: /tmp/digests - merge-multiple: true - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: Log in to the Container registry - uses: docker/login-action@v3 - with: - registry: ${{ env.REGISTRY }} - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Extract metadata for Docker images (default slim tag) - id: meta - uses: docker/metadata-action@v5 - with: - images: ${{ env.FULL_IMAGE_NAME }} - tags: | - type=ref,event=branch - type=ref,event=tag - type=sha,prefix=git- - type=semver,pattern={{version}} - type=semver,pattern={{major}}.{{minor}} - type=raw,enable=${{ github.ref == 'refs/heads/main' }},prefix=,suffix=,value=slim - flavor: | - latest=${{ github.ref == 'refs/heads/main' }} - suffix=-slim,onlatest=true - - - name: Create manifest list and push - working-directory: /tmp/digests - run: | - docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \ - $(printf '${{ env.FULL_IMAGE_NAME }}@sha256:%s ' *) - - - name: Inspect image - run: | - docker buildx imagetools inspect ${{ env.FULL_IMAGE_NAME }}:${{ steps.meta.outputs.version }} - - # Copy images from GHCR to Docker Hub (best-effort, won't block GHCR) - copy-to-dockerhub: - runs-on: ubuntu-latest - if: github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v') - needs: [merge-main-images, merge-cuda-images, merge-cuda126-images, merge-ollama-images, merge-slim-images] - continue-on-error: true - strategy: - fail-fast: false - matrix: - include: - - variant: main - suffix: "" - - variant: cuda - suffix: "-cuda" - - variant: cuda126 - suffix: "-cuda126" - - variant: ollama - suffix: "-ollama" - - variant: slim - suffix: "-slim" - steps: - - name: Set repository and image name to lowercase - run: | - echo "IMAGE_NAME=${IMAGE_NAME,,}" >>${GITHUB_ENV} - echo "FULL_IMAGE_NAME=ghcr.io/${IMAGE_NAME,,}" >>${GITHUB_ENV} - env: - IMAGE_NAME: '${{ github.repository }}' - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: Log in to the Container registry - uses: docker/login-action@v3 - with: - registry: ${{ env.REGISTRY }} - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Log in to Docker Hub - uses: docker/login-action@v3 - with: - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_TOKEN }} - - - name: Determine source and destination tags - id: tags - run: | - DOCKERHUB_IMAGE="openwebui/open-webui" - SUFFIX="${{ matrix.suffix }}" - - if [[ "${{ github.ref }}" == refs/tags/v* ]]; then - # For version tags: copy version tag and major.minor tag - VERSION="${{ github.ref_name }}" - VERSION="${VERSION#v}" - MAJOR_MINOR="${VERSION%.*}" - - echo "tags<> $GITHUB_OUTPUT - echo "${VERSION}${SUFFIX}" >> $GITHUB_OUTPUT - echo "${MAJOR_MINOR}${SUFFIX}" >> $GITHUB_OUTPUT - echo "EOF" >> $GITHUB_OUTPUT - else - # For main branch - if [ -z "$SUFFIX" ]; then - echo "tags=latest" >> $GITHUB_OUTPUT - else - # e.g. latest-cuda -> also tag as just "cuda" - VARIANT_NAME="${SUFFIX#-}" - echo "tags<> $GITHUB_OUTPUT - echo "latest${SUFFIX}" >> $GITHUB_OUTPUT - echo "${VARIANT_NAME}" >> $GITHUB_OUTPUT - echo "EOF" >> $GITHUB_OUTPUT - fi - fi - - echo "dockerhub_image=${DOCKERHUB_IMAGE}" >> $GITHUB_OUTPUT - - - name: Copy images from GHCR to Docker Hub - run: | - DOCKERHUB_IMAGE="${{ steps.tags.outputs.dockerhub_image }}" - SUFFIX="${{ matrix.suffix }}" - - # Determine the source tag on GHCR - if [[ "${{ github.ref }}" == refs/tags/v* ]]; then - VERSION="${{ github.ref_name }}" - VERSION="${VERSION#v}" - SOURCE_TAG="${VERSION}${SUFFIX}" - else - if [ -z "$SUFFIX" ]; then - SOURCE_TAG="latest" - else - SOURCE_TAG="latest${SUFFIX}" - fi - fi - - SOURCE="${{ env.FULL_IMAGE_NAME }}:${SOURCE_TAG}" - - echo "Copying from ${SOURCE} to Docker Hub..." - - # Copy each destination tag - while IFS= read -r TAG; do - [ -z "$TAG" ] && continue - DEST="${DOCKERHUB_IMAGE}:${TAG}" - echo " -> ${DEST}" - docker buildx imagetools create -t "${DEST}" "${SOURCE}" - done <<< "${{ steps.tags.outputs.tags }}" diff --git a/.github/workflows/docker.yaml b/.github/workflows/docker.yaml new file mode 100644 index 0000000000..3fd5a5f5bc --- /dev/null +++ b/.github/workflows/docker.yaml @@ -0,0 +1,333 @@ +name: Create and publish Docker images with specific build args + +on: + workflow_dispatch: + push: + branches: + - main + - dev + tags: + - v* + +concurrency: + group: docker-${{ github.ref }} + cancel-in-progress: true + +env: + REGISTRY: ghcr.io + +jobs: + build: + runs-on: ${{ matrix.platform.runner }} + permissions: + contents: read + packages: write + strategy: + fail-fast: false + matrix: + platform: + - arch: linux/amd64 + runner: ubuntu-latest + - arch: linux/arm64 + runner: ubuntu-24.04-arm + variant: + - name: main + suffix: "" + build_args: "" + free_disk: false + - name: cuda + suffix: "-cuda" + build_args: "USE_CUDA=true" + free_disk: true + - name: cuda126 + suffix: "-cuda126" + build_args: | + USE_CUDA=true + USE_CUDA_VER=cu126 + free_disk: true + - name: ollama + suffix: "-ollama" + build_args: "USE_OLLAMA=true" + free_disk: false + - name: slim + suffix: "-slim" + build_args: "USE_SLIM=true" + free_disk: false + + steps: + - name: Prepare environment + run: | + echo "IMAGE_NAME=${GITHUB_REPOSITORY,,}" >> ${GITHUB_ENV} + echo "FULL_IMAGE_NAME=${REGISTRY}/${GITHUB_REPOSITORY,,}" >> ${GITHUB_ENV} + platform=${{ matrix.platform.arch }} + echo "PLATFORM_PAIR=${platform//\//-}" >> ${GITHUB_ENV} + + - name: Free disk space + if: matrix.variant.free_disk + run: rm -rf /opt/hostedtoolcache + + - name: Checkout repository + uses: actions/checkout@v5 + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to the Container registry + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract metadata for Docker images + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.FULL_IMAGE_NAME }} + tags: | + type=ref,event=branch + type=ref,event=tag + type=sha,prefix=git- + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + ${{ matrix.variant.suffix != '' && format('type=raw,enable={0},prefix=,suffix=,value={1}', github.ref == 'refs/heads/main', matrix.variant.name) || '' }} + flavor: | + latest=${{ github.ref == 'refs/heads/main' }} + ${{ matrix.variant.suffix != '' && format('suffix={0},onlatest=true', matrix.variant.suffix) || '' }} + + - name: Extract metadata for Docker cache + id: cache-meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.FULL_IMAGE_NAME }} + tags: | + type=ref,event=branch + ${{ github.ref_type == 'tag' && 'type=raw,value=main' || '' }} + flavor: | + prefix=cache-${{ matrix.variant.name }}-${{ matrix.platform.arch }}- + latest=false + + - name: Build Docker image + uses: docker/build-push-action@v5 + id: build + with: + context: . + push: true + platforms: ${{ matrix.platform.arch }} + labels: ${{ steps.meta.outputs.labels }} + outputs: type=image,name=${{ env.FULL_IMAGE_NAME }},push-by-digest=true,name-canonical=true,push=true + cache-from: type=registry,ref=${{ steps.cache-meta.outputs.tags }} + cache-to: type=registry,ref=${{ steps.cache-meta.outputs.tags }},mode=max + sbom: true + build-args: | + BUILD_HASH=${{ github.sha }} + ${{ matrix.variant.build_args }} + + - name: Export digest + run: | + mkdir -p /tmp/digests + digest="${{ steps.build.outputs.digest }}" + touch "/tmp/digests/${digest#sha256:}" + + - name: Upload digest + uses: actions/upload-artifact@v4 + with: + name: digests-${{ matrix.variant.name }}-${{ env.PLATFORM_PAIR }} + path: /tmp/digests/* + if-no-files-found: error + retention-days: 1 + + merge: + runs-on: ubuntu-latest + needs: [build] + if: !cancelled() + permissions: + contents: read + packages: write + strategy: + fail-fast: false + matrix: + variant: + - name: main + suffix: "" + - name: cuda + suffix: "-cuda" + - name: cuda126 + suffix: "-cuda126" + - name: ollama + suffix: "-ollama" + - name: slim + suffix: "-slim" + + steps: + - name: Prepare environment + run: | + echo "IMAGE_NAME=${GITHUB_REPOSITORY,,}" >> ${GITHUB_ENV} + echo "FULL_IMAGE_NAME=${REGISTRY}/${GITHUB_REPOSITORY,,}" >> ${GITHUB_ENV} + + - name: Download digests + id: download + uses: actions/download-artifact@v5 + with: + pattern: digests-${{ matrix.variant.name }}-* + path: /tmp/digests + merge-multiple: true + continue-on-error: true + + - name: Check digests + id: check + run: | + count=$(find /tmp/digests -type f 2>/dev/null | wc -l | tr -d ' ') + echo "digest_count=$count" >> $GITHUB_OUTPUT + if [ "$count" -lt 2 ]; then + echo "::warning::${{ matrix.variant.name }}: found $count digest(s), need 2 (one per arch). Skipping merge." + echo "skip=true" >> $GITHUB_OUTPUT + else + echo "skip=false" >> $GITHUB_OUTPUT + fi + + - name: Set up Docker Buildx + if: steps.check.outputs.skip != 'true' + uses: docker/setup-buildx-action@v3 + + - name: Log in to the Container registry + if: steps.check.outputs.skip != 'true' + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract metadata for Docker images + if: steps.check.outputs.skip != 'true' + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.FULL_IMAGE_NAME }} + tags: | + type=ref,event=branch + type=ref,event=tag + type=sha,prefix=git- + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + ${{ matrix.variant.suffix != '' && format('type=raw,enable={0},prefix=,suffix=,value={1}', github.ref == 'refs/heads/main', matrix.variant.name) || '' }} + flavor: | + latest=${{ github.ref == 'refs/heads/main' }} + ${{ matrix.variant.suffix != '' && format('suffix={0},onlatest=true', matrix.variant.suffix) || '' }} + + - name: Create manifest list and push + if: steps.check.outputs.skip != 'true' + working-directory: /tmp/digests + run: | + docker buildx imagetools create \ + $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \ + $(printf '${{ env.FULL_IMAGE_NAME }}@sha256:%s ' *) + + - name: Inspect image + if: steps.check.outputs.skip != 'true' + run: | + docker buildx imagetools inspect ${{ env.FULL_IMAGE_NAME }}:${{ steps.meta.outputs.version }} + + copy-to-dockerhub: + runs-on: ubuntu-latest + if: >- + !cancelled() && + (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v')) + needs: [merge] + continue-on-error: true + strategy: + fail-fast: false + matrix: + include: + - variant: main + suffix: "" + - variant: cuda + suffix: "-cuda" + - variant: cuda126 + suffix: "-cuda126" + - variant: ollama + suffix: "-ollama" + - variant: slim + suffix: "-slim" + + steps: + - name: Prepare environment + run: | + echo "IMAGE_NAME=${GITHUB_REPOSITORY,,}" >> ${GITHUB_ENV} + echo "FULL_IMAGE_NAME=${REGISTRY}/${GITHUB_REPOSITORY,,}" >> ${GITHUB_ENV} + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to the Container registry + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Log in to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Determine source and destination tags + id: tags + run: | + DOCKERHUB_IMAGE="openwebui/open-webui" + SUFFIX="${{ matrix.suffix }}" + + if [[ "${{ github.ref }}" == refs/tags/v* ]]; then + VERSION="${{ github.ref_name }}" + VERSION="${VERSION#v}" + MAJOR_MINOR="${VERSION%.*}" + + echo "tags<> $GITHUB_OUTPUT + echo "${VERSION}${SUFFIX}" >> $GITHUB_OUTPUT + echo "${MAJOR_MINOR}${SUFFIX}" >> $GITHUB_OUTPUT + echo "EOF" >> $GITHUB_OUTPUT + else + if [ -z "$SUFFIX" ]; then + echo "tags=latest" >> $GITHUB_OUTPUT + else + VARIANT_NAME="${SUFFIX#-}" + echo "tags<> $GITHUB_OUTPUT + echo "latest${SUFFIX}" >> $GITHUB_OUTPUT + echo "${VARIANT_NAME}" >> $GITHUB_OUTPUT + echo "EOF" >> $GITHUB_OUTPUT + fi + fi + + echo "dockerhub_image=${DOCKERHUB_IMAGE}" >> $GITHUB_OUTPUT + + - name: Copy images from GHCR to Docker Hub + run: | + DOCKERHUB_IMAGE="${{ steps.tags.outputs.dockerhub_image }}" + SUFFIX="${{ matrix.suffix }}" + + if [[ "${{ github.ref }}" == refs/tags/v* ]]; then + VERSION="${{ github.ref_name }}" + VERSION="${VERSION#v}" + SOURCE_TAG="${VERSION}${SUFFIX}" + else + if [ -z "$SUFFIX" ]; then + SOURCE_TAG="latest" + else + SOURCE_TAG="latest${SUFFIX}" + fi + fi + + SOURCE="${{ env.FULL_IMAGE_NAME }}:${SOURCE_TAG}" + + echo "Copying from ${SOURCE} to Docker Hub..." + + while IFS= read -r TAG; do + [ -z "$TAG" ] && continue + DEST="${DOCKERHUB_IMAGE}:${TAG}" + echo " -> ${DEST}" + docker buildx imagetools create -t "${DEST}" "${SOURCE}" + done <<< "${{ steps.tags.outputs.tags }}" diff --git a/.github/workflows/format-backend.yaml b/.github/workflows/format-backend.yaml deleted file mode 100644 index ee2d689d89..0000000000 --- a/.github/workflows/format-backend.yaml +++ /dev/null @@ -1,46 +0,0 @@ -name: Python CI - -on: - push: - branches: - - main - - dev - paths: - - 'backend/**' - - 'pyproject.toml' - - 'uv.lock' - pull_request: - branches: - - main - - dev - paths: - - 'backend/**' - - 'pyproject.toml' - - 'uv.lock' - -jobs: - build: - name: 'Format Backend' - runs-on: ubuntu-latest - - strategy: - matrix: - python-version: - - 3.11.x - - 3.12.x - - steps: - - uses: actions/checkout@v5 - - - name: Set up Python - uses: actions/setup-python@v6 - with: - python-version: '${{ matrix.python-version }}' - - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install "ruff>=0.15.5" - - - name: Ruff format check - run: ruff format --check . --exclude .venv --exclude venv diff --git a/.github/workflows/format-build-frontend.yaml b/.github/workflows/format-build-frontend.yaml deleted file mode 100644 index eaa1072fbc..0000000000 --- a/.github/workflows/format-build-frontend.yaml +++ /dev/null @@ -1,65 +0,0 @@ -name: Frontend Build - -on: - push: - branches: - - main - - dev - paths-ignore: - - 'backend/**' - - 'pyproject.toml' - - 'uv.lock' - pull_request: - branches: - - main - - dev - paths-ignore: - - 'backend/**' - - 'pyproject.toml' - - 'uv.lock' - -jobs: - build: - name: 'Format & Build Frontend' - runs-on: ubuntu-latest - steps: - - name: Checkout Repository - uses: actions/checkout@v5 - - - name: Setup Node.js - uses: actions/setup-node@v5 - with: - node-version: '22' - - - name: Install Dependencies - run: npm install --force - - - name: Format Frontend - run: npm run format - - - name: Run i18next - run: npm run i18n:parse - - - name: Check for Changes After Format - run: git diff --exit-code - - - name: Build Frontend - run: npm run build - - test-frontend: - name: 'Frontend Unit Tests' - runs-on: ubuntu-latest - steps: - - name: Checkout Repository - uses: actions/checkout@v5 - - - name: Setup Node.js - uses: actions/setup-node@v5 - with: - node-version: '22' - - - name: Install Dependencies - run: npm ci --force - - - name: Run vitest - run: npm run test:frontend diff --git a/.github/workflows/frontend.yaml b/.github/workflows/frontend.yaml new file mode 100644 index 0000000000..9b5f2a099f --- /dev/null +++ b/.github/workflows/frontend.yaml @@ -0,0 +1,63 @@ +# ───────────────────────────────────────────────────────────────────────────── +# Frontend CI — Lint, format check, build, and unit tests +# Runs on pushes and PRs to main/dev, skipping backend-only changes +# ───────────────────────────────────────────────────────────────────────────── +name: Frontend Build + +on: + push: + branches: [main, dev] + paths-ignore: ['backend/**', 'pyproject.toml', 'uv.lock'] + pull_request: + branches: [main, dev] + paths-ignore: ['backend/**', 'pyproject.toml', 'uv.lock'] + +concurrency: + group: frontend-${{ github.ref }} + cancel-in-progress: true + +jobs: + # ── Format, i18n, and production build ──────────────────────────────────── + format-and-build: + name: Format & Build + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - uses: actions/checkout@v5 + + - uses: actions/setup-node@v5 + with: + node-version: '22' + + - name: Install dependencies + run: npm install --force + + - name: Verify code formatting + run: npm run format + + - name: Verify i18n strings + run: npm run i18n:parse + + - name: Ensure working tree is clean + run: git diff --exit-code + + - name: Production build + run: npm run build + + # ── Vitest unit tests ──────────────────────────────────────────────────── + unit-tests: + name: Unit Tests + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - uses: actions/checkout@v5 + + - uses: actions/setup-node@v5 + with: + node-version: '22' + + - name: Install dependencies (frozen lockfile) + run: npm ci --force + + - name: Execute test suite + run: npm run test:frontend diff --git a/.github/workflows/integration-test.disabled b/.github/workflows/integration-test.disabled deleted file mode 100644 index b248df4b5d..0000000000 --- a/.github/workflows/integration-test.disabled +++ /dev/null @@ -1,255 +0,0 @@ -name: Integration Test - -on: - push: - branches: - - main - - dev - pull_request: - branches: - - main - - dev - -jobs: - cypress-run: - name: Run Cypress Integration Tests - runs-on: ubuntu-latest - steps: - - name: Maximize build space - uses: AdityaGarg8/remove-unwanted-software@v4.1 - with: - remove-android: 'true' - remove-haskell: 'true' - remove-codeql: 'true' - - - name: Checkout Repository - uses: actions/checkout@v4 - - - name: Build and run Compose Stack - run: | - docker compose \ - --file docker-compose.yaml \ - --file docker-compose.api.yaml \ - --file docker-compose.a1111-test.yaml \ - up --detach --build - - - name: Delete Docker build cache - run: | - docker builder prune --all --force - - - name: Wait for Ollama to be up - timeout-minutes: 5 - run: | - until curl --output /dev/null --silent --fail http://localhost:11434; do - printf '.' - sleep 1 - done - echo "Service is up!" - - - name: Preload Ollama model - run: | - docker exec ollama ollama pull qwen:0.5b-chat-v1.5-q2_K - - - name: Cypress run - uses: cypress-io/github-action@v6 - env: - LIBGL_ALWAYS_SOFTWARE: 1 - with: - browser: chrome - wait-on: 'http://localhost:3000' - config: baseUrl=http://localhost:3000 - - - uses: actions/upload-artifact@v4 - if: always() - name: Upload Cypress videos - with: - name: cypress-videos - path: cypress/videos - if-no-files-found: ignore - - - name: Extract Compose logs - if: always() - run: | - docker compose logs > compose-logs.txt - - - uses: actions/upload-artifact@v4 - if: always() - name: Upload Compose logs - with: - name: compose-logs - path: compose-logs.txt - if-no-files-found: ignore - - # pytest: - # name: Run Backend Tests - # runs-on: ubuntu-latest - # steps: - # - uses: actions/checkout@v4 - - # - name: Set up Python - # uses: actions/setup-python@v5 - # with: - # python-version: ${{ matrix.python-version }} - - # - name: Install dependencies - # run: | - # python -m pip install --upgrade pip - # pip install -r backend/requirements.txt - - # - name: pytest run - # run: | - # ls -al - # cd backend - # PYTHONPATH=. pytest . -o log_cli=true -o log_cli_level=INFO - - migration_test: - name: Run Migration Tests - runs-on: ubuntu-latest - services: - postgres: - image: postgres - env: - POSTGRES_PASSWORD: postgres - options: >- - --health-cmd pg_isready - --health-interval 10s - --health-timeout 5s - --health-retries 5 - ports: - - 5432:5432 - # mysql: - # image: mysql - # env: - # MYSQL_ROOT_PASSWORD: mysql - # MYSQL_DATABASE: mysql - # options: >- - # --health-cmd "mysqladmin ping -h localhost" - # --health-interval 10s - # --health-timeout 5s - # --health-retries 5 - # ports: - # - 3306:3306 - steps: - - name: Checkout Repository - uses: actions/checkout@v4 - - - name: Set up Python - uses: actions/setup-python@v5 - with: - python-version: ${{ matrix.python-version }} - - - name: Set up uv - uses: yezz123/setup-uv@v4 - with: - uv-venv: venv - - - name: Activate virtualenv - run: | - . venv/bin/activate - echo PATH=$PATH >> $GITHUB_ENV - - - name: Install dependencies - run: | - uv pip install -r backend/requirements.txt - - - name: Test backend with SQLite - id: sqlite - env: - WEBUI_SECRET_KEY: secret-key - GLOBAL_LOG_LEVEL: debug - run: | - cd backend - uvicorn open_webui.main:app --port "8080" --forwarded-allow-ips '*' & - UVICORN_PID=$! - # Wait up to 40 seconds for the server to start - for i in {1..40}; do - curl -s http://localhost:8080/api/config > /dev/null && break - sleep 1 - if [ $i -eq 40 ]; then - echo "Server failed to start" - kill -9 $UVICORN_PID - exit 1 - fi - done - # Check that the server is still running after 5 seconds - sleep 5 - if ! kill -0 $UVICORN_PID; then - echo "Server has stopped" - exit 1 - fi - - - name: Test backend with Postgres - if: success() || steps.sqlite.conclusion == 'failure' - env: - WEBUI_SECRET_KEY: secret-key - GLOBAL_LOG_LEVEL: debug - DATABASE_URL: postgresql://postgres:postgres@localhost:5432/postgres - DATABASE_POOL_SIZE: 10 - DATABASE_POOL_MAX_OVERFLOW: 10 - DATABASE_POOL_TIMEOUT: 30 - run: | - cd backend - uvicorn open_webui.main:app --port "8081" --forwarded-allow-ips '*' & - UVICORN_PID=$! - # Wait up to 20 seconds for the server to start - for i in {1..20}; do - curl -s http://localhost:8081/api/config > /dev/null && break - sleep 1 - if [ $i -eq 20 ]; then - echo "Server failed to start" - kill -9 $UVICORN_PID - exit 1 - fi - done - # Check that the server is still running after 5 seconds - sleep 5 - if ! kill -0 $UVICORN_PID; then - echo "Server has stopped" - exit 1 - fi - - # Check that service will reconnect to postgres when connection will be closed - status_code=$(curl --write-out %{http_code} -s --output /dev/null http://localhost:8081/health/db) - if [[ "$status_code" -ne 200 ]] ; then - echo "Server has failed before postgres reconnect check" - exit 1 - fi - - echo "Terminating all connections to postgres..." - python -c "import os, psycopg2 as pg2; \ - conn = pg2.connect(dsn=os.environ['DATABASE_URL'].replace('+pool', '')); \ - cur = conn.cursor(); \ - cur.execute('SELECT pg_terminate_backend(psa.pid) FROM pg_stat_activity psa WHERE datname = current_database() AND pid <> pg_backend_pid();')" - - status_code=$(curl --write-out %{http_code} -s --output /dev/null http://localhost:8081/health/db) - if [[ "$status_code" -ne 200 ]] ; then - echo "Server has not reconnected to postgres after connection was closed: returned status $status_code" - exit 1 - fi - -# - name: Test backend with MySQL -# if: success() || steps.sqlite.conclusion == 'failure' || steps.postgres.conclusion == 'failure' -# env: -# WEBUI_SECRET_KEY: secret-key -# GLOBAL_LOG_LEVEL: debug -# DATABASE_URL: mysql://root:mysql@localhost:3306/mysql -# run: | -# cd backend -# uvicorn open_webui.main:app --port "8083" --forwarded-allow-ips '*' & -# UVICORN_PID=$! -# # Wait up to 20 seconds for the server to start -# for i in {1..20}; do -# curl -s http://localhost:8083/api/config > /dev/null && break -# sleep 1 -# if [ $i -eq 20 ]; then -# echo "Server failed to start" -# kill -9 $UVICORN_PID -# exit 1 -# fi -# done -# # Check that the server is still running after 5 seconds -# sleep 5 -# if ! kill -0 $UVICORN_PID; then -# echo "Server has stopped" -# exit 1 -# fi diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000000..8654c2f7db --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,69 @@ +# ───────────────────────────────────────────────────────────────────────────── +# Release — Create GitHub release from CHANGELOG, trigger Docker builds +# Runs on pushes to main when package.json version changes +# ───────────────────────────────────────────────────────────────────────────── +name: Release + +on: + push: + branches: [main] + +concurrency: + group: release-${{ github.ref }} + cancel-in-progress: false + +jobs: + # ── Create release and trigger downstream workflows ────────────────────── + publish: + runs-on: ubuntu-latest + timeout-minutes: 10 + permissions: + contents: write + steps: + - uses: actions/checkout@v5 + + - name: Abort if package.json unchanged + run: | + git diff --cached --diff-filter=d package.json || { + echo "package.json not modified — skipping release" + exit 1 + } + + - name: Read version + id: pkg + run: echo "version=$(jq -r '.version' package.json)" >> $GITHUB_OUTPUT + + - name: Extract release notes from CHANGELOG + run: | + VER="${{ steps.pkg.outputs.version }}" + awk "/^## \[${VER}\]/{found=1; next} /^## \[/{if(found) exit} found{print}" \ + CHANGELOG.md > /tmp/release-notes.md + + - name: Publish GitHub release + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + gh release create "v${{ steps.pkg.outputs.version }}" \ + --title "v${{ steps.pkg.outputs.version }}" \ + --notes-file /tmp/release-notes.md + + - name: Archive source + uses: actions/upload-artifact@v4 + with: + name: release-archive + path: | + . + !.git + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Dispatch Docker build + uses: actions/github-script@v8 + with: + script: | + github.rest.actions.createWorkflowDispatch({ + owner: context.repo.owner, + repo: context.repo.repo, + workflow_id: 'docker.yaml', + ref: 'v${{ steps.pkg.outputs.version }}', + }) diff --git a/CHANGELOG.md b/CHANGELOG.md index 95d97dcea0..310d62dc61 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,140 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.9.6] - 2026-06-01 + +### Added + +- 📦 **Official knowledge base sync tool.** A new companion tool from Open WebUI, oikb, keeps a knowledge base in sync with a local directory, GitHub repo, S3 bucket, Confluence space, or any of more than 40 other sources, uploading only new and changed files using the incremental sync support added in this release. [oikb](https://github.com/open-webui/oikb) +- 📂 **Smart directory sync for knowledge bases.** Local directories can now be synced into a knowledge base in one action: file checksums are compared against what's already stored, and only added or modified files are uploaded while removed files and orphaned subdirectories are cleaned up, with the directory structure mirrored automatically and per-file progress shown throughout. [#19190](https://github.com/open-webui/open-webui/issues/19190), [#19394](https://github.com/open-webui/open-webui/issues/19394), [Commit](https://github.com/open-webui/open-webui/commit/60c9db1cb81d021589cb49bee8744a799b51211f), [Commit](https://github.com/open-webui/open-webui/commit/73bdf86766d3f44467cdec436786fe089481baf3), [Commit](https://github.com/open-webui/open-webui/commit/9835b3f1dd7aa9c92ff08a634a0f97cc2a046e42), [Commit](https://github.com/open-webui/open-webui/commit/97252fa609440573251d8e75090f347ed1e51e1d), [Commit](https://github.com/open-webui/open-webui/commit/8f2d346e10c47b57bf6b5a6aa02d453488a88b89), [Commit](https://github.com/open-webui/open-webui/commit/1527eb6e01d441225c979d12d78b7625edf1086f) +- 🗂️ **Knowledge base folders.** Files inside a knowledge base can now be organized into nested folders, with breadcrumb navigation that makes it much easier to manage and find content in large collections. [Commit](https://github.com/open-webui/open-webui/commit/c2cbc47ca76ebfa21e1d36279dbd859283cbbfb1), [Commit](https://github.com/open-webui/open-webui/commit/ab0ee858b73738c5b77d1d455e66e66bc61df1c4), [Commit](https://github.com/open-webui/open-webui/commit/2ad327a4dce64a8eedf5eb41c73b8520344fdd82), [Commit](https://github.com/open-webui/open-webui/commit/171150c1e12b5713f77a5ea0dd19c6581c7bef2e), [Commit](https://github.com/open-webui/open-webui/commit/e7d2ddbb1d14c03a52797751d24a98132aac0cd8), [Commit](https://github.com/open-webui/open-webui/commit/32a417bbf66ed28a5ca8a759ad3a87b2a2aa358a) +- 🧰 **Filesystem tool for knowledge bases.** A new built-in tool, enabled via the "ENABLE_KB_EXEC" environment variable, lets AI models browse and search knowledge base contents using familiar filesystem commands such as 'ls', 'cat', 'grep', 'find', 'head', 'tail', and 'sed', including pipes between them. [Commit](https://github.com/open-webui/open-webui/commit/5b125c24d4eae925d3287efa626595bab29d5c33), [Commit](https://github.com/open-webui/open-webui/commit/ecec86dd32aa396dd5a383e9be5b0f16c4346c48), [Commit](https://github.com/open-webui/open-webui/commit/9ef579ce4be5b82f9281b085a621fed5b04e0d26), [Commit](https://github.com/open-webui/open-webui/commit/2f642754ac6b8c430228a7fc289357a6c6652235), [Commit](https://github.com/open-webui/open-webui/commit/3b00e5721a60518ea317c4fd61a6d0d182961a2f), [Commit](https://github.com/open-webui/open-webui/commit/4e78b355efe0611f27fddef591c69f7c1259a9f6), [Commit](https://github.com/open-webui/open-webui/commit/74f95a9b0d6b89b241701422170d54e21e701baa), [Commit](https://github.com/open-webui/open-webui/commit/cc16e06c32de48bdf464db5814483a49f3527b63), [Commit](https://github.com/open-webui/open-webui/commit/1ea54c3217f487990b6a4c0f0d8ea675a479b6e1) +- ✏️ **File renaming in knowledge bases.** Files inside a knowledge base can now be renamed directly from the workspace, with the new name reflected wherever the file is referenced. [Commit](https://github.com/open-webui/open-webui/commit/3127f1b46255626ea92eb0b598e45078287303f1) +- 😀 **Emoji picker in message input.** A new emoji button in the rich text formatting toolbar lets you browse and insert emojis directly into your messages. [#24704](https://github.com/open-webui/open-webui/pull/24704) +- 🪄 **Per-chat skills toggle.** Skills can now be turned on or off for a conversation directly from the chat Integrations menu, the same way tools and capabilities already work, instead of only through the model preset. [#25036](https://github.com/open-webui/open-webui/issues/25036), [#25037](https://github.com/open-webui/open-webui/pull/25037) +- 🔎 **Access preview for users and groups.** Administrators can now preview exactly which models, knowledge bases, and tools a given user or group can access, making it easier to audit and verify permission setups. [Commit](https://github.com/open-webui/open-webui/commit/9c14740ffb009d550dcbd5d6c599dac57053f112) +- 📄 **Configurable knowledge base file page size.** Administrators can now request a larger page size when listing a knowledge base's files through the API, reducing the number of requests needed to retrieve large collections instead of paging through fixed increments of 30. [#25148](https://github.com/open-webui/open-webui/issues/25148), [Commit](https://github.com/open-webui/open-webui/commit/a4d1b3e9378a61a11dd9822a8dbb525f39753081) +- 🔃 **Persistent processing indicator for knowledge files.** Files still being processed in a knowledge base now keep showing a processing indicator across page reloads, so you can tell what's still ingesting after navigating away and back. [#25031](https://github.com/open-webui/open-webui/issues/25031), [Commit](https://github.com/open-webui/open-webui/commit/ad9f2eeb15a448147d5e47e6a391cabb9aefd0ea) +- 📑 **MinerU file type configuration.** Administrators can now configure which file types are processed by the MinerU document loader, via the new "MINERU_FILE_EXTENSIONS" setting, extending it beyond PDF to formats like DOCX, PPTX, and XLSX. [Commit](https://github.com/open-webui/open-webui/commit/d4030a8aa5d48c2a1cb06c461566844aca2530ab) +- 📃 **Legacy Word document support.** Older ".doc" Word files can now have their text extracted by the default document extraction engine, in addition to the modern ".docx" format. [Commit](https://github.com/open-webui/open-webui/commit/9e3e24e304f8ff210494380b46f614a2984cafb3) +- 📁 **Create subfolders from the folder header.** Chat folders can now have subfolders created directly from the folder header in the chat view, not just from the sidebar. [Commit](https://github.com/open-webui/open-webui/commit/1f0948bcbef2af73b155535ea27762c522260afc) +- ⚡ **Faster initial page loads.** The configuration endpoint that loads on every page visit no longer runs an unnecessary user-count query, making the initial application load lighter on the database, especially on instances with many users. [Commit](https://github.com/open-webui/open-webui/commit/0adc090dcbe636c9c9645d8ec7f6b89ea514870b) +- 🚀 **Faster tool-enabled chat completions.** Chat completions that use multiple tools now start faster because the tools they reference are fetched from the database in a single batch query instead of one query per tool. [#24808](https://github.com/open-webui/open-webui/pull/24808), [Commit](https://github.com/open-webui/open-webui/commit/cc94a90b4d4d690bc7cb9f7124f2d6e552973970) +- 🏎️ **More responsive web search under load.** Web search through SearXNG, Google PSE, Brave, Serper, and Serpstack now uses non-blocking network calls, so the server stays responsive to other users while a search is in flight, and concurrent multi-query searches complete faster. [Commit](https://github.com/open-webui/open-webui/commit/b94245d2ee191e8ef118bf9de1ff7539503bfec9) +- 🐎 **Lighter Ollama backend connections.** Requests to Ollama backends now reuse a shared connection pool instead of opening a fresh session each time, reducing TCP and TLS handshake overhead for installs that poll Ollama frequently or have multiple backends configured. [Commit](https://github.com/open-webui/open-webui/commit/5d9a09a88a9094ebfcd249340be3aaee544b34d0) +- 💽 **Fewer redundant model-list writes.** On multi-instance deployments backed by Redis, the model list is no longer rewritten when it hasn't changed, cutting a major source of redundant writes. [#25469](https://github.com/open-webui/open-webui/issues/25469), [#25474](https://github.com/open-webui/open-webui/pull/25474), [Commit](https://github.com/open-webui/open-webui/commit/fd76b51ab2ad4c4192f2c98153e47888712c2009) +- 📉 **Faster websocket disconnect cleanup.** Disconnecting from a collaborative session no longer triggers a scan across the entire Redis keyspace, using a per-session index instead, which keeps disconnects cheap on large deployments. [#25466](https://github.com/open-webui/open-webui/issues/25466), [Commit](https://github.com/open-webui/open-webui/commit/c7de057a4a54cc80f366ad949417e78fae756d4c) +- 📝 **Frontmatter auto-fill for tools, functions, and skills.** Opening a tool, function, or skill editor now auto-fills the name, id, and description fields from the file's frontmatter, saving you from re-entering metadata already declared in the source. [#24649](https://github.com/open-webui/open-webui/pull/24649), [Commit](https://github.com/open-webui/open-webui/commit/ef975649b26d3e7cd589c49be2fc77cee80fad8f) +- 🪪 **More user placeholders in custom headers.** Custom-header templates for direct connections and tool servers now support "{{USER_EMAIL}}" and "{{USER_ROLE}}" alongside the existing user and session placeholders. [Commit](https://github.com/open-webui/open-webui/commit/ed73ef3d8df988b0e9646b82df5b1a453202ef8d) +- ⏱️ **Configurable MCP connection timeout.** The timeout for the initial handshake with an MCP tool server is now configurable via the new "MCP_INITIALIZE_TIMEOUT" setting, so servers that are slow to start or expose many tools can finish connecting instead of timing out. [#25011](https://github.com/open-webui/open-webui/pull/25011), [Commit](https://github.com/open-webui/open-webui/commit/4297c02b121180e239a61c483ac8477cc557d4ef) +- 📐 **Profile image size limit.** Administrators can now cap the size of inline profile images via the new "PROFILE_IMAGE_MAX_DATA_URI_SIZE" setting, bounding how much database and cache space inline avatars and model icons can consume. [#25468](https://github.com/open-webui/open-webui/issues/25468), [#25476](https://github.com/open-webui/open-webui/pull/25476) +- 🎫 **Wildcard OAuth role mapping.** Administrators can now set "\*" in the allowed OAuth roles to grant the user role to any authenticated OAuth user, instead of having to enumerate every accepted role. [#25062](https://github.com/open-webui/open-webui/pull/25062), [Commit](https://github.com/open-webui/open-webui/commit/07cbc91a8eba3a9a3b39588b2ae5de916930af70) +- 📊 **Paginated feedback history.** The feedback and evaluation history list is now paginated, keeping it responsive for instances that have accumulated large numbers of feedback entries. [Commit](https://github.com/open-webui/open-webui/commit/160a6694e4bd66fc42e6361947516e8e15414ef3) +- 🔘 **Bulk enable or disable automations.** Automations can now be enabled or disabled in bulk from an actions menu on the automations page, instead of toggling each one individually. [Commit](https://github.com/open-webui/open-webui/commit/675e9bee5af8b9fb390fcb235a6c2c4766e78c75) +- ➡️ **Optional auto-redirect to single sign-on.** Administrators can now enable "OAUTH_AUTO_REDIRECT" so that, on deployments with a single sign-on provider and no other login methods, users are sent straight to the provider instead of seeing a login page first. [#25067](https://github.com/open-webui/open-webui/pull/25067), [Commit](https://github.com/open-webui/open-webui/commit/d64ef1803d2f5cedb7b3a308151d08ff2cd2b8e1) +- ☁️ **Azure AI Foundry v1 with Entra ID.** Open WebUI now supports Azure AI Foundry's OpenAI v1 endpoint together with Microsoft Entra ID authentication, so these connections work without manual workarounds. [#24761](https://github.com/open-webui/open-webui/issues/24761), [#24985](https://github.com/open-webui/open-webui/pull/24985), [Commit](https://github.com/open-webui/open-webui/commit/eb4eebc3ce1042cb0d393bf890c1895db6e08b19) +- 🌎 **Linkup web search provider.** Administrators can now select Linkup as the web search provider from the admin settings, with options to configure the API key and search depth. [#24752](https://github.com/open-webui/open-webui/pull/24752), [Commit](https://github.com/open-webui/open-webui/commit/56c0d00e13c74d665124ec9f1cae78e83ca60b6a) +- 🧊 **Valkey vector database support.** Valkey can now be used as the vector database backend, configurable through new "VALKEY_URL" and related settings including index type, distance metric, and HNSW tuning. [#24769](https://github.com/open-webui/open-webui/pull/24769), [Commit](https://github.com/open-webui/open-webui/commit/c0f1aa291938bbef62db8d4013dbc498b0abea15) +- 🔄 **General improvements.** Various improvements were implemented across the application to enhance performance, stability, and security. +- 🌐 **Translation updates.** Translations for Spanish (Spain), Swedish, German, Korean, Catalan, Russian, Irish, Simplified Chinese, Traditional Chinese, Finnish, Polish, Turkish, and Malay were enhanced and expanded. + +### Fixed + +- 🛡️ **Security Advisory**: This release includes security and access-control fixes. We recommend updating production deployments at your earliest convenience. Not all security fixes in this version may be enumerated in the fixed section — some may be withheld for a short time to give administrators time to upgrade. [Advisories](https://github.com/open-webui/open-webui/security) +- 🛡️ **Tool server permission enforcement.** The per-user permission for inline tool servers is now enforced on chat-completion requests, so users without that permission can no longer bypass the admin setting by supplying tool servers directly in their requests. [Commit](https://github.com/open-webui/open-webui/commit/5cc1eb517094e3507915664817285f1e6e37a16d) +- 🔒 **Knowledge base access check in search tool.** The built-in knowledge search tool now verifies that the caller can access a knowledge base before searching it by id, preventing users from reading the contents of knowledge bases they have not been granted access to. [#25113](https://github.com/open-webui/open-webui/pull/25113) +- 🗄️ **Cross-user access to retrieval collections.** Resolving the documents used for retrieval now verifies the caller's access to each referenced file and rejects client-supplied collection names, preventing a crafted request from pulling another user's files or vector collections into its context. [Commit](https://github.com/open-webui/open-webui/commit/ee47c9c833f8889f3abe99d2266c56e8f8d40230) +- 🔣 **Collection name validation.** Vector collection names are now rejected unless they contain only safe characters, preventing malformed names from reaching the vector store or breaking out of a database query expression. [#24982](https://github.com/open-webui/open-webui/pull/24982) +- 🚫 **Unscoped retrieval collections denied by default.** Retrieval requests for collection names that don't correspond to a known file, memory, web-search, or knowledge base are now denied for non-admins by default, with a new "ENABLE_RETRIEVAL_UNSCOPED_COLLECTIONS" setting to restore the previous behavior if needed. [Commit](https://github.com/open-webui/open-webui/commit/c93f071700520a60833b7f9616c292b11f210880) +- 📜 **Prompt history authorization.** Comparing, deleting, and restoring prompt versions now verify the history entry belongs to the prompt you're authorized for, preventing access to or modification of another prompt's version history. [#25056](https://github.com/open-webui/open-webui/pull/25056) +- 🚦 **Code interpreter permission on the legacy path.** The legacy code-execution path now enforces the same permission and capability checks as the current one, so users without the code interpreter permission can no longer trigger code execution through it. [#24724](https://github.com/open-webui/open-webui/pull/24724) +- 🧱 **API key endpoint restriction bypass.** The endpoint allow-list that limits which paths an API key may reach is now matched against the routed request path directly, preventing a crafted request from slipping past the restriction. [#25123](https://github.com/open-webui/open-webui/pull/25123) +- 🚧 **System prompt bypass via request parameter.** The flag that skips a model's configured system prompt can no longer be set by external clients through a request parameter, so admin-configured system prompts can't be bypassed from the API. [#25156](https://github.com/open-webui/open-webui/pull/25156) +- 🚪 **Terminal proxy path traversal.** The terminal proxy now fully decodes request paths before validating them, blocking multi-encoded payloads that could otherwise escape the intended path. [#25157](https://github.com/open-webui/open-webui/pull/25157) +- 🪤 **Cache file path traversal.** The cache file server now requires an exact directory boundary match, closing a gap where a sibling directory whose name began with the cache directory's name could be used to serve files from outside it. [#25086](https://github.com/open-webui/open-webui/pull/25086) +- 🔀 **Ollama backend selection access check.** Requests can no longer target an arbitrary Ollama backend by index; a caller-supplied backend selector is now verified against the backends that actually serve the requested model. [Commit](https://github.com/open-webui/open-webui/commit/7139797be04030b9d1016782f1dbb251c5fe68bb) +- 🔓 **Cross-user file exfiltration via image URLs.** When a chat message references a file by id in an "image_url" field, the server now resolves that file only for its owner, an administrator, or a user with an explicit read grant, preventing other authenticated users from extracting a file's contents by routing it through the model. [#24625](https://github.com/open-webui/open-webui/pull/24625), [Commit](https://github.com/open-webui/open-webui/commit/c75fe8e74b72617c51282cc3ea0a2e8d9cdd9140) +- 📌 **Chat file attachment access checks.** Attaching files to a chat now links only files the caller can read, preventing a user from associating another user's file with their chat to access its contents. [#25054](https://github.com/open-webui/open-webui/pull/25054) +- 🧾 **Model knowledge file ownership checks.** Creating or updating a model now verifies that any knowledge files attached to it are files the editor can access, preventing another user's files from being attached to a model. [#25055](https://github.com/open-webui/open-webui/pull/25055), [Commit](https://github.com/open-webui/open-webui/commit/27fb20c13a4bf8501f7a485abe0654eb5880980d) +- 📅 **Calendar event move authorization.** Updating a calendar event to move it into a different calendar now requires write access on the destination calendar, preventing users from injecting events into calendars they cannot write to. [#24764](https://github.com/open-webui/open-webui/pull/24764) +- 📣 **Channel chat access control.** Generating a response in a channel context now verifies the caller's access to that channel and scopes the included messages, preventing access to channels or messages the user isn't permitted to see. [#24725](https://github.com/open-webui/open-webui/pull/24725) +- 🕸️ **Web loader SSRF gating with Playwright.** When the Playwright-based web loader is in use, page navigations and redirects are now validated the same way as the default loader, closing a gap where the Playwright path could reach internal or otherwise blocked URLs. [#24756](https://github.com/open-webui/open-webui/pull/24756) +- 🛂 **DNS rebinding protection for URL fetches.** The IP address validated for an outbound URL fetch is now the same one used for the actual connection, closing a DNS rebinding window where an attacker-controlled hostname could resolve to a public IP during the safety check and then to a private IP when the connection was opened. [#24759](https://github.com/open-webui/open-webui/pull/24759) +- 🪞 **OAuth profile picture redirect handling.** The OAuth profile picture fetch now follows redirects only when administrators have explicitly allowed it, closing a window where a redirect from an externally validated URL could be used to reach internal addresses. [#24809](https://github.com/open-webui/open-webui/pull/24809) +- 🧼 **Model profile image script injection.** Model profile images are now validated on save and only served inline when they are a known-safe image type, preventing a crafted SVG profile image from running scripts in other users' browsers, while existing legacy images that fail validation are cleared gracefully instead of breaking the model list. [#25060](https://github.com/open-webui/open-webui/pull/25060), [#25173](https://github.com/open-webui/open-webui/pull/25173) +- 🧯 **Diagram rendering script injection.** Mermaid diagrams rendered in chat are now sanitized before display, preventing a crafted diagram from running scripts in the viewer's browser. [#25219](https://github.com/open-webui/open-webui/pull/25219) +- 🔐 **Shared-chat file write protection.** Access to a file through a shared chat now only grants read access, so users who can read a shared chat can no longer modify or delete files attached to it. [#24755](https://github.com/open-webui/open-webui/pull/24755) +- 🔏 **Cross-origin embed prompt control.** When Open WebUI is embedded in an iframe on a different origin, the embedding page can now only drive the chat input or submit prompts if the user has explicitly opted in via the "iframe Sandbox Allow Same Origin" setting, preventing untrusted host pages from triggering confirmation dialogs or controlling the chat. [#24767](https://github.com/open-webui/open-webui/pull/24767), [Commit](https://github.com/open-webui/open-webui/commit/eb3076c1b02d90c2ce6e6d3beb08a37987c740ec) +- 🗂️ **Chat folder ownership checks.** Creating a chat or updating a chat's folder now verifies the referenced folder belongs to the current user, preventing chats from being associated with folders owned by other people. [#24588](https://github.com/open-webui/open-webui/pull/24588) +- 🧩 **Chat recovery from corrupted history.** Chats whose internal message graph was left in a malformed state by a failed regeneration now open and load correctly, with missing roles, parent references, and current-message pointers reconstructed automatically instead of breaking the chat. [#24424](https://github.com/open-webui/open-webui/issues/24424), [#24157](https://github.com/open-webui/open-webui/issues/24157), [#20474](https://github.com/open-webui/open-webui/issues/20474), [#24799](https://github.com/open-webui/open-webui/pull/24799), [Commit](https://github.com/open-webui/open-webui/commit/d310a0777c4c48ec772bdc9a510005d5e91b09c7) +- 📨 **Imported chats with folders appear correctly.** Importing grouped chats no longer leaves them invisible when a referenced folder is missing; such chats now appear in the chat list instead of being silently orphaned. [#24910](https://github.com/open-webui/open-webui/issues/24910), [Commit](https://github.com/open-webui/open-webui/commit/7f7cd210186cb6a67e028c17974eb210fc7ba9fd) +- 🎟️ **MCP tool server sessions stay connected.** OAuth-authenticated MCP tool server sessions are no longer mistakenly refreshed and deleted by the single sign-on session handler, so those connections stay active. [#24618](https://github.com/open-webui/open-webui/issues/24618), [Commit](https://github.com/open-webui/open-webui/commit/c8eb8edca4174ec68fabd071d6b08c0bc07f8117) +- 🤝 **MCP OAuth scope discovery.** The OAuth flow for MCP tool servers now reads the scopes a server advertises through its Protected Resource Metadata, so connecting to servers that declare their own scopes succeeds. [#24730](https://github.com/open-webui/open-webui/issues/24730), [#24690](https://github.com/open-webui/open-webui/pull/24690) +- 🔍 **Web search reliability.** Web search again fetches page content reliably with the default web loader engine, a new "USER_AGENT" environment variable lets administrators set a real browser user-agent so fetches aren't blocked by Cloudflare, Wikipedia, and other bot-detection systems, and the startup script no longer fails to launch when these new environment variables are unset. [#24560](https://github.com/open-webui/open-webui/issues/24560), [#24793](https://github.com/open-webui/open-webui/issues/24793), [#24683](https://github.com/open-webui/open-webui/pull/24683), [Commit](https://github.com/open-webui/open-webui/commit/f60733758272c4532cd032e518c9cd73f648043a) +- 🔥 **Firecrawl web search results.** Web search using Firecrawl now returns results correctly regardless of which response format the Firecrawl version uses. [#24712](https://github.com/open-webui/open-webui/pull/24712) +- 🦅 **Kagi web search.** Web search using Kagi works again after its API endpoint and request method were updated to match Kagi's current API. [#25015](https://github.com/open-webui/open-webui/pull/25015) +- 🔢 **Bracketed numbers in code blocks.** Numbers in square brackets such as "[0]" inside code blocks are no longer stripped out as if they were source citations, so code displays and copies correctly. [#24948](https://github.com/open-webui/open-webui/issues/24948), [Commit](https://github.com/open-webui/open-webui/commit/e90a618f4555cea2c024bb60c1332ca04eed96da) +- 🔌 **API chat completions reliability.** Direct calls to the chat completions API no longer fail with an internal error when no chat session identifier is supplied. [#24553](https://github.com/open-webui/open-webui/issues/24553), [#25235](https://github.com/open-webui/open-webui/issues/25235), [Commit](https://github.com/open-webui/open-webui/commit/bc244fdc90504824b76654880898bf3f6649c299), [Commit](https://github.com/open-webui/open-webui/commit/f16b5c446027eae1bd767617bac2fdf54b24d6fc) +- 🖼️ **ComfyUI image generation and editing.** Generating and editing images via a ComfyUI backend now works again, including when ComfyUI is hosted on a private or internal network where URL validation was previously blocking the admin-configured endpoint. [#24565](https://github.com/open-webui/open-webui/issues/24565), [Commit](https://github.com/open-webui/open-webui/commit/7dcd932ad7cb5feab007737c6e0281def5cd47fc), [Commit](https://github.com/open-webui/open-webui/commit/8aa2a42dc7887512e697ff85d044220945ced40f) +- 🖌️ **Image generation with non-standard response headers.** Image generation now works with backends that return valid JSON without a standard content-type header, instead of rejecting the response. [#24838](https://github.com/open-webui/open-webui/pull/24838) +- 🐘 **Knowledge search on large documents.** Searching knowledge bases on PostgreSQL no longer fails when scanning across documents with very large extracted text content. [#24670](https://github.com/open-webui/open-webui/issues/24670), [Commit](https://github.com/open-webui/open-webui/commit/d74ee34d9128295faa116c919bd1dcca77744975) +- 💬 **Chat title generation.** Automatically generated chat titles now use the model currently selected in the dropdown for the active chat and fall back to the model from the active message branch otherwise, and a clear message is shown if no model is available instead of an unhelpful error. [#24604](https://github.com/open-webui/open-webui/issues/24604), [#24745](https://github.com/open-webui/open-webui/issues/24745), [Commit](https://github.com/open-webui/open-webui/commit/e5c8f8110a88739e011d8ab23e5cb11b4768469f), [Commit](https://github.com/open-webui/open-webui/commit/3c5e7968f0b5130890e17f1f2a2a52297cb145a6) +- 🧮 **Message search and analytics consistency.** Edits, deletions, and branch changes made in a chat are now reflected in message search results and analytics counts instead of leaving stale entries behind. [#25205](https://github.com/open-webui/open-webui/pull/25205), [Commit](https://github.com/open-webui/open-webui/commit/aa06200f789a4c3fc54ea9a141cd347d76cda4c5) +- 🩹 **Graceful handling of in-chat task failures.** When web search query generation, image prompt generation, or a tool call fails or references a missing tool, the chat now falls back or surfaces a clear error instead of breaking partway through the response. [#25038](https://github.com/open-webui/open-webui/issues/25038), [#25144](https://github.com/open-webui/open-webui/issues/25144), [Commit](https://github.com/open-webui/open-webui/commit/b64fd988f02b8a4295193892b24b00bdf635a4dc) +- 🎛️ **Filter changes to message output.** Filter functions that modify a message's structured output after generation now have those changes saved and displayed, instead of being discarded when only the output, not the text content, was changed. [#24884](https://github.com/open-webui/open-webui/pull/24884) +- ⏩ **Titles and tags reflect filtered output.** Outlet filters now run before automatic title, tag, and follow-up generation, so those are based on the final filtered message instead of the unfiltered version. [#24717](https://github.com/open-webui/open-webui/pull/24717) +- 💾 **Action-replaced message content persists.** Message content replaced by an action function through its event emitter is now kept when the chat is saved, instead of reverting to the original after a page reload. [#24585](https://github.com/open-webui/open-webui/issues/24585), [#25485](https://github.com/open-webui/open-webui/pull/25485) +- 🏷️ **Skill mentions in messages.** Mentioning a skill in a message now keeps the skill's name as readable text instead of removing it, and selecting a skill without typing anything no longer causes an error on providers that reject empty messages. [#24929](https://github.com/open-webui/open-webui/issues/24929), [Commit](https://github.com/open-webui/open-webui/commit/01810e32ad51305ca3247e8f83da95f6e6260f0c) +- 🧹 **Usage timer cleanup on send failure.** The background usage-stats timer started during message generation is now always cleared, even when sending a message fails, preventing leaked timers from accumulating over a session. [#25478](https://github.com/open-webui/open-webui/pull/25478) +- 🗑️ **Background tasks stop when a chat is removed.** Deleting or archiving a chat now cancels any in-flight generation or title and tag tasks for it, instead of leaving orphaned background work running. [#25050](https://github.com/open-webui/open-webui/pull/25050), [Commit](https://github.com/open-webui/open-webui/commit/778dba1d6b8dc3962163fa7bf9d802d9a07fba26) +- ⌨️ **Responsive knowledge file search.** Searching for knowledge files in the chat picker and model knowledge selector now matches on file names by default instead of scanning the full extracted text of every document on each keystroke, keeping the search responsive on large deployments, with content search available as an explicit opt-in. [#25082](https://github.com/open-webui/open-webui/issues/25082), [#25119](https://github.com/open-webui/open-webui/pull/25119), [Commit](https://github.com/open-webui/open-webui/commit/591e0aafa1d5e21dcd9fa1279dada2076968b9cb) +- 📥 **Document processing with empty embeddings.** Saving documents to the vector database no longer crashes when an embedding step returns no vectors, allowing the process to continue instead of failing the whole upload. [#25166](https://github.com/open-webui/open-webui/issues/25166) +- 🔤 **Non-UTF-8 text and CSV uploads.** Text and CSV files saved in legacy encodings, including Latin-1, Windows-1252, and Chinese encodings such as GB18030, are now detected and loaded correctly instead of being rejected as binary or failing with an empty-content error. [#25172](https://github.com/open-webui/open-webui/issues/25172), [#24973](https://github.com/open-webui/open-webui/issues/24973), [Commit](https://github.com/open-webui/open-webui/commit/6f0277db52d005420480abb0702d421525d6ea8b), [Commit](https://github.com/open-webui/open-webui/commit/1bbb2b933d4cee70a5102eca4109e77b8625f8fc) +- 🧽 **Null bytes in nested data no longer break saves.** Data containing null bytes nested inside structured fields is now sanitized correctly before being written, preventing database errors that the previous check failed to catch. [#25018](https://github.com/open-webui/open-webui/pull/25018), [Commit](https://github.com/open-webui/open-webui/commit/e3ab4bd212e44c39f439ec4ff5df7c3dbd046895) +- 🧠 **Clear error when no embedding model is configured.** Using knowledge or retrieval features without a loaded embedding model now returns a clear setup error explaining what to configure, instead of failing with a cryptic crash. [Commit](https://github.com/open-webui/open-webui/commit/55ca719bbf76306ed647424c728f730d0bef21f9) +- 🧲 **Memory search quality.** Memory searches now apply the configured embedding query prefix, so retrieval works correctly with embedding models that require one for queries. [#24921](https://github.com/open-webui/open-webui/pull/24921), [Commit](https://github.com/open-webui/open-webui/commit/ce4dca47cb19a6582fd8a550806c89ab297c038d) +- 📚 **Knowledge tool context overflow.** The built-in tool that lists a model's knowledge no longer dumps every file in every knowledge base into the model's context; it now returns summaries by default and paginates file listings only for a requested knowledge base. [#25105](https://github.com/open-webui/open-webui/pull/25105), [Commit](https://github.com/open-webui/open-webui/commit/0e73f7af099e1f9437c55a5c2d16741c97dbd57f) +- ⏳ **Terminal session stability.** The terminal proxy no longer hangs when one direction of the connection closes before the other, so terminal sessions shut down cleanly instead of stalling. [#25464](https://github.com/open-webui/open-webui/issues/25464), [#25479](https://github.com/open-webui/open-webui/pull/25479) +- 🧷 **Tool call continuity with strict providers.** Chats that contain incomplete tool calls or orphaned tool results no longer fail to continue when sent to providers that strictly validate tool pairings, such as Anthropic and AWS Bedrock Converse. [#24758](https://github.com/open-webui/open-webui/issues/24758), [#24940](https://github.com/open-webui/open-webui/issues/24940), [#24798](https://github.com/open-webui/open-webui/pull/24798), [Commit](https://github.com/open-webui/open-webui/commit/cfa6908d579e1f7f202a321289029996130b8411) +- 🛑 **Stream termination for pipe functions.** Streamed responses from pipe functions now always send the standard end-of-stream marker, so chat clients and external integrations reliably detect when a response is complete instead of waiting on streams that already finished. [#24763](https://github.com/open-webui/open-webui/pull/24763) +- 🔊 **Non-blocking text-to-speech transcoding.** Converting text-to-speech audio to MP3 no longer blocks the server's event loop, so other requests stay responsive even while a TTS response is being transcoded. [#24876](https://github.com/open-webui/open-webui/pull/24876) +- 🎚️ **Default text-to-speech voice.** Text-to-speech requests now honor the voice specified in the request and fall back to the configured default only when none is given, instead of always using the admin default or failing. [#15143](https://github.com/open-webui/open-webui/issues/15143), [#25035](https://github.com/open-webui/open-webui/issues/25035), [Commit](https://github.com/open-webui/open-webui/commit/f16b5c446027eae1bd767617bac2fdf54b24d6fc), [Commit](https://github.com/open-webui/open-webui/commit/750604a11d4adcb5ae568b9fc93010031ad394f9) +- 🪝 **Reliable knowledge base file linking.** Files uploaded to a knowledge collection are now linked on the server as part of the upload itself, so they remain attached to the collection even if you navigate away or close the page before processing finishes. [#24807](https://github.com/open-webui/open-webui/issues/24807), [Commit](https://github.com/open-webui/open-webui/commit/d0b17f056911ec73df2b686a0d92bb789391db27) +- ☁️ **Azure connections on custom hostnames.** Connections marked as the Azure provider now use the Azure code path even when the endpoint does not contain "azure" in its hostname, fixing custom Azure deployments served from non-standard domains. [#24882](https://github.com/open-webui/open-webui/pull/24882), [Commit](https://github.com/open-webui/open-webui/commit/c8f851bd2de3d127f1e818fc116c27174f96d3f1) +- 🗓️ **Clearing calendar event fields.** Removing the description or location from a calendar event now saves correctly instead of silently keeping the previous value. [#25026](https://github.com/open-webui/open-webui/issues/25026), [Commit](https://github.com/open-webui/open-webui/commit/91810f1c4e93d4f559bdf61cd085cadc288a732d), [Commit](https://github.com/open-webui/open-webui/commit/78b1637a035d71099262412e5dee3e4d65c7fb2f) +- 💭 **Advanced parameter settings.** Custom reasoning tags and custom model parameters are now saved correctly instead of being dropped, and the presence penalty and repeat penalty no longer save the frequency penalty's value instead of their own. [#25183](https://github.com/open-webui/open-webui/pull/25183), [#25200](https://github.com/open-webui/open-webui/pull/25200), [#25204](https://github.com/open-webui/open-webui/pull/25204) +- 📏 **Long username display.** Long usernames no longer overflow their containers in the admin user list, user modals, and sidebar. [#25185](https://github.com/open-webui/open-webui/pull/25185) +- 🎯 **All skills selectable in the model editor.** The model editor's skills selector now lists every skill you have access to, with a search box for large lists, instead of showing only the first 30 with no way to reach the rest. [#24873](https://github.com/open-webui/open-webui/issues/24873), [Commit](https://github.com/open-webui/open-webui/commit/936d5f2676dfbd2ba763b30af33a8557dcda9b05) +- 🔔 **Accurate knowledge upload feedback.** Dragging files into a knowledge base no longer shows an upload notification before the upload has actually been processed. [#25484](https://github.com/open-webui/open-webui/pull/25484) +- ♿ **High-contrast timestamp readability.** The user message timestamp now uses the correct colors in high-contrast mode instead of inverted ones, keeping it readable. [#25461](https://github.com/open-webui/open-webui/pull/25461) +- ♿ **Keyboard and screen reader access to menus.** The integrations, more-options, and user menus are now real buttons with labels and keyboard support, so they can be opened with the keyboard and announced by screen readers. [Commit](https://github.com/open-webui/open-webui/commit/346dab3d8f909fc321a49ea2be633ea5c4c4a349) +- 🖱️ **Focus-loss handling in editors.** Workspace and admin editors for models, tools, functions, and skills again respond correctly when the browser window loses focus, after the wrong event name was being listened for. [#25459](https://github.com/open-webui/open-webui/pull/25459) +- 🛟 **Resilience to corrupted local storage.** Corrupted data in the browser's local storage no longer crashes the interface; affected settings and dismissed-banner state now fall back to safe defaults. [#25481](https://github.com/open-webui/open-webui/pull/25481) +- 📶 **Quieter reconnection notifications.** Brief connection interruptions, such as backgrounding a mobile tab, no longer flash a "connection lost" warning, and the "reconnected" message only appears if a disconnect was actually shown. [Commit](https://github.com/open-webui/open-webui/commit/77c8c54b1ea07189e21313f9cd401f3b956bb93a) +- 🍎 **Safari PDF handling.** PDF processing now works in Safari, which doesn't support the stream iteration the previous code relied on. [#25151](https://github.com/open-webui/open-webui/issues/25151), [#25473](https://github.com/open-webui/open-webui/pull/25473) +- 🎙️ **Voice mode mute shortcut listing.** The keyboard shortcut for muting voice mode now appears in the keyboard shortcuts help modal. [#25193](https://github.com/open-webui/open-webui/pull/25193) +- 📎 **Document attachments in channel model replies.** Tagging a model in a channel thread now forwards uploaded non-image documents such as PDFs and DOCX files into the model's context, so document summarization and comparison workflows that already worked in direct chat now work in channels too. [#24896](https://github.com/open-webui/open-webui/issues/24896), [#24898](https://github.com/open-webui/open-webui/pull/24898), [Commit](https://github.com/open-webui/open-webui/commit/7e9d41d664d7065a92ac93606d18e88c45eb36b6) +- 🙈 **Hidden models in channel mentions.** Models marked as hidden no longer appear in the channel message-input model mention selector, matching how hidden models are excluded elsewhere in the interface. [#24892](https://github.com/open-webui/open-webui/pull/24892) +- 🧵 **Channel thread and pinned message stability.** Opening a channel thread or the pinned messages view no longer fails to render when a message or its data is missing. [#25209](https://github.com/open-webui/open-webui/pull/25209) +- 📺 **YouTube short link transcripts.** Pasting a "youtu.be" short link into a chat now loads the video transcript correctly instead of failing with an empty-content error. [#24856](https://github.com/open-webui/open-webui/issues/24856), [Commit](https://github.com/open-webui/open-webui/commit/1e36a206008c3abce5ccdc98d5750e30a7345b98) +- 🙉 **Hidden models in default-model and automation pickers.** The admin pickers for default models and default pinned models, and the automation model dropdown, now filter out hidden models, consistent with how hidden models are treated elsewhere. [#24869](https://github.com/open-webui/open-webui/issues/24869), [Commit](https://github.com/open-webui/open-webui/commit/1fa3050f069a72de1daaaa29233b545c4deaea51), [Commit](https://github.com/open-webui/open-webui/commit/4705c2d98812c1189c2ae4960399bc8888d4861c) +- 🔊 **Speech-to-text SSL setting honored.** Speech-to-text requests now respect the "AIOHTTP_CLIENT_SESSION_SSL" setting, so administrators using self-signed certificates or custom SSL configurations can use STT engines that were previously failing TLS verification. [#24568](https://github.com/open-webui/open-webui/issues/24568), [#24857](https://github.com/open-webui/open-webui/pull/24857), [Commit](https://github.com/open-webui/open-webui/commit/2ca91ceeeca6a6a21e5a6ad68ea3ab8c9d9f6deb), [Commit](https://github.com/open-webui/open-webui/commit/94b66b17972e0ad77954df4b81a6bb86a7a6a04b) +- 🔗 **Placeholders in MCP connection headers.** Custom header templates configured on MCP server connections now have their "{{USER_ID}}", "{{USER_NAME}}", "{{USER_EMAIL}}", "{{USER_ROLE}}", "{{CHAT_ID}}", and "{{MESSAGE_ID}}" placeholders interpolated at request time, matching how custom headers already work for direct connections and tool servers. [#24822](https://github.com/open-webui/open-webui/pull/24822) +- 🪟 **Bing search CLI smoke test.** Running the Bing web-search module from the command line for a quick connectivity check no longer raises an error about missing arguments. [#24765](https://github.com/open-webui/open-webui/issues/24765), [#24768](https://github.com/open-webui/open-webui/pull/24768) +- 🩺 **Database health check recovery.** After a transient database connection error, the health check endpoint now recovers automatically instead of staying permanently broken on the affected worker. [Commit](https://github.com/open-webui/open-webui/commit/0b81520e072bb4ed15532b8e153b43dd3243feaf) +- 🥾 **Startup on non-Unicode consoles.** Open WebUI no longer crashes at startup when the console can't encode the banner's box-drawing characters, such as on Windows or with redirected or headless output, falling back to a plain-text banner instead. [#24965](https://github.com/open-webui/open-webui/issues/24965), [#25482](https://github.com/open-webui/open-webui/pull/25482) +- 🆕 **First admin signup after a reset.** Creating the first administrator account is no longer blocked by a previously stored signup setting, so a fresh or reset instance can always be bootstrapped. [#24821](https://github.com/open-webui/open-webui/pull/24821) +- 🪵 **JSON exception logging.** With JSON log formatting enabled, exceptions are now recorded correctly with a structured type, message, and stacktrace instead of being dropped, and a logging failure can no longer crash the application. [#25135](https://github.com/open-webui/open-webui/issues/25135), [Commit](https://github.com/open-webui/open-webui/commit/79bf3d28d88e78b0136ef3c3c9f8e7bb85d3cea9) +- 🧭 **Workspace skills permission.** Users granted only the "workspace.skills" permission can now see the workspace entry in the sidebar and are correctly routed to the skills page from the workspace index. [#24729](https://github.com/open-webui/open-webui/pull/24729) +- 🔁 **Resilient database migrations.** Database migrations now skip tables, indexes, and columns that already exist and add missing primary keys to legacy tables, so upgrades succeed even when parts of the schema were manually or partially created beforehand. [Commit](https://github.com/open-webui/open-webui/commit/81f611fb73c726cfcfc20ee57a926a543f07e95f), [Commit](https://github.com/open-webui/open-webui/commit/f0e88dadc8502ea05b1a00f3155bee7d1cf32249), [Commit](https://github.com/open-webui/open-webui/commit/bd9f82d5a681ee94bde44325033447500a0c76bf), [Commit](https://github.com/open-webui/open-webui/commit/459b1c3fda2ec3579fbd2ab408d0fdeb07c96b99), [Commit](https://github.com/open-webui/open-webui/commit/6df09a4039d181f4e2d41324e93cd36c6fb27dfd), [Commit](https://github.com/open-webui/open-webui/commit/95840e307a66429168775de389b329487a4311c4), [Commit](https://github.com/open-webui/open-webui/commit/6b1df94bf933af92f5c7d093a2d92e2e50d6fdd0), [Commit](https://github.com/open-webui/open-webui/commit/98d3b2308564e2112290773ea240b90419ebb48d), [Commit](https://github.com/open-webui/open-webui/commit/1b9d22e324181b96511a15cbacc40fdbb439ad79), [Commit](https://github.com/open-webui/open-webui/commit/dc0f8ae6f2372b6e5dd42f3520813b09c54146c0), [Commit](https://github.com/open-webui/open-webui/commit/ee3b14233a642ff1bc38aef03a2418afcf5d6c1d), [Commit](https://github.com/open-webui/open-webui/commit/1004dad2749bcc369d511315a808d4bb5277854c), [Commit](https://github.com/open-webui/open-webui/commit/db2b3d7fd86cc7d76424671ead3c62a3f6302fe7), [Commit](https://github.com/open-webui/open-webui/commit/2e1b671e8db49f47d69fd59ece2b58e109ec8b84), [Commit](https://github.com/open-webui/open-webui/commit/9a8969ca93f8c9109d956518a085be3e2afacc07), [Commit](https://github.com/open-webui/open-webui/commit/9717ada92fdb3761a217d309f322ee9fbe418d0d), [Commit](https://github.com/open-webui/open-webui/commit/d7cfc1e46a8f3e5c8f6758c12ef641f41fb51a39), [Commit](https://github.com/open-webui/open-webui/commit/9263b7568eaa83f43e4a118c98490c4f7aaba570), [Commit](https://github.com/open-webui/open-webui/commit/73d2065227e651cf90a2cfade721516815743ab4), [#24722](https://github.com/open-webui/open-webui/pull/24722) + +### Changed + +- ⚠️ **Database Migrations**: This release includes database schema changes; we strongly recommend backing up your database and all associated data before upgrading in production environments. If you are running a multi-worker, multi-server, or load-balanced deployment, all instances must be updated simultaneously, rolling updates are not supported and will cause application failures due to schema incompatibility. +- ⚙️ **Tool-call iteration cap renamed and raised.** The environment variable that limits how many tool calls a single chat response may make is now "CHAT_RESPONSE_MAX_TOOL_CALL_ITERATIONS", with its default raised from 30 to 256 and a new "-1" value for unlimited; the previous "CHAT_RESPONSE_MAX_TOOL_CALL_RETRIES" name continues to work as a fallback, and chats that hit the cap now show a clear error in-chat instead of stopping silently. [#24918](https://github.com/open-webui/open-webui/pull/24918), [Commit](https://github.com/open-webui/open-webui/commit/2b99945d2726bdf5aed7b1712f9e3b7b622671df) +- 🔐 **Reduced public "/api/config" exposure.** The "/api/config" response no longer includes several feature flags ("enable_api_keys", "enable_password_change_form", "enable_version_update_check", "enable_public_active_users_count", "enable_easter_eggs") for unauthenticated callers, reducing information disclosure to anonymous visitors. [Commit](https://github.com/open-webui/open-webui/commit/245e0ee029e9e10617f62953a9e6f67dd00ecf81), [Commit](https://github.com/open-webui/open-webui/commit/ae06e199d5d3f65296a978cc35079bdacba596d2) +- 🔑 **"WEBUI_SECRET_KEY" is now a hard requirement even for unsupported deployments.** Deployments that start the backend in an explicitly unsupported way (such as invoking uvicorn directly) without setting "WEBUI_SECRET_KEY" will now refuse to start instead of falling back to an empty key; the supported start methods (start.sh, start_windows.bat, and "open-webui serve") still set or auto-generate it automatically, so standard deployments are unaffected. Direct Uvicorn startup is not supported. [#25218](https://github.com/open-webui/open-webui/pull/25218) + ## [0.9.5] - 2026-05-09 ### Added diff --git a/backend/open_webui/config.py b/backend/open_webui/config.py index 6a29504cb6..57f4712027 100644 --- a/backend/open_webui/config.py +++ b/backend/open_webui/config.py @@ -1,43 +1,103 @@ -import asyncio +from __future__ import annotations + +import base64 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 typing import Optional, Union from urllib.parse import urlparse +import redis import requests -from pydantic import BaseModel -from sqlalchemy import JSON, Column, DateTime, Integer, func from authlib.integrations.starlette_client import OAuth - +from pydantic import BaseModel 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, + REDIS_KEY_PREFIX, + REDIS_SENTINEL_HOSTS, + REDIS_SENTINEL_PORT, + REDIS_URL, 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 +from open_webui.internal.config import ( + STATE as _state, +) +from open_webui.internal.config import ( + AppConfig, + ConfigVar, +) + +# ── Persistent configuration layer ────────────────────────────────────────── +from open_webui.internal.config import ( # noqa: F401 + ConfigTable as Config, +) +from open_webui.internal.config import ( + _all_configs as PERSISTENT_CONFIG_REGISTRY, +) +from open_webui.internal.config import ( + initialize as _initialize_config, +) + + +def get_config(): + return _state.snapshot + + +def save_to_db(data): + _state.persist(data) + + +async def async_save_to_db(data): + await _state.persist_async(data) + + +def save_config(config): + try: + _state.persist(config) + for s in PERSISTENT_CONFIG_REGISTRY: + s.refresh() + except Exception: + log.exception('Failed to save config') + return False + return True + + +async def async_save_config(config): + try: + await _state.persist_async(config) + for s in PERSISTENT_CONFIG_REGISTRY: + s.refresh() + except Exception: + log.exception('Failed to save config') + return False + return True + + +def reset_config(): + _state.clear() + + +async def async_reset_config(): + await _state.clear_async() + + +def get_config_value(config_path: str): + return _state.read(config_path) class EndpointFilter(logging.Filter): @@ -45,24 +105,21 @@ class EndpointFilter(logging.Filter): return record.getMessage().find('/health') == -1 -# Filter out /endpoint logging.getLogger('uvicorn.access').addFilter(EndpointFilter()) #################################### -# Config helpers +# Initialization #################################### -# Function to run the alembic migrations def run_migrations(): log.info('Running migrations') try: from alembic import command - from alembic.config import Config + from alembic.config import Config as AlembicConfig - alembic_cfg = Config(OPEN_WEBUI_DIR / 'alembic.ini') + alembic_cfg = AlembicConfig(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)) @@ -75,321 +132,3305 @@ 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 +# Migrate legacy config.json → database on first run if os.path.exists(f'{DATA_DIR}/config.json'): - data = load_json_config() - save_to_db(data) + with open(f'{DATA_DIR}/config.json', 'r') as _f: + save_to_db(json.load(_f)) os.rename(f'{DATA_DIR}/config.json', f'{DATA_DIR}/old_config.json') -DEFAULT_CONFIG = { - 'version': 0, - 'ui': {}, -} +ENABLE_PERSISTENT_CONFIG = os.getenv('ENABLE_PERSISTENT_CONFIG', 'True').lower() == 'true' +ENABLE_OAUTH_PERSISTENT_CONFIG = os.getenv('ENABLE_OAUTH_PERSISTENT_CONFIG', 'False').lower() == 'true' -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 _sync_to_redis(self): - """Push all in-memory config values to Redis, e.g. after a bulk import.""" - if not self._redis or not ENABLE_PERSISTENT_CONFIG: - return - for key, pc in self._state.items(): - redis_key = f'{self._redis_key_prefix}:config:{key}' - try: - self._redis.set(redis_key, json.dumps(pc.value)) - except Exception as e: - log.error(f'Failed to sync config key {key} to Redis: {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', +# Bootstrap the persistent config subsystem +CONFIG_DATA = _initialize_config( + enable_persistent=ENABLE_PERSISTENT_CONFIG, + enable_oauth_persistent=ENABLE_OAUTH_PERSISTENT_CONFIG, ) -ENABLE_API_KEYS_ENDPOINT_RESTRICTIONS = PersistentConfig( +#################################### +# 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}') + + +# --- Storage Provider --- + +STORAGE_PROVIDER = os.getenv('STORAGE_PROVIDER', 'local') # defaults to local, s3 +STORAGE_LOCAL_CACHE = os.getenv('STORAGE_LOCAL_CACHE', 'true').lower() == 'true' + +S3_ACCESS_KEY_ID = os.getenv('S3_ACCESS_KEY_ID', None) +S3_SECRET_ACCESS_KEY = os.getenv('S3_SECRET_ACCESS_KEY', None) +S3_REGION_NAME = os.getenv('S3_REGION_NAME', None) +S3_BUCKET_NAME = os.getenv('S3_BUCKET_NAME', None) +S3_KEY_PREFIX = os.getenv('S3_KEY_PREFIX', None) +S3_ENDPOINT_URL = os.getenv('S3_ENDPOINT_URL', None) +S3_USE_ACCELERATE_ENDPOINT = os.getenv('S3_USE_ACCELERATE_ENDPOINT', 'false').lower() == 'true' +S3_ADDRESSING_STYLE = os.getenv('S3_ADDRESSING_STYLE', None) +S3_ENABLE_TAGGING = os.getenv('S3_ENABLE_TAGGING', 'false').lower() == 'true' + +GCS_BUCKET_NAME = os.getenv('GCS_BUCKET_NAME', None) +GOOGLE_APPLICATION_CREDENTIALS_JSON = os.getenv('GOOGLE_APPLICATION_CREDENTIALS_JSON', None) + +AZURE_STORAGE_ENDPOINT = os.getenv('AZURE_STORAGE_ENDPOINT', None) +AZURE_STORAGE_CONTAINER_NAME = os.getenv('AZURE_STORAGE_CONTAINER_NAME', None) +AZURE_STORAGE_KEY = os.getenv('AZURE_STORAGE_KEY', None) + +#################################### +# File Upload DIR +#################################### + +UPLOAD_DIR = DATA_DIR / 'uploads' +UPLOAD_DIR.mkdir(parents=True, exist_ok=True) + + +#################################### +# Cache DIR +#################################### + +CACHE_DIR = DATA_DIR / 'cache' +CACHE_DIR.mkdir(parents=True, exist_ok=True) + + +#################################### +# CUSTOM_NAME (Legacy) +#################################### + +CUSTOM_NAME = os.getenv('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 + + +#################################### +# DIRECT CONNECTIONS +#################################### + +ENABLE_DIRECT_CONNECTIONS = ConfigVar( + 'ENABLE_DIRECT_CONNECTIONS', + 'direct.enable', + os.getenv('ENABLE_DIRECT_CONNECTIONS', 'False').lower() == 'true', +) + +#################################### +# OLLAMA_BASE_URL +#################################### + +ENABLE_OLLAMA_API = ConfigVar( + 'ENABLE_OLLAMA_API', + 'ollama.enable', + os.getenv('ENABLE_OLLAMA_API', 'True').lower() == 'true', +) + +OLLAMA_API_BASE_URL = os.getenv('OLLAMA_API_BASE_URL', 'http://localhost:11434/api') + +OLLAMA_BASE_URL = os.getenv('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.getenv('K8S_FLAG', '') +USE_OLLAMA_DOCKER = os.getenv('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.getenv('OLLAMA_BASE_URL', '') in ('', '/ollama') and not os.getenv('OLLAMA_BASE_URLS', ''): + OLLAMA_BASE_URL = _resolve_ollama_base_url(OLLAMA_BASE_URL) + + +OLLAMA_BASE_URLS = os.getenv('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 = ConfigVar('OLLAMA_BASE_URLS', 'ollama.base_urls', OLLAMA_BASE_URLS) + +OLLAMA_API_CONFIGS = ConfigVar( + 'OLLAMA_API_CONFIGS', + 'ollama.api_configs', + {}, +) + +#################################### +# OPENAI_API +#################################### + + +ENABLE_OPENAI_API = ConfigVar( + 'ENABLE_OPENAI_API', + 'openai.enable', + os.getenv('ENABLE_OPENAI_API', 'True').lower() == 'true', +) + + +OPENAI_API_KEY = os.getenv('OPENAI_API_KEY', '') +OPENAI_API_BASE_URL = os.getenv('OPENAI_API_BASE_URL', '') + +GEMINI_API_KEY = os.getenv('GEMINI_API_KEY', '') +GEMINI_API_BASE_URL = os.getenv('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.getenv('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 = ConfigVar('OPENAI_API_KEYS', 'openai.api_keys', OPENAI_API_KEYS) + +OPENAI_API_BASE_URLS = os.getenv('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 = ConfigVar('OPENAI_API_BASE_URLS', 'openai.api_base_urls', OPENAI_API_BASE_URLS) + +OPENAI_API_CONFIGS = ConfigVar( + '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 = ConfigVar( + 'ENABLE_BASE_MODELS_CACHE', + 'models.base_models_cache', + os.getenv('ENABLE_BASE_MODELS_CACHE', 'False').lower() == 'true', +) + + +#################################### +# TOOL_SERVERS +#################################### + +try: + tool_server_connections = json.loads(os.getenv('TOOL_SERVER_CONNECTIONS', '[]')) +except Exception as e: + log.exception(f'Error loading TOOL_SERVER_CONNECTIONS: {e}') + tool_server_connections = [] + + +TOOL_SERVER_CONNECTIONS = ConfigVar( + 'TOOL_SERVER_CONNECTIONS', + 'tool_server.connections', + tool_server_connections, +) + +OAUTH_CLIENT_TIMEOUT = ConfigVar( + 'OAUTH_CLIENT_TIMEOUT', + 'oauth.client.timeout', + os.getenv('OAUTH_CLIENT_TIMEOUT', ''), +) + +#################################### +# TERMINAL_SERVER +#################################### + +terminal_server_connections = json.loads(os.getenv('TERMINAL_SERVER_CONNECTIONS', '[]')) + +TERMINAL_SERVER_CONNECTIONS = ConfigVar( + 'TERMINAL_SERVER_CONNECTIONS', + 'terminal_server.connections', + terminal_server_connections, +) + +try: + TERMINAL_PROXY_HEADERS = json.loads(os.getenv('TERMINAL_PROXY_HEADERS', '{}')) +except Exception: + TERMINAL_PROXY_HEADERS = {} + +#################################### +# Code Interpreter +#################################### + +ENABLE_CODE_EXECUTION = ConfigVar( + 'ENABLE_CODE_EXECUTION', + 'code_execution.enable', + os.getenv('ENABLE_CODE_EXECUTION', 'True').lower() == 'true', +) + +CODE_EXECUTION_ENGINE = ConfigVar( + 'CODE_EXECUTION_ENGINE', + 'code_execution.engine', + os.getenv('CODE_EXECUTION_ENGINE', 'pyodide'), +) + +CODE_EXECUTION_JUPYTER_URL = ConfigVar( + 'CODE_EXECUTION_JUPYTER_URL', + 'code_execution.jupyter.url', + os.getenv('CODE_EXECUTION_JUPYTER_URL', ''), +) + +CODE_EXECUTION_JUPYTER_AUTH = ConfigVar( + 'CODE_EXECUTION_JUPYTER_AUTH', + 'code_execution.jupyter.auth', + os.getenv('CODE_EXECUTION_JUPYTER_AUTH', ''), +) + +CODE_EXECUTION_JUPYTER_AUTH_TOKEN = ConfigVar( + 'CODE_EXECUTION_JUPYTER_AUTH_TOKEN', + 'code_execution.jupyter.auth_token', + os.getenv('CODE_EXECUTION_JUPYTER_AUTH_TOKEN', ''), +) + + +CODE_EXECUTION_JUPYTER_AUTH_PASSWORD = ConfigVar( + 'CODE_EXECUTION_JUPYTER_AUTH_PASSWORD', + 'code_execution.jupyter.auth_password', + os.getenv('CODE_EXECUTION_JUPYTER_AUTH_PASSWORD', ''), +) + +CODE_EXECUTION_JUPYTER_TIMEOUT = ConfigVar( + 'CODE_EXECUTION_JUPYTER_TIMEOUT', + 'code_execution.jupyter.timeout', + int(os.getenv('CODE_EXECUTION_JUPYTER_TIMEOUT', '60')), +) + +ENABLE_CODE_INTERPRETER = ConfigVar( + 'ENABLE_CODE_INTERPRETER', + 'code_interpreter.enable', + os.getenv('ENABLE_CODE_INTERPRETER', 'True').lower() == 'true', +) + +ENABLE_MEMORIES = ConfigVar( + 'ENABLE_MEMORIES', + 'memories.enable', + os.getenv('ENABLE_MEMORIES', 'True').lower() == 'true', +) + +CODE_INTERPRETER_ENGINE = ConfigVar( + 'CODE_INTERPRETER_ENGINE', + 'code_interpreter.engine', + os.getenv('CODE_INTERPRETER_ENGINE', 'pyodide'), +) + +CODE_INTERPRETER_PROMPT_TEMPLATE = ConfigVar( + 'CODE_INTERPRETER_PROMPT_TEMPLATE', + 'code_interpreter.prompt_template', + os.getenv('CODE_INTERPRETER_PROMPT_TEMPLATE', ''), +) + +CODE_INTERPRETER_JUPYTER_URL = ConfigVar( + 'CODE_INTERPRETER_JUPYTER_URL', + 'code_interpreter.jupyter.url', + os.getenv('CODE_INTERPRETER_JUPYTER_URL', os.getenv('CODE_EXECUTION_JUPYTER_URL', '')), +) + +CODE_INTERPRETER_JUPYTER_AUTH = ConfigVar( + 'CODE_INTERPRETER_JUPYTER_AUTH', + 'code_interpreter.jupyter.auth', + os.getenv( + 'CODE_INTERPRETER_JUPYTER_AUTH', + os.getenv('CODE_EXECUTION_JUPYTER_AUTH', ''), + ), +) + +CODE_INTERPRETER_JUPYTER_AUTH_TOKEN = ConfigVar( + 'CODE_INTERPRETER_JUPYTER_AUTH_TOKEN', + 'code_interpreter.jupyter.auth_token', + os.getenv( + 'CODE_INTERPRETER_JUPYTER_AUTH_TOKEN', + os.getenv('CODE_EXECUTION_JUPYTER_AUTH_TOKEN', ''), + ), +) + + +CODE_INTERPRETER_JUPYTER_AUTH_PASSWORD = ConfigVar( + 'CODE_INTERPRETER_JUPYTER_AUTH_PASSWORD', + 'code_interpreter.jupyter.auth_password', + os.getenv( + 'CODE_INTERPRETER_JUPYTER_AUTH_PASSWORD', + os.getenv('CODE_EXECUTION_JUPYTER_AUTH_PASSWORD', ''), + ), +) + +CODE_INTERPRETER_JUPYTER_TIMEOUT = ConfigVar( + 'CODE_INTERPRETER_JUPYTER_TIMEOUT', + 'code_interpreter.jupyter.timeout', + int( + os.getenv( + 'CODE_INTERPRETER_JUPYTER_TIMEOUT', + os.getenv('CODE_EXECUTION_JUPYTER_TIMEOUT', '60'), + ) + ), +) + +CODE_INTERPRETER_BLOCKED_MODULES = [ + library.strip() for library in os.getenv('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.getenv('VECTOR_DB', 'chroma') + +# Chroma +CHROMA_DATA_PATH = f'{DATA_DIR}/vector_db' + +if VECTOR_DB == 'chroma': + import chromadb + + CHROMA_TENANT = os.getenv('CHROMA_TENANT', chromadb.DEFAULT_TENANT) + CHROMA_DATABASE = os.getenv('CHROMA_DATABASE', chromadb.DEFAULT_DATABASE) + CHROMA_HTTP_HOST = os.getenv('CHROMA_HTTP_HOST', '') + CHROMA_HTTP_PORT = int(os.getenv('CHROMA_HTTP_PORT', '8000')) + CHROMA_CLIENT_AUTH_PROVIDER = os.getenv('CHROMA_CLIENT_AUTH_PROVIDER', '') + CHROMA_CLIENT_AUTH_CREDENTIALS = os.getenv('CHROMA_CLIENT_AUTH_CREDENTIALS', '') + # Comma-separated list of header=value pairs + CHROMA_HTTP_HEADERS = os.getenv('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.getenv('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.getenv('MARIADB_VECTOR_DB_URL', '').strip() + +MARIADB_VECTOR_INITIALIZE_MAX_VECTOR_LENGTH = int( + os.getenv('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.getenv('MARIADB_VECTOR_DISTANCE_STRATEGY', 'cosine').strip().lower() + +# HNSW M parameter (MariaDB VECTOR INDEX ... M=) +MARIADB_VECTOR_INDEX_M = int(os.getenv('MARIADB_VECTOR_INDEX_M', '8').strip() or '8') + +# Pooling (MariaDB-Vector) +MARIADB_VECTOR_POOL_SIZE = os.getenv('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.getenv('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.getenv('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.getenv('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.getenv('MILVUS_URI', f'{DATA_DIR}/vector_db/milvus.db') +MILVUS_DB = os.getenv('MILVUS_DB', 'default') +MILVUS_TOKEN = os.getenv('MILVUS_TOKEN', None) +MILVUS_INDEX_TYPE = os.getenv('MILVUS_INDEX_TYPE', 'HNSW') +MILVUS_METRIC_TYPE = os.getenv('MILVUS_METRIC_TYPE', 'COSINE') +MILVUS_HNSW_M = int(os.getenv('MILVUS_HNSW_M', '16')) +MILVUS_HNSW_EFCONSTRUCTION = int(os.getenv('MILVUS_HNSW_EFCONSTRUCTION', '100')) +MILVUS_IVF_FLAT_NLIST = int(os.getenv('MILVUS_IVF_FLAT_NLIST', '128')) +MILVUS_DISKANN_MAX_DEGREE = int(os.getenv('MILVUS_DISKANN_MAX_DEGREE', '56')) +MILVUS_DISKANN_SEARCH_LIST_SIZE = int(os.getenv('MILVUS_DISKANN_SEARCH_LIST_SIZE', '100')) +ENABLE_MILVUS_MULTITENANCY_MODE = os.getenv('ENABLE_MILVUS_MULTITENANCY_MODE', 'false').lower() == 'true' +# Hyphens not allowed, need to use underscores in collection names +MILVUS_COLLECTION_PREFIX = os.getenv('MILVUS_COLLECTION_PREFIX', 'open_webui') + +# Qdrant +QDRANT_URI = os.getenv('QDRANT_URI', None) +QDRANT_API_KEY = os.getenv('QDRANT_API_KEY', None) +QDRANT_ON_DISK = os.getenv('QDRANT_ON_DISK', 'false').lower() == 'true' +QDRANT_PREFER_GRPC = os.getenv('QDRANT_PREFER_GRPC', 'false').lower() == 'true' +QDRANT_GRPC_PORT = int(os.getenv('QDRANT_GRPC_PORT', '6334')) +QDRANT_TIMEOUT = int(os.getenv('QDRANT_TIMEOUT', '5')) +QDRANT_HNSW_M = int(os.getenv('QDRANT_HNSW_M', '16')) +ENABLE_QDRANT_MULTITENANCY_MODE = os.getenv('ENABLE_QDRANT_MULTITENANCY_MODE', 'true').lower() == 'true' +QDRANT_COLLECTION_PREFIX = os.getenv('QDRANT_COLLECTION_PREFIX', 'open-webui') + +WEAVIATE_HTTP_HOST = os.getenv('WEAVIATE_HTTP_HOST', '') +WEAVIATE_GRPC_HOST = os.getenv('WEAVIATE_GRPC_HOST', '') +WEAVIATE_HTTP_PORT = int(os.getenv('WEAVIATE_HTTP_PORT', '8080')) +WEAVIATE_GRPC_PORT = int(os.getenv('WEAVIATE_GRPC_PORT', '50051')) +WEAVIATE_API_KEY = os.getenv('WEAVIATE_API_KEY') +WEAVIATE_HTTP_SECURE = os.getenv('WEAVIATE_HTTP_SECURE', 'false').lower() == 'true' +WEAVIATE_GRPC_SECURE = os.getenv('WEAVIATE_GRPC_SECURE', 'false').lower() == 'true' +WEAVIATE_SKIP_INIT_CHECKS = os.getenv('WEAVIATE_SKIP_INIT_CHECKS', 'false').lower() == 'true' + +# OpenSearch +OPENSEARCH_URI = os.getenv('OPENSEARCH_URI', 'https://localhost:9200') +OPENSEARCH_SSL = os.getenv('OPENSEARCH_SSL', 'true').lower() == 'true' +OPENSEARCH_CERT_VERIFY = os.getenv('OPENSEARCH_CERT_VERIFY', 'false').lower() == 'true' +OPENSEARCH_USERNAME = os.getenv('OPENSEARCH_USERNAME', None) +OPENSEARCH_PASSWORD = os.getenv('OPENSEARCH_PASSWORD', None) + +# ElasticSearch +ELASTICSEARCH_URL = os.getenv('ELASTICSEARCH_URL', 'https://localhost:9200') +ELASTICSEARCH_CA_CERTS = os.getenv('ELASTICSEARCH_CA_CERTS', None) +ELASTICSEARCH_API_KEY = os.getenv('ELASTICSEARCH_API_KEY', None) +ELASTICSEARCH_USERNAME = os.getenv('ELASTICSEARCH_USERNAME', None) +ELASTICSEARCH_PASSWORD = os.getenv('ELASTICSEARCH_PASSWORD', None) +ELASTICSEARCH_CLOUD_ID = os.getenv('ELASTICSEARCH_CLOUD_ID', None) +SSL_ASSERT_FINGERPRINT = os.getenv('SSL_ASSERT_FINGERPRINT', None) +ELASTICSEARCH_INDEX_PREFIX = os.getenv('ELASTICSEARCH_INDEX_PREFIX', 'open_webui_collections') +# Pgvector +PGVECTOR_DB_URL = os.getenv('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.getenv('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.getenv('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.getenv('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.getenv('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.getenv('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.getenv('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.getenv('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.getenv('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.getenv('OPENGAUSS_DB_URL', DATABASE_URL) + +OPENGAUSS_INITIALIZE_MAX_VECTOR_LENGTH = int(os.getenv('OPENGAUSS_INITIALIZE_MAX_VECTOR_LENGTH', '1536')) + +OPENGAUSS_POOL_SIZE = os.getenv('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.getenv('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.getenv('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.getenv('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.getenv('PINECONE_API_KEY', None) +PINECONE_ENVIRONMENT = os.getenv('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.getenv('ORACLE_DB_USE_WALLET', 'false').lower() == 'true' +ORACLE_DB_USER = os.getenv('ORACLE_DB_USER', None) # +ORACLE_DB_PASSWORD = os.getenv('ORACLE_DB_PASSWORD', None) # +ORACLE_DB_DSN = os.getenv('ORACLE_DB_DSN', None) # +ORACLE_WALLET_DIR = os.getenv('ORACLE_WALLET_DIR', None) +ORACLE_WALLET_PASSWORD = os.getenv('ORACLE_WALLET_PASSWORD', None) +ORACLE_VECTOR_LENGTH = os.getenv('ORACLE_VECTOR_LENGTH', 768) + +ORACLE_DB_POOL_MIN = int(os.getenv('ORACLE_DB_POOL_MIN', 2)) +ORACLE_DB_POOL_MAX = int(os.getenv('ORACLE_DB_POOL_MAX', 10)) +ORACLE_DB_POOL_INCREMENT = int(os.getenv('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.getenv('S3_VECTOR_BUCKET_NAME', None) +S3_VECTOR_REGION = os.getenv('S3_VECTOR_REGION', None) + +# Valkey Vector Store +VALKEY_URL = os.getenv('VALKEY_URL', '') +VALKEY_COLLECTION_PREFIX = os.getenv('VALKEY_COLLECTION_PREFIX', 'open_webui') +VALKEY_INDEX_TYPE = os.getenv('VALKEY_INDEX_TYPE', 'HNSW').upper() +VALKEY_DISTANCE_METRIC = os.getenv('VALKEY_DISTANCE_METRIC', 'COSINE').upper() +VALKEY_HNSW_M = int(os.getenv('VALKEY_HNSW_M', '16')) +VALKEY_HNSW_EF_CONSTRUCTION = int(os.getenv('VALKEY_HNSW_EF_CONSTRUCTION', '200')) +VALKEY_HNSW_EF_RUNTIME = int(os.getenv('VALKEY_HNSW_EF_RUNTIME', '10')) + +#################################### +# Information Retrieval (RAG) +#################################### + + +# If configured, Google Drive will be available as an upload option. +ENABLE_GOOGLE_DRIVE_INTEGRATION = ConfigVar( + 'ENABLE_GOOGLE_DRIVE_INTEGRATION', + 'google_drive.enable', + os.getenv('ENABLE_GOOGLE_DRIVE_INTEGRATION', 'False').lower() == 'true', +) + +GOOGLE_DRIVE_CLIENT_ID = ConfigVar( + 'GOOGLE_DRIVE_CLIENT_ID', + 'google_drive.client_id', + os.getenv('GOOGLE_DRIVE_CLIENT_ID', ''), +) + +GOOGLE_DRIVE_API_KEY = ConfigVar( + 'GOOGLE_DRIVE_API_KEY', + 'google_drive.api_key', + os.getenv('GOOGLE_DRIVE_API_KEY', ''), +) + +ENABLE_ONEDRIVE_INTEGRATION = ConfigVar( + 'ENABLE_ONEDRIVE_INTEGRATION', + 'onedrive.enable', + os.getenv('ENABLE_ONEDRIVE_INTEGRATION', 'False').lower() == 'true', +) + + +ONEDRIVE_CLIENT_ID = os.getenv('ONEDRIVE_CLIENT_ID', '') +ONEDRIVE_CLIENT_ID_PERSONAL = os.getenv('ONEDRIVE_CLIENT_ID_PERSONAL', ONEDRIVE_CLIENT_ID) +ONEDRIVE_CLIENT_ID_BUSINESS = os.getenv('ONEDRIVE_CLIENT_ID_BUSINESS', ONEDRIVE_CLIENT_ID) + +ENABLE_ONEDRIVE_PERSONAL = os.getenv('ENABLE_ONEDRIVE_PERSONAL', 'True').lower() == 'true' and bool( + ONEDRIVE_CLIENT_ID_PERSONAL +) +ENABLE_ONEDRIVE_BUSINESS = os.getenv('ENABLE_ONEDRIVE_BUSINESS', 'True').lower() == 'true' and bool( + ONEDRIVE_CLIENT_ID_BUSINESS +) + +ONEDRIVE_SHAREPOINT_URL = ConfigVar( + 'ONEDRIVE_SHAREPOINT_URL', + 'onedrive.sharepoint_url', + os.getenv('ONEDRIVE_SHAREPOINT_URL', ''), +) + +ONEDRIVE_SHAREPOINT_TENANT_ID = ConfigVar( + 'ONEDRIVE_SHAREPOINT_TENANT_ID', + 'onedrive.sharepoint_tenant_id', + os.getenv('ONEDRIVE_SHAREPOINT_TENANT_ID', ''), +) + +# RAG Content Extraction +CONTENT_EXTRACTION_ENGINE = ConfigVar( + 'CONTENT_EXTRACTION_ENGINE', + 'rag.CONTENT_EXTRACTION_ENGINE', + os.getenv('CONTENT_EXTRACTION_ENGINE', '').lower(), +) + +DATALAB_MARKER_API_KEY = ConfigVar( + 'DATALAB_MARKER_API_KEY', + 'rag.datalab_marker_api_key', + os.getenv('DATALAB_MARKER_API_KEY', ''), +) + +DATALAB_MARKER_API_BASE_URL = ConfigVar( + 'DATALAB_MARKER_API_BASE_URL', + 'rag.datalab_marker_api_base_url', + os.getenv('DATALAB_MARKER_API_BASE_URL', ''), +) + +DATALAB_MARKER_ADDITIONAL_CONFIG = ConfigVar( + 'DATALAB_MARKER_ADDITIONAL_CONFIG', + 'rag.datalab_marker_additional_config', + os.getenv('DATALAB_MARKER_ADDITIONAL_CONFIG', ''), +) + +DATALAB_MARKER_USE_LLM = ConfigVar( + 'DATALAB_MARKER_USE_LLM', + 'rag.DATALAB_MARKER_USE_LLM', + os.getenv('DATALAB_MARKER_USE_LLM', 'false').lower() == 'true', +) + +DATALAB_MARKER_SKIP_CACHE = ConfigVar( + 'DATALAB_MARKER_SKIP_CACHE', + 'rag.datalab_marker_skip_cache', + os.getenv('DATALAB_MARKER_SKIP_CACHE', 'false').lower() == 'true', +) + +DATALAB_MARKER_FORCE_OCR = ConfigVar( + 'DATALAB_MARKER_FORCE_OCR', + 'rag.datalab_marker_force_ocr', + os.getenv('DATALAB_MARKER_FORCE_OCR', 'false').lower() == 'true', +) + +DATALAB_MARKER_PAGINATE = ConfigVar( + 'DATALAB_MARKER_PAGINATE', + 'rag.datalab_marker_paginate', + os.getenv('DATALAB_MARKER_PAGINATE', 'false').lower() == 'true', +) + +DATALAB_MARKER_STRIP_EXISTING_OCR = ConfigVar( + 'DATALAB_MARKER_STRIP_EXISTING_OCR', + 'rag.datalab_marker_strip_existing_ocr', + os.getenv('DATALAB_MARKER_STRIP_EXISTING_OCR', 'false').lower() == 'true', +) + +DATALAB_MARKER_DISABLE_IMAGE_EXTRACTION = ConfigVar( + 'DATALAB_MARKER_DISABLE_IMAGE_EXTRACTION', + 'rag.datalab_marker_disable_image_extraction', + os.getenv('DATALAB_MARKER_DISABLE_IMAGE_EXTRACTION', 'false').lower() == 'true', +) + +DATALAB_MARKER_FORMAT_LINES = ConfigVar( + 'DATALAB_MARKER_FORMAT_LINES', + 'rag.datalab_marker_format_lines', + os.getenv('DATALAB_MARKER_FORMAT_LINES', 'false').lower() == 'true', +) + +DATALAB_MARKER_OUTPUT_FORMAT = ConfigVar( + 'DATALAB_MARKER_OUTPUT_FORMAT', + 'rag.datalab_marker_output_format', + os.getenv('DATALAB_MARKER_OUTPUT_FORMAT', 'markdown'), +) + +MINERU_API_MODE = ConfigVar( + 'MINERU_API_MODE', + 'rag.mineru_api_mode', + os.getenv('MINERU_API_MODE', 'local'), # "local" or "cloud" +) + +MINERU_API_URL = ConfigVar( + 'MINERU_API_URL', + 'rag.mineru_api_url', + os.getenv('MINERU_API_URL', 'http://localhost:8000'), +) + +MINERU_API_TIMEOUT = ConfigVar( + 'MINERU_API_TIMEOUT', + 'rag.mineru_api_timeout', + os.getenv('MINERU_API_TIMEOUT', '300'), +) + +MINERU_API_KEY = ConfigVar( + 'MINERU_API_KEY', + 'rag.mineru_api_key', + os.getenv('MINERU_API_KEY', ''), +) + +mineru_params = os.getenv('MINERU_PARAMS', '') +try: + mineru_params = json.loads(mineru_params) +except json.JSONDecodeError: + mineru_params = {} + +MINERU_PARAMS = ConfigVar( + 'MINERU_PARAMS', + 'rag.mineru_params', + mineru_params, +) + +MINERU_FILE_EXTENSIONS = ConfigVar( + 'MINERU_FILE_EXTENSIONS', + 'rag.mineru_file_extensions', + [ext.strip() for ext in os.getenv('MINERU_FILE_EXTENSIONS', 'pdf').split(',') if ext.strip()], +) + +EXTERNAL_DOCUMENT_LOADER_URL = ConfigVar( + 'EXTERNAL_DOCUMENT_LOADER_URL', + 'rag.external_document_loader_url', + os.getenv('EXTERNAL_DOCUMENT_LOADER_URL', ''), +) + +EXTERNAL_DOCUMENT_LOADER_API_KEY = ConfigVar( + 'EXTERNAL_DOCUMENT_LOADER_API_KEY', + 'rag.external_document_loader_api_key', + os.getenv('EXTERNAL_DOCUMENT_LOADER_API_KEY', ''), +) + +TIKA_SERVER_URL = ConfigVar( + 'TIKA_SERVER_URL', + 'rag.tika_server_url', + os.getenv('TIKA_SERVER_URL', 'http://tika:9998'), # Default for sidecar deployment +) + +DOCLING_SERVER_URL = ConfigVar( + 'DOCLING_SERVER_URL', + 'rag.docling_server_url', + os.getenv('DOCLING_SERVER_URL', 'http://docling:5001'), +) + +DOCLING_API_KEY = ConfigVar( + '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 = ConfigVar( + 'DOCLING_PARAMS', + 'rag.docling_params', + docling_params, +) + +DOCUMENT_INTELLIGENCE_ENDPOINT = ConfigVar( + 'DOCUMENT_INTELLIGENCE_ENDPOINT', + 'rag.document_intelligence_endpoint', + os.getenv('DOCUMENT_INTELLIGENCE_ENDPOINT', ''), +) + +DOCUMENT_INTELLIGENCE_KEY = ConfigVar( + 'DOCUMENT_INTELLIGENCE_KEY', + 'rag.document_intelligence_key', + os.getenv('DOCUMENT_INTELLIGENCE_KEY', ''), +) + +DOCUMENT_INTELLIGENCE_MODEL = ConfigVar( + 'DOCUMENT_INTELLIGENCE_MODEL', + 'rag.document_intelligence_model', + os.getenv('DOCUMENT_INTELLIGENCE_MODEL', 'prebuilt-layout'), +) + +MISTRAL_OCR_API_BASE_URL = ConfigVar( + '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 = ConfigVar( + 'MISTRAL_OCR_API_KEY', + 'rag.mistral_ocr_api_key', + os.getenv('MISTRAL_OCR_API_KEY', ''), +) + +PADDLEOCR_VL_BASE_URL = ConfigVar( + 'PADDLEOCR_VL_BASE_URL', + 'rag.paddleocr_vl_base_url', + os.getenv('PADDLEOCR_VL_BASE_URL', 'http://localhost:8080'), +) + +PADDLEOCR_VL_TOKEN = ConfigVar( + 'PADDLEOCR_VL_TOKEN', + 'rag.paddleocr_vl_token', + os.getenv('PADDLEOCR_VL_TOKEN', ''), +) + +BYPASS_EMBEDDING_AND_RETRIEVAL = ConfigVar( + 'BYPASS_EMBEDDING_AND_RETRIEVAL', + 'rag.bypass_embedding_and_retrieval', + os.getenv('BYPASS_EMBEDDING_AND_RETRIEVAL', 'False').lower() == 'true', +) + + +RAG_TOP_K = ConfigVar('RAG_TOP_K', 'rag.top_k', int(os.getenv('RAG_TOP_K', '3'))) +RAG_TOP_K_RERANKER = ConfigVar( + 'RAG_TOP_K_RERANKER', + 'rag.top_k_reranker', + int(os.getenv('RAG_TOP_K_RERANKER', '3')), +) +RAG_RELEVANCE_THRESHOLD = ConfigVar( + 'RAG_RELEVANCE_THRESHOLD', + 'rag.relevance_threshold', + float(os.getenv('RAG_RELEVANCE_THRESHOLD', '0.0')), +) +RAG_HYBRID_BM25_WEIGHT = ConfigVar( + 'RAG_HYBRID_BM25_WEIGHT', + 'rag.hybrid_bm25_weight', + float(os.getenv('RAG_HYBRID_BM25_WEIGHT', '0.5')), +) + +ENABLE_RAG_HYBRID_SEARCH = ConfigVar( + 'ENABLE_RAG_HYBRID_SEARCH', + 'rag.enable_hybrid_search', + os.getenv('ENABLE_RAG_HYBRID_SEARCH', '').lower() == 'true', +) + +ENABLE_RAG_HYBRID_SEARCH_ENRICHED_TEXTS = ConfigVar( + 'ENABLE_RAG_HYBRID_SEARCH_ENRICHED_TEXTS', + 'rag.enable_hybrid_search_enriched_texts', + os.getenv('ENABLE_RAG_HYBRID_SEARCH_ENRICHED_TEXTS', 'False').lower() == 'true', +) + +RAG_FULL_CONTEXT = ConfigVar( + 'RAG_FULL_CONTEXT', + 'rag.full_context', + os.getenv('RAG_FULL_CONTEXT', 'False').lower() == 'true', +) + +RAG_FILE_MAX_COUNT = ConfigVar( + 'RAG_FILE_MAX_COUNT', + 'rag.file.max_count', + (int(os.getenv('RAG_FILE_MAX_COUNT')) if os.getenv('RAG_FILE_MAX_COUNT') else None), +) + +RAG_FILE_MAX_SIZE = ConfigVar( + 'RAG_FILE_MAX_SIZE', + 'rag.file.max_size', + (int(os.getenv('RAG_FILE_MAX_SIZE')) if os.getenv('RAG_FILE_MAX_SIZE') else None), +) + +FILE_IMAGE_COMPRESSION_WIDTH = ConfigVar( + 'FILE_IMAGE_COMPRESSION_WIDTH', + 'file.image_compression_width', + (int(os.getenv('FILE_IMAGE_COMPRESSION_WIDTH')) if os.getenv('FILE_IMAGE_COMPRESSION_WIDTH') else None), +) + +FILE_IMAGE_COMPRESSION_HEIGHT = ConfigVar( + 'FILE_IMAGE_COMPRESSION_HEIGHT', + 'file.image_compression_height', + (int(os.getenv('FILE_IMAGE_COMPRESSION_HEIGHT')) if os.getenv('FILE_IMAGE_COMPRESSION_HEIGHT') else None), +) + + +RAG_ALLOWED_FILE_EXTENSIONS = ConfigVar( + 'RAG_ALLOWED_FILE_EXTENSIONS', + 'rag.file.allowed_extensions', + [ext.strip() for ext in os.getenv('RAG_ALLOWED_FILE_EXTENSIONS', '').split(',') if ext.strip()], +) + +RAG_EMBEDDING_ENGINE = ConfigVar( + 'RAG_EMBEDDING_ENGINE', + 'rag.embedding_engine', + os.getenv('RAG_EMBEDDING_ENGINE', ''), +) + +PDF_EXTRACT_IMAGES = ConfigVar( + 'PDF_EXTRACT_IMAGES', + 'rag.pdf_extract_images', + os.getenv('PDF_EXTRACT_IMAGES', 'False').lower() == 'true', +) + +PDF_LOADER_MODE = ConfigVar( + 'PDF_LOADER_MODE', + 'rag.pdf_loader_mode', + os.getenv('PDF_LOADER_MODE', 'page'), +) + +RAG_EMBEDDING_MODEL = ConfigVar( + 'RAG_EMBEDDING_MODEL', + 'rag.embedding_model', + os.getenv('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.getenv('RAG_EMBEDDING_MODEL_AUTO_UPDATE', 'True').lower() == 'true' +) + +RAG_EMBEDDING_MODEL_TRUST_REMOTE_CODE = os.getenv('RAG_EMBEDDING_MODEL_TRUST_REMOTE_CODE', 'True').lower() == 'true' + +RAG_EMBEDDING_BATCH_SIZE = ConfigVar( + 'RAG_EMBEDDING_BATCH_SIZE', + 'rag.embedding_batch_size', + int(os.getenv('RAG_EMBEDDING_BATCH_SIZE') or os.getenv('RAG_EMBEDDING_OPENAI_BATCH_SIZE', '1')), +) + +ENABLE_ASYNC_EMBEDDING = ConfigVar( + 'ENABLE_ASYNC_EMBEDDING', + 'rag.enable_async_embedding', + os.getenv('ENABLE_ASYNC_EMBEDDING', 'True').lower() == 'true', +) + +RAG_EMBEDDING_CONCURRENT_REQUESTS = ConfigVar( + 'RAG_EMBEDDING_CONCURRENT_REQUESTS', + 'rag.embedding_concurrent_requests', + int(os.getenv('RAG_EMBEDDING_CONCURRENT_REQUESTS', '0')), +) + +RAG_EMBEDDING_QUERY_PREFIX = os.getenv('RAG_EMBEDDING_QUERY_PREFIX', None) + +RAG_EMBEDDING_CONTENT_PREFIX = os.getenv('RAG_EMBEDDING_CONTENT_PREFIX', None) + +RAG_EMBEDDING_PREFIX_FIELD_NAME = os.getenv('RAG_EMBEDDING_PREFIX_FIELD_NAME', None) + +RAG_RERANKING_ENGINE = ConfigVar( + 'RAG_RERANKING_ENGINE', + 'rag.reranking_engine', + os.getenv('RAG_RERANKING_ENGINE', ''), +) + +RAG_RERANKING_MODEL = ConfigVar( + 'RAG_RERANKING_MODEL', + 'rag.reranking_model', + os.getenv('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.getenv('RAG_RERANKING_MODEL_AUTO_UPDATE', 'True').lower() == 'true' +) + +RAG_RERANKING_MODEL_TRUST_REMOTE_CODE = os.getenv('RAG_RERANKING_MODEL_TRUST_REMOTE_CODE', 'True').lower() == 'true' + +RAG_RERANKING_BATCH_SIZE = ConfigVar( + 'RAG_RERANKING_BATCH_SIZE', + 'rag.reranking_batch_size', + int(os.getenv('RAG_RERANKING_BATCH_SIZE', '32')), +) + +RAG_EXTERNAL_RERANKER_URL = ConfigVar( + 'RAG_EXTERNAL_RERANKER_URL', + 'rag.external_reranker_url', + os.getenv('RAG_EXTERNAL_RERANKER_URL', ''), +) + +RAG_EXTERNAL_RERANKER_API_KEY = ConfigVar( + 'RAG_EXTERNAL_RERANKER_API_KEY', + 'rag.external_reranker_api_key', + os.getenv('RAG_EXTERNAL_RERANKER_API_KEY', ''), +) + +RAG_EXTERNAL_RERANKER_TIMEOUT = ConfigVar( + 'RAG_EXTERNAL_RERANKER_TIMEOUT', + 'rag.external_reranker_timeout', + os.getenv('RAG_EXTERNAL_RERANKER_TIMEOUT', ''), +) + + +RAG_TEXT_SPLITTER = ConfigVar( + 'RAG_TEXT_SPLITTER', + 'rag.text_splitter', + os.getenv('RAG_TEXT_SPLITTER', ''), +) + +ENABLE_MARKDOWN_HEADER_TEXT_SPLITTER = ConfigVar( + 'ENABLE_MARKDOWN_HEADER_TEXT_SPLITTER', + 'rag.enable_markdown_header_text_splitter', + os.getenv('ENABLE_MARKDOWN_HEADER_TEXT_SPLITTER', 'True').lower() == 'true', +) + + +TIKTOKEN_CACHE_DIR = os.getenv('TIKTOKEN_CACHE_DIR', f'{CACHE_DIR}/tiktoken') +TIKTOKEN_ENCODING_NAME = ConfigVar( + 'TIKTOKEN_ENCODING_NAME', + 'rag.tiktoken_encoding_name', + os.getenv('TIKTOKEN_ENCODING_NAME', 'cl100k_base'), +) + + +CHUNK_SIZE = ConfigVar('CHUNK_SIZE', 'rag.chunk_size', int(os.getenv('CHUNK_SIZE', '1000'))) + +CHUNK_MIN_SIZE_TARGET = ConfigVar( + 'CHUNK_MIN_SIZE_TARGET', + 'rag.chunk_min_size_target', + int(os.getenv('CHUNK_MIN_SIZE_TARGET', '0')), +) + +CHUNK_OVERLAP = ConfigVar( + 'CHUNK_OVERLAP', + 'rag.chunk_overlap', + int(os.getenv('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 = ConfigVar( + 'RAG_TEMPLATE', + 'rag.template', + os.getenv('RAG_TEMPLATE', DEFAULT_RAG_TEMPLATE), +) + +RAG_OPENAI_API_BASE_URL = ConfigVar( + '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 = ConfigVar( + 'RAG_OPENAI_API_KEY', + 'rag.openai_api_key', + os.getenv('RAG_OPENAI_API_KEY', OPENAI_API_KEY), +) + +RAG_AZURE_OPENAI_BASE_URL = ConfigVar( + 'RAG_AZURE_OPENAI_BASE_URL', + 'rag.azure_openai.base_url', + os.getenv('RAG_AZURE_OPENAI_BASE_URL', ''), +) +RAG_AZURE_OPENAI_API_KEY = ConfigVar( + 'RAG_AZURE_OPENAI_API_KEY', + 'rag.azure_openai.api_key', + os.getenv('RAG_AZURE_OPENAI_API_KEY', ''), +) +RAG_AZURE_OPENAI_API_VERSION = ConfigVar( + 'RAG_AZURE_OPENAI_API_VERSION', + 'rag.azure_openai.api_version', + os.getenv('RAG_AZURE_OPENAI_API_VERSION', ''), +) + +RAG_OLLAMA_BASE_URL = ConfigVar( + 'RAG_OLLAMA_BASE_URL', + 'rag.ollama.url', + os.getenv('RAG_OLLAMA_BASE_URL', OLLAMA_BASE_URL), +) + +RAG_OLLAMA_API_KEY = ConfigVar( + '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 = ConfigVar( + 'YOUTUBE_LOADER_LANGUAGE', + 'rag.youtube_loader_language', + os.getenv('YOUTUBE_LOADER_LANGUAGE', 'en').split(','), +) + +YOUTUBE_LOADER_PROXY_URL = ConfigVar( + 'YOUTUBE_LOADER_PROXY_URL', + 'rag.youtube_loader_proxy_url', + os.getenv('YOUTUBE_LOADER_PROXY_URL', ''), +) + + +#################################### +# Web Search (RAG) +#################################### + +ENABLE_WEB_SEARCH = ConfigVar( + 'ENABLE_WEB_SEARCH', + 'rag.web.search.enable', + os.getenv('ENABLE_WEB_SEARCH', 'False').lower() == 'true', +) + +WEB_SEARCH_ENGINE = ConfigVar( + 'WEB_SEARCH_ENGINE', + 'rag.web.search.engine', + os.getenv('WEB_SEARCH_ENGINE', ''), +) + +BYPASS_WEB_SEARCH_EMBEDDING_AND_RETRIEVAL = ConfigVar( + '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 = ConfigVar( + '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 = ConfigVar( + '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 = ConfigVar( + 'WEB_SEARCH_DOMAIN_FILTER_LIST', + 'rag.web.search.domain.filter_list', + web_search_domain_filter_list, +) + +WEB_SEARCH_CONCURRENT_REQUESTS = ConfigVar( + 'WEB_SEARCH_CONCURRENT_REQUESTS', + 'rag.web.search.concurrent_requests', + int(os.getenv('WEB_SEARCH_CONCURRENT_REQUESTS', '0')), +) + +WEB_FETCH_MAX_CONTENT_LENGTH = ConfigVar( + 'WEB_FETCH_MAX_CONTENT_LENGTH', + 'rag.web.fetch.max_content_length', + (int(os.getenv('WEB_FETCH_MAX_CONTENT_LENGTH')) if os.getenv('WEB_FETCH_MAX_CONTENT_LENGTH') else None), +) + +WEB_LOADER_ENGINE = ConfigVar( + 'WEB_LOADER_ENGINE', + 'rag.web.loader.engine', + os.getenv('WEB_LOADER_ENGINE', ''), +) + + +WEB_LOADER_CONCURRENT_REQUESTS = ConfigVar( + 'WEB_LOADER_CONCURRENT_REQUESTS', + 'rag.web.loader.concurrent_requests', + int(os.getenv('WEB_LOADER_CONCURRENT_REQUESTS', '10')), +) + +WEB_LOADER_TIMEOUT = ConfigVar( + 'WEB_LOADER_TIMEOUT', + 'rag.web.loader.timeout', + os.getenv('WEB_LOADER_TIMEOUT', ''), +) + + +ENABLE_WEB_LOADER_SSL_VERIFICATION = ConfigVar( + 'ENABLE_WEB_LOADER_SSL_VERIFICATION', + 'rag.web.loader.ssl_verification', + os.getenv('ENABLE_WEB_LOADER_SSL_VERIFICATION', 'True').lower() == 'true', +) + +WEB_SEARCH_TRUST_ENV = ConfigVar( + 'WEB_SEARCH_TRUST_ENV', + 'rag.web.search.trust_env', + os.getenv('WEB_SEARCH_TRUST_ENV', 'True').lower() == 'true', +) + + +OLLAMA_CLOUD_WEB_SEARCH_API_KEY = ConfigVar( + 'OLLAMA_CLOUD_WEB_SEARCH_API_KEY', + 'rag.web.search.ollama_cloud_api_key', + os.getenv('OLLAMA_CLOUD_API_KEY', ''), +) + +SEARXNG_QUERY_URL = ConfigVar( + 'SEARXNG_QUERY_URL', + 'rag.web.search.searxng_query_url', + os.getenv('SEARXNG_QUERY_URL', ''), +) + +SEARXNG_LANGUAGE = ConfigVar( + 'SEARXNG_LANGUAGE', + 'rag.web.search.searxng_language', + os.getenv('SEARXNG_LANGUAGE', 'all'), +) + +YACY_QUERY_URL = ConfigVar( + 'YACY_QUERY_URL', + 'rag.web.search.yacy_query_url', + os.getenv('YACY_QUERY_URL', ''), +) + +YACY_USERNAME = ConfigVar( + 'YACY_USERNAME', + 'rag.web.search.yacy_username', + os.getenv('YACY_USERNAME', ''), +) + +YACY_PASSWORD = ConfigVar( + 'YACY_PASSWORD', + 'rag.web.search.yacy_password', + os.getenv('YACY_PASSWORD', ''), +) + +GOOGLE_PSE_API_KEY = ConfigVar( + 'GOOGLE_PSE_API_KEY', + 'rag.web.search.google_pse_api_key', + os.getenv('GOOGLE_PSE_API_KEY', ''), +) + +GOOGLE_PSE_ENGINE_ID = ConfigVar( + 'GOOGLE_PSE_ENGINE_ID', + 'rag.web.search.google_pse_engine_id', + os.getenv('GOOGLE_PSE_ENGINE_ID', ''), +) + +BRAVE_SEARCH_API_KEY = ConfigVar( + 'BRAVE_SEARCH_API_KEY', + 'rag.web.search.brave_search_api_key', + os.getenv('BRAVE_SEARCH_API_KEY', ''), +) + +BRAVE_SEARCH_CONTEXT_TOKENS = ConfigVar( + 'BRAVE_SEARCH_CONTEXT_TOKENS', + 'rag.web.search.brave_search_context_tokens', + int(os.getenv('BRAVE_SEARCH_CONTEXT_TOKENS', '8192')), +) + +KAGI_SEARCH_API_KEY = ConfigVar( + 'KAGI_SEARCH_API_KEY', + 'rag.web.search.kagi_search_api_key', + os.getenv('KAGI_SEARCH_API_KEY', ''), +) + +MOJEEK_SEARCH_API_KEY = ConfigVar( + 'MOJEEK_SEARCH_API_KEY', + 'rag.web.search.mojeek_search_api_key', + os.getenv('MOJEEK_SEARCH_API_KEY', ''), +) + +BOCHA_SEARCH_API_KEY = ConfigVar( + 'BOCHA_SEARCH_API_KEY', + 'rag.web.search.bocha_search_api_key', + os.getenv('BOCHA_SEARCH_API_KEY', ''), +) + +SERPSTACK_API_KEY = ConfigVar( + 'SERPSTACK_API_KEY', + 'rag.web.search.serpstack_api_key', + os.getenv('SERPSTACK_API_KEY', ''), +) + +SERPSTACK_HTTPS = ConfigVar( + 'SERPSTACK_HTTPS', + 'rag.web.search.serpstack_https', + os.getenv('SERPSTACK_HTTPS', 'True').lower() == 'true', +) + +SERPER_API_KEY = ConfigVar( + 'SERPER_API_KEY', + 'rag.web.search.serper_api_key', + os.getenv('SERPER_API_KEY', ''), +) + +SERPLY_API_KEY = ConfigVar( + 'SERPLY_API_KEY', + 'rag.web.search.serply_api_key', + os.getenv('SERPLY_API_KEY', ''), +) + +DDGS_BACKEND = ConfigVar( + 'DDGS_BACKEND', + 'rag.web.search.ddgs_backend', + os.getenv('DDGS_BACKEND', 'auto'), +) + +JINA_API_KEY = ConfigVar( + 'JINA_API_KEY', + 'rag.web.search.jina_api_key', + os.getenv('JINA_API_KEY', ''), +) + +JINA_API_BASE_URL = ConfigVar( + 'JINA_API_BASE_URL', + 'rag.web.search.jina_api_base_url', + os.getenv('JINA_API_BASE_URL', ''), +) + +SEARCHAPI_API_KEY = ConfigVar( + 'SEARCHAPI_API_KEY', + 'rag.web.search.searchapi_api_key', + os.getenv('SEARCHAPI_API_KEY', ''), +) + +SEARCHAPI_ENGINE = ConfigVar( + 'SEARCHAPI_ENGINE', + 'rag.web.search.searchapi_engine', + os.getenv('SEARCHAPI_ENGINE', ''), +) + +SERPAPI_API_KEY = ConfigVar( + 'SERPAPI_API_KEY', + 'rag.web.search.serpapi_api_key', + os.getenv('SERPAPI_API_KEY', ''), +) + +SERPAPI_ENGINE = ConfigVar( + 'SERPAPI_ENGINE', + 'rag.web.search.serpapi_engine', + os.getenv('SERPAPI_ENGINE', ''), +) + +BING_SEARCH_V7_ENDPOINT = ConfigVar( + 'BING_SEARCH_V7_ENDPOINT', + 'rag.web.search.bing_search_v7_endpoint', + os.getenv('BING_SEARCH_V7_ENDPOINT', 'https://api.bing.microsoft.com/v7.0/search'), +) + +BING_SEARCH_V7_SUBSCRIPTION_KEY = ConfigVar( + 'BING_SEARCH_V7_SUBSCRIPTION_KEY', + 'rag.web.search.bing_search_v7_subscription_key', + os.getenv('BING_SEARCH_V7_SUBSCRIPTION_KEY', ''), +) + +AZURE_AI_SEARCH_API_KEY = ConfigVar( + 'AZURE_AI_SEARCH_API_KEY', + 'rag.web.search.azure_ai_search_api_key', + os.getenv('AZURE_AI_SEARCH_API_KEY', ''), +) + +AZURE_AI_SEARCH_ENDPOINT = ConfigVar( + 'AZURE_AI_SEARCH_ENDPOINT', + 'rag.web.search.azure_ai_search_endpoint', + os.getenv('AZURE_AI_SEARCH_ENDPOINT', ''), +) + +AZURE_AI_SEARCH_INDEX_NAME = ConfigVar( + 'AZURE_AI_SEARCH_INDEX_NAME', + 'rag.web.search.azure_ai_search_index_name', + os.getenv('AZURE_AI_SEARCH_INDEX_NAME', ''), +) + +EXA_API_KEY = ConfigVar( + 'EXA_API_KEY', + 'rag.web.search.exa_api_key', + os.getenv('EXA_API_KEY', ''), +) + +PERPLEXITY_API_KEY = ConfigVar( + 'PERPLEXITY_API_KEY', + 'rag.web.search.perplexity_api_key', + os.getenv('PERPLEXITY_API_KEY', ''), +) + +PERPLEXITY_MODEL = ConfigVar( + 'PERPLEXITY_MODEL', + 'rag.web.search.perplexity_model', + os.getenv('PERPLEXITY_MODEL', 'sonar'), +) + +PERPLEXITY_SEARCH_CONTEXT_USAGE = ConfigVar( + 'PERPLEXITY_SEARCH_CONTEXT_USAGE', + 'rag.web.search.perplexity_search_context_usage', + os.getenv('PERPLEXITY_SEARCH_CONTEXT_USAGE', 'medium'), +) + +PERPLEXITY_SEARCH_API_URL = ConfigVar( + '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 = ConfigVar( + 'SOUGOU_API_SID', + 'rag.web.search.sougou_api_sid', + os.getenv('SOUGOU_API_SID', ''), +) + +SOUGOU_API_SK = ConfigVar( + 'SOUGOU_API_SK', + 'rag.web.search.sougou_api_sk', + os.getenv('SOUGOU_API_SK', ''), +) + +TAVILY_API_KEY = ConfigVar( + 'TAVILY_API_KEY', + 'rag.web.search.tavily_api_key', + os.getenv('TAVILY_API_KEY', ''), +) + +TAVILY_EXTRACT_DEPTH = ConfigVar( + 'TAVILY_EXTRACT_DEPTH', + 'rag.web.search.tavily_extract_depth', + os.getenv('TAVILY_EXTRACT_DEPTH', 'basic'), +) + +PLAYWRIGHT_WS_URL = ConfigVar( + 'PLAYWRIGHT_WS_URL', + 'rag.web.loader.playwright_ws_url', + os.getenv('PLAYWRIGHT_WS_URL', ''), +) + +PLAYWRIGHT_TIMEOUT = ConfigVar( + 'PLAYWRIGHT_TIMEOUT', + 'rag.web.loader.playwright_timeout', + int(os.getenv('PLAYWRIGHT_TIMEOUT', '10000')), +) + +FIRECRAWL_API_KEY = ConfigVar( + 'FIRECRAWL_API_KEY', + 'rag.web.loader.firecrawl_api_key', + os.getenv('FIRECRAWL_API_KEY', ''), +) + +FIRECRAWL_API_BASE_URL = ConfigVar( + 'FIRECRAWL_API_BASE_URL', + 'rag.web.loader.firecrawl_api_url', + os.getenv('FIRECRAWL_API_BASE_URL', 'https://api.firecrawl.dev'), +) + +FIRECRAWL_TIMEOUT = ConfigVar( + 'FIRECRAWL_TIMEOUT', + 'rag.web.loader.firecrawl_timeout', + os.getenv('FIRECRAWL_TIMEOUT', ''), +) + +EXTERNAL_WEB_SEARCH_URL = ConfigVar( + 'EXTERNAL_WEB_SEARCH_URL', + 'rag.web.search.external_web_search_url', + os.getenv('EXTERNAL_WEB_SEARCH_URL', ''), +) + +EXTERNAL_WEB_SEARCH_API_KEY = ConfigVar( + 'EXTERNAL_WEB_SEARCH_API_KEY', + 'rag.web.search.external_web_search_api_key', + os.getenv('EXTERNAL_WEB_SEARCH_API_KEY', ''), +) + +EXTERNAL_WEB_LOADER_URL = ConfigVar( + 'EXTERNAL_WEB_LOADER_URL', + 'rag.web.loader.external_web_loader_url', + os.getenv('EXTERNAL_WEB_LOADER_URL', ''), +) + +EXTERNAL_WEB_LOADER_API_KEY = ConfigVar( + 'EXTERNAL_WEB_LOADER_API_KEY', + 'rag.web.loader.external_web_loader_api_key', + os.getenv('EXTERNAL_WEB_LOADER_API_KEY', ''), +) + +YANDEX_WEB_SEARCH_URL = ConfigVar( + 'YANDEX_WEB_SEARCH_URL', + 'rag.web.search.yandex_web_search_url', + os.getenv('YANDEX_WEB_SEARCH_URL', ''), +) + +YANDEX_WEB_SEARCH_API_KEY = ConfigVar( + 'YANDEX_WEB_SEARCH_API_KEY', + 'rag.web.search.yandex_web_search_api_key', + os.getenv('YANDEX_WEB_SEARCH_API_KEY', ''), +) + +YANDEX_WEB_SEARCH_CONFIG = ConfigVar( + 'YANDEX_WEB_SEARCH_CONFIG', + 'rag.web.search.yandex_web_search_config', + os.getenv('YANDEX_WEB_SEARCH_CONFIG', ''), +) + +YOUCOM_API_KEY = ConfigVar( + 'YOUCOM_API_KEY', + 'rag.web.search.youcom_api_key', + os.getenv('YOUCOM_API_KEY', ''), +) + +LINKUP_API_KEY = ConfigVar( + 'LINKUP_API_KEY', + 'rag.web.search.linkup_api_key', + os.getenv('LINKUP_API_KEY', ''), +) + +linkup_search_params = os.getenv('LINKUP_SEARCH_PARAMS', '') +try: + linkup_search_params = json.loads(linkup_search_params) +except json.JSONDecodeError: + linkup_search_params = {} + +LINKUP_SEARCH_PARAMS = ConfigVar( + 'LINKUP_SEARCH_PARAMS', + 'rag.web.search.linkup_search_params', + linkup_search_params, +) + +#################################### +# Images +#################################### + +ENABLE_IMAGE_GENERATION = ConfigVar( + 'ENABLE_IMAGE_GENERATION', + 'image_generation.enable', + os.getenv('ENABLE_IMAGE_GENERATION', '').lower() == 'true', +) + +IMAGE_GENERATION_ENGINE = ConfigVar( + 'IMAGE_GENERATION_ENGINE', + 'image_generation.engine', + os.getenv('IMAGE_GENERATION_ENGINE', 'openai'), +) + +IMAGE_GENERATION_MODEL = ConfigVar( + '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 = ConfigVar('IMAGE_SIZE', 'image_generation.size', os.getenv('IMAGE_SIZE', '512x512')) + +IMAGE_STEPS = ConfigVar('IMAGE_STEPS', 'image_generation.steps', int(os.getenv('IMAGE_STEPS', 50))) + +ENABLE_IMAGE_PROMPT_GENERATION = ConfigVar( + 'ENABLE_IMAGE_PROMPT_GENERATION', + 'image_generation.prompt.enable', + os.getenv('ENABLE_IMAGE_PROMPT_GENERATION', 'true').lower() == 'true', +) + +AUTOMATIC1111_BASE_URL = ConfigVar( + 'AUTOMATIC1111_BASE_URL', + 'image_generation.automatic1111.base_url', + os.getenv('AUTOMATIC1111_BASE_URL', ''), +) +AUTOMATIC1111_API_AUTH = ConfigVar( + '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 = ConfigVar( + 'AUTOMATIC1111_PARAMS', + 'image_generation.automatic1111.api_params', + automatic1111_params, +) + +COMFYUI_BASE_URL = ConfigVar( + 'COMFYUI_BASE_URL', + 'image_generation.comfyui.base_url', + os.getenv('COMFYUI_BASE_URL', ''), +) + +COMFYUI_API_KEY = ConfigVar( + '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 = ConfigVar( + '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 = ConfigVar( + 'COMFYUI_WORKFLOW_NODES', + 'image_generation.comfyui.nodes', + comfyui_workflow_nodes, +) + +IMAGES_OPENAI_API_BASE_URL = ConfigVar( + '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 = ConfigVar( + 'IMAGES_OPENAI_API_VERSION', + 'image_generation.openai.api_version', + os.getenv('IMAGES_OPENAI_API_VERSION', ''), +) + +IMAGES_OPENAI_API_KEY = ConfigVar( + '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 = ConfigVar('IMAGES_OPENAI_API_PARAMS', 'image_generation.openai.params', images_openai_params) + + +IMAGES_GEMINI_API_BASE_URL = ConfigVar( + '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 = ConfigVar( + 'IMAGES_GEMINI_API_KEY', + 'image_generation.gemini.api_key', + os.getenv('IMAGES_GEMINI_API_KEY', GEMINI_API_KEY), +) + +IMAGES_GEMINI_ENDPOINT_METHOD = ConfigVar( + 'IMAGES_GEMINI_ENDPOINT_METHOD', + 'image_generation.gemini.endpoint_method', + os.getenv('IMAGES_GEMINI_ENDPOINT_METHOD', ''), +) + +ENABLE_IMAGE_EDIT = ConfigVar( + 'ENABLE_IMAGE_EDIT', + 'images.edit.enable', + os.getenv('ENABLE_IMAGE_EDIT', '').lower() == 'true', +) + +IMAGE_EDIT_ENGINE = ConfigVar( + 'IMAGE_EDIT_ENGINE', + 'images.edit.engine', + os.getenv('IMAGE_EDIT_ENGINE', 'openai'), +) + +IMAGE_EDIT_MODEL = ConfigVar( + 'IMAGE_EDIT_MODEL', + 'images.edit.model', + os.getenv('IMAGE_EDIT_MODEL', ''), +) + +IMAGE_EDIT_SIZE = ConfigVar('IMAGE_EDIT_SIZE', 'images.edit.size', os.getenv('IMAGE_EDIT_SIZE', '')) + +IMAGES_EDIT_OPENAI_API_BASE_URL = ConfigVar( + '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 = ConfigVar( + 'IMAGES_EDIT_OPENAI_API_VERSION', + 'images.edit.openai.api_version', + os.getenv('IMAGES_EDIT_OPENAI_API_VERSION', ''), +) + +IMAGES_EDIT_OPENAI_API_KEY = ConfigVar( + '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 = ConfigVar( + '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 = ConfigVar( + '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 = ConfigVar( + 'IMAGES_EDIT_COMFYUI_BASE_URL', + 'images.edit.comfyui.base_url', + os.getenv('IMAGES_EDIT_COMFYUI_BASE_URL', ''), +) +IMAGES_EDIT_COMFYUI_API_KEY = ConfigVar( + 'IMAGES_EDIT_COMFYUI_API_KEY', + 'images.edit.comfyui.api_key', + os.getenv('IMAGES_EDIT_COMFYUI_API_KEY', ''), +) + +IMAGES_EDIT_COMFYUI_WORKFLOW = ConfigVar( + '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 = ConfigVar( + 'IMAGES_EDIT_COMFYUI_WORKFLOW_NODES', + 'images.edit.comfyui.nodes', + images_edit_comfyui_workflow_nodes, +) + +#################################### +# Audio +#################################### + +# Transcription +WHISPER_MODEL = ConfigVar( + '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.getenv('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 = ConfigVar( + '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 = ConfigVar( + '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 = ConfigVar( + 'AUDIO_STT_OPENAI_API_KEY', + 'audio.stt.openai.api_key', + os.getenv('AUDIO_STT_OPENAI_API_KEY', OPENAI_API_KEY), +) + +AUDIO_STT_ENGINE = ConfigVar( + 'AUDIO_STT_ENGINE', + 'audio.stt.engine', + os.getenv('AUDIO_STT_ENGINE', ''), +) + +AUDIO_STT_MODEL = ConfigVar( + 'AUDIO_STT_MODEL', + 'audio.stt.model', + os.getenv('AUDIO_STT_MODEL', ''), +) + +AUDIO_STT_SUPPORTED_CONTENT_TYPES = ConfigVar( + 'AUDIO_STT_SUPPORTED_CONTENT_TYPES', + 'audio.stt.supported_content_types', + [ + content_type.strip() + for content_type in os.getenv('AUDIO_STT_SUPPORTED_CONTENT_TYPES', '').split(',') + if content_type.strip() + ], +) + +AUDIO_STT_ALLOWED_EXTENSIONS = ConfigVar( + 'AUDIO_STT_ALLOWED_EXTENSIONS', + 'audio.stt.allowed_extensions', + [ + ext.strip() + for ext in os.getenv( + 'AUDIO_STT_ALLOWED_EXTENSIONS', + 'mp3,wav,m4a,webm,ogg,flac,mp4,mpga,mpeg', + ).split(',') + if ext.strip() + ], +) + +AUDIO_STT_AZURE_API_KEY = ConfigVar( + 'AUDIO_STT_AZURE_API_KEY', + 'audio.stt.azure.api_key', + os.getenv('AUDIO_STT_AZURE_API_KEY', ''), +) + +AUDIO_STT_AZURE_REGION = ConfigVar( + 'AUDIO_STT_AZURE_REGION', + 'audio.stt.azure.region', + os.getenv('AUDIO_STT_AZURE_REGION', ''), +) + +AUDIO_STT_AZURE_LOCALES = ConfigVar( + 'AUDIO_STT_AZURE_LOCALES', + 'audio.stt.azure.locales', + os.getenv('AUDIO_STT_AZURE_LOCALES', ''), +) + +AUDIO_STT_AZURE_BASE_URL = ConfigVar( + 'AUDIO_STT_AZURE_BASE_URL', + 'audio.stt.azure.base_url', + os.getenv('AUDIO_STT_AZURE_BASE_URL', ''), +) + +AUDIO_STT_AZURE_MAX_SPEAKERS = ConfigVar( + 'AUDIO_STT_AZURE_MAX_SPEAKERS', + 'audio.stt.azure.max_speakers', + os.getenv('AUDIO_STT_AZURE_MAX_SPEAKERS', ''), +) + +AUDIO_STT_MISTRAL_API_KEY = ConfigVar( + 'AUDIO_STT_MISTRAL_API_KEY', + 'audio.stt.mistral.api_key', + os.getenv('AUDIO_STT_MISTRAL_API_KEY', ''), +) + +AUDIO_STT_MISTRAL_API_BASE_URL = ConfigVar( + '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 = ConfigVar( + '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 = ConfigVar( + '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 = ConfigVar( + '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 = ConfigVar( + 'AUDIO_TTS_OPENAI_PARAMS', + 'audio.tts.openai.params', + audio_tts_openai_params, +) + + +AUDIO_TTS_API_KEY = ConfigVar( + 'AUDIO_TTS_API_KEY', + 'audio.tts.api_key', + os.getenv('AUDIO_TTS_API_KEY', ''), +) + +AUDIO_TTS_ENGINE = ConfigVar( + 'AUDIO_TTS_ENGINE', + 'audio.tts.engine', + os.getenv('AUDIO_TTS_ENGINE', ''), +) + + +AUDIO_TTS_MODEL = ConfigVar( + 'AUDIO_TTS_MODEL', + 'audio.tts.model', + os.getenv('AUDIO_TTS_MODEL', 'tts-1'), # OpenAI default model +) + +AUDIO_TTS_VOICE = ConfigVar( + 'AUDIO_TTS_VOICE', + 'audio.tts.voice', + os.getenv('AUDIO_TTS_VOICE', 'alloy'), # OpenAI default voice +) + +AUDIO_TTS_SPLIT_ON = ConfigVar( + 'AUDIO_TTS_SPLIT_ON', + 'audio.tts.split_on', + os.getenv('AUDIO_TTS_SPLIT_ON', 'punctuation'), +) + +AUDIO_TTS_AZURE_SPEECH_REGION = ConfigVar( + 'AUDIO_TTS_AZURE_SPEECH_REGION', + 'audio.tts.azure.speech_region', + os.getenv('AUDIO_TTS_AZURE_SPEECH_REGION', ''), +) + +AUDIO_TTS_AZURE_SPEECH_BASE_URL = ConfigVar( + '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 = ConfigVar( + '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 = ConfigVar( + 'AUDIO_TTS_MISTRAL_API_KEY', + 'audio.tts.mistral.api_key', + os.getenv('AUDIO_TTS_MISTRAL_API_KEY', ''), +) + +AUDIO_TTS_MISTRAL_API_BASE_URL = ConfigVar( + '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'), +) + +#################################### +# WEBUI +#################################### + + +WEBUI_URL = ConfigVar('WEBUI_URL', 'webui.url', os.getenv('WEBUI_URL', '')) + + +ENABLE_SIGNUP = ConfigVar( + 'ENABLE_SIGNUP', + 'ui.enable_signup', + (False if not WEBUI_AUTH else os.getenv('ENABLE_SIGNUP', 'True').lower() == 'true'), +) + +ENABLE_LOGIN_FORM = ConfigVar( + 'ENABLE_LOGIN_FORM', + 'ui.enable_login_form', + os.getenv('ENABLE_LOGIN_FORM', 'True').lower() == 'true', +) + +ENABLE_PASSWORD_CHANGE_FORM = ConfigVar( + 'ENABLE_PASSWORD_CHANGE_FORM', + 'ui.enable_password_change_form', + os.getenv('ENABLE_PASSWORD_CHANGE_FORM', 'True').lower() == 'true', +) + +ENABLE_PASSWORD_AUTH = os.getenv('ENABLE_PASSWORD_AUTH', 'True').lower() == 'true' + +DEFAULT_LOCALE = ConfigVar( + 'DEFAULT_LOCALE', + 'ui.default_locale', + os.getenv('DEFAULT_LOCALE', ''), +) + +DEFAULT_MODELS = ConfigVar('DEFAULT_MODELS', 'ui.default_models', os.getenv('DEFAULT_MODELS', None)) + +DEFAULT_PINNED_MODELS = ConfigVar( + 'DEFAULT_PINNED_MODELS', + 'ui.default_pinned_models', + os.getenv('DEFAULT_PINNED_MODELS', None), +) + +try: + default_prompt_suggestions = json.loads(os.getenv('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 = ConfigVar( + 'DEFAULT_PROMPT_SUGGESTIONS', + 'ui.prompt_suggestions', + default_prompt_suggestions, +) + +MODEL_ORDER_LIST = ConfigVar( + 'MODEL_ORDER_LIST', + 'ui.model_order_list', + [], +) + +try: + default_model_metadata = json.loads(os.getenv('DEFAULT_MODEL_METADATA', '{}')) +except Exception as e: + log.exception(f'Error loading DEFAULT_MODEL_METADATA: {e}') + default_model_metadata = {} + +DEFAULT_MODEL_METADATA = ConfigVar( + 'DEFAULT_MODEL_METADATA', + 'models.default_metadata', + default_model_metadata, +) + +try: + default_model_params = json.loads(os.getenv('DEFAULT_MODEL_PARAMS', '{}')) +except Exception as e: + log.exception(f'Error loading DEFAULT_MODEL_PARAMS: {e}') + default_model_params = {} + +DEFAULT_MODEL_PARAMS = ConfigVar( + 'DEFAULT_MODEL_PARAMS', + 'models.default_params', + default_model_params, +) + +DEFAULT_USER_ROLE = ConfigVar( + 'DEFAULT_USER_ROLE', + 'ui.default_user_role', + os.getenv('DEFAULT_USER_ROLE', 'pending'), +) + +DEFAULT_GROUP_ID = ConfigVar( + 'DEFAULT_GROUP_ID', + 'ui.default_group_id', + os.getenv('DEFAULT_GROUP_ID', ''), +) + +PENDING_USER_OVERLAY_TITLE = ConfigVar( + 'PENDING_USER_OVERLAY_TITLE', + 'ui.pending_user_overlay_title', + os.getenv('PENDING_USER_OVERLAY_TITLE', ''), +) + +PENDING_USER_OVERLAY_CONTENT = ConfigVar( + 'PENDING_USER_OVERLAY_CONTENT', + 'ui.pending_user_overlay_content', + os.getenv('PENDING_USER_OVERLAY_CONTENT', ''), +) + + +RESPONSE_WATERMARK = ConfigVar( + 'RESPONSE_WATERMARK', + 'ui.watermark', + os.getenv('RESPONSE_WATERMARK', ''), +) + +IFRAME_CSP = os.getenv('IFRAME_CSP', '') + +USER_PERMISSIONS_WORKSPACE_MODELS_ACCESS = ( + os.getenv('USER_PERMISSIONS_WORKSPACE_MODELS_ACCESS', 'False').lower() == 'true' +) + +USER_PERMISSIONS_WORKSPACE_KNOWLEDGE_ACCESS = ( + os.getenv('USER_PERMISSIONS_WORKSPACE_KNOWLEDGE_ACCESS', 'False').lower() == 'true' +) + +USER_PERMISSIONS_WORKSPACE_PROMPTS_ACCESS = ( + os.getenv('USER_PERMISSIONS_WORKSPACE_PROMPTS_ACCESS', 'False').lower() == 'true' +) + +USER_PERMISSIONS_WORKSPACE_TOOLS_ACCESS = ( + os.getenv('USER_PERMISSIONS_WORKSPACE_TOOLS_ACCESS', 'False').lower() == 'true' +) + +USER_PERMISSIONS_WORKSPACE_SKILLS_ACCESS = ( + os.getenv('USER_PERMISSIONS_WORKSPACE_SKILLS_ACCESS', 'False').lower() == 'true' +) + +USER_PERMISSIONS_WORKSPACE_MODELS_IMPORT = ( + os.getenv('USER_PERMISSIONS_WORKSPACE_MODELS_IMPORT', 'False').lower() == 'true' +) + +USER_PERMISSIONS_WORKSPACE_MODELS_EXPORT = ( + os.getenv('USER_PERMISSIONS_WORKSPACE_MODELS_EXPORT', 'False').lower() == 'true' +) + +USER_PERMISSIONS_WORKSPACE_PROMPTS_IMPORT = ( + os.getenv('USER_PERMISSIONS_WORKSPACE_PROMPTS_IMPORT', 'False').lower() == 'true' +) + +USER_PERMISSIONS_WORKSPACE_PROMPTS_EXPORT = ( + os.getenv('USER_PERMISSIONS_WORKSPACE_PROMPTS_EXPORT', 'False').lower() == 'true' +) + +USER_PERMISSIONS_WORKSPACE_TOOLS_IMPORT = ( + os.getenv('USER_PERMISSIONS_WORKSPACE_TOOLS_IMPORT', 'False').lower() == 'true' +) + +USER_PERMISSIONS_WORKSPACE_TOOLS_EXPORT = ( + os.getenv('USER_PERMISSIONS_WORKSPACE_TOOLS_EXPORT', 'False').lower() == 'true' +) + + +USER_PERMISSIONS_WORKSPACE_MODELS_ALLOW_SHARING = ( + os.getenv('USER_PERMISSIONS_WORKSPACE_MODELS_ALLOW_SHARING', 'False').lower() == 'true' +) + +USER_PERMISSIONS_WORKSPACE_MODELS_ALLOW_PUBLIC_SHARING = ( + os.getenv('USER_PERMISSIONS_WORKSPACE_MODELS_ALLOW_PUBLIC_SHARING', 'False').lower() == 'true' +) + +USER_PERMISSIONS_WORKSPACE_KNOWLEDGE_ALLOW_SHARING = ( + os.getenv('USER_PERMISSIONS_WORKSPACE_KNOWLEDGE_ALLOW_SHARING', 'False').lower() == 'true' +) + +USER_PERMISSIONS_WORKSPACE_KNOWLEDGE_ALLOW_PUBLIC_SHARING = ( + os.getenv('USER_PERMISSIONS_WORKSPACE_KNOWLEDGE_ALLOW_PUBLIC_SHARING', 'False').lower() == 'true' +) + +USER_PERMISSIONS_WORKSPACE_PROMPTS_ALLOW_SHARING = ( + os.getenv('USER_PERMISSIONS_WORKSPACE_PROMPTS_ALLOW_SHARING', 'False').lower() == 'true' +) + +USER_PERMISSIONS_WORKSPACE_PROMPTS_ALLOW_PUBLIC_SHARING = ( + os.getenv('USER_PERMISSIONS_WORKSPACE_PROMPTS_ALLOW_PUBLIC_SHARING', 'False').lower() == 'true' +) + + +USER_PERMISSIONS_WORKSPACE_TOOLS_ALLOW_SHARING = ( + os.getenv('USER_PERMISSIONS_WORKSPACE_TOOLS_ALLOW_SHARING', 'False').lower() == 'true' +) + +USER_PERMISSIONS_WORKSPACE_TOOLS_ALLOW_PUBLIC_SHARING = ( + os.getenv('USER_PERMISSIONS_WORKSPACE_TOOLS_ALLOW_PUBLIC_SHARING', 'False').lower() == 'true' +) + +USER_PERMISSIONS_WORKSPACE_SKILLS_ALLOW_SHARING = ( + os.getenv('USER_PERMISSIONS_WORKSPACE_SKILLS_ALLOW_SHARING', 'False').lower() == 'true' +) + +USER_PERMISSIONS_WORKSPACE_SKILLS_ALLOW_PUBLIC_SHARING = ( + os.getenv('USER_PERMISSIONS_WORKSPACE_SKILLS_ALLOW_PUBLIC_SHARING', 'False').lower() == 'true' +) + + +USER_PERMISSIONS_NOTES_ALLOW_SHARING = os.getenv('USER_PERMISSIONS_NOTES_ALLOW_SHARING', 'False').lower() == 'true' + +USER_PERMISSIONS_NOTES_ALLOW_PUBLIC_SHARING = ( + os.getenv('USER_PERMISSIONS_NOTES_ALLOW_PUBLIC_SHARING', 'False').lower() == 'true' +) + +USER_PERMISSIONS_CALENDAR_ALLOW_PUBLIC_SHARING = ( + os.getenv('USER_PERMISSIONS_CALENDAR_ALLOW_PUBLIC_SHARING', 'False').lower() == 'true' +) + +USER_PERMISSIONS_ACCESS_GRANTS_ALLOW_USERS = ( + os.getenv('USER_PERMISSIONS_ACCESS_GRANTS_ALLOW_USERS', 'True').lower() == 'true' +) + + +USER_PERMISSIONS_CHAT_CONTROLS = os.getenv('USER_PERMISSIONS_CHAT_CONTROLS', 'True').lower() == 'true' + +USER_PERMISSIONS_CHAT_VALVES = os.getenv('USER_PERMISSIONS_CHAT_VALVES', 'True').lower() == 'true' + +USER_PERMISSIONS_CHAT_SYSTEM_PROMPT = os.getenv('USER_PERMISSIONS_CHAT_SYSTEM_PROMPT', 'True').lower() == 'true' + +USER_PERMISSIONS_CHAT_PARAMS = os.getenv('USER_PERMISSIONS_CHAT_PARAMS', 'True').lower() == 'true' + +USER_PERMISSIONS_CHAT_FILE_UPLOAD = os.getenv('USER_PERMISSIONS_CHAT_FILE_UPLOAD', 'True').lower() == 'true' + +USER_PERMISSIONS_CHAT_WEB_UPLOAD = os.getenv('USER_PERMISSIONS_CHAT_WEB_UPLOAD', 'True').lower() == 'true' + +USER_PERMISSIONS_CHAT_DELETE = os.getenv('USER_PERMISSIONS_CHAT_DELETE', 'True').lower() == 'true' + +USER_PERMISSIONS_CHAT_DELETE_MESSAGE = os.getenv('USER_PERMISSIONS_CHAT_DELETE_MESSAGE', 'True').lower() == 'true' + +USER_PERMISSIONS_CHAT_CONTINUE_RESPONSE = os.getenv('USER_PERMISSIONS_CHAT_CONTINUE_RESPONSE', 'True').lower() == 'true' + +USER_PERMISSIONS_CHAT_REGENERATE_RESPONSE = ( + os.getenv('USER_PERMISSIONS_CHAT_REGENERATE_RESPONSE', 'True').lower() == 'true' +) + +USER_PERMISSIONS_CHAT_RATE_RESPONSE = os.getenv('USER_PERMISSIONS_CHAT_RATE_RESPONSE', 'True').lower() == 'true' + +USER_PERMISSIONS_CHAT_EDIT = os.getenv('USER_PERMISSIONS_CHAT_EDIT', 'True').lower() == 'true' + +USER_PERMISSIONS_CHAT_SHARE = os.getenv('USER_PERMISSIONS_CHAT_SHARE', 'True').lower() == 'true' + +USER_PERMISSIONS_CHAT_ALLOW_PUBLIC_SHARING = ( + os.getenv('USER_PERMISSIONS_CHAT_ALLOW_PUBLIC_SHARING', 'False').lower() == 'true' +) + +USER_PERMISSIONS_CHAT_EXPORT = os.getenv('USER_PERMISSIONS_CHAT_EXPORT', 'True').lower() == 'true' + +USER_PERMISSIONS_CHAT_STT = os.getenv('USER_PERMISSIONS_CHAT_STT', 'True').lower() == 'true' + +USER_PERMISSIONS_CHAT_TTS = os.getenv('USER_PERMISSIONS_CHAT_TTS', 'True').lower() == 'true' + +USER_PERMISSIONS_CHAT_CALL = os.getenv('USER_PERMISSIONS_CHAT_CALL', 'True').lower() == 'true' + +USER_PERMISSIONS_CHAT_MULTIPLE_MODELS = os.getenv('USER_PERMISSIONS_CHAT_MULTIPLE_MODELS', 'True').lower() == 'true' + +USER_PERMISSIONS_CHAT_TEMPORARY = os.getenv('USER_PERMISSIONS_CHAT_TEMPORARY', 'True').lower() == 'true' + +USER_PERMISSIONS_CHAT_TEMPORARY_ENFORCED = ( + os.getenv('USER_PERMISSIONS_CHAT_TEMPORARY_ENFORCED', 'False').lower() == 'true' +) + + +USER_PERMISSIONS_FEATURES_DIRECT_TOOL_SERVERS = ( + os.getenv('USER_PERMISSIONS_FEATURES_DIRECT_TOOL_SERVERS', 'False').lower() == 'true' +) + +USER_PERMISSIONS_FEATURES_WEB_SEARCH = os.getenv('USER_PERMISSIONS_FEATURES_WEB_SEARCH', 'True').lower() == 'true' + +USER_PERMISSIONS_FEATURES_IMAGE_GENERATION = ( + os.getenv('USER_PERMISSIONS_FEATURES_IMAGE_GENERATION', 'True').lower() == 'true' +) + +USER_PERMISSIONS_FEATURES_CODE_INTERPRETER = ( + os.getenv('USER_PERMISSIONS_FEATURES_CODE_INTERPRETER', 'True').lower() == 'true' +) + +USER_PERMISSIONS_FEATURES_FOLDERS = os.getenv('USER_PERMISSIONS_FEATURES_FOLDERS', 'True').lower() == 'true' + +USER_PERMISSIONS_FEATURES_NOTES = os.getenv('USER_PERMISSIONS_FEATURES_NOTES', 'True').lower() == 'true' + +USER_PERMISSIONS_FEATURES_CHANNELS = os.getenv('USER_PERMISSIONS_FEATURES_CHANNELS', 'True').lower() == 'true' + +USER_PERMISSIONS_FEATURES_API_KEYS = os.getenv('USER_PERMISSIONS_FEATURES_API_KEYS', 'False').lower() == 'true' + +USER_PERMISSIONS_FEATURES_MEMORIES = os.getenv('USER_PERMISSIONS_FEATURES_MEMORIES', 'True').lower() == 'true' + +USER_PERMISSIONS_FEATURES_AUTOMATIONS = os.getenv('USER_PERMISSIONS_FEATURES_AUTOMATIONS', 'False').lower() == 'true' + +USER_PERMISSIONS_FEATURES_CALENDAR = os.getenv('USER_PERMISSIONS_FEATURES_CALENDAR', 'True').lower() == 'true' + + +USER_PERMISSIONS_SETTINGS_INTERFACE = os.getenv('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, + 'public_chats': USER_PERMISSIONS_CHAT_ALLOW_PUBLIC_SHARING, + 'public_calendars': USER_PERMISSIONS_CALENDAR_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 = ConfigVar( + 'USER_PERMISSIONS', + 'user.permissions', + DEFAULT_USER_PERMISSIONS, +) + +ENABLE_FOLDERS = ConfigVar( + 'ENABLE_FOLDERS', + 'folders.enable', + os.getenv('ENABLE_FOLDERS', 'True').lower() == 'true', +) + +FOLDER_MAX_FILE_COUNT = ConfigVar( + 'FOLDER_MAX_FILE_COUNT', + 'folders.max_file_count', + os.getenv('FOLDER_MAX_FILE_COUNT', ''), +) + +ENABLE_CHANNELS = ConfigVar( + 'ENABLE_CHANNELS', + 'channels.enable', + os.getenv('ENABLE_CHANNELS', 'False').lower() == 'true', +) + +ENABLE_CALENDAR = ConfigVar( + 'ENABLE_CALENDAR', + 'calendar.enable', + os.getenv('ENABLE_CALENDAR', 'True').lower() == 'true', +) + +ENABLE_AUTOMATIONS = ConfigVar( + 'ENABLE_AUTOMATIONS', + 'automations.enable', + os.getenv('ENABLE_AUTOMATIONS', 'True').lower() == 'true', +) + +AUTOMATION_MAX_COUNT = ConfigVar( + 'AUTOMATION_MAX_COUNT', + 'automations.max_count', + os.getenv('AUTOMATION_MAX_COUNT', ''), +) + +AUTOMATION_MIN_INTERVAL = ConfigVar( + 'AUTOMATION_MIN_INTERVAL', + 'automations.min_interval', + os.getenv('AUTOMATION_MIN_INTERVAL', ''), +) + +ENABLE_NOTES = ConfigVar( + 'ENABLE_NOTES', + 'notes.enable', + os.getenv('ENABLE_NOTES', 'True').lower() == 'true', +) + +ENABLE_USER_STATUS = ConfigVar( + 'ENABLE_USER_STATUS', + 'users.enable_status', + os.getenv('ENABLE_USER_STATUS', 'True').lower() == 'true', +) + +ENABLE_EVALUATION_ARENA_MODELS = ConfigVar( + 'ENABLE_EVALUATION_ARENA_MODELS', + 'evaluation.arena.enable', + os.getenv('ENABLE_EVALUATION_ARENA_MODELS', 'True').lower() == 'true', +) +EVALUATION_ARENA_MODELS = ConfigVar( + '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 = ConfigVar('WEBHOOK_URL', 'webhook_url', os.getenv('WEBHOOK_URL', '')) + +ENABLE_ADMIN_EXPORT = os.getenv('ENABLE_ADMIN_EXPORT', 'True').lower() == 'true' + +ENABLE_ADMIN_WORKSPACE_CONTENT_ACCESS = os.getenv('ENABLE_ADMIN_WORKSPACE_CONTENT_ACCESS', 'True').lower() == 'true' + +BYPASS_ADMIN_ACCESS_CONTROL = ( + os.getenv( + 'BYPASS_ADMIN_ACCESS_CONTROL', + os.getenv('ENABLE_ADMIN_WORKSPACE_CONTENT_ACCESS', 'True'), + ).lower() + == 'true' +) + +ENABLE_ADMIN_CHAT_ACCESS = os.getenv('ENABLE_ADMIN_CHAT_ACCESS', 'True').lower() == 'true' + +ENABLE_ADMIN_ANALYTICS = os.getenv('ENABLE_ADMIN_ANALYTICS', 'True').lower() == 'true' + +ENABLE_COMMUNITY_SHARING = ConfigVar( + 'ENABLE_COMMUNITY_SHARING', + 'ui.enable_community_sharing', + os.getenv('ENABLE_COMMUNITY_SHARING', 'True').lower() == 'true', +) + +ENABLE_MESSAGE_RATING = ConfigVar( + 'ENABLE_MESSAGE_RATING', + 'ui.enable_message_rating', + os.getenv('ENABLE_MESSAGE_RATING', 'True').lower() == 'true', +) + +ENABLE_USER_WEBHOOKS = ConfigVar( + 'ENABLE_USER_WEBHOOKS', + 'ui.enable_user_webhooks', + os.getenv('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.getenv('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.getenv('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: str | None = None + content: str + dismissible: bool + timestamp: int + + +try: + banners = json.loads(os.getenv('WEBUI_BANNERS', '[]')) + banners = [BannerModel(**banner) for banner in banners] +except Exception as e: + log.exception(f'Error loading WEBUI_BANNERS: {e}') + banners = [] + +WEBUI_BANNERS = ConfigVar('WEBUI_BANNERS', 'ui.banners', banners) + + +SHOW_ADMIN_DETAILS = ConfigVar( + 'SHOW_ADMIN_DETAILS', + 'auth.admin.show', + os.getenv('SHOW_ADMIN_DETAILS', 'true').lower() == 'true', +) + +ADMIN_EMAIL = ConfigVar( + 'ADMIN_EMAIL', + 'auth.admin.email', + os.getenv('ADMIN_EMAIL', None), +) + + +#################################### +# TASKS +#################################### + + +TASK_MODEL = ConfigVar( + 'TASK_MODEL', + 'task.model.default', + os.getenv('TASK_MODEL', ''), +) + +TASK_MODEL_EXTERNAL = ConfigVar( + 'TASK_MODEL_EXTERNAL', + 'task.model.external', + os.getenv('TASK_MODEL_EXTERNAL', ''), +) + +TITLE_GENERATION_PROMPT_TEMPLATE = ConfigVar( + 'TITLE_GENERATION_PROMPT_TEMPLATE', + 'task.title.prompt_template', + os.getenv('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 = ConfigVar( + 'TAGS_GENERATION_PROMPT_TEMPLATE', + 'task.tags.prompt_template', + os.getenv('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 = ConfigVar( + 'IMAGE_PROMPT_GENERATION_PROMPT_TEMPLATE', + 'task.image.prompt_template', + os.getenv('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 = ConfigVar( + 'FOLLOW_UP_GENERATION_PROMPT_TEMPLATE', + 'task.follow_up.prompt_template', + os.getenv('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 = ConfigVar( + 'ENABLE_FOLLOW_UP_GENERATION', + 'task.follow_up.enable', + os.getenv('ENABLE_FOLLOW_UP_GENERATION', 'True').lower() == 'true', +) + +ENABLE_TAGS_GENERATION = ConfigVar( + 'ENABLE_TAGS_GENERATION', + 'task.tags.enable', + os.getenv('ENABLE_TAGS_GENERATION', 'True').lower() == 'true', +) + +ENABLE_TITLE_GENERATION = ConfigVar( + 'ENABLE_TITLE_GENERATION', + 'task.title.enable', + os.getenv('ENABLE_TITLE_GENERATION', 'True').lower() == 'true', +) + + +ENABLE_SEARCH_QUERY_GENERATION = ConfigVar( + 'ENABLE_SEARCH_QUERY_GENERATION', + 'task.query.search.enable', + os.getenv('ENABLE_SEARCH_QUERY_GENERATION', 'True').lower() == 'true', +) + +ENABLE_RETRIEVAL_QUERY_GENERATION = ConfigVar( + 'ENABLE_RETRIEVAL_QUERY_GENERATION', + 'task.query.retrieval.enable', + os.getenv('ENABLE_RETRIEVAL_QUERY_GENERATION', 'True').lower() == 'true', +) + + +QUERY_GENERATION_PROMPT_TEMPLATE = ConfigVar( + 'QUERY_GENERATION_PROMPT_TEMPLATE', + 'task.query.prompt_template', + os.getenv('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 = ConfigVar( + 'ENABLE_AUTOCOMPLETE_GENERATION', + 'task.autocomplete.enable', + os.getenv('ENABLE_AUTOCOMPLETE_GENERATION', 'False').lower() == 'true', +) + +AUTOCOMPLETE_GENERATION_INPUT_MAX_LENGTH = ConfigVar( + 'AUTOCOMPLETE_GENERATION_INPUT_MAX_LENGTH', + 'task.autocomplete.input_max_length', + int(os.getenv('AUTOCOMPLETE_GENERATION_INPUT_MAX_LENGTH', '-1')), +) + +AUTOCOMPLETE_GENERATION_PROMPT_TEMPLATE = ConfigVar( + 'AUTOCOMPLETE_GENERATION_PROMPT_TEMPLATE', + 'task.autocomplete.prompt_template', + os.getenv('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 = ConfigVar( + 'VOICE_MODE_PROMPT_TEMPLATE', + 'task.voice.prompt_template', + os.getenv('VOICE_MODE_PROMPT_TEMPLATE', ''), +) + +ENABLE_VOICE_MODE_PROMPT = ConfigVar( + 'ENABLE_VOICE_MODE_PROMPT', + 'task.voice.prompt.enable', + os.getenv('ENABLE_VOICE_MODE_PROMPT', 'True').lower() == 'true', +) + +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 = ConfigVar( + 'TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE', + 'task.tools.prompt_template', + os.getenv('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}}""" + + +#################################### +# Auth +#################################### + +ENABLE_API_KEYS = ConfigVar( + 'ENABLE_API_KEYS', + 'auth.enable_api_keys', + os.getenv('ENABLE_API_KEYS', 'False').lower() == 'true', +) + +ENABLE_API_KEYS_ENDPOINT_RESTRICTIONS = ConfigVar( 'ENABLE_API_KEYS_ENDPOINT_RESTRICTIONS', 'auth.api_key.endpoint_restrictions', - os.environ.get( + os.getenv( 'ENABLE_API_KEYS_ENDPOINT_RESTRICTIONS', - os.environ.get('ENABLE_API_KEY_ENDPOINT_RESTRICTIONS', 'False'), + os.getenv('ENABLE_API_KEY_ENDPOINT_RESTRICTIONS', 'False'), ).lower() == 'true', ) -API_KEYS_ALLOWED_ENDPOINTS = PersistentConfig( +API_KEYS_ALLOWED_ENDPOINTS = ConfigVar( 'API_KEYS_ALLOWED_ENDPOINTS', 'auth.api_key.allowed_endpoints', - os.environ.get('API_KEYS_ALLOWED_ENDPOINTS', os.environ.get('API_KEY_ALLOWED_ENDPOINTS', '')), + os.getenv('API_KEYS_ALLOWED_ENDPOINTS', os.getenv('API_KEY_ALLOWED_ENDPOINTS', '')), ) -JWT_EXPIRES_IN = PersistentConfig('JWT_EXPIRES_IN', 'auth.jwt_expiry', os.environ.get('JWT_EXPIRES_IN', '4w')) +JWT_EXPIRES_IN = ConfigVar('JWT_EXPIRES_IN', 'auth.jwt_expiry', os.getenv('JWT_EXPIRES_IN', '4w')) if JWT_EXPIRES_IN.value == '-1': log.warning( @@ -401,56 +3442,60 @@ if JWT_EXPIRES_IN.value == '-1': # OAuth config #################################### -ENABLE_OAUTH_PERSISTENT_CONFIG = os.environ.get('ENABLE_OAUTH_PERSISTENT_CONFIG', 'False').lower() == 'true' - -ENABLE_OAUTH_SIGNUP = PersistentConfig( +ENABLE_OAUTH_SIGNUP = ConfigVar( 'ENABLE_OAUTH_SIGNUP', 'oauth.enable_signup', - os.environ.get('ENABLE_OAUTH_SIGNUP', 'False').lower() == 'true', + os.getenv('ENABLE_OAUTH_SIGNUP', 'False').lower() == 'true', ) -OAUTH_REFRESH_TOKEN_INCLUDE_SCOPE = PersistentConfig( +OAUTH_AUTO_REDIRECT = ConfigVar( + 'OAUTH_AUTO_REDIRECT', + 'oauth.auto_redirect', + os.getenv('OAUTH_AUTO_REDIRECT', 'False').lower() == 'true', +) + +OAUTH_REFRESH_TOKEN_INCLUDE_SCOPE = ConfigVar( 'OAUTH_REFRESH_TOKEN_INCLUDE_SCOPE', 'oauth.refresh_token_include_scope', - os.environ.get('OAUTH_REFRESH_TOKEN_INCLUDE_SCOPE', 'False').lower() == 'true', + os.getenv('OAUTH_REFRESH_TOKEN_INCLUDE_SCOPE', 'False').lower() == 'true', ) -OAUTH_MERGE_ACCOUNTS_BY_EMAIL = PersistentConfig( +OAUTH_MERGE_ACCOUNTS_BY_EMAIL = ConfigVar( 'OAUTH_MERGE_ACCOUNTS_BY_EMAIL', 'oauth.merge_accounts_by_email', - os.environ.get('OAUTH_MERGE_ACCOUNTS_BY_EMAIL', 'False').lower() == 'true', + os.getenv('OAUTH_MERGE_ACCOUNTS_BY_EMAIL', 'False').lower() == 'true', ) OAUTH_PROVIDERS = {} -GOOGLE_CLIENT_ID = PersistentConfig( +GOOGLE_CLIENT_ID = ConfigVar( 'GOOGLE_CLIENT_ID', 'oauth.google.client_id', - os.environ.get('GOOGLE_CLIENT_ID', ''), + os.getenv('GOOGLE_CLIENT_ID', ''), ) -GOOGLE_CLIENT_SECRET = PersistentConfig( +GOOGLE_CLIENT_SECRET = ConfigVar( 'GOOGLE_CLIENT_SECRET', 'oauth.google.client_secret', - os.environ.get('GOOGLE_CLIENT_SECRET', ''), + os.getenv('GOOGLE_CLIENT_SECRET', ''), ) -GOOGLE_OAUTH_SCOPE = PersistentConfig( +GOOGLE_OAUTH_SCOPE = ConfigVar( 'GOOGLE_OAUTH_SCOPE', 'oauth.google.scope', - os.environ.get('GOOGLE_OAUTH_SCOPE', 'openid email profile'), + os.getenv('GOOGLE_OAUTH_SCOPE', 'openid email profile'), ) -GOOGLE_REDIRECT_URI = PersistentConfig( +GOOGLE_REDIRECT_URI = ConfigVar( 'GOOGLE_REDIRECT_URI', 'oauth.google.redirect_uri', - os.environ.get('GOOGLE_REDIRECT_URI', ''), + os.getenv('GOOGLE_REDIRECT_URI', ''), ) GOOGLE_OAUTH_AUTHORIZE_PARAMS = {} -_google_oauth_authorize_params = os.environ.get('GOOGLE_OAUTH_AUTHORIZE_PARAMS', '') +_google_oauth_authorize_params = os.getenv('GOOGLE_OAUTH_AUTHORIZE_PARAMS', '') if _google_oauth_authorize_params: try: _parsed = json.loads(_google_oauth_authorize_params) @@ -461,288 +3506,286 @@ if _google_oauth_authorize_params: except (json.JSONDecodeError, TypeError): log.warning('GOOGLE_OAUTH_AUTHORIZE_PARAMS is not valid JSON, ignoring') -MICROSOFT_CLIENT_ID = PersistentConfig( +MICROSOFT_CLIENT_ID = ConfigVar( 'MICROSOFT_CLIENT_ID', 'oauth.microsoft.client_id', - os.environ.get('MICROSOFT_CLIENT_ID', ''), + os.getenv('MICROSOFT_CLIENT_ID', ''), ) -MICROSOFT_CLIENT_SECRET = PersistentConfig( +MICROSOFT_CLIENT_SECRET = ConfigVar( 'MICROSOFT_CLIENT_SECRET', 'oauth.microsoft.client_secret', - os.environ.get('MICROSOFT_CLIENT_SECRET', ''), + os.getenv('MICROSOFT_CLIENT_SECRET', ''), ) -MICROSOFT_CLIENT_TENANT_ID = PersistentConfig( +MICROSOFT_CLIENT_TENANT_ID = ConfigVar( 'MICROSOFT_CLIENT_TENANT_ID', 'oauth.microsoft.tenant_id', - os.environ.get('MICROSOFT_CLIENT_TENANT_ID', ''), + os.getenv('MICROSOFT_CLIENT_TENANT_ID', ''), ) -MICROSOFT_CLIENT_LOGIN_BASE_URL = PersistentConfig( +MICROSOFT_CLIENT_LOGIN_BASE_URL = ConfigVar( 'MICROSOFT_CLIENT_LOGIN_BASE_URL', 'oauth.microsoft.login_base_url', - os.environ.get('MICROSOFT_CLIENT_LOGIN_BASE_URL', 'https://login.microsoftonline.com'), + os.getenv('MICROSOFT_CLIENT_LOGIN_BASE_URL', 'https://login.microsoftonline.com'), ) -MICROSOFT_CLIENT_PICTURE_URL = PersistentConfig( +MICROSOFT_CLIENT_PICTURE_URL = ConfigVar( 'MICROSOFT_CLIENT_PICTURE_URL', 'oauth.microsoft.picture_url', - os.environ.get( + os.getenv( 'MICROSOFT_CLIENT_PICTURE_URL', 'https://graph.microsoft.com/v1.0/me/photo/$value', ), ) -MICROSOFT_OAUTH_SCOPE = PersistentConfig( +MICROSOFT_OAUTH_SCOPE = ConfigVar( 'MICROSOFT_OAUTH_SCOPE', 'oauth.microsoft.scope', - os.environ.get('MICROSOFT_OAUTH_SCOPE', 'openid email profile'), + os.getenv('MICROSOFT_OAUTH_SCOPE', 'openid email profile'), ) -MICROSOFT_REDIRECT_URI = PersistentConfig( +MICROSOFT_REDIRECT_URI = ConfigVar( 'MICROSOFT_REDIRECT_URI', 'oauth.microsoft.redirect_uri', - os.environ.get('MICROSOFT_REDIRECT_URI', ''), + os.getenv('MICROSOFT_REDIRECT_URI', ''), ) -GITHUB_CLIENT_ID = PersistentConfig( +GITHUB_CLIENT_ID = ConfigVar( 'GITHUB_CLIENT_ID', 'oauth.github.client_id', - os.environ.get('GITHUB_CLIENT_ID', ''), + os.getenv('GITHUB_CLIENT_ID', ''), ) -GITHUB_CLIENT_SECRET = PersistentConfig( +GITHUB_CLIENT_SECRET = ConfigVar( 'GITHUB_CLIENT_SECRET', 'oauth.github.client_secret', - os.environ.get('GITHUB_CLIENT_SECRET', ''), + os.getenv('GITHUB_CLIENT_SECRET', ''), ) -GITHUB_CLIENT_SCOPE = PersistentConfig( +GITHUB_CLIENT_SCOPE = ConfigVar( 'GITHUB_CLIENT_SCOPE', 'oauth.github.scope', - os.environ.get('GITHUB_CLIENT_SCOPE', 'user:email'), + os.getenv('GITHUB_CLIENT_SCOPE', 'user:email'), ) -GITHUB_CLIENT_REDIRECT_URI = PersistentConfig( +GITHUB_CLIENT_REDIRECT_URI = ConfigVar( 'GITHUB_CLIENT_REDIRECT_URI', 'oauth.github.redirect_uri', - os.environ.get('GITHUB_CLIENT_REDIRECT_URI', ''), + os.getenv('GITHUB_CLIENT_REDIRECT_URI', ''), ) -OAUTH_CLIENT_ID = PersistentConfig( +OAUTH_CLIENT_ID = ConfigVar( 'OAUTH_CLIENT_ID', 'oauth.oidc.client_id', - os.environ.get('OAUTH_CLIENT_ID', ''), + os.getenv('OAUTH_CLIENT_ID', ''), ) -OAUTH_CLIENT_SECRET = PersistentConfig( +OAUTH_CLIENT_SECRET = ConfigVar( 'OAUTH_CLIENT_SECRET', 'oauth.oidc.client_secret', - os.environ.get('OAUTH_CLIENT_SECRET', ''), + os.getenv('OAUTH_CLIENT_SECRET', ''), ) -OPENID_PROVIDER_URL = PersistentConfig( +OPENID_PROVIDER_URL = ConfigVar( 'OPENID_PROVIDER_URL', 'oauth.oidc.provider_url', - os.environ.get('OPENID_PROVIDER_URL', ''), + os.getenv('OPENID_PROVIDER_URL', ''), ) -OPENID_END_SESSION_ENDPOINT = PersistentConfig( +OPENID_END_SESSION_ENDPOINT = ConfigVar( 'OPENID_END_SESSION_ENDPOINT', 'oauth.oidc.end_session_endpoint', - os.environ.get('OPENID_END_SESSION_ENDPOINT', ''), + os.getenv('OPENID_END_SESSION_ENDPOINT', ''), ) -OPENID_REDIRECT_URI = PersistentConfig( +OPENID_REDIRECT_URI = ConfigVar( 'OPENID_REDIRECT_URI', 'oauth.oidc.redirect_uri', - os.environ.get('OPENID_REDIRECT_URI', ''), + os.getenv('OPENID_REDIRECT_URI', ''), ) -OAUTH_SCOPES = PersistentConfig( +OAUTH_SCOPES = ConfigVar( 'OAUTH_SCOPES', 'oauth.oidc.scopes', - os.environ.get('OAUTH_SCOPES', 'openid email profile'), + os.getenv('OAUTH_SCOPES', 'openid email profile'), ) -OAUTH_TIMEOUT = PersistentConfig( +OAUTH_TIMEOUT = ConfigVar( 'OAUTH_TIMEOUT', 'oauth.oidc.oauth_timeout', - os.environ.get('OAUTH_TIMEOUT', ''), + os.getenv('OAUTH_TIMEOUT', ''), ) -OAUTH_TOKEN_ENDPOINT_AUTH_METHOD = PersistentConfig( +OAUTH_TOKEN_ENDPOINT_AUTH_METHOD = ConfigVar( 'OAUTH_TOKEN_ENDPOINT_AUTH_METHOD', 'oauth.oidc.token_endpoint_auth_method', - os.environ.get('OAUTH_TOKEN_ENDPOINT_AUTH_METHOD', None), + os.getenv('OAUTH_TOKEN_ENDPOINT_AUTH_METHOD', None), ) -OAUTH_CODE_CHALLENGE_METHOD = PersistentConfig( +OAUTH_CODE_CHALLENGE_METHOD = ConfigVar( 'OAUTH_CODE_CHALLENGE_METHOD', 'oauth.oidc.code_challenge_method', - os.environ.get('OAUTH_CODE_CHALLENGE_METHOD', None), + os.getenv('OAUTH_CODE_CHALLENGE_METHOD', None), ) -OAUTH_PROVIDER_NAME = PersistentConfig( +OAUTH_PROVIDER_NAME = ConfigVar( 'OAUTH_PROVIDER_NAME', 'oauth.oidc.provider_name', - os.environ.get('OAUTH_PROVIDER_NAME', 'SSO'), + os.getenv('OAUTH_PROVIDER_NAME', 'SSO'), ) -OAUTH_SUB_CLAIM = PersistentConfig( +OAUTH_SUB_CLAIM = ConfigVar( 'OAUTH_SUB_CLAIM', 'oauth.oidc.sub_claim', - os.environ.get('OAUTH_SUB_CLAIM', None), + os.getenv('OAUTH_SUB_CLAIM', None), ) -OAUTH_USERNAME_CLAIM = PersistentConfig( +OAUTH_USERNAME_CLAIM = ConfigVar( 'OAUTH_USERNAME_CLAIM', 'oauth.oidc.username_claim', - os.environ.get('OAUTH_USERNAME_CLAIM', 'name'), + os.getenv('OAUTH_USERNAME_CLAIM', 'name'), ) -OAUTH_PICTURE_CLAIM = PersistentConfig( +OAUTH_PICTURE_CLAIM = ConfigVar( 'OAUTH_PICTURE_CLAIM', 'oauth.oidc.avatar_claim', - os.environ.get('OAUTH_PICTURE_CLAIM', 'picture'), + os.getenv('OAUTH_PICTURE_CLAIM', 'picture'), ) -OAUTH_EMAIL_CLAIM = PersistentConfig( +OAUTH_EMAIL_CLAIM = ConfigVar( 'OAUTH_EMAIL_CLAIM', 'oauth.oidc.email_claim', - os.environ.get('OAUTH_EMAIL_CLAIM', 'email'), + os.getenv('OAUTH_EMAIL_CLAIM', 'email'), ) -OAUTH_GROUPS_CLAIM = PersistentConfig( +OAUTH_GROUPS_CLAIM = ConfigVar( 'OAUTH_GROUPS_CLAIM', 'oauth.oidc.group_claim', - os.environ.get('OAUTH_GROUPS_CLAIM', os.environ.get('OAUTH_GROUP_CLAIM', 'groups')), + os.getenv('OAUTH_GROUPS_CLAIM', os.getenv('OAUTH_GROUP_CLAIM', 'groups')), ) -FEISHU_CLIENT_ID = PersistentConfig( +FEISHU_CLIENT_ID = ConfigVar( 'FEISHU_CLIENT_ID', 'oauth.feishu.client_id', - os.environ.get('FEISHU_CLIENT_ID', ''), + os.getenv('FEISHU_CLIENT_ID', ''), ) -FEISHU_CLIENT_SECRET = PersistentConfig( +FEISHU_CLIENT_SECRET = ConfigVar( 'FEISHU_CLIENT_SECRET', 'oauth.feishu.client_secret', - os.environ.get('FEISHU_CLIENT_SECRET', ''), + os.getenv('FEISHU_CLIENT_SECRET', ''), ) -FEISHU_OAUTH_SCOPE = PersistentConfig( +FEISHU_OAUTH_SCOPE = ConfigVar( 'FEISHU_OAUTH_SCOPE', 'oauth.feishu.scope', - os.environ.get('FEISHU_OAUTH_SCOPE', 'contact:user.base:readonly'), + os.getenv('FEISHU_OAUTH_SCOPE', 'contact:user.base:readonly'), ) -FEISHU_REDIRECT_URI = PersistentConfig( +FEISHU_REDIRECT_URI = ConfigVar( 'FEISHU_REDIRECT_URI', 'oauth.feishu.redirect_uri', - os.environ.get('FEISHU_REDIRECT_URI', ''), + os.getenv('FEISHU_REDIRECT_URI', ''), ) -ENABLE_OAUTH_ROLE_MANAGEMENT = PersistentConfig( +ENABLE_OAUTH_ROLE_MANAGEMENT = ConfigVar( 'ENABLE_OAUTH_ROLE_MANAGEMENT', 'oauth.enable_role_mapping', - os.environ.get('ENABLE_OAUTH_ROLE_MANAGEMENT', 'False').lower() == 'true', + os.getenv('ENABLE_OAUTH_ROLE_MANAGEMENT', 'False').lower() == 'true', ) -ENABLE_OAUTH_GROUP_MANAGEMENT = PersistentConfig( +ENABLE_OAUTH_GROUP_MANAGEMENT = ConfigVar( 'ENABLE_OAUTH_GROUP_MANAGEMENT', 'oauth.enable_group_mapping', - os.environ.get('ENABLE_OAUTH_GROUP_MANAGEMENT', 'False').lower() == 'true', + os.getenv('ENABLE_OAUTH_GROUP_MANAGEMENT', 'False').lower() == 'true', ) -ENABLE_OAUTH_GROUP_CREATION = PersistentConfig( +ENABLE_OAUTH_GROUP_CREATION = ConfigVar( 'ENABLE_OAUTH_GROUP_CREATION', 'oauth.enable_group_creation', - os.environ.get('ENABLE_OAUTH_GROUP_CREATION', 'False').lower() == 'true', + os.getenv('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 = os.getenv('OAUTH_GROUP_DEFAULT_SHARE', 'true').strip().lower() +OAUTH_GROUP_DEFAULT_SHARE = ConfigVar( '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 = ConfigVar( 'OAUTH_BLOCKED_GROUPS', 'oauth.blocked_groups', - os.environ.get('OAUTH_BLOCKED_GROUPS', '[]'), + os.getenv('OAUTH_BLOCKED_GROUPS', '[]'), ) -OAUTH_GROUPS_SEPARATOR = os.environ.get('OAUTH_GROUPS_SEPARATOR', ';') +OAUTH_GROUPS_SEPARATOR = os.getenv('OAUTH_GROUPS_SEPARATOR', ';') -OAUTH_ROLES_CLAIM = PersistentConfig( +OAUTH_ROLES_CLAIM = ConfigVar( 'OAUTH_ROLES_CLAIM', 'oauth.roles_claim', - os.environ.get('OAUTH_ROLES_CLAIM', 'roles'), + os.getenv('OAUTH_ROLES_CLAIM', 'roles'), ) -OAUTH_ROLES_SEPARATOR = os.environ.get('OAUTH_ROLES_SEPARATOR', ',') +OAUTH_ROLES_SEPARATOR = os.getenv('OAUTH_ROLES_SEPARATOR', ',') -OAUTH_ALLOWED_ROLES = PersistentConfig( +OAUTH_ALLOWED_ROLES = ConfigVar( '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 - ) + for role in os.getenv('OAUTH_ALLOWED_ROLES', f'user{OAUTH_ROLES_SEPARATOR}admin').split(OAUTH_ROLES_SEPARATOR) if role ], ) -OAUTH_ADMIN_ROLES = PersistentConfig( +OAUTH_ADMIN_ROLES = ConfigVar( 'OAUTH_ADMIN_ROLES', 'oauth.admin_roles', - [role.strip() for role in os.environ.get('OAUTH_ADMIN_ROLES', 'admin').split(OAUTH_ROLES_SEPARATOR) if role], + [role.strip() for role in os.getenv('OAUTH_ADMIN_ROLES', 'admin').split(OAUTH_ROLES_SEPARATOR) if role], ) -OAUTH_ALLOWED_DOMAINS = PersistentConfig( +OAUTH_ALLOWED_DOMAINS = ConfigVar( 'OAUTH_ALLOWED_DOMAINS', 'oauth.allowed_domains', - [domain.strip() for domain in os.environ.get('OAUTH_ALLOWED_DOMAINS', '*').split(',')], + [domain.strip() for domain in os.getenv('OAUTH_ALLOWED_DOMAINS', '*').split(',')], ) -OAUTH_UPDATE_PICTURE_ON_LOGIN = PersistentConfig( +OAUTH_UPDATE_PICTURE_ON_LOGIN = ConfigVar( 'OAUTH_UPDATE_PICTURE_ON_LOGIN', 'oauth.update_picture_on_login', - os.environ.get('OAUTH_UPDATE_PICTURE_ON_LOGIN', 'False').lower() == 'true', + os.getenv('OAUTH_UPDATE_PICTURE_ON_LOGIN', 'False').lower() == 'true', ) -OAUTH_UPDATE_NAME_ON_LOGIN = PersistentConfig( +OAUTH_UPDATE_NAME_ON_LOGIN = ConfigVar( 'OAUTH_UPDATE_NAME_ON_LOGIN', 'oauth.update_name_on_login', - os.environ.get('OAUTH_UPDATE_NAME_ON_LOGIN', 'False').lower() == 'true', + os.getenv('OAUTH_UPDATE_NAME_ON_LOGIN', 'False').lower() == 'true', ) -OAUTH_UPDATE_EMAIL_ON_LOGIN = PersistentConfig( +OAUTH_UPDATE_EMAIL_ON_LOGIN = ConfigVar( 'OAUTH_UPDATE_EMAIL_ON_LOGIN', 'oauth.update_email_on_login', - os.environ.get('OAUTH_UPDATE_EMAIL_ON_LOGIN', 'False').lower() == 'true', + os.getenv('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' + os.getenv('OAUTH_ACCESS_TOKEN_REQUEST_INCLUDE_CLIENT_ID', 'False').lower() == 'true' ) -OAUTH_AUDIENCE = PersistentConfig( +OAUTH_AUDIENCE = ConfigVar( 'OAUTH_AUDIENCE', 'oauth.audience', - os.environ.get('OAUTH_AUDIENCE', ''), + os.getenv('OAUTH_AUDIENCE', ''), ) OAUTH_AUTHORIZE_PARAMS = {} -_oauth_authorize_params = os.environ.get('OAUTH_AUTHORIZE_PARAMS', '') +_oauth_authorize_params = os.getenv('OAUTH_AUTHORIZE_PARAMS', '') if _oauth_authorize_params: try: _parsed = json.loads(_oauth_authorize_params) @@ -909,3337 +3952,96 @@ def load_oauth_providers(): 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, -) - -OAUTH_CLIENT_TIMEOUT = PersistentConfig( - 'OAUTH_CLIENT_TIMEOUT', - 'oauth.client.timeout', - os.environ.get('OAUTH_CLIENT_TIMEOUT', ''), -) - -#################################### -# 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, -) - -try: - TERMINAL_PROXY_HEADERS = json.loads(os.environ.get('TERMINAL_PROXY_HEADERS', '{}')) -except Exception: - TERMINAL_PROXY_HEADERS = {} - -#################################### -# 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', - [], -) - -try: - default_model_metadata = json.loads(os.environ.get('DEFAULT_MODEL_METADATA', '{}')) -except Exception as e: - log.exception(f'Error loading DEFAULT_MODEL_METADATA: {e}') - default_model_metadata = {} - -DEFAULT_MODEL_METADATA = PersistentConfig( - 'DEFAULT_MODEL_METADATA', - 'models.default_metadata', - default_model_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', ''), -) - -IFRAME_CSP = os.environ.get('IFRAME_CSP', '') - -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_CALENDAR_ALLOW_PUBLIC_SHARING = ( - os.environ.get('USER_PERMISSIONS_CALENDAR_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_ALLOW_PUBLIC_SHARING = ( - os.environ.get('USER_PERMISSIONS_CHAT_ALLOW_PUBLIC_SHARING', 'False').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, - 'public_chats': USER_PERMISSIONS_CHAT_ALLOW_PUBLIC_SHARING, - 'public_calendars': USER_PERMISSIONS_CALENDAR_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', ''), -) - -ENABLE_VOICE_MODE_PROMPT = PersistentConfig( - 'ENABLE_VOICE_MODE_PROMPT', - 'task.voice.prompt.enable', - os.environ.get('ENABLE_VOICE_MODE_PROMPT', 'True').lower() == 'true', -) - -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', -) - - -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) - -ENABLE_ONEDRIVE_PERSONAL = os.environ.get('ENABLE_ONEDRIVE_PERSONAL', 'True').lower() == 'true' and bool( - ONEDRIVE_CLIENT_ID_PERSONAL -) -ENABLE_ONEDRIVE_BUSINESS = os.environ.get('ENABLE_ONEDRIVE_BUSINESS', 'True').lower() == 'true' and bool( - ONEDRIVE_CLIENT_ID_BUSINESS -) - -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', 'True').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', ''), -) - -BRAVE_SEARCH_CONTEXT_TOKENS = PersistentConfig( - 'BRAVE_SEARCH_CONTEXT_TOKENS', - 'rag.web.search.brave_search_context_tokens', - int(os.getenv('BRAVE_SEARCH_CONTEXT_TOKENS', '8192')), -) - -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_ALLOWED_EXTENSIONS = PersistentConfig( - 'AUDIO_STT_ALLOWED_EXTENSIONS', - 'audio.stt.allowed_extensions', - [ - ext.strip() - for ext in os.environ.get( - 'AUDIO_STT_ALLOWED_EXTENSIONS', - 'mp3,wav,m4a,webm,ogg,flac,mp4,mpga,mpeg', - ).split(',') - if ext.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 = ConfigVar( 'ENABLE_LDAP', 'ldap.enable', - os.environ.get('ENABLE_LDAP', 'false').lower() == 'true', + os.getenv('ENABLE_LDAP', 'false').lower() == 'true', ) -LDAP_SERVER_LABEL = PersistentConfig( +LDAP_SERVER_LABEL = ConfigVar( 'LDAP_SERVER_LABEL', 'ldap.server.label', - os.environ.get('LDAP_SERVER_LABEL', 'LDAP Server'), + os.getenv('LDAP_SERVER_LABEL', 'LDAP Server'), ) -LDAP_SERVER_HOST = PersistentConfig( +LDAP_SERVER_HOST = ConfigVar( 'LDAP_SERVER_HOST', 'ldap.server.host', - os.environ.get('LDAP_SERVER_HOST', 'localhost'), + os.getenv('LDAP_SERVER_HOST', 'localhost'), ) -LDAP_SERVER_PORT = PersistentConfig( +LDAP_SERVER_PORT = ConfigVar( 'LDAP_SERVER_PORT', 'ldap.server.port', - int(os.environ.get('LDAP_SERVER_PORT', '389')), + int(os.getenv('LDAP_SERVER_PORT', '389')), ) -LDAP_ATTRIBUTE_FOR_MAIL = PersistentConfig( +LDAP_ATTRIBUTE_FOR_MAIL = ConfigVar( 'LDAP_ATTRIBUTE_FOR_MAIL', 'ldap.server.attribute_for_mail', - os.environ.get('LDAP_ATTRIBUTE_FOR_MAIL', 'mail'), + os.getenv('LDAP_ATTRIBUTE_FOR_MAIL', 'mail'), ) -LDAP_ATTRIBUTE_FOR_USERNAME = PersistentConfig( +LDAP_ATTRIBUTE_FOR_USERNAME = ConfigVar( 'LDAP_ATTRIBUTE_FOR_USERNAME', 'ldap.server.attribute_for_username', - os.environ.get('LDAP_ATTRIBUTE_FOR_USERNAME', 'uid'), + os.getenv('LDAP_ATTRIBUTE_FOR_USERNAME', 'uid'), ) -LDAP_APP_DN = PersistentConfig('LDAP_APP_DN', 'ldap.server.app_dn', os.environ.get('LDAP_APP_DN', '')) +LDAP_APP_DN = ConfigVar('LDAP_APP_DN', 'ldap.server.app_dn', os.getenv('LDAP_APP_DN', '')) -LDAP_APP_PASSWORD = PersistentConfig( +LDAP_APP_PASSWORD = ConfigVar( 'LDAP_APP_PASSWORD', 'ldap.server.app_password', - os.environ.get('LDAP_APP_PASSWORD', ''), + os.getenv('LDAP_APP_PASSWORD', ''), ) -LDAP_SEARCH_BASE = PersistentConfig('LDAP_SEARCH_BASE', 'ldap.server.users_dn', os.environ.get('LDAP_SEARCH_BASE', '')) +LDAP_SEARCH_BASE = ConfigVar('LDAP_SEARCH_BASE', 'ldap.server.users_dn', os.getenv('LDAP_SEARCH_BASE', '')) -LDAP_SEARCH_FILTERS = PersistentConfig( +LDAP_SEARCH_FILTERS = ConfigVar( 'LDAP_SEARCH_FILTER', 'ldap.server.search_filter', - os.environ.get('LDAP_SEARCH_FILTER', os.environ.get('LDAP_SEARCH_FILTERS', '')), + os.getenv('LDAP_SEARCH_FILTER', os.getenv('LDAP_SEARCH_FILTERS', '')), ) -LDAP_USE_TLS = PersistentConfig( +LDAP_USE_TLS = ConfigVar( 'LDAP_USE_TLS', 'ldap.server.use_tls', - os.environ.get('LDAP_USE_TLS', 'True').lower() == 'true', + os.getenv('LDAP_USE_TLS', 'True').lower() == 'true', ) -LDAP_CA_CERT_FILE = PersistentConfig( +LDAP_CA_CERT_FILE = ConfigVar( 'LDAP_CA_CERT_FILE', 'ldap.server.ca_cert_file', - os.environ.get('LDAP_CA_CERT_FILE', ''), + os.getenv('LDAP_CA_CERT_FILE', ''), ) -LDAP_VALIDATE_CERT = PersistentConfig( +LDAP_VALIDATE_CERT = ConfigVar( 'LDAP_VALIDATE_CERT', 'ldap.server.validate_cert', - os.environ.get('LDAP_VALIDATE_CERT', 'True').lower() == 'true', + os.getenv('LDAP_VALIDATE_CERT', 'True').lower() == 'true', ) -LDAP_CIPHERS = PersistentConfig('LDAP_CIPHERS', 'ldap.server.ciphers', os.environ.get('LDAP_CIPHERS', 'ALL')) +LDAP_CIPHERS = ConfigVar('LDAP_CIPHERS', 'ldap.server.ciphers', os.getenv('LDAP_CIPHERS', 'ALL')) -# For LDAP Group Management -ENABLE_LDAP_GROUP_MANAGEMENT = PersistentConfig( +ENABLE_LDAP_GROUP_MANAGEMENT = ConfigVar( 'ENABLE_LDAP_GROUP_MANAGEMENT', 'ldap.group.enable_management', - os.environ.get('ENABLE_LDAP_GROUP_MANAGEMENT', 'False').lower() == 'true', + os.getenv('ENABLE_LDAP_GROUP_MANAGEMENT', 'False').lower() == 'true', ) -ENABLE_LDAP_GROUP_CREATION = PersistentConfig( +ENABLE_LDAP_GROUP_CREATION = ConfigVar( 'ENABLE_LDAP_GROUP_CREATION', 'ldap.group.enable_creation', - os.environ.get('ENABLE_LDAP_GROUP_CREATION', 'False').lower() == 'true', + os.getenv('ENABLE_LDAP_GROUP_CREATION', 'False').lower() == 'true', ) -LDAP_ATTRIBUTE_FOR_GROUPS = PersistentConfig( +LDAP_ATTRIBUTE_FOR_GROUPS = ConfigVar( 'LDAP_ATTRIBUTE_FOR_GROUPS', 'ldap.server.attribute_for_groups', - os.environ.get('LDAP_ATTRIBUTE_FOR_GROUPS', 'memberOf'), + os.getenv('LDAP_ATTRIBUTE_FOR_GROUPS', 'memberOf'), ) diff --git a/backend/open_webui/constants.py b/backend/open_webui/constants.py index ad1bdf4a20..132f3ac19a 100644 --- a/backend/open_webui/constants.py +++ b/backend/open_webui/constants.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from enum import Enum @@ -72,11 +74,11 @@ class ERROR_MESSAGES(str, Enum): 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.' + DB_NOT_SQLITE = 'This feature is only available with SQLite databases.' - INVALID_URL = 'Oops! The URL you provided is invalid. Please double-check and try again.' + INVALID_URL = '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."}' + WEB_SEARCH_ERROR = lambda err='': err if err else 'Something went wrong while searching the web.' OLLAMA_API_DISABLED = 'The Ollama API is disabled. Please enable it to use this feature.' diff --git a/backend/open_webui/env.py b/backend/open_webui/env.py index 903b3effcc..2f6b4ed632 100644 --- a/backend/open_webui/env.py +++ b/backend/open_webui/env.py @@ -9,15 +9,13 @@ import shutil import sys import traceback from pathlib import Path -from typing import Any +from typing import Any, Optional from uuid import uuid4 import markdown from bs4 import BeautifulSoup from cryptography.hazmat.primitives import serialization -from open_webui.constants import ERROR_MESSAGES - #################################### # Load .env file #################################### @@ -41,29 +39,27 @@ try: except ImportError: print('dotenv not installed, skipping...') -DOCKER = os.environ.get('DOCKER', 'False').lower() == 'true' +DOCKER = os.getenv('DOCKER', 'False').lower() == 'true' -# device type for embedding models - "cpu" (default), "cuda" (nvidia gpu required), or "mps" (apple silicon) -# choosing this correctly can lead to better performance -USE_CUDA = os.environ.get('USE_CUDA_DOCKER', 'false') +USE_CUDA = os.getenv('USE_CUDA_DOCKER', 'false') +DEVICE_TYPE = 'cpu' +_cuda_error: Optional[str] = None if USE_CUDA.lower() == 'true': try: - import torch + import torch # noqa: E402 - assert torch.cuda.is_available(), 'CUDA not available' + if not torch.cuda.is_available(): + raise RuntimeError('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}' + except Exception as exc: + _cuda_error = f'CUDA unavailable (USE_CUDA_DOCKER=true), falling back to CPU: {exc}' os.environ['USE_CUDA_DOCKER'] = 'false' USE_CUDA = 'false' - DEVICE_TYPE = 'cpu' -else: - DEVICE_TYPE = 'cpu' -if sys.platform == 'darwin': +if sys.platform == 'darwin' and DEVICE_TYPE == 'cpu': try: - import torch + import torch # noqa: E402 if torch.backends.mps.is_available() and torch.backends.mps.is_built(): DEVICE_TYPE = 'mps' @@ -105,43 +101,37 @@ class JSONFormatter(logging.Formatter): return json.dumps(log_entry, ensure_ascii=False, default=str) -LOG_FORMAT = os.environ.get('LOG_FORMAT', '').lower() +LOG_FORMAT = os.getenv('LOG_FORMAT', '').lower() -GLOBAL_LOG_LEVEL = os.environ.get('GLOBAL_LOG_LEVEL', '').upper() +GLOBAL_LOG_LEVEL = os.getenv('GLOBAL_LOG_LEVEL', '').upper() if GLOBAL_LOG_LEVEL in logging.getLevelNamesMapping(): + _log_cfg: dict[str, Any] = {'level': GLOBAL_LOG_LEVEL, 'force': True} if LOG_FORMAT == 'json': - _handler = logging.StreamHandler(sys.stdout) - _handler.setFormatter(JSONFormatter()) - logging.basicConfig(handlers=[_handler], level=GLOBAL_LOG_LEVEL, force=True) + _json_handler = logging.StreamHandler(sys.stdout) + _json_handler.setFormatter(JSONFormatter()) + _log_cfg['handlers'] = [_json_handler] else: - logging.basicConfig(stream=sys.stdout, level=GLOBAL_LOG_LEVEL, force=True) + _log_cfg['stream'] = sys.stdout + logging.basicConfig(**_log_cfg) else: GLOBAL_LOG_LEVEL = 'INFO' log = logging.getLogger(__name__) -log.info(f'GLOBAL_LOG_LEVEL: {GLOBAL_LOG_LEVEL}') +log.info('GLOBAL_LOG_LEVEL: %s', GLOBAL_LOG_LEVEL) -if 'cuda_error' in locals(): - log.exception(cuda_error) - del cuda_error +if _cuda_error: + log.error(_cuda_error) + _cuda_error = None 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') +ENV = os.getenv('ENV', 'dev') -FROM_INIT_PY = os.environ.get('FROM_INIT_PY', 'False').lower() == 'true' +FROM_INIT_PY = os.getenv('FROM_INIT_PY', 'False').lower() == 'true' if FROM_INIT_PY: PACKAGE_DATA = {'version': importlib.metadata.version('open-webui')} @@ -154,10 +144,10 @@ else: VERSION = PACKAGE_DATA['version'] -DEPLOYMENT_ID = os.environ.get('DEPLOYMENT_ID', '') -INSTANCE_ID = os.environ.get('INSTANCE_ID', str(uuid4())) +DEPLOYMENT_ID = os.getenv('DEPLOYMENT_ID', '') +INSTANCE_ID = os.getenv('INSTANCE_ID', str(uuid4())) -ENABLE_DB_MIGRATIONS = os.environ.get('ENABLE_DB_MIGRATIONS', 'True').lower() == 'true' +ENABLE_DB_MIGRATIONS = os.getenv('ENABLE_DB_MIGRATIONS', 'True').lower() == 'true' # Function to parse each section @@ -219,62 +209,6 @@ for version in soup.find_all('h2'): 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' - -#################################### -# ENABLE_PROFILE_IMAGE_URL_FORWARDING -#################################### - -# When True (default), the user and model profile-image endpoints -# honour external http(s) URLs stored in profile_image_url by issuing a -# 302 redirect to the original origin. Set to False to suppress the -# redirect (prevents client-side IP/UA/Referer leaks to attacker- -# controlled origins) and fall through to the default image instead. -ENABLE_PROFILE_IMAGE_URL_FORWARDING = os.environ.get('ENABLE_PROFILE_IMAGE_URL_FORWARDING', 'True').lower() == 'true' - -PROFILE_IMAGE_ALLOWED_MIME_TYPES = frozenset( - t.strip() - for t in os.environ.get( - 'PROFILE_IMAGE_ALLOWED_MIME_TYPES', - 'image/png,image/jpeg,image/gif,image/webp', - ).split(',') - if t.strip() -) - -#################################### -# WEBUI_BUILD_HASH -#################################### - -WEBUI_BUILD_HASH = os.environ.get('WEBUI_BUILD_HASH', 'dev-build') - #################################### # DATA/FRONTEND BUILD DIR #################################### @@ -324,11 +258,11 @@ if os.path.exists(f'{DATA_DIR}/ollama.db'): else: pass -DATABASE_URL = os.environ.get('DATABASE_URL', f'sqlite:///{DATA_DIR}/webui.db') +DATABASE_URL = os.getenv('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_TYPE = os.getenv('DATABASE_TYPE') +DATABASE_USER = os.getenv('DATABASE_USER') +DATABASE_PASSWORD = os.getenv('DATABASE_PASSWORD') DATABASE_CRED = '' if DATABASE_USER: @@ -339,16 +273,16 @@ if 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'), + 'db_host': os.getenv('DATABASE_HOST'), + 'db_port': os.getenv('DATABASE_PORT'), + 'db_name': os.getenv('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'): +elif DATABASE_TYPE == 'sqlite+sqlcipher' and not os.getenv('DATABASE_URL'): # Handle SQLCipher with local file when DATABASE_URL wasn't explicitly set DATABASE_URL = f'sqlite+sqlcipher:///{DATA_DIR}/webui.db' @@ -356,47 +290,33 @@ elif DATABASE_TYPE == 'sqlite+sqlcipher' and not os.environ.get('DATABASE_URL'): if 'postgres://' in DATABASE_URL: DATABASE_URL = DATABASE_URL.replace('postgres://', 'postgresql://') -DATABASE_SCHEMA = os.environ.get('DATABASE_SCHEMA', None) +DATABASE_SCHEMA = os.getenv('DATABASE_SCHEMA', None) -DATABASE_POOL_SIZE = os.environ.get('DATABASE_POOL_SIZE', None) +_pool_size_raw = os.getenv('DATABASE_POOL_SIZE') +try: + DATABASE_POOL_SIZE = int(_pool_size_raw) if _pool_size_raw else None +except (ValueError, TypeError): + DATABASE_POOL_SIZE = None -if DATABASE_POOL_SIZE is not 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 == '': +_pool_overflow_raw = os.getenv('DATABASE_POOL_MAX_OVERFLOW', '0') +try: + DATABASE_POOL_MAX_OVERFLOW = int(_pool_overflow_raw) if _pool_overflow_raw else 0 +except (ValueError, TypeError): 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 == '': +_pool_timeout_raw = os.getenv('DATABASE_POOL_TIMEOUT', '30') +try: + DATABASE_POOL_TIMEOUT = int(_pool_timeout_raw) if _pool_timeout_raw else 30 +except (ValueError, TypeError): 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 == '': +_pool_recycle_raw = os.getenv('DATABASE_POOL_RECYCLE', '3600') +try: + DATABASE_POOL_RECYCLE = int(_pool_recycle_raw) if _pool_recycle_raw else 3600 +except (ValueError, TypeError): 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' +DATABASE_ENABLE_SQLITE_WAL = os.getenv('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. @@ -404,63 +324,56 @@ DATABASE_ENABLE_SQLITE_WAL = os.environ.get('DATABASE_ENABLE_SQLITE_WAL', 'True' # 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') +DATABASE_SQLITE_PRAGMA_SYNCHRONOUS = os.getenv('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') +DATABASE_SQLITE_PRAGMA_BUSY_TIMEOUT = os.getenv('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') +DATABASE_SQLITE_PRAGMA_CACHE_SIZE = os.getenv('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') +DATABASE_SQLITE_PRAGMA_TEMP_STORE = os.getenv('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') +DATABASE_SQLITE_PRAGMA_MMAP_SIZE = os.getenv('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_SQLITE_PRAGMA_JOURNAL_SIZE_LIMIT = os.getenv('DATABASE_SQLITE_PRAGMA_JOURNAL_SIZE_LIMIT', '67108864') -DATABASE_USER_ACTIVE_STATUS_UPDATE_INTERVAL = os.environ.get('DATABASE_USER_ACTIVE_STATUS_UPDATE_INTERVAL', None) +DATABASE_USER_ACTIVE_STATUS_UPDATE_INTERVAL = os.getenv('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' +DATABASE_ENABLE_SESSION_SHARING = os.getenv('DATABASE_ENABLE_SESSION_SHARING', 'False').lower() == 'true' +ENABLE_PUBLIC_ACTIVE_USERS_COUNT = os.getenv('ENABLE_PUBLIC_ACTIVE_USERS_COUNT', 'True').lower() == 'true' +RESET_CONFIG_ON_START = os.getenv('RESET_CONFIG_ON_START', 'False').lower() == 'true' +ENABLE_REALTIME_CHAT_SAVE = os.getenv('ENABLE_REALTIME_CHAT_SAVE', 'False').lower() == 'true' +ENABLE_QUERIES_CACHE = os.getenv('ENABLE_QUERIES_CACHE', 'False').lower() == 'true' +RAG_SYSTEM_CONTEXT = os.getenv('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_URL = os.getenv('REDIS_URL', '') +REDIS_CLUSTER = os.getenv('REDIS_CLUSTER', 'False').lower() == 'true' -REDIS_KEY_PREFIX = os.environ.get('REDIS_KEY_PREFIX', 'open-webui') +REDIS_KEY_PREFIX = os.getenv('REDIS_KEY_PREFIX', 'open-webui') -REDIS_SENTINEL_HOSTS = os.environ.get('REDIS_SENTINEL_HOSTS', '') -REDIS_SENTINEL_PORT = os.environ.get('REDIS_SENTINEL_PORT', '26379') +REDIS_SENTINEL_HOSTS = os.getenv('REDIS_SENTINEL_HOSTS', '') +REDIS_SENTINEL_PORT = os.getenv('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') +REDIS_SENTINEL_MAX_RETRY_COUNT = os.getenv('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: @@ -469,7 +382,7 @@ except ValueError: REDIS_SENTINEL_MAX_RETRY_COUNT = 2 -REDIS_SOCKET_CONNECT_TIMEOUT = os.environ.get('REDIS_SOCKET_CONNECT_TIMEOUT', '') +REDIS_SOCKET_CONNECT_TIMEOUT = os.getenv('REDIS_SOCKET_CONNECT_TIMEOUT', '') try: REDIS_SOCKET_CONNECT_TIMEOUT = float(REDIS_SOCKET_CONNECT_TIMEOUT) except ValueError: @@ -480,7 +393,7 @@ except ValueError: # 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' +REDIS_SOCKET_KEEPALIVE = os.getenv('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 @@ -488,7 +401,7 @@ REDIS_SOCKET_KEEPALIVE = os.environ.get('REDIS_SOCKET_KEEPALIVE', 'False').lower # 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', '') +REDIS_HEALTH_CHECK_INTERVAL = os.getenv('REDIS_HEALTH_CHECK_INTERVAL', '') try: REDIS_HEALTH_CHECK_INTERVAL = int(REDIS_HEALTH_CHECK_INTERVAL) if REDIS_HEALTH_CHECK_INTERVAL <= 0: @@ -496,7 +409,7 @@ try: except ValueError: REDIS_HEALTH_CHECK_INTERVAL = None -REDIS_RECONNECT_DELAY = os.environ.get('REDIS_RECONNECT_DELAY', '') +REDIS_RECONNECT_DELAY = os.getenv('REDIS_RECONNECT_DELAY', '') if REDIS_RECONNECT_DELAY == '': REDIS_RECONNECT_DELAY = None @@ -509,257 +422,24 @@ else: REDIS_RECONNECT_DELAY = None #################################### -# UVICORN WORKERS +# Uvicorn #################################### -# 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 = max(int(os.getenv('UVICORN_WORKERS', '1')), 1) +except (ValueError, TypeError): 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() - ) - - -#################################### -# 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' +ENABLE_WEBSOCKET_SUPPORT = os.getenv('ENABLE_WEBSOCKET_SUPPORT', 'True').lower() == 'true' -WEBSOCKET_MANAGER = os.environ.get('WEBSOCKET_MANAGER', '') +WEBSOCKET_MANAGER = os.getenv('WEBSOCKET_MANAGER', '') -WEBSOCKET_REDIS_OPTIONS = os.environ.get('WEBSOCKET_REDIS_OPTIONS', '') +WEBSOCKET_REDIS_OPTIONS = os.getenv('WEBSOCKET_REDIS_OPTIONS', '') if WEBSOCKET_REDIS_OPTIONS == '': @@ -775,39 +455,39 @@ else: 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_URL = os.getenv('WEBSOCKET_REDIS_URL', REDIS_URL) +WEBSOCKET_REDIS_CLUSTER = os.getenv('WEBSOCKET_REDIS_CLUSTER', str(REDIS_CLUSTER)).lower() == 'true' -websocket_redis_lock_timeout = os.environ.get('WEBSOCKET_REDIS_LOCK_TIMEOUT', '60') +websocket_redis_lock_timeout = os.getenv('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_SENTINEL_HOSTS = os.getenv('WEBSOCKET_SENTINEL_HOSTS', '') +WEBSOCKET_SENTINEL_PORT = os.getenv('WEBSOCKET_SENTINEL_PORT', '26379') +WEBSOCKET_SERVER_LOGGING = os.getenv('WEBSOCKET_SERVER_LOGGING', 'False').lower() == 'true' WEBSOCKET_SERVER_ENGINEIO_LOGGING = ( - os.environ.get( + os.getenv( 'WEBSOCKET_SERVER_ENGINEIO_LOGGING', - os.environ.get('WEBSOCKET_SERVER_LOGGING', 'False'), + os.getenv('WEBSOCKET_SERVER_LOGGING', 'False'), ).lower() == 'true' ) -WEBSOCKET_SERVER_PING_TIMEOUT = os.environ.get('WEBSOCKET_SERVER_PING_TIMEOUT', '20') +WEBSOCKET_SERVER_PING_TIMEOUT = os.getenv('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') +WEBSOCKET_SERVER_PING_INTERVAL = os.getenv('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', '') +WEBSOCKET_EVENT_CALLER_TIMEOUT = os.getenv('WEBSOCKET_EVENT_CALLER_TIMEOUT', '') if WEBSOCKET_EVENT_CALLER_TIMEOUT == '': WEBSOCKET_EVENT_CALLER_TIMEOUT = None @@ -818,58 +498,44 @@ else: WEBSOCKET_EVENT_CALLER_TIMEOUT = 300 -REQUESTS_VERIFY = os.environ.get('REQUESTS_VERIFY', 'True').lower() == 'true' +REQUESTS_VERIFY = os.getenv('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_timeout_raw = os.getenv('AIOHTTP_CLIENT_TIMEOUT', '') +try: + AIOHTTP_CLIENT_TIMEOUT = int(_aiohttp_timeout_raw) if _aiohttp_timeout_raw else None +except (ValueError, TypeError): + AIOHTTP_CLIENT_TIMEOUT = 300 -AIOHTTP_CLIENT_SESSION_SSL = os.environ.get('AIOHTTP_CLIENT_SESSION_SSL', 'True').lower() == 'true' +AIOHTTP_CLIENT_SESSION_SSL = os.getenv('AIOHTTP_CLIENT_SESSION_SSL', 'True').lower() == 'true' # When False (default), outbound HTTP requests do not follow 3xx redirects. -# This prevents redirect-based SSRF where a public URL 302-redirects to an -# internal address (RFC 1918, loopback, cloud-metadata 169.254.169.254). -# Set to True only if your deployment requires redirect following and you -# have other SSRF protections in place (e.g. egress firewall). -AIOHTTP_CLIENT_ALLOW_REDIRECTS = os.environ.get('AIOHTTP_CLIENT_ALLOW_REDIRECTS', 'False').lower() == 'true' +AIOHTTP_CLIENT_ALLOW_REDIRECTS = os.getenv('AIOHTTP_CLIENT_ALLOW_REDIRECTS', 'False').lower() == 'true' -AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST = os.environ.get( +# Optional User-Agent override for outbound web-loader fetches. When set, +# SafeWebBaseLoader sends this value instead of the default python-requests UA +# which is aggressively blocked by Cloudflare, Wikipedia, and similar services. +USER_AGENT = os.getenv('USER_AGENT', '') + +_model_list_timeout_raw = os.getenv( 'AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST', - os.environ.get('AIOHTTP_CLIENT_TIMEOUT_OPENAI_MODEL_LIST', '10'), + os.getenv('AIOHTTP_CLIENT_TIMEOUT_OPENAI_MODEL_LIST', '10'), ) +try: + AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST = int(_model_list_timeout_raw) if _model_list_timeout_raw else None +except (ValueError, TypeError): + AIOHTTP_CLIENT_TIMEOUT_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 +_tool_data_timeout_raw = os.getenv('AIOHTTP_CLIENT_TIMEOUT_TOOL_SERVER_DATA', '10') +try: + AIOHTTP_CLIENT_TIMEOUT_TOOL_SERVER_DATA = int(_tool_data_timeout_raw) if _tool_data_timeout_raw else None +except (ValueError, TypeError): + AIOHTTP_CLIENT_TIMEOUT_TOOL_SERVER_DATA = 10 -AIOHTTP_CLIENT_TIMEOUT_TOOL_SERVER_DATA = os.environ.get('AIOHTTP_CLIENT_TIMEOUT_TOOL_SERVER_DATA', '10') +AIOHTTP_CLIENT_SESSION_TOOL_SERVER_SSL = os.getenv('AIOHTTP_CLIENT_SESSION_TOOL_SERVER_SSL', 'True').lower() == 'true' -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', '') +AIOHTTP_CLIENT_TIMEOUT_TOOL_SERVER = os.getenv('AIOHTTP_CLIENT_TIMEOUT_TOOL_SERVER', '') if AIOHTTP_CLIENT_TIMEOUT_TOOL_SERVER == '': AIOHTTP_CLIENT_TIMEOUT_TOOL_SERVER = AIOHTTP_CLIENT_TIMEOUT @@ -879,12 +545,21 @@ else: except Exception: AIOHTTP_CLIENT_TIMEOUT_TOOL_SERVER = AIOHTTP_CLIENT_TIMEOUT +# Timeout (in seconds) for the MCP session.initialize() handshake. +# The handshake performs a list-tools round-trip and can take tens of +# seconds on cold-start servers or servers exposing many tools. +MCP_INITIALIZE_TIMEOUT = os.getenv('MCP_INITIALIZE_TIMEOUT', '10') +try: + MCP_INITIALIZE_TIMEOUT = int(MCP_INITIALIZE_TIMEOUT) +except (ValueError, TypeError): + MCP_INITIALIZE_TIMEOUT = 10 + #################################### # AIOHTTP Connection Pool #################################### -AIOHTTP_POOL_CONNECTIONS = os.environ.get('AIOHTTP_POOL_CONNECTIONS', '') +AIOHTTP_POOL_CONNECTIONS = os.getenv('AIOHTTP_POOL_CONNECTIONS', '') if AIOHTTP_POOL_CONNECTIONS == '': AIOHTTP_POOL_CONNECTIONS = None else: @@ -893,7 +568,7 @@ else: except ValueError: AIOHTTP_POOL_CONNECTIONS = None -AIOHTTP_POOL_CONNECTIONS_PER_HOST = os.environ.get('AIOHTTP_POOL_CONNECTIONS_PER_HOST', '') +AIOHTTP_POOL_CONNECTIONS_PER_HOST = os.getenv('AIOHTTP_POOL_CONNECTIONS_PER_HOST', '') if AIOHTTP_POOL_CONNECTIONS_PER_HOST == '': AIOHTTP_POOL_CONNECTIONS_PER_HOST = None else: @@ -902,7 +577,7 @@ else: except ValueError: AIOHTTP_POOL_CONNECTIONS_PER_HOST = None -AIOHTTP_POOL_DNS_TTL = os.environ.get('AIOHTTP_POOL_DNS_TTL', '300') +AIOHTTP_POOL_DNS_TTL = os.getenv('AIOHTTP_POOL_DNS_TTL', '300') try: AIOHTTP_POOL_DNS_TTL = int(AIOHTTP_POOL_DNS_TTL) if AIOHTTP_POOL_DNS_TTL < 0: @@ -910,7 +585,7 @@ try: except ValueError: AIOHTTP_POOL_DNS_TTL = 300 -RAG_EMBEDDING_TIMEOUT = os.environ.get('RAG_EMBEDDING_TIMEOUT', '') +RAG_EMBEDDING_TIMEOUT = os.getenv('RAG_EMBEDDING_TIMEOUT', '') if RAG_EMBEDDING_TIMEOUT == '': RAG_EMBEDDING_TIMEOUT = None @@ -921,17 +596,342 @@ else: RAG_EMBEDDING_TIMEOUT = None +#################################### +# Auth +#################################### + +WEBUI_AUTH = os.getenv('WEBUI_AUTH', 'True').lower() == 'true' + +ENABLE_INITIAL_ADMIN_SIGNUP = os.getenv('ENABLE_INITIAL_ADMIN_SIGNUP', 'False').lower() == 'true' +ENABLE_SIGNUP_PASSWORD_CONFIRMATION = os.getenv('ENABLE_SIGNUP_PASSWORD_CONFIRMATION', 'False').lower() == 'true' + +#################################### +# Secret key & cookies +#################################### + +# WEBUI_JWT_SECRET_KEY is deprecated; use WEBUI_SECRET_KEY instead. +# No hardcoded fallback by design: the supported start scripts set/auto-generate it; unset is rejected below. +WEBUI_SECRET_KEY = os.getenv( + 'WEBUI_SECRET_KEY', + os.getenv('WEBUI_JWT_SECRET_KEY', ''), +) + +WEBUI_SESSION_COOKIE_SAME_SITE = os.getenv('WEBUI_SESSION_COOKIE_SAME_SITE', 'lax') +WEBUI_SESSION_COOKIE_SECURE = os.getenv('WEBUI_SESSION_COOKIE_SECURE', 'false').lower() == 'true' +WEBUI_AUTH_COOKIE_SAME_SITE = os.getenv('WEBUI_AUTH_COOKIE_SAME_SITE', WEBUI_SESSION_COOKIE_SAME_SITE) +WEBUI_AUTH_COOKIE_SECURE = ( + os.getenv( + 'WEBUI_AUTH_COOKIE_SECURE', + os.getenv('WEBUI_SESSION_COOKIE_SECURE', 'false'), + ).lower() + == 'true' +) + +if WEBUI_AUTH and WEBUI_SECRET_KEY == '': + raise SystemExit( + 'WEBUI_SECRET_KEY is not set. It is a hard requirement when authentication is enabled.\n' + 'The supported start methods set or auto-generate it for you: use start.sh (Linux/macOS), ' + 'start_windows.bat (Windows), or `open-webui serve`.\n' + 'If you start the backend another way (e.g. invoking uvicorn directly, which is unsupported), ' + 'you must set WEBUI_SECRET_KEY yourself to a long random value.\n' + 'See https://docs.openwebui.com/reference/env-configuration#webui_secret_key' + ) + +ENABLE_COMPRESSION_MIDDLEWARE = os.getenv('ENABLE_COMPRESSION_MIDDLEWARE', 'True').lower() == 'true' + +#################################### +# Admin Account Runtime Creation +#################################### + +# Optional env vars for creating an admin account on startup +# Useful for headless/automated deployments +WEBUI_ADMIN_EMAIL = os.getenv('WEBUI_ADMIN_EMAIL', '') +WEBUI_ADMIN_PASSWORD = os.getenv('WEBUI_ADMIN_PASSWORD', '') +WEBUI_ADMIN_NAME = os.getenv('WEBUI_ADMIN_NAME', 'Admin') + +WEBUI_AUTH_TRUSTED_EMAIL_HEADER = os.getenv('WEBUI_AUTH_TRUSTED_EMAIL_HEADER', None) +WEBUI_AUTH_TRUSTED_NAME_HEADER = os.getenv('WEBUI_AUTH_TRUSTED_NAME_HEADER', None) +WEBUI_AUTH_TRUSTED_GROUPS_HEADER = os.getenv('WEBUI_AUTH_TRUSTED_GROUPS_HEADER', None) +WEBUI_AUTH_TRUSTED_ROLE_HEADER = os.getenv('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.getenv('CUSTOM_API_KEY_HEADER', 'x-api-key') + +ENABLE_PASSWORD_VALIDATION = os.getenv('ENABLE_PASSWORD_VALIDATION', 'False').lower() == 'true' +PASSWORD_VALIDATION_REGEX_PATTERN = os.getenv( + '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.getenv('PASSWORD_VALIDATION_HINT', '') + + +BYPASS_MODEL_ACCESS_CONTROL = os.getenv('BYPASS_MODEL_ACCESS_CONTROL', 'False').lower() == 'true' +BYPASS_RETRIEVAL_ACCESS_CONTROL = os.getenv('BYPASS_RETRIEVAL_ACCESS_CONTROL', 'False').lower() == 'true' + +# When True, collection names that do not match any known file-*, user-memory-*, +# web-search-*, or knowledge-base collection are allowed through access control +# for non-admin users. When False (default), unknown collection names are +# denied — closing the legacy unscoped namespace. +ENABLE_RETRIEVAL_UNSCOPED_COLLECTIONS = os.getenv('ENABLE_RETRIEVAL_UNSCOPED_COLLECTIONS', '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.getenv('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.getenv('ENABLE_OPENAI_API_PASSTHROUGH', 'False').lower() == 'true' + +WEBUI_AUTH_SIGNOUT_REDIRECT_URL = os.getenv('WEBUI_AUTH_SIGNOUT_REDIRECT_URL', None) + +#################################### +# OAUTH Configuration +#################################### +ENABLE_OAUTH_EMAIL_FALLBACK = os.getenv('ENABLE_OAUTH_EMAIL_FALLBACK', 'False').lower() == 'true' + +ENABLE_OAUTH_ID_TOKEN_COOKIE = os.getenv('ENABLE_OAUTH_ID_TOKEN_COOKIE', 'True').lower() == 'true' + +OAUTH_CLIENT_INFO_ENCRYPTION_KEY = os.getenv('OAUTH_CLIENT_INFO_ENCRYPTION_KEY', WEBUI_SECRET_KEY) + +OAUTH_SESSION_TOKEN_ENCRYPTION_KEY = os.getenv('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.getenv('OAUTH_MAX_SESSIONS_PER_USER', '10')) + +# Token Exchange Configuration +# Allows external apps to exchange OAuth tokens for OpenWebUI tokens +ENABLE_OAUTH_TOKEN_EXCHANGE = os.getenv('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.getenv('ENABLE_OAUTH_BACKCHANNEL_LOGOUT', 'False').lower() == 'true' + +#################################### +# SCIM Configuration +#################################### + +ENABLE_SCIM = os.getenv('ENABLE_SCIM', os.getenv('SCIM_ENABLED', 'False')).lower() == 'true' +SCIM_TOKEN = os.getenv('SCIM_TOKEN', '') +SCIM_AUTH_PROVIDER = os.getenv('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.getenv('LICENSE_KEY', '') + +LICENSE_BLOB = None +LICENSE_BLOB_PATH = os.getenv('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.getenv('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() + ) + + +#################################### +# WEBUI Identity +#################################### + +WEBUI_NAME = os.getenv('WEBUI_NAME', 'Open WebUI') +if WEBUI_NAME != 'Open WebUI': + WEBUI_NAME += ' (Open WebUI)' + +WEBUI_FAVICON_URL = 'https://openwebui.com/favicon.png' +WEBUI_BUILD_HASH = os.getenv('WEBUI_BUILD_HASH', 'dev-build') +TRUSTED_SIGNATURE_KEY = os.getenv('TRUSTED_SIGNATURE_KEY', '') + +#################################### +# Feature flags +#################################### + +SAFE_MODE = os.getenv('SAFE_MODE', 'False').lower() == 'true' +ENABLE_EASTER_EGGS = os.getenv('ENABLE_EASTER_EGGS', 'True').lower() == 'true' +ENABLE_STAR_SESSIONS_MIDDLEWARE = os.getenv('ENABLE_STAR_SESSIONS_MIDDLEWARE', 'False').lower() == 'true' +ENABLE_KB_EXEC = os.getenv('ENABLE_KB_EXEC', 'False').lower() == 'true' + +ENABLE_PROFILE_IMAGE_URL_FORWARDING = os.getenv('ENABLE_PROFILE_IMAGE_URL_FORWARDING', 'True').lower() == 'true' +PROFILE_IMAGE_ALLOWED_MIME_TYPES = frozenset( + t.strip() + for t in os.getenv( + 'PROFILE_IMAGE_ALLOWED_MIME_TYPES', + 'image/png,image/jpeg,image/gif,image/webp', + ).split(',') + if t.strip() +) + +# Max stored length (bytes) of a data:image profile URI; bounds Postgres/Redis +# bloat from inline avatars and model icons. Unset (default) disables the cap. +_profile_image_max_data_uri_size = os.getenv('PROFILE_IMAGE_MAX_DATA_URI_SIZE', '').strip() +PROFILE_IMAGE_MAX_DATA_URI_SIZE = int(_profile_image_max_data_uri_size) if _profile_image_max_data_uri_size else None + +#################################### +# Forward Headers +#################################### + +ENABLE_FORWARD_USER_INFO_HEADERS = os.getenv('ENABLE_FORWARD_USER_INFO_HEADERS', 'False').lower() == 'true' + +FORWARD_USER_INFO_HEADER_USER_NAME = os.getenv('FORWARD_USER_INFO_HEADER_USER_NAME', 'X-OpenWebUI-User-Name') +FORWARD_USER_INFO_HEADER_USER_ID = os.getenv('FORWARD_USER_INFO_HEADER_USER_ID', 'X-OpenWebUI-User-Id') +FORWARD_USER_INFO_HEADER_USER_EMAIL = os.getenv('FORWARD_USER_INFO_HEADER_USER_EMAIL', 'X-OpenWebUI-User-Email') +FORWARD_USER_INFO_HEADER_USER_ROLE = os.getenv('FORWARD_USER_INFO_HEADER_USER_ROLE', 'X-OpenWebUI-User-Role') +FORWARD_SESSION_INFO_HEADER_MESSAGE_ID = os.getenv('FORWARD_SESSION_INFO_HEADER_MESSAGE_ID', 'X-OpenWebUI-Message-Id') +FORWARD_SESSION_INFO_HEADER_CHAT_ID = os.getenv('FORWARD_SESSION_INFO_HEADER_CHAT_ID', 'X-OpenWebUI-Chat-Id') + +# If set while ENABLE_FORWARD_USER_INFO_HEADERS is True, send one signed HS256 JWT +# (FORWARD_USER_INFO_HEADER_JWT) instead of separate X-OpenWebUI-User-* headers. +FORWARD_USER_INFO_HEADER_JWT_SECRET = (os.environ.get('FORWARD_USER_INFO_HEADER_JWT_SECRET') or '').strip() or None +FORWARD_USER_INFO_HEADER_JWT = os.environ.get('FORWARD_USER_INFO_HEADER_JWT', 'X-OpenWebUI-User-Jwt') +try: + FORWARD_USER_INFO_HEADER_JWT_EXPIRES_SECONDS = int( + os.environ.get('FORWARD_USER_INFO_HEADER_JWT_EXPIRES_SECONDS', '300') + ) +except ValueError: + FORWARD_USER_INFO_HEADER_JWT_EXPIRES_SECONDS = 300 + +#################################### +# Progressive Web App +#################################### + +EXTERNAL_PWA_MANIFEST_URL = os.getenv('EXTERNAL_PWA_MANIFEST_URL', None) + +#################################### +# GROUP DEFAULTS +#################################### + +# Controls the default "Who can share to this group" setting for new groups. +# Env var values: "true" (anyone), "false" (no one), "members" (only group members). +_default_group_share = os.getenv('DEFAULT_GROUP_SHARE_PERMISSION', 'members').strip().lower() +DEFAULT_GROUP_SHARE_PERMISSION = 'members' if _default_group_share == 'members' else _default_group_share == 'true' + +#################################### +# MODELS +#################################### + +ENABLE_CUSTOM_MODEL_FALLBACK = os.getenv('ENABLE_CUSTOM_MODEL_FALLBACK', 'False').lower() == 'true' + +MODELS_CACHE_TTL = os.getenv('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.getenv('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.getenv('ENABLE_IMAGE_CONTENT_TYPE_EXTENSION_FALLBACK', 'False').lower() == 'true' +) + +CHAT_RESPONSE_STREAM_DELTA_CHUNK_SIZE = os.getenv('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 + + +# Maximum tool-call iterations per chat response. Set to -1 for unlimited. +# The old CHAT_RESPONSE_MAX_TOOL_CALL_RETRIES name is accepted as a fallback. +CHAT_RESPONSE_MAX_TOOL_CALL_ITERATIONS = os.getenv( + 'CHAT_RESPONSE_MAX_TOOL_CALL_ITERATIONS', + os.getenv('CHAT_RESPONSE_MAX_TOOL_CALL_RETRIES', '256'), +) + +if CHAT_RESPONSE_MAX_TOOL_CALL_ITERATIONS == '': + CHAT_RESPONSE_MAX_TOOL_CALL_ITERATIONS = 256 +else: + try: + CHAT_RESPONSE_MAX_TOOL_CALL_ITERATIONS = int(CHAT_RESPONSE_MAX_TOOL_CALL_ITERATIONS) + except Exception: + CHAT_RESPONSE_MAX_TOOL_CALL_ITERATIONS = 256 + +# -1 means unlimited (no cap). +if CHAT_RESPONSE_MAX_TOOL_CALL_ITERATIONS == -1: + CHAT_RESPONSE_MAX_TOOL_CALL_ITERATIONS = None + + +# 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.getenv('ENABLE_RESPONSES_API_STATEFUL', 'False').lower() == 'true' + + +CHAT_STREAM_RESPONSE_CHUNK_MAX_BUFFER_SIZE = os.getenv('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 + + #################################### # SENTENCE TRANSFORMERS #################################### -SENTENCE_TRANSFORMERS_BACKEND = os.environ.get('SENTENCE_TRANSFORMERS_BACKEND', '') +SENTENCE_TRANSFORMERS_BACKEND = os.getenv('SENTENCE_TRANSFORMERS_BACKEND', '') if SENTENCE_TRANSFORMERS_BACKEND == '': SENTENCE_TRANSFORMERS_BACKEND = 'torch' -SENTENCE_TRANSFORMERS_MODEL_KWARGS = os.environ.get('SENTENCE_TRANSFORMERS_MODEL_KWARGS', '') +SENTENCE_TRANSFORMERS_MODEL_KWARGS = os.getenv('SENTENCE_TRANSFORMERS_MODEL_KWARGS', '') if SENTENCE_TRANSFORMERS_MODEL_KWARGS == '': SENTENCE_TRANSFORMERS_MODEL_KWARGS = None else: @@ -941,14 +941,12 @@ else: SENTENCE_TRANSFORMERS_MODEL_KWARGS = None -SENTENCE_TRANSFORMERS_CROSS_ENCODER_BACKEND = os.environ.get('SENTENCE_TRANSFORMERS_CROSS_ENCODER_BACKEND', '') +SENTENCE_TRANSFORMERS_CROSS_ENCODER_BACKEND = os.getenv('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', '' -) +SENTENCE_TRANSFORMERS_CROSS_ENCODER_MODEL_KWARGS = os.getenv('SENTENCE_TRANSFORMERS_CROSS_ENCODER_MODEL_KWARGS', '') if SENTENCE_TRANSFORMERS_CROSS_ENCODER_MODEL_KWARGS == '': SENTENCE_TRANSFORMERS_CROSS_ENCODER_MODEL_KWARGS = None else: @@ -961,22 +959,34 @@ else: # 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' + os.getenv('SENTENCE_TRANSFORMERS_CROSS_ENCODER_SIGMOID_ACTIVATION_FUNCTION', 'True').lower() == 'true' ) +#################################### +# TOOLS/FUNCTIONS PIP OPTIONS +#################################### + +ENABLE_PIP_INSTALL_FRONTMATTER_REQUIREMENTS = ( + os.getenv('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() + + #################################### # 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' +ENABLE_VERSION_UPDATE_CHECK = os.getenv('ENABLE_VERSION_UPDATE_CHECK', 'true').lower() == 'true' +OFFLINE_MODE = os.getenv('OFFLINE_MODE', 'false').lower() == 'true' if OFFLINE_MODE: os.environ['HF_HUB_OFFLINE'] = '1' ENABLE_VERSION_UPDATE_CHECK = False #################################### -# AUDIT LOGGING +# Audit logging #################################### @@ -998,7 +1008,7 @@ AUDIT_UVICORN_LOGGER_NAMES = os.getenv('AUDIT_UVICORN_LOGGER_NAMES', 'uvicorn.ac # 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) + MAX_BODY_LOG_SIZE = int(os.getenv('MAX_BODY_LOG_SIZE') or 2048) except ValueError: MAX_BODY_LOG_SIZE = 2048 @@ -1021,66 +1031,39 @@ ENABLE_AUDIT_GET_REQUESTS = os.getenv('ENABLE_AUDIT_GET_REQUESTS', 'False').lowe # 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' +ENABLE_OTEL = os.getenv('ENABLE_OTEL', 'False').lower() == 'true' +ENABLE_OTEL_TRACES = os.getenv('ENABLE_OTEL_TRACES', 'False').lower() == 'true' +ENABLE_OTEL_METRICS = os.getenv('ENABLE_OTEL_METRICS', 'False').lower() == 'true' +ENABLE_OTEL_LOGS = os.getenv('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_EXPORTER_OTLP_ENDPOINT = os.getenv('OTEL_EXPORTER_OTLP_ENDPOINT', 'http://localhost:4317') +OTEL_METRICS_EXPORTER_OTLP_ENDPOINT = os.getenv('OTEL_METRICS_EXPORTER_OTLP_ENDPOINT', OTEL_EXPORTER_OTLP_ENDPOINT) +OTEL_LOGS_EXPORTER_OTLP_ENDPOINT = os.getenv('OTEL_LOGS_EXPORTER_OTLP_ENDPOINT', OTEL_EXPORTER_OTLP_ENDPOINT) +OTEL_EXPORTER_OTLP_INSECURE = os.getenv('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' + os.getenv('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' + os.getenv('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_SERVICE_NAME = os.getenv('OTEL_SERVICE_NAME', 'open-webui') +OTEL_RESOURCE_ATTRIBUTES = os.getenv('OTEL_RESOURCE_ATTRIBUTES', '') # e.g. key1=val1,key2=val2 +OTEL_TRACES_SAMPLER = os.getenv('OTEL_TRACES_SAMPLER', 'parentbased_always_on').lower() +OTEL_BASIC_AUTH_USERNAME = os.getenv('OTEL_BASIC_AUTH_USERNAME', '') +OTEL_BASIC_AUTH_PASSWORD = os.getenv('OTEL_BASIC_AUTH_PASSWORD', '') +OTEL_METRICS_EXPORT_INTERVAL_MILLIS = int(os.getenv('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_METRICS_BASIC_AUTH_USERNAME = os.getenv('OTEL_METRICS_BASIC_AUTH_USERNAME', OTEL_BASIC_AUTH_USERNAME) +OTEL_METRICS_BASIC_AUTH_PASSWORD = os.getenv('OTEL_METRICS_BASIC_AUTH_PASSWORD', OTEL_BASIC_AUTH_PASSWORD) +OTEL_LOGS_BASIC_AUTH_USERNAME = os.getenv('OTEL_LOGS_BASIC_AUTH_USERNAME', OTEL_BASIC_AUTH_USERNAME) +OTEL_LOGS_BASIC_AUTH_PASSWORD = os.getenv('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_OTLP_SPAN_EXPORTER = os.getenv('OTEL_OTLP_SPAN_EXPORTER', 'grpc').lower() # grpc or http -OTEL_METRICS_OTLP_SPAN_EXPORTER = os.environ.get( +OTEL_METRICS_OTLP_SPAN_EXPORTER = os.getenv( '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 = os.getenv( '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 index a3f99bb182..4a82cf7f26 100644 --- a/backend/open_webui/functions.py +++ b/backend/open_webui/functions.py @@ -1,11 +1,10 @@ -import logging -import sys +import asyncio import inspect import json -import asyncio - -from pydantic import BaseModel +import logging +import sys from typing import AsyncGenerator, Generator, Iterator + from fastapi import ( Depends, FastAPI, @@ -16,40 +15,35 @@ from fastapi import ( UploadFile, status, ) +from pydantic import BaseModel from starlette.responses import Response, StreamingResponse - +from open_webui.config import BYPASS_ADMIN_ACCESS_CONTROL from open_webui.constants import ERROR_MESSAGES +from open_webui.env import BYPASS_MODEL_ACCESS_CONTROL, GLOBAL_LOG_LEVEL +from open_webui.models.functions import Functions +from open_webui.models.models import Models +from open_webui.models.users import UserModel 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, + prepend_to_first_user_message_content, ) from open_webui.utils.payload import ( apply_model_params_to_body_openai, apply_system_prompt_to_body, ) +from open_webui.utils.plugin import ( + get_function_module_from_cache, + load_function_module_by_id, +) logging.basicConfig(stream=sys.stdout, level=GLOBAL_LOG_LEVEL) log = logging.getLogger(__name__) @@ -324,11 +318,10 @@ async def generate_function_chat_completion(request, form_data, user, models: di 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]' + 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: diff --git a/backend/open_webui/internal/config.py b/backend/open_webui/internal/config.py new file mode 100644 index 0000000000..46f5b1b67d --- /dev/null +++ b/backend/open_webui/internal/config.py @@ -0,0 +1,265 @@ +"""Database-backed configuration with environment variable defaults.""" + +from __future__ import annotations + +import asyncio +import json +import logging +from datetime import datetime +from functools import reduce +from typing import Any, Optional, Union + +import redis +from open_webui.internal.db import Base, get_async_db, get_db +from open_webui.utils.redis import get_redis_connection +from sqlalchemy import JSON, Column, DateTime, Integer, func, select + +log = logging.getLogger(__name__) + + +# ── Model ──────────────────────────────────────────────────────────────────── + + +class ConfigTable(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()) + + +# ── Blob ───────────────────────────────────────────────────────────────────── + + +class ConfigState: + """In-memory mirror of the single-row config JSON blob.""" + + __slots__ = ('_data',) + + def __init__(self) -> None: + self._data: dict[str, Any] = {} + + @property + def snapshot(self) -> dict: + return self._data + + def read(self, path: str) -> Any: + return reduce( + lambda n, k: n.get(k) if isinstance(n, dict) else None, + path.split('.'), + self._data, + ) + + def write(self, path: str, value: Any) -> None: + keys = path.split('.') + reduce(lambda d, k: d.setdefault(k, {}), keys[:-1], self._data)[keys[-1]] = value + + def replace(self, data: dict) -> None: + self._data = data + + def load(self) -> dict: + with get_db() as db: + row = db.query(ConfigTable).order_by(ConfigTable.id.desc()).first() + self._data = row.data if row else {'version': 0, 'ui': {}} + return self._data + + def persist(self, data: dict | None = None) -> None: + if data is not None: + self._data = data + with get_db() as db: + row = db.query(ConfigTable).first() + if row is None: + db.add(ConfigTable(data=self._data, version=0)) + else: + row.data, row.updated_at = self._data, datetime.now() + db.add(row) + db.commit() + + async def persist_async(self, data: dict | None = None) -> None: + if data is not None: + self._data = data + async with get_async_db() as db: + result = await db.execute(select(ConfigTable).limit(1)) + row = result.scalars().first() + if row is None: + db.add(ConfigTable(data=self._data, version=0)) + else: + row.data, row.updated_at = self._data, datetime.now() + db.add(row) + await db.commit() + + def clear(self) -> None: + with get_db() as db: + db.query(ConfigTable).delete() + db.commit() + + async def clear_async(self) -> None: + from sqlalchemy import delete as sa_delete + + async with get_async_db() as db: + await db.execute(sa_delete(ConfigTable)) + await db.commit() + + +STATE = ConfigState() + + +# ── ConfigVar ────────────────────────────────────────────────────────────────── + + +_persist_enabled: bool = True +_oauth_persist_enabled: bool = False +_all_configs: list[ConfigVar] = [] + + +def initialize(*, enable_persistent: bool = True, enable_oauth_persistent: bool = False) -> dict: + global _persist_enabled, _oauth_persist_enabled + _persist_enabled = enable_persistent + _oauth_persist_enabled = enable_oauth_persistent + return STATE.load() + + +class ConfigVar: + __slots__ = ('env_name', 'config_path', 'env_value', 'config_value', 'value') + + def __init__(self, env_name: str, config_path: str, env_value: Any) -> None: + self.env_name = env_name + self.config_path = config_path + self.env_value = env_value + self.config_value = STATE.read(config_path) + + if self.config_value is not None and _persist_enabled: + if config_path.startswith('oauth.') and not _oauth_persist_enabled: + log.info("Skipping DB value for '%s' (OAuth persistence disabled)", env_name) + self.value = env_value + else: + log.info("'%s' loaded from database", env_name) + self.value = self.config_value + else: + self.value = env_value + + _all_configs.append(self) + + def __str__(self) -> str: + return str(self.value) + + def __repr__(self) -> str: + return f'' + + @property + def __dict__(self): # type: ignore[override] + raise TypeError(f"ConfigVar('{self.env_name}') cannot be cast to dict; use .value") + + def __getattribute__(self, item: str): + if item == '__dict__': + raise TypeError('ConfigVar cannot be cast to dict; use .value') + return super().__getattribute__(item) + + def refresh(self) -> None: + current = STATE.read(self.config_path) + if current is not None: + self.value = current + log.info('Refreshed %s → %s', self.env_name, self.value) + + def commit(self) -> None: + log.info("Persisting '%s'", self.env_name) + STATE.write(self.config_path, self.value) + self.config_value = self.value + STATE.persist() + + async def commit_async(self) -> None: + log.info("Persisting '%s'", self.env_name) + STATE.write(self.config_path, self.value) + self.config_value = self.value + await STATE.persist_async() + + +# ── AppConfig ────────────────────────────────────────────────────────── + + +class AppConfig: + """Attribute-style container for ConfigVars with optional Redis sync.""" + + def __init__( + self, + *, + redis_url: Optional[str] = None, + redis_sentinels: Optional[list] = None, + redis_cluster: bool = False, + redis_key_prefix: str = 'open-webui', + ) -> None: + super().__setattr__('_entries', {}) + super().__setattr__('_key_prefix', redis_key_prefix) + + # If sentinels weren't explicitly provided, read from env. + if redis_sentinels is None: + from open_webui.env import REDIS_SENTINEL_HOSTS, REDIS_SENTINEL_PORT + from open_webui.utils.redis import get_sentinels_from_env + + redis_sentinels = get_sentinels_from_env(REDIS_SENTINEL_HOSTS, REDIS_SENTINEL_PORT) + + rc: Union[redis.Redis, redis.cluster.RedisCluster, None] = None + if redis_url: + rc = get_redis_connection(redis_url, redis_sentinels or [], redis_cluster, decode_responses=True) + super().__setattr__('_rc', rc) + + def __setattr__(self, name: str, value: Any) -> None: + entries: dict = super().__getattribute__('_entries') + + if isinstance(value, ConfigVar): + entries[name] = value + return + + entries[name].value = value + + try: + asyncio.get_running_loop().create_task(self._write_async(name)) + except RuntimeError: + entries[name].commit() + + rc = super().__getattribute__('_rc') + if rc and _persist_enabled: + prefix = super().__getattribute__('_key_prefix') + try: + rc.set(f'{prefix}:config:{name}', json.dumps(entries[name].value)) + except Exception as exc: + log.error("Redis write failed for '%s': %s", name, exc) + + async def _write_async(self, name: str) -> None: + try: + await self._entries[name].commit_async() + except Exception as exc: + log.error("Async persist failed for '%s': %s", name, exc) + + def __getattr__(self, name: str) -> Any: + entries = super().__getattribute__('_entries') + if name not in entries: + raise AttributeError(f"No config key '{name}'") + + rc = super().__getattribute__('_rc') + if rc and _persist_enabled: + prefix = super().__getattribute__('_key_prefix') + try: + raw = rc.get(f'{prefix}:config:{name}') + if raw is not None: + decoded = json.loads(raw) + if entries[name].value != decoded: + entries[name].value = decoded + log.info("Updated '%s' from Redis", name) + except Exception as exc: + log.error("Redis read failed for '%s': %s", name, exc) + + return entries[name].value + + def _sync_to_redis(self) -> None: + rc = super().__getattribute__('_rc') + if not rc or not _persist_enabled: + return + prefix = super().__getattribute__('_key_prefix') + for name, s in super().__getattribute__('_entries').items(): + try: + rc.set(f'{prefix}:config:{name}', json.dumps(s.value)) + except Exception as exc: + log.error("Redis sync failed for '%s': %s", name, exc) diff --git a/backend/open_webui/internal/db.py b/backend/open_webui/internal/db.py index 9a6576fd7c..d3402b0310 100644 --- a/backend/open_webui/internal/db.py +++ b/backend/open_webui/internal/db.py @@ -1,36 +1,36 @@ -import os -import sys +from __future__ import annotations + import json import logging +import os +import sys 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_ENABLE_SESSION_SHARING, + DATABASE_ENABLE_SQLITE_WAL, 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_SCHEMA, 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, + DATABASE_SQLITE_PRAGMA_MMAP_SIZE, + DATABASE_SQLITE_PRAGMA_SYNCHRONOUS, + DATABASE_SQLITE_PRAGMA_TEMP_STORE, + DATABASE_URL, ENABLE_DB_MIGRATIONS, + OPEN_WEBUI_DIR, ) -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 import Dialect, MetaData, create_engine, event, types +from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine from sqlalchemy.ext.declarative import declarative_base -from sqlalchemy.orm import scoped_session, sessionmaker, Session -from sqlalchemy.pool import QueuePool, NullPool +from sqlalchemy.orm import Session, scoped_session, sessionmaker +from sqlalchemy.pool import NullPool, QueuePool from sqlalchemy.sql.type_api import _T from typing_extensions import Self @@ -117,61 +117,25 @@ 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 +class JSONField(types.TypeDecorator): # TEXT-backed JSON storage + """Store arbitrary Python objects as JSON-encoded TEXT. + + Used instead of native JSON columns for portability across SQLite and + PostgreSQL. Values are serialized with ``json.dumps`` on write and + deserialized with ``json.loads`` on read. + """ + + impl = types.UnicodeText cache_ok = True - def process_bind_param(self, value: Optional[_T], dialect: Dialect) -> Any: - return json.dumps(value) + def process_bind_param(self, value: _T | None, dialect: Dialect) -> Any: + return json.dumps(value) if value is not None else None - def process_result_value(self, value: Optional[_T], dialect: Dialect) -> Any: - if value is not None: - return json.loads(value) + def process_result_value(self, value: _T | None, dialect: Dialect) -> Any: + return json.loads(value) if value is not None else None - 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) + def copy(self, **kwargs: Any) -> Self: + return JSONField(length=self.impl.length) # Normalize SSL params from the URL once; the sync engine needs them @@ -410,7 +374,7 @@ async def get_async_db(): @asynccontextmanager -async def get_async_db_context(db: Optional[AsyncSession] = None): +async def get_async_db_context(db: AsyncSession | None = 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 diff --git a/backend/open_webui/internal/migrations/001_initial_schema.py b/backend/open_webui/internal/migrations/001_initial_schema.py deleted file mode 100644 index 4268201ae7..0000000000 --- a/backend/open_webui/internal/migrations/001_initial_schema.py +++ /dev/null @@ -1,253 +0,0 @@ -"""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 deleted file mode 100644 index e3e557602b..0000000000 --- a/backend/open_webui/internal/migrations/002_add_local_sharing.py +++ /dev/null @@ -1,45 +0,0 @@ -"""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 deleted file mode 100644 index acb63fc728..0000000000 --- a/backend/open_webui/internal/migrations/003_add_auth_api_key.py +++ /dev/null @@ -1,45 +0,0 @@ -"""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 deleted file mode 100644 index abed1727b9..0000000000 --- a/backend/open_webui/internal/migrations/004_add_archived.py +++ /dev/null @@ -1,45 +0,0 @@ -"""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 deleted file mode 100644 index bff311e2d4..0000000000 --- a/backend/open_webui/internal/migrations/005_add_updated_at.py +++ /dev/null @@ -1,125 +0,0 @@ -"""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 deleted file mode 100644 index 86f90eb880..0000000000 --- a/backend/open_webui/internal/migrations/006_migrate_timestamps_and_charfields.py +++ /dev/null @@ -1,129 +0,0 @@ -"""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 deleted file mode 100644 index 19a26c3515..0000000000 --- a/backend/open_webui/internal/migrations/007_add_user_last_active_at.py +++ /dev/null @@ -1,78 +0,0 @@ -"""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 deleted file mode 100644 index f3af64fe95..0000000000 --- a/backend/open_webui/internal/migrations/008_add_memory.py +++ /dev/null @@ -1,52 +0,0 @@ -"""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 deleted file mode 100644 index 45f4a3d163..0000000000 --- a/backend/open_webui/internal/migrations/009_add_models.py +++ /dev/null @@ -1,60 +0,0 @@ -"""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 deleted file mode 100644 index e523d6a098..0000000000 --- a/backend/open_webui/internal/migrations/010_migrate_modelfiles_to_models.py +++ /dev/null @@ -1,130 +0,0 @@ -"""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 deleted file mode 100644 index 73d27392f7..0000000000 --- a/backend/open_webui/internal/migrations/011_add_user_settings.py +++ /dev/null @@ -1,47 +0,0 @@ -"""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 deleted file mode 100644 index a488678c3c..0000000000 --- a/backend/open_webui/internal/migrations/012_add_tools.py +++ /dev/null @@ -1,60 +0,0 @@ -"""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 deleted file mode 100644 index db77cfff3a..0000000000 --- a/backend/open_webui/internal/migrations/013_add_user_info.py +++ /dev/null @@ -1,47 +0,0 @@ -"""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 deleted file mode 100644 index 9c01ac08c3..0000000000 --- a/backend/open_webui/internal/migrations/014_add_files.py +++ /dev/null @@ -1,54 +0,0 @@ -"""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 deleted file mode 100644 index 488e546ab1..0000000000 --- a/backend/open_webui/internal/migrations/015_add_functions.py +++ /dev/null @@ -1,60 +0,0 @@ -"""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 deleted file mode 100644 index 57a2dfbd5b..0000000000 --- a/backend/open_webui/internal/migrations/016_add_valves_and_is_active.py +++ /dev/null @@ -1,49 +0,0 @@ -"""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 deleted file mode 100644 index f998c742d1..0000000000 --- a/backend/open_webui/internal/migrations/017_add_user_oauth_sub.py +++ /dev/null @@ -1,44 +0,0 @@ -"""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 deleted file mode 100644 index 7f7cd4f725..0000000000 --- a/backend/open_webui/internal/migrations/018_add_function_is_global.py +++ /dev/null @@ -1,48 +0,0 @@ -"""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 deleted file mode 100644 index 3d54d02e3a..0000000000 --- a/backend/open_webui/internal/wrappers.py +++ /dev/null @@ -1,84 +0,0 @@ -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 index d47c88fc54..e05497c616 100644 --- a/backend/open_webui/main.py +++ b/backend/open_webui/main.py @@ -1,31 +1,26 @@ +from __future__ import annotations + import asyncio import inspect import json import logging import mimetypes import os +import random +import re import shutil import sys import time -import random -import re +from contextlib import asynccontextmanager +from typing import Optional +from urllib.parse import parse_qs, urlencode, urlparse 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 aiocache import cached from fastapi import ( + BackgroundTasks, Depends, FastAPI, File, @@ -33,30 +28,516 @@ from fastapi import ( HTTPException, Request, UploadFile, - status, applications, - BackgroundTasks, + status, ) -from fastapi.openapi.docs import get_swagger_ui_html - from fastapi.middleware.cors import CORSMiddleware +from fastapi.openapi.docs import get_swagger_ui_html from fastapi.responses import FileResponse, JSONResponse, RedirectResponse from fastapi.staticfiles import StaticFiles - -from starlette_compress import CompressMiddleware - +from pydantic import BaseModel +from redis import Redis +from sqlalchemy import text +from sqlalchemy.ext.asyncio import AsyncSession +from starlette.datastructures import Headers 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 starlette_compress import CompressMiddleware +from starsessions import ( + SessionAutoloadMiddleware, +) from starsessions import ( SessionMiddleware as StarSessionsMiddleware, - SessionAutoloadMiddleware, ) from starsessions.stores.redis import RedisStore +from open_webui.config import ( + ADMIN_EMAIL, + API_KEYS_ALLOWED_ENDPOINTS, + AUDIO_STT_ALLOWED_EXTENSIONS, + AUDIO_STT_AZURE_API_KEY, + AUDIO_STT_AZURE_BASE_URL, + AUDIO_STT_AZURE_LOCALES, + AUDIO_STT_AZURE_MAX_SPEAKERS, + AUDIO_STT_AZURE_REGION, + # Audio + AUDIO_STT_ENGINE, + AUDIO_STT_MISTRAL_API_BASE_URL, + AUDIO_STT_MISTRAL_API_KEY, + AUDIO_STT_MISTRAL_USE_CHAT_COMPLETIONS, + AUDIO_STT_MODEL, + AUDIO_STT_OPENAI_API_BASE_URL, + AUDIO_STT_OPENAI_API_KEY, + AUDIO_STT_SUPPORTED_CONTENT_TYPES, + AUDIO_TTS_API_KEY, + AUDIO_TTS_AZURE_SPEECH_BASE_URL, + AUDIO_TTS_AZURE_SPEECH_OUTPUT_FORMAT, + AUDIO_TTS_AZURE_SPEECH_REGION, + AUDIO_TTS_ENGINE, + AUDIO_TTS_MISTRAL_API_BASE_URL, + AUDIO_TTS_MISTRAL_API_KEY, + AUDIO_TTS_MODEL, + AUDIO_TTS_OPENAI_API_BASE_URL, + AUDIO_TTS_OPENAI_API_KEY, + AUDIO_TTS_OPENAI_PARAMS, + AUDIO_TTS_SPLIT_ON, + AUDIO_TTS_VOICE, + AUTOCOMPLETE_GENERATION_INPUT_MAX_LENGTH, + AUTOCOMPLETE_GENERATION_PROMPT_TEMPLATE, + # Image + AUTOMATIC1111_API_AUTH, + AUTOMATIC1111_BASE_URL, + AUTOMATIC1111_PARAMS, + AUTOMATION_MAX_COUNT, + AUTOMATION_MIN_INTERVAL, + BING_SEARCH_V7_ENDPOINT, + BING_SEARCH_V7_SUBSCRIPTION_KEY, + BOCHA_SEARCH_API_KEY, + BRAVE_SEARCH_API_KEY, + BRAVE_SEARCH_CONTEXT_TOKENS, + BYPASS_ADMIN_ACCESS_CONTROL, + BYPASS_EMBEDDING_AND_RETRIEVAL, + BYPASS_WEB_SEARCH_EMBEDDING_AND_RETRIEVAL, + BYPASS_WEB_SEARCH_WEB_LOADER, + CACHE_DIR, + CHUNK_MIN_SIZE_TARGET, + CHUNK_OVERLAP, + CHUNK_SIZE, + CODE_EXECUTION_ENGINE, + CODE_EXECUTION_JUPYTER_AUTH, + CODE_EXECUTION_JUPYTER_AUTH_PASSWORD, + CODE_EXECUTION_JUPYTER_AUTH_TOKEN, + CODE_EXECUTION_JUPYTER_TIMEOUT, + CODE_EXECUTION_JUPYTER_URL, + CODE_INTERPRETER_ENGINE, + CODE_INTERPRETER_JUPYTER_AUTH, + CODE_INTERPRETER_JUPYTER_AUTH_PASSWORD, + CODE_INTERPRETER_JUPYTER_AUTH_TOKEN, + CODE_INTERPRETER_JUPYTER_TIMEOUT, + CODE_INTERPRETER_JUPYTER_URL, + CODE_INTERPRETER_PROMPT_TEMPLATE, + COMFYUI_API_KEY, + COMFYUI_BASE_URL, + COMFYUI_WORKFLOW, + COMFYUI_WORKFLOW_NODES, + CONTENT_EXTRACTION_ENGINE, + CORS_ALLOW_ORIGIN, + DATALAB_MARKER_ADDITIONAL_CONFIG, + DATALAB_MARKER_API_BASE_URL, + DATALAB_MARKER_API_KEY, + DATALAB_MARKER_DISABLE_IMAGE_EXTRACTION, + DATALAB_MARKER_FORCE_OCR, + DATALAB_MARKER_FORMAT_LINES, + DATALAB_MARKER_OUTPUT_FORMAT, + DATALAB_MARKER_PAGINATE, + DATALAB_MARKER_SKIP_CACHE, + DATALAB_MARKER_STRIP_EXISTING_OCR, + DATALAB_MARKER_USE_LLM, + DDGS_BACKEND, + DEEPGRAM_API_KEY, + DEFAULT_ARENA_MODEL, + DEFAULT_GROUP_ID, + DEFAULT_LOCALE, + DEFAULT_MODEL_METADATA, + DEFAULT_MODEL_PARAMS, + DEFAULT_MODELS, + DEFAULT_PINNED_MODELS, + DEFAULT_PROMPT_SUGGESTIONS, + DEFAULT_RAG_TEMPLATE, + DEFAULT_USER_ROLE, + DOCLING_API_KEY, + DOCLING_PARAMS, + DOCLING_SERVER_URL, + DOCUMENT_INTELLIGENCE_ENDPOINT, + DOCUMENT_INTELLIGENCE_KEY, + DOCUMENT_INTELLIGENCE_MODEL, + ENABLE_ADMIN_ANALYTICS, + # Admin + ENABLE_ADMIN_CHAT_ACCESS, + ENABLE_ADMIN_EXPORT, + ENABLE_API_KEYS, + ENABLE_API_KEYS_ENDPOINT_RESTRICTIONS, + ENABLE_ASYNC_EMBEDDING, + ENABLE_AUTOCOMPLETE_GENERATION, + ENABLE_AUTOMATIONS, + # Model list + ENABLE_BASE_MODELS_CACHE, + ENABLE_CALENDAR, + ENABLE_CHANNELS, + # Code Execution + ENABLE_CODE_EXECUTION, + ENABLE_CODE_INTERPRETER, + ENABLE_COMMUNITY_SHARING, + # Direct Connections + ENABLE_DIRECT_CONNECTIONS, + ENABLE_EVALUATION_ARENA_MODELS, + ENABLE_FOLDERS, + ENABLE_FOLLOW_UP_GENERATION, + ENABLE_GOOGLE_DRIVE_INTEGRATION, + ENABLE_IMAGE_EDIT, + ENABLE_IMAGE_GENERATION, + ENABLE_IMAGE_PROMPT_GENERATION, + # WebUI (LDAP) + ENABLE_LDAP, + ENABLE_LDAP_GROUP_CREATION, + # LDAP Group Management + ENABLE_LDAP_GROUP_MANAGEMENT, + ENABLE_LOGIN_FORM, + ENABLE_MARKDOWN_HEADER_TEXT_SPLITTER, + ENABLE_MEMORIES, + ENABLE_MESSAGE_RATING, + ENABLE_NOTES, + # WebUI (OAuth) + ENABLE_OAUTH_ROLE_MANAGEMENT, + # Ollama + ENABLE_OLLAMA_API, + ENABLE_ONEDRIVE_BUSINESS, + ENABLE_ONEDRIVE_INTEGRATION, + ENABLE_ONEDRIVE_PERSONAL, + # OpenAI + ENABLE_OPENAI_API, + ENABLE_PASSWORD_CHANGE_FORM, + ENABLE_RAG_HYBRID_SEARCH, + ENABLE_RAG_HYBRID_SEARCH_ENRICHED_TEXTS, + ENABLE_RAG_LOCAL_WEB_FETCH, + ENABLE_RETRIEVAL_QUERY_GENERATION, + ENABLE_SEARCH_QUERY_GENERATION, + ENABLE_SIGNUP, + ENABLE_TAGS_GENERATION, + ENABLE_TITLE_GENERATION, + ENABLE_USER_STATUS, + ENABLE_USER_WEBHOOKS, + ENABLE_VOICE_MODE_PROMPT, + ENABLE_WEB_LOADER_SSL_VERIFICATION, + # Retrieval (Web Search) + ENABLE_WEB_SEARCH, + # Misc + ENV, + EVALUATION_ARENA_MODELS, + EXA_API_KEY, + EXTERNAL_DOCUMENT_LOADER_API_KEY, + EXTERNAL_DOCUMENT_LOADER_URL, + EXTERNAL_WEB_LOADER_API_KEY, + EXTERNAL_WEB_LOADER_URL, + EXTERNAL_WEB_SEARCH_API_KEY, + EXTERNAL_WEB_SEARCH_URL, + FILE_IMAGE_COMPRESSION_HEIGHT, + FILE_IMAGE_COMPRESSION_WIDTH, + FIRECRAWL_API_BASE_URL, + FIRECRAWL_API_KEY, + FIRECRAWL_TIMEOUT, + FOLDER_MAX_FILE_COUNT, + FOLLOW_UP_GENERATION_PROMPT_TEMPLATE, + FRONTEND_BUILD_DIR, + GOOGLE_DRIVE_API_KEY, + GOOGLE_DRIVE_CLIENT_ID, + GOOGLE_PSE_API_KEY, + GOOGLE_PSE_ENGINE_ID, + IFRAME_CSP, + IMAGE_EDIT_ENGINE, + IMAGE_EDIT_MODEL, + IMAGE_EDIT_SIZE, + IMAGE_GENERATION_ENGINE, + IMAGE_GENERATION_MODEL, + IMAGE_PROMPT_GENERATION_PROMPT_TEMPLATE, + IMAGE_SIZE, + IMAGE_STEPS, + IMAGES_EDIT_COMFYUI_API_KEY, + IMAGES_EDIT_COMFYUI_BASE_URL, + IMAGES_EDIT_COMFYUI_WORKFLOW, + IMAGES_EDIT_COMFYUI_WORKFLOW_NODES, + IMAGES_EDIT_GEMINI_API_BASE_URL, + IMAGES_EDIT_GEMINI_API_KEY, + IMAGES_EDIT_OPENAI_API_BASE_URL, + IMAGES_EDIT_OPENAI_API_KEY, + IMAGES_EDIT_OPENAI_API_VERSION, + IMAGES_GEMINI_API_BASE_URL, + IMAGES_GEMINI_API_KEY, + IMAGES_GEMINI_ENDPOINT_METHOD, + IMAGES_OPENAI_API_BASE_URL, + IMAGES_OPENAI_API_KEY, + IMAGES_OPENAI_API_PARAMS, + IMAGES_OPENAI_API_VERSION, + JINA_API_BASE_URL, + JINA_API_KEY, + JWT_EXPIRES_IN, + KAGI_SEARCH_API_KEY, + LDAP_APP_DN, + LDAP_APP_PASSWORD, + LDAP_ATTRIBUTE_FOR_GROUPS, + LDAP_ATTRIBUTE_FOR_MAIL, + LDAP_ATTRIBUTE_FOR_USERNAME, + LDAP_CA_CERT_FILE, + LDAP_CIPHERS, + LDAP_SEARCH_BASE, + LDAP_SEARCH_FILTERS, + LDAP_SERVER_HOST, + LDAP_SERVER_LABEL, + LDAP_SERVER_PORT, + LDAP_USE_TLS, + LDAP_VALIDATE_CERT, + MINERU_API_KEY, + MINERU_API_MODE, + MINERU_API_TIMEOUT, + MINERU_API_URL, + MINERU_FILE_EXTENSIONS, + MINERU_PARAMS, + MISTRAL_OCR_API_BASE_URL, + MISTRAL_OCR_API_KEY, + MODEL_ORDER_LIST, + MOJEEK_SEARCH_API_KEY, + OAUTH_ADMIN_ROLES, + OAUTH_ALLOWED_ROLES, + OAUTH_AUTO_REDIRECT, + OAUTH_EMAIL_CLAIM, + OAUTH_PICTURE_CLAIM, + OAUTH_PROVIDERS, + OAUTH_ROLES_CLAIM, + OAUTH_SUB_CLAIM, + OAUTH_USERNAME_CLAIM, + OLLAMA_API_CONFIGS, + OLLAMA_BASE_URLS, + OLLAMA_CLOUD_WEB_SEARCH_API_KEY, + ONEDRIVE_CLIENT_ID_BUSINESS, + ONEDRIVE_CLIENT_ID_PERSONAL, + ONEDRIVE_SHAREPOINT_TENANT_ID, + ONEDRIVE_SHAREPOINT_URL, + OPENAI_API_BASE_URLS, + OPENAI_API_CONFIGS, + OPENAI_API_KEYS, + PADDLEOCR_VL_BASE_URL, + PADDLEOCR_VL_TOKEN, + PDF_EXTRACT_IMAGES, + PDF_LOADER_MODE, + PENDING_USER_OVERLAY_CONTENT, + PENDING_USER_OVERLAY_TITLE, + PERPLEXITY_API_KEY, + PERPLEXITY_MODEL, + PERPLEXITY_SEARCH_API_URL, + PERPLEXITY_SEARCH_CONTEXT_USAGE, + PLAYWRIGHT_TIMEOUT, + PLAYWRIGHT_WS_URL, + QUERY_GENERATION_PROMPT_TEMPLATE, + RAG_ALLOWED_FILE_EXTENSIONS, + RAG_AZURE_OPENAI_API_KEY, + RAG_AZURE_OPENAI_API_VERSION, + RAG_AZURE_OPENAI_BASE_URL, + RAG_EMBEDDING_BATCH_SIZE, + RAG_EMBEDDING_CONCURRENT_REQUESTS, + RAG_EMBEDDING_ENGINE, + RAG_EMBEDDING_MODEL, + RAG_EMBEDDING_MODEL_AUTO_UPDATE, + RAG_EMBEDDING_MODEL_TRUST_REMOTE_CODE, + RAG_EXTERNAL_RERANKER_API_KEY, + RAG_EXTERNAL_RERANKER_TIMEOUT, + RAG_EXTERNAL_RERANKER_URL, + RAG_FILE_MAX_COUNT, + RAG_FILE_MAX_SIZE, + RAG_FULL_CONTEXT, + RAG_HYBRID_BM25_WEIGHT, + RAG_OLLAMA_API_KEY, + RAG_OLLAMA_BASE_URL, + RAG_OPENAI_API_BASE_URL, + RAG_OPENAI_API_KEY, + RAG_RELEVANCE_THRESHOLD, + RAG_RERANKING_BATCH_SIZE, + RAG_RERANKING_ENGINE, + RAG_RERANKING_MODEL, + RAG_RERANKING_MODEL_AUTO_UPDATE, + RAG_RERANKING_MODEL_TRUST_REMOTE_CODE, + # Retrieval + RAG_TEMPLATE, + RAG_TEXT_SPLITTER, + RAG_TOP_K, + RAG_TOP_K_RERANKER, + RESPONSE_WATERMARK, + SEARCHAPI_API_KEY, + SEARCHAPI_ENGINE, + SEARXNG_LANGUAGE, + SEARXNG_QUERY_URL, + SERPAPI_API_KEY, + SERPAPI_ENGINE, + SERPER_API_KEY, + SERPLY_API_KEY, + SERPSTACK_API_KEY, + SERPSTACK_HTTPS, + SHOW_ADMIN_DETAILS, + SOUGOU_API_SID, + SOUGOU_API_SK, + STATIC_DIR, + TAGS_GENERATION_PROMPT_TEMPLATE, + # Tasks + TASK_MODEL, + TASK_MODEL_EXTERNAL, + TAVILY_API_KEY, + TAVILY_EXTRACT_DEPTH, + # Terminal Server + TERMINAL_SERVER_CONNECTIONS, + # Thread pool size for FastAPI/AnyIO + THREAD_POOL_SIZE, + TIKA_SERVER_URL, + TIKTOKEN_ENCODING_NAME, + TITLE_GENERATION_PROMPT_TEMPLATE, + # Tool Server Configs + TOOL_SERVER_CONNECTIONS, + TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE, + UPLOAD_DIR, + USER_PERMISSIONS, + VOICE_MODE_PROMPT_TEMPLATE, + WEB_FETCH_MAX_CONTENT_LENGTH, + WEB_LOADER_CONCURRENT_REQUESTS, + WEB_LOADER_ENGINE, + WEB_LOADER_TIMEOUT, + WEB_SEARCH_CONCURRENT_REQUESTS, + WEB_SEARCH_DOMAIN_FILTER_LIST, + WEB_SEARCH_ENGINE, + WEB_SEARCH_RESULT_COUNT, + WEB_SEARCH_TRUST_ENV, + WEBHOOK_URL, + # WebUI + WEBUI_AUTH, + WEBUI_BANNERS, + WEBUI_NAME, + WEBUI_URL, + WHISPER_LANGUAGE, + WHISPER_MODEL, + WHISPER_MODEL_AUTO_UPDATE, + WHISPER_MODEL_DIR, + WHISPER_VAD_FILTER, + YACY_PASSWORD, + YACY_QUERY_URL, + YACY_USERNAME, + YANDEX_WEB_SEARCH_API_KEY, + YANDEX_WEB_SEARCH_CONFIG, + YANDEX_WEB_SEARCH_URL, + YOUCOM_API_KEY, + LINKUP_API_KEY, + LINKUP_SEARCH_PARAMS, + YOUTUBE_LOADER_LANGUAGE, + YOUTUBE_LOADER_PROXY_URL, + AppConfig, + async_reset_config, + reset_config, +) +from open_webui.constants import ERROR_MESSAGES, TASKS +from open_webui.env import ( + AIOHTTP_CLIENT_SESSION_SSL, + AUDIT_EXCLUDED_PATHS, + AUDIT_INCLUDED_PATHS, + AUDIT_LOG_LEVEL, + BYPASS_MODEL_ACCESS_CONTROL, + CHANGELOG, + DEPLOYMENT_ID, + ENABLE_AUDIT_GET_REQUESTS, + ENABLE_COMPRESSION_MIDDLEWARE, + ENABLE_CUSTOM_MODEL_FALLBACK, + ENABLE_EASTER_EGGS, + # OAuth Back-Channel Logout + ENABLE_OAUTH_BACKCHANNEL_LOGOUT, + ENABLE_OTEL, + ENABLE_PUBLIC_ACTIVE_USERS_COUNT, + # SCIM + ENABLE_SCIM, + ENABLE_SIGNUP_PASSWORD_CONFIRMATION, + ENABLE_STAR_SESSIONS_MIDDLEWARE, + ENABLE_VERSION_UPDATE_CHECK, + ENABLE_WEBSOCKET_SUPPORT, + EXTERNAL_PWA_MANIFEST_URL, + GLOBAL_LOG_LEVEL, + INSTANCE_ID, + LICENSE_KEY, + LOG_FORMAT, + MAX_BODY_LOG_SIZE, + # Redis + REDIS_CLUSTER, + REDIS_KEY_PREFIX, + REDIS_URL, + RESET_CONFIG_ON_START, + SAFE_MODE, + SCIM_TOKEN, + VERSION, + # Admin Account Runtime Creation + WEBUI_ADMIN_EMAIL, + WEBUI_ADMIN_NAME, + WEBUI_ADMIN_PASSWORD, + WEBUI_AUTH_SIGNOUT_REDIRECT_URL, + WEBUI_AUTH_TRUSTED_EMAIL_HEADER, + WEBUI_AUTH_TRUSTED_NAME_HEADER, + WEBUI_BUILD_HASH, + WEBUI_SECRET_KEY, + WEBUI_SESSION_COOKIE_SAME_SITE, + WEBUI_SESSION_COOKIE_SECURE, +) +from open_webui.internal.db import ScopedSession, engine, get_async_session +from open_webui.models.access_grants import AccessGrants +from open_webui.models.channels import Channels +from open_webui.models.chats import ChatForm, Chats +from open_webui.models.functions import Functions +from open_webui.models.messages import Messages +from open_webui.models.models import Models +from open_webui.models.users import UserModel, Users +from open_webui.routers import ( + analytics, + audio, + auths, + automations, + calendar, + channels, + chats, + configs, + evaluations, + files, + folders, + functions, + groups, + images, + knowledge, + memories, + models, + notes, + ollama, + openai, + pipelines, + prompts, + retrieval, + scim, + skills, + tasks, + terminals, + tools, + users, + utils, +) +from open_webui.routers.retrieval import ( + get_ef, + get_embedding_function, + get_reranking_function, + get_rf, +) +from open_webui.socket.main import ( + MODELS, + get_event_emitter, + get_models_in_use, + get_user_id_from_session_pool, + periodic_session_pool_cleanup, + periodic_usage_pool_cleanup, +) +from open_webui.socket.main import ( + app as socket_app, +) +from open_webui.tasks import ( + cleanup_task, + create_task, + has_active_tasks, + list_task_ids_by_item_id, + list_tasks, + redis_task_command_listener, + stop_item_tasks, + stop_task, +) # Import from tasks.py from open_webui.utils import logger +from open_webui.utils.actions import chat_action as chat_action_handler from open_webui.utils.asgi_middleware import ( AuthTokenMiddleware, CommitSessionMiddleware, @@ -64,538 +545,48 @@ from open_webui.utils.asgi_middleware import ( 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.utils.auth import ( + create_admin_user, + decode_token, + get_admin_user, + get_http_authorization_cred, + get_license_data, + get_verified_user, ) -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_ALLOWED_EXTENSIONS, - 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, - BRAVE_SEARCH_CONTEXT_TOKENS, - 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, - IFRAME_CSP, - # 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, - ENABLE_VOICE_MODE_PROMPT, - 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 ( + chat_completed as chat_completed_handler, ) 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.logger import start_logger 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.models import ( + check_model_access, + get_all_base_models, + get_all_models, + get_filtered_models, ) -from open_webui.utils.plugin import install_tool_and_function_dependencies from open_webui.utils.oauth import ( + OAuthClientInformationFull, + OAuthClientManager, + OAuthManager, + decrypt_data, + encrypt_data, 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.plugin import install_tool_and_function_dependencies +from open_webui.utils.redis import get_redis_client, get_redis_connection 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, - has_active_tasks, - cleanup_task, - 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 +from open_webui.utils.session_pool import get_session +from open_webui.utils.tools import set_terminal_servers, set_tool_servers if SAFE_MODE: print('SAFE MODE ENABLED') @@ -621,7 +612,7 @@ class SPAStaticFiles(StaticFiles): if LOG_FORMAT != 'json': - print(rf""" + banner = rf""" ██████╗ ██████╗ ███████╗███╗ ██╗ ██╗ ██╗███████╗██████╗ ██╗ ██╗██╗ ██╔═══██╗██╔══██╗██╔════╝████╗ ██║ ██║ ██║██╔════╝██╔══██╗██║ ██║██║ ██║ ██║██████╔╝█████╗ ██╔██╗ ██║ ██║ █╗ ██║█████╗ ██████╔╝██║ ██║██║ @@ -633,7 +624,12 @@ if LOG_FORMAT != 'json': 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 -""") +""" + try: + print(banner) + except UnicodeEncodeError: + # Stdout can't encode the box-drawing banner (Windows cp1252, redirected/headless stdout); fall back to ASCII. + print(f'Open WebUI v{VERSION} - building the best AI user interface.\nhttps://github.com/open-webui/open-webui') @asynccontextmanager @@ -665,12 +661,7 @@ async def lifespan(app: FastAPI): 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, - ) + app.state.redis = get_redis_client(async_mode=True) if app.state.redis is not None: app.state.redis_task_command_listener = asyncio.create_task(redis_task_command_listener(app)) @@ -777,7 +768,6 @@ 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, ) @@ -878,6 +868,7 @@ app.state.BASE_MODELS = [] 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.OAUTH_AUTO_REDIRECT = OAUTH_AUTO_REDIRECT app.state.config.ENABLE_PASSWORD_CHANGE_FORM = ENABLE_PASSWORD_CHANGE_FORM app.state.config.ENABLE_API_KEYS = ENABLE_API_KEYS @@ -928,7 +919,7 @@ 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 +from open_webui.utils.access_control import has_permission, migrate_access_control connections = app.state.config.TOOL_SERVER_CONNECTIONS if any('access_control' in c.get('config', {}) for c in connections): @@ -1042,6 +1033,7 @@ 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.MINERU_FILE_EXTENSIONS = MINERU_FILE_EXTENSIONS app.state.config.TEXT_SPLITTER = RAG_TEXT_SPLITTER app.state.config.ENABLE_MARKDOWN_HEADER_TEXT_SPLITTER = ENABLE_MARKDOWN_HEADER_TEXT_SPLITTER @@ -1145,6 +1137,8 @@ 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.LINKUP_API_KEY = LINKUP_API_KEY +app.state.config.LINKUP_SEARCH_PARAMS = LINKUP_SEARCH_PARAMS app.state.config.PLAYWRIGHT_WS_URL = PLAYWRIGHT_WS_URL @@ -1761,9 +1755,24 @@ async def chat_completion( form_data.pop('id', None) user_message = form_data.pop('user_message', None) or form_data.pop('parent_message', None) + + # Drop tool_servers if caller lacks features.direct_tool_servers — + # mirrors the storage-side strip in user/settings/update. + tool_servers = form_data.pop('tool_servers', None) + if ( + tool_servers + and user.role != 'admin' + and not await has_permission( + user.id, + 'features.direct_tool_servers', + request.app.state.config.USER_PERMISSIONS, + ) + ): + tool_servers = None + metadata = { 'user_id': user.id, - 'chat_id': form_data.pop('chat_id', None), + 'chat_id': form_data.pop('chat_id', None) or '', 'user_message': user_message, 'user_message_id': user_message.get('id') if user_message else None, 'assistant_message_id': form_data.pop('assistant_message_id', None), @@ -1771,7 +1780,7 @@ async def chat_completion( '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), + 'tool_servers': tool_servers, 'files': form_data.get('files', None), 'features': form_data.get('features', {}), 'variables': form_data.get('variables', {}), @@ -1796,6 +1805,44 @@ async def chat_completion( if metadata.get('chat_id') and user: chat_id = metadata['chat_id'] + + # Gate channel: branch — caller needs write access on the channel + # and the supplied message_id must belong to that channel. + if chat_id.startswith('channel:'): + channel_id = chat_id.removeprefix('channel:') + channel = await Channels.get_channel_by_id(channel_id) + if not channel: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=ERROR_MESSAGES.NOT_FOUND, + ) + if user.role != 'admin': + if channel.type in ['group', 'dm']: + if not await Channels.is_user_channel_member(channel.id, user.id): + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail=ERROR_MESSAGES.DEFAULT(), + ) + else: + if not await AccessGrants.has_access( + user_id=user.id, + resource_type='channel', + resource_id=channel.id, + permission='write', + ): + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail=ERROR_MESSAGES.DEFAULT(), + ) + target_message_id = list(message_ids.values())[0] if message_ids else None + if target_message_id: + target_message = await Messages.get_message_by_id(target_message_id) + if target_message and target_message.channel_id != channel.id: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail=ERROR_MESSAGES.DEFAULT(), + ) + if not chat_id.startswith('local:') and not chat_id.startswith( 'channel:' ): # temporary/channel chats are not stored @@ -2014,7 +2061,9 @@ async def chat_completion( 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:') and not metadata['chat_id'].startswith('channel:'): + if not metadata.get('chat_id', '').startswith('local:') and not metadata.get( + 'chat_id', '' + ).startswith('channel:'): await Chats.upsert_message_to_chat_by_id_and_message_id( metadata['chat_id'], metadata['message_id'], @@ -2341,11 +2390,11 @@ async def get_app_config(request: Request): 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 + onboarding = not await Users.has_users() + + user_count = await Users.get_num_users() if app.state.LICENSE_METADATA else None return { **({'onboarding': True} if onboarding else {}), @@ -2353,22 +2402,27 @@ async def get_app_config(request: Request): '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()}}, + 'oauth': { + 'providers': {name: config.get('name', name) for name, config in OAUTH_PROVIDERS.items()}, + 'auto_redirect': app.state.config.OAUTH_AUTO_REDIRECT, + }, 'features': { + # --- Public: required by login/signup page pre-auth --- '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, + # --- Authenticated: only consumed by logged-in frontend --- **( { + 'enable_api_keys': app.state.config.ENABLE_API_KEYS, + 'enable_password_change_form': app.state.config.ENABLE_PASSWORD_CHANGE_FORM, + '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, @@ -2409,7 +2463,7 @@ async def get_app_config(request: Request): '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, + **({'user_count': user_count} if user_count is not None else {}), 'code': { 'engine': app.state.config.CODE_EXECUTION_ENGINE, 'interpreter_engine': app.state.config.CODE_INTERPRETER_ENGINE, @@ -2563,9 +2617,7 @@ async def get_current_usage(user=Depends(get_verified_user)): raise HTTPException(status_code=500, detail='Internal Server Error') -############################ -# OAuth Login & Callback -############################ +# --- OAuth Login & Callback --- # Initialize OAuth client manager with any MCP tool servers using OAuth 2.1 @@ -2678,7 +2730,7 @@ async def register_client(request, client_id: str) -> bool: 'oauth_client_info': encrypt_data(oauth_client_info.model_dump(mode='json')), }, } - # Re-assign the full list to trigger AppConfig.__setattr__ → PersistentConfig.save() + # Re-assign the full list to trigger AppConfig.__setattr__ → ConfigVar.save() # (in-place list mutation via list[idx] = ... does not trigger __setattr__) request.app.state.config.TOOL_SERVER_CONNECTIONS = connections except Exception as e: @@ -2754,12 +2806,6 @@ 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( @@ -2768,6 +2814,15 @@ async def oauth_login_callback( response: Response, db: AsyncSession = Depends(get_async_session), ): + """Handle the OAuth provider callback. + + Resolution order: + 1. Match by subject ID bound to the provider. + 2. If ``OAUTH_MERGE_ACCOUNTS_BY_EMAIL`` is enabled, match by email + (note: some providers do not verify email addresses). + 3. If no match and ``ENABLE_OAUTH_SIGNUP`` is enabled, create a new user + (fails if the email is already registered). + """ return await oauth_manager.handle_callback(request, provider, response, db=db) @@ -2842,7 +2897,23 @@ async def get_opensearch_xml(): def _sync_db_ping() -> None: - ScopedSession.execute(text('SELECT 1;')).all() + """Verify the database is reachable with a simple SELECT 1. + + Uses a raw connection from the engine pool instead of the thread-local + ScopedSession. This is necessary because CommitSessionMiddleware + deliberately skips healthcheck paths (/health, /ready, /health/db), + so any ScopedSession opened on a healthcheck worker thread is never + rolled back or removed. If the session ever enters an invalid state + (e.g. after a transient connection error), it stays broken on that + thread permanently, causing PendingRollbackError on every subsequent + probe — exactly the failure reported in #24605. + + A raw ``engine.connect()`` context manager obtains a fresh connection + from the pool, executes the ping, and deterministically returns the + connection regardless of success or failure. + """ + with engine.connect() as conn: + conn.execute(text('SELECT 1')) async def async_db_ping() -> None: @@ -2896,11 +2967,14 @@ async def readiness_check(): @app.get('/health/db') -async def healthcheck_with_db(): +async def check_db_health(): + """Verify database connectivity by issuing a lightweight ping.""" await async_db_ping() return {'status': True} +# --- static assets & files --- +# Serve build-time static assets (CSS, JS, images, favicon, etc.) app.mount('/static', StaticFiles(directory=STATIC_DIR), name='static') @@ -2909,9 +2983,17 @@ async def serve_cache_file( path: str, user=Depends(get_verified_user), ): + """Serve cached files (e.g. tool outputs) with path-traversal protection. + + Only ``image/*``, ``audio/*``, and ``video/*`` MIME types are served inline; + everything else gets a ``Content-Disposition: attachment`` header to prevent + XSS from user-generated HTML stored in the cache directory. + """ file_path = os.path.abspath(os.path.join(CACHE_DIR, path)) - # prevent path traversal - if not file_path.startswith(os.path.abspath(CACHE_DIR)): + # trailing os.sep is required: without it, a path resolving to a sibling + # whose name starts with the cache-dir basename (e.g. cache_backup) passes + cache_root = os.path.abspath(CACHE_DIR) + os.sep + if not file_path.startswith(cache_root): 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') diff --git a/backend/open_webui/migrations/env.py b/backend/open_webui/migrations/env.py index 961a92becf..6fcd9a64cd 100644 --- a/backend/open_webui/migrations/env.py +++ b/backend/open_webui/migrations/env.py @@ -1,120 +1,84 @@ -import logging -from logging.config import fileConfig +from __future__ import annotations -from alembic import context +# Alembic environment configuration runner. +# Coordinates database migrations in both offline and online execution modes. +import logging.config +import logging +import alembic.context +from open_webui.env import DATABASE_PASSWORD, DATABASE_URL, LOG_FORMAT +from open_webui.internal.db import extract_ssl_params_from_url, reattach_ssl_params_to_url 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 +from sqlalchemy import create_engine, engine_from_config, pool -# 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. +alembic_config = alembic.context.config +if alembic_config.config_file_name: + logging.config.fileConfig(alembic_config.config_file_name, disable_existing_loggers=False) 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('%', '%%')) + for log_handler in logging.root.handlers: + log_handler.setFormatter(JSONFormatter()) +migration_metadata = Auth.metadata +target_db_url = DATABASE_URL +base_url, ssl_query_params = extract_ssl_params_from_url(target_db_url) +if ssl_query_params: + target_db_url = reattach_ssl_params_to_url(base_url, ssl_query_params) +if target_db_url: + alembic_config.set_main_option('sqlalchemy.url', target_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, + """Execute Alembic migrations in offline mode (outputs raw SQL DDL).""" + db_connection_url = alembic_config.get_main_option('sqlalchemy.url') + alembic.context.configure( + url=db_connection_url, + target_metadata=migration_metadata, literal_binds=True, dialect_opts={'paramstyle': 'named'}, ) + with alembic.context.begin_transaction(): + alembic.context.run_migrations() - with context.begin_transaction(): - context.run_migrations() + +def _get_engine_connectable(): + """Build the database engine based on target URL and authentication credentials.""" + if target_db_url and target_db_url.startswith('sqlite+sqlcipher://'): + if not DATABASE_PASSWORD or not DATABASE_PASSWORD.strip(): + raise ValueError('DATABASE_PASSWORD is required when using sqlite+sqlcipher:// URLs') + raw_db_path = target_db_url.replace('sqlite+sqlcipher://', '') + if raw_db_path.startswith('/'): + raw_db_path = raw_db_path[1:] + + def _sqlite_cipher_creator(): + import sqlcipher3 + + cipher_conn = sqlcipher3.connect(raw_db_path, check_same_thread=False) + cipher_conn.execute(f"PRAGMA key = '{DATABASE_PASSWORD}'") + return cipher_conn + + return create_engine('sqlite://', creator=_sqlite_cipher_creator, echo=False) + return engine_from_config( + alembic_config.get_section(alembic_config.config_ini_section, {}), + prefix='sqlalchemy.', + poolclass=pool.NullPool, + ) 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, + """Execute migrations against a live database connection.""" + live_connectable = _get_engine_connectable() + with live_connectable.connect() as live_connection: + alembic.context.configure( + connection=live_connection, + target_metadata=migration_metadata, ) - 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() + with alembic.context.begin_transaction(): + alembic.context.run_migrations() -if context.is_offline_mode(): - run_migrations_offline() -else: - run_migrations_online() +# Alembic execution entrypoint branch +if alembic.context.is_offline_mode(): + run_migrations_offline() # run in offline mode +if not alembic.context.is_offline_mode(): + run_migrations_online() # run in online mode diff --git a/backend/open_webui/migrations/util.py b/backend/open_webui/migrations/util.py index 6ea2a5f4bb..1c55faf0e9 100644 --- a/backend/open_webui/migrations/util.py +++ b/backend/open_webui/migrations/util.py @@ -1,15 +1,20 @@ -from alembic import op -from sqlalchemy import Inspector +from __future__ import annotations + +"""Alembic migration utilities.""" + +from alembic import op # noqa: E402 — alembic runtime context +from sqlalchemy import inspect # metadata inspection -def get_existing_tables(): - con = op.get_bind() - inspector = Inspector.from_engine(con) - tables = set(inspector.get_table_names()) - return tables +# --- database helper functions --- +def get_existing_tables() -> set[str]: + """Return table names already present in the database.""" + conn = op.get_bind() + return set(inspect(conn).get_table_names()) -def get_revision_id(): +def get_revision_id() -> str: + """Generate a short random revision identifier.""" import uuid - return str(uuid.uuid4()).replace('-', '')[:12] + return uuid.uuid4().hex[:12] diff --git a/backend/open_webui/migrations/versions/018012973d35_add_indexes.py b/backend/open_webui/migrations/versions/018012973d35_add_indexes.py index c5016e1a8b..ca3313c7f0 100644 --- a/backend/open_webui/migrations/versions/018012973d35_add_indexes.py +++ b/backend/open_webui/migrations/versions/018012973d35_add_indexes.py @@ -6,8 +6,8 @@ Create Date: 2025-08-13 03:00:00.000000 """ -from alembic import op import sqlalchemy as sa +from alembic import op revision = '018012973d35' down_revision = 'd31026856c01' @@ -16,18 +16,34 @@ depends_on = None def upgrade(): + conn = op.get_bind() + inspector = sa.inspect(conn) + + def _idx_exists(table, idx_name): + return any(i['name'] == idx_name for i in inspector.get_indexes(table)) + # 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']) + if not _idx_exists('chat', 'folder_id_idx'): + op.create_index('folder_id_idx', 'chat', ['folder_id']) + if not _idx_exists('chat', 'user_id_pinned_idx'): + op.create_index('user_id_pinned_idx', 'chat', ['user_id', 'pinned']) + if not _idx_exists('chat', 'user_id_archived_idx'): + op.create_index('user_id_archived_idx', 'chat', ['user_id', 'archived']) + if not _idx_exists('chat', 'updated_at_user_id_idx'): + op.create_index('updated_at_user_id_idx', 'chat', ['updated_at', 'user_id']) + if not _idx_exists('chat', 'folder_id_user_id_idx'): + 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']) + if not _idx_exists('tag', 'user_id_idx'): + op.create_index('user_id_idx', 'tag', ['user_id']) - # Function table index - op.create_index('is_global_idx', 'function', ['is_global']) + # Function table index (only if is_global column exists — added by a later migration) + conn = op.get_bind() + inspector = sa.inspect(conn) + func_cols = {c['name'] for c in inspector.get_columns('function')} + if 'is_global' in func_cols and not _idx_exists('function', 'is_global_idx'): + op.create_index('is_global_idx', 'function', ['is_global']) def downgrade(): diff --git a/backend/open_webui/migrations/versions/1af9b942657b_migrate_tags.py b/backend/open_webui/migrations/versions/1af9b942657b_migrate_tags.py index caffb7e3b4..00d36375d0 100644 --- a/backend/open_webui/migrations/versions/1af9b942657b_migrate_tags.py +++ b/backend/open_webui/migrations/versions/1af9b942657b_migrate_tags.py @@ -6,13 +6,13 @@ 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 +import sqlalchemy as sa +from alembic import op +from sqlalchemy.engine.reflection import Inspector +from sqlalchemy.sql import column, select, table, update + revision = '1af9b942657b' down_revision = '242a2047eae0' branch_labels = None @@ -93,8 +93,11 @@ def upgrade(): 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='{}')) + chat_columns = {c['name'] for c in inspector.get_columns('chat')} + if 'pinned' not in chat_columns: + op.add_column('chat', sa.Column('pinned', sa.Boolean(), nullable=True)) + if 'meta' not in chat_columns: + 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( diff --git a/backend/open_webui/migrations/versions/242a2047eae0_update_chat_table.py b/backend/open_webui/migrations/versions/242a2047eae0_update_chat_table.py index 7fadb05a92..e1c42bfb70 100644 --- a/backend/open_webui/migrations/versions/242a2047eae0_update_chat_table.py +++ b/backend/open_webui/migrations/versions/242a2047eae0_update_chat_table.py @@ -6,12 +6,12 @@ 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 +import sqlalchemy as sa +from alembic import op +from sqlalchemy.sql import select, table, update + revision = '242a2047eae0' down_revision = '6a39f3d8e55c' branch_labels = None @@ -47,29 +47,32 @@ def upgrade(): # 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()), - ) + # Step 3: Migrate data from 'old_chat' to 'chat' (only if old_chat exists) + # Re-check columns after potential rename above + current_cols = {c['name'] for c in inspector.get_columns('chat')} + if 'old_chat' in current_cols: + 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 + # - 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)) + 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') + # Step 4: Drop 'old_chat' column + print("Dropping 'old_chat' column") + op.drop_column('chat', 'old_chat') def downgrade(): 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 index 51a8e329f1..1c0cb6da81 100644 --- 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 @@ -8,9 +8,9 @@ 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 +import sqlalchemy as sa +from alembic import op # revision identifiers, used by Alembic. revision: str = '2f1211949ecc' @@ -20,63 +20,76 @@ depends_on: Union[str, Sequence[str], None] = None def upgrade() -> None: + conn = op.get_bind() + inspector = sa.inspect(conn) + # 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)) + cm_cols = {c['name'] for c in inspector.get_columns('channel_member')} + if 'status' not in cm_cols: + op.add_column('channel_member', sa.Column('status', sa.Text(), nullable=True)) + if 'is_active' not in cm_cols: + op.add_column( + 'channel_member', + sa.Column( + 'is_active', + sa.Boolean(), + nullable=False, + default=True, + server_default=sa.sql.expression.true(), + ), + ) + if 'is_channel_muted' not in cm_cols: + op.add_column( + 'channel_member', + sa.Column( + 'is_channel_muted', + sa.Boolean(), + nullable=False, + default=False, + server_default=sa.sql.expression.false(), + ), + ) + if 'is_channel_pinned' not in cm_cols: + op.add_column( + 'channel_member', + sa.Column( + 'is_channel_pinned', + sa.Boolean(), + nullable=False, + default=False, + server_default=sa.sql.expression.false(), + ), + ) + if 'data' not in cm_cols: + op.add_column('channel_member', sa.Column('data', sa.JSON(), nullable=True)) + if 'meta' not in cm_cols: + op.add_column('channel_member', sa.Column('meta', sa.JSON(), nullable=True)) + if 'joined_at' not in cm_cols: + op.add_column('channel_member', sa.Column('joined_at', sa.BigInteger(), nullable=False)) + if 'left_at' not in cm_cols: + op.add_column('channel_member', sa.Column('left_at', sa.BigInteger(), nullable=True)) + if 'last_read_at' not in cm_cols: + op.add_column('channel_member', sa.Column('last_read_at', sa.BigInteger(), nullable=True)) + if 'updated_at' not in cm_cols: + 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)) + msg_cols = {c['name'] for c in inspector.get_columns('message')} + if 'is_pinned' not in msg_cols: + op.add_column( + 'message', + sa.Column( + 'is_pinned', + sa.Boolean(), + nullable=False, + default=False, + server_default=sa.sql.expression.false(), + ), + ) + if 'pinned_at' not in msg_cols: + op.add_column('message', sa.Column('pinned_at', sa.BigInteger(), nullable=True)) + if 'pinned_by' not in msg_cols: + op.add_column('message', sa.Column('pinned_by', sa.Text(), nullable=True)) def downgrade() -> None: 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 index c412107032..69dd73c700 100644 --- a/backend/open_webui/migrations/versions/374d2f66af06_add_prompt_history_table.py +++ b/backend/open_webui/migrations/versions/374d2f66af06_add_prompt_history_table.py @@ -6,11 +6,11 @@ Create Date: 2026-01-23 17:15:00.000000 """ -from typing import Sequence, Union import uuid +from typing import Sequence, Union -from alembic import op import sqlalchemy as sa +from alembic import op revision: str = '374d2f66af06' down_revision: Union[str, None] = 'c440947495f3' @@ -20,150 +20,167 @@ depends_on: Union[str, Sequence[str], None] = None def upgrade() -> None: conn = op.get_bind() + inspector = sa.inspect(conn) + existing_tables = set(inspector.get_table_names()) - # 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. + # If the final state already exists (prompt has 'id' PK + prompt_history exists), + # the migration completed successfully on a prior run — nothing to do. + if 'prompt_history' in existing_tables and 'prompt_new' not in existing_tables: + # prompt_history exists and prompt_new was already renamed → done + prompt_cols = {c['name'] for c in inspector.get_columns('prompt')} + if 'id' in prompt_cols and 'version_id' in prompt_cols: + return - 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, + # Step 1: Read existing data from OLD table (schema: command as PK) + # Only read if the old-schema prompt table still exists (has 'command' but no 'version_id') + existing_prompts = [] + if 'prompt' in existing_tables and 'prompt_new' not in existing_tables: + prompt_cols = {c['name'] for c in inspector.get_columns('prompt')} + if 'command' in prompt_cols and 'version_id' not in prompt_cols: + 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()), ) - ).fetchall() - except Exception: - # Fallback if table doesn't exist (new install) - existing_prompts = [] + 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: + 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, - ) + # Step 2: Create new prompt table with 'id' as PRIMARY KEY (if not already created) + if 'prompt_new' not in existing_tables: + 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), ) - # 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 3: Create prompt_history table (if not already created) + if 'prompt_history' not in existing_tables: + 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 5: Replace old table with new one - op.drop_table('prompt') - op.rename_table('prompt_new', 'prompt') + # Step 4: Migrate data (only if we have old data to migrate) + if existing_prompts: + 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 (only if prompt_new exists) + # Re-check tables after potential creation above + inspector.clear_cache() + current_tables = set(inspector.get_table_names()) + if 'prompt_new' in current_tables: + if 'prompt' in current_tables: + op.drop_table('prompt') + op.rename_table('prompt_new', 'prompt') def downgrade() -> None: diff --git a/backend/open_webui/migrations/versions/3781e22d8b01_update_message_table.py b/backend/open_webui/migrations/versions/3781e22d8b01_update_message_table.py index 170137f23c..7dd3ed33cb 100644 --- a/backend/open_webui/migrations/versions/3781e22d8b01_update_message_table.py +++ b/backend/open_webui/migrations/versions/3781e22d8b01_update_message_table.py @@ -6,8 +6,8 @@ Create Date: 2024-12-30 03:00:00.000000 """ -from alembic import op import sqlalchemy as sa +from alembic import op revision = '3781e22d8b01' down_revision = '7826ab40b532' @@ -16,38 +16,50 @@ depends_on = None def upgrade(): + conn = op.get_bind() + inspector = sa.inspect(conn) + existing_tables = set(inspector.get_table_names()) + # Add 'type' column to the 'channel' table - op.add_column( - 'channel', - sa.Column( - 'type', - sa.Text(), - nullable=True, - ), - ) + channel_cols = {c['name'] for c in inspector.get_columns('channel')} + if 'type' not in channel_cols: + 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), - ) + message_cols = {c['name'] for c in inspector.get_columns('message')} + if 'parent_id' not in message_cols: + 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 - ) + if 'message_reaction' not in existing_tables: + 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 - ) + if 'channel_member' not in existing_tables: + 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(): 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 index 4bf24d3b46..daf0aef172 100644 --- a/backend/open_webui/migrations/versions/37f288994c47_add_group_member_table.py +++ b/backend/open_webui/migrations/versions/37f288994c47_add_group_member_table.py @@ -6,13 +6,13 @@ Create Date: 2025-11-17 03:45:25.123939 """ -import uuid -import time import json +import time +import uuid from typing import Sequence, Union -from alembic import op import sqlalchemy as sa +from alembic import op # revision identifiers, used by Alembic. revision: str = '37f288994c47' @@ -22,6 +22,13 @@ depends_on: Union[str, Sequence[str], None] = None def upgrade() -> None: + conn = op.get_bind() + inspector = sa.inspect(conn) + existing_tables = set(inspector.get_table_names()) + + if 'group_member' in existing_tables: + return # Already created — skip everything + # 1. Create new table op.create_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 index d415f500f3..ff6fe65118 100644 --- a/backend/open_webui/migrations/versions/38d63c18f30f_add_oauth_session_table.py +++ b/backend/open_webui/migrations/versions/38d63c18f30f_add_oauth_session_table.py @@ -8,8 +8,8 @@ Create Date: 2025-09-08 14:19:59.583921 from typing import Sequence, Union -from alembic import op import sqlalchemy as sa +from alembic import op # revision identifiers, used by Alembic. revision: str = '38d63c18f30f' @@ -19,50 +19,39 @@ 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') + existing_tables = set(inspector.get_table_names()) - pk_columns = inspector.get_pk_constraint('user')['constrained_columns'] - id_column = next((col for col in columns if col['name'] == 'id'), None) + # ── Create oauth_session table (idempotent) ─────────────────────── + if 'oauth_session' not in existing_tables: + 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), + ) - 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 (idempotent — no-ops when table was just created + # with the columns above, and safe to call if indexes already exist). + existing_indexes = ( + {idx['name'] for idx in inspector.get_indexes('oauth_session')} if 'oauth_session' in existing_tables else set() ) - # 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']) + if 'idx_oauth_session_user_id' not in existing_indexes: + op.create_index('idx_oauth_session_user_id', 'oauth_session', ['user_id']) + if 'idx_oauth_session_expires_at' not in existing_indexes: + op.create_index('idx_oauth_session_expires_at', 'oauth_session', ['expires_at']) + if 'idx_oauth_session_user_provider' not in existing_indexes: + op.create_index('idx_oauth_session_user_provider', 'oauth_session', ['user_id', 'provider']) def downgrade() -> None: diff --git a/backend/open_webui/migrations/versions/3ab32c4b8f59_update_tags.py b/backend/open_webui/migrations/versions/3ab32c4b8f59_update_tags.py index 31bd355ede..8aaa4d4d47 100644 --- a/backend/open_webui/migrations/versions/3ab32c4b8f59_update_tags.py +++ b/backend/open_webui/migrations/versions/3ab32c4b8f59_update_tags.py @@ -6,13 +6,13 @@ 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 +import sqlalchemy as sa +from alembic import op +from sqlalchemy.engine.reflection import Inspector +from sqlalchemy.sql import column, select, table, update + revision = '3ab32c4b8f59' down_revision = '1af9b942657b' branch_labels = None diff --git a/backend/open_webui/migrations/versions/3af16a1c9fb6_update_user_table.py b/backend/open_webui/migrations/versions/3af16a1c9fb6_update_user_table.py index 629c1c8c24..e3767a71a7 100644 --- a/backend/open_webui/migrations/versions/3af16a1c9fb6_update_user_table.py +++ b/backend/open_webui/migrations/versions/3af16a1c9fb6_update_user_table.py @@ -8,8 +8,8 @@ Create Date: 2025-08-21 02:07:18.078283 from typing import Sequence, Union -from alembic import op import sqlalchemy as sa +from alembic import op # revision identifiers, used by Alembic. revision: str = '3af16a1c9fb6' @@ -19,10 +19,18 @@ 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)) + conn = op.get_bind() + inspector = sa.inspect(conn) + user_cols = {c['name'] for c in inspector.get_columns('user')} + + if 'username' not in user_cols: + op.add_column('user', sa.Column('username', sa.String(length=50), nullable=True)) + if 'bio' not in user_cols: + op.add_column('user', sa.Column('bio', sa.Text(), nullable=True)) + if 'gender' not in user_cols: + op.add_column('user', sa.Column('gender', sa.Text(), nullable=True)) + if 'date_of_birth' not in user_cols: + op.add_column('user', sa.Column('date_of_birth', sa.Date(), nullable=True)) def downgrade() -> None: diff --git a/backend/open_webui/migrations/versions/3c9b0ca343fd_add_knowledge_directory_table.py b/backend/open_webui/migrations/versions/3c9b0ca343fd_add_knowledge_directory_table.py new file mode 100644 index 0000000000..90b1bb0085 --- /dev/null +++ b/backend/open_webui/migrations/versions/3c9b0ca343fd_add_knowledge_directory_table.py @@ -0,0 +1,72 @@ +"""add knowledge_directory table + +Revision ID: 3c9b0ca343fd +Revises: a0b1c2d3e4f5 +Create Date: 2026-05-13 21:58:40.832482 + +""" + +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + +# revision identifiers, used by Alembic. +revision: str = '3c9b0ca343fd' +down_revision: Union[str, None] = 'a0b1c2d3e4f5' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + conn = op.get_bind() + inspector = sa.inspect(conn) + existing_tables = set(inspector.get_table_names()) + + if 'knowledge_directory' not in existing_tables: + # Create knowledge_directory table + op.create_table( + 'knowledge_directory', + sa.Column('id', sa.Text(), nullable=False), + sa.Column('knowledge_id', sa.Text(), nullable=False), + sa.Column('parent_id', sa.Text(), nullable=True), + sa.Column('name', sa.Text(), nullable=False), + sa.Column('user_id', sa.Text(), nullable=False), + sa.Column('created_at', sa.BigInteger(), nullable=False), + sa.Column('updated_at', sa.BigInteger(), nullable=False), + sa.ForeignKeyConstraint(['knowledge_id'], ['knowledge.id'], ondelete='CASCADE'), + sa.ForeignKeyConstraint(['parent_id'], ['knowledge_directory.id'], ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint( + 'knowledge_id', 'parent_id', 'name', name='uq_knowledge_directory_knowledge_parent_name' + ), + ) + op.create_index('ix_knowledge_directory_knowledge_id', 'knowledge_directory', ['knowledge_id']) + op.create_index('ix_knowledge_directory_parent_id', 'knowledge_directory', ['parent_id']) + + # Add directory_id column to knowledge_file + kf_cols = {c['name'] for c in inspector.get_columns('knowledge_file')} + if 'directory_id' not in kf_cols: + with op.batch_alter_table('knowledge_file') as batch: + batch.add_column(sa.Column('directory_id', sa.Text(), nullable=True)) + batch.create_foreign_key( + 'fk_knowledge_file_directory_id', + 'knowledge_directory', + ['directory_id'], + ['id'], + ondelete='SET NULL', + ) + batch.create_index('ix_knowledge_file_directory_id', ['directory_id']) + + +def downgrade() -> None: + # Remove directory_id from knowledge_file + with op.batch_alter_table('knowledge_file') as batch: + batch.drop_index('ix_knowledge_file_directory_id') + batch.drop_constraint('fk_knowledge_file_directory_id', type_='foreignkey') + batch.drop_column('directory_id') + + # Drop knowledge_directory table + op.drop_index('ix_knowledge_directory_parent_id', table_name='knowledge_directory') + op.drop_index('ix_knowledge_directory_knowledge_id', table_name='knowledge_directory') + op.drop_table('knowledge_directory') 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 index f772987a44..70c0562749 100644 --- a/backend/open_webui/migrations/versions/3e0e00844bb0_add_knowledge_file_table.py +++ b/backend/open_webui/migrations/versions/3e0e00844bb0_add_knowledge_file_table.py @@ -6,16 +6,15 @@ Create Date: 2025-12-02 06:54:19.401334 """ +import json +import time +import uuid 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 +import sqlalchemy as sa +from alembic import op +from sqlalchemy import inspect # revision identifiers, used by Alembic. revision: str = '3e0e00844bb0' @@ -25,6 +24,13 @@ depends_on: Union[str, Sequence[str], None] = None def upgrade() -> None: + conn = op.get_bind() + inspector = sa.inspect(conn) + existing_tables = set(inspector.get_table_names()) + + if 'knowledge_file' in existing_tables: + return # Already created — skip everything + op.create_table( 'knowledge_file', sa.Column('id', sa.Text(), primary_key=True), diff --git a/backend/open_webui/migrations/versions/461111b60977_add_missing_primary_keys_to_legacy_.py b/backend/open_webui/migrations/versions/461111b60977_add_missing_primary_keys_to_legacy_.py new file mode 100644 index 0000000000..1e5901e275 --- /dev/null +++ b/backend/open_webui/migrations/versions/461111b60977_add_missing_primary_keys_to_legacy_.py @@ -0,0 +1,74 @@ +"""add missing primary keys to legacy peewee tables + +Revision ID: 461111b60977 +Revises: 3c9b0ca343fd +Create Date: 2026-05-14 04:38:14.000000 + +""" + +from typing import Sequence, Union + +import sqlalchemy as sa +from alembic import op + +# revision identifiers, used by Alembic. +revision: str = '461111b60977' +down_revision: Union[str, None] = '3c9b0ca343fd' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + +# Tables bootstrapped by the old Peewee migration layer that may have +# UNIQUE(id) but no PRIMARY KEY constraint. Fresh Alembic installs +# already have correct PKs from 7e5b5dc7342b_init.py. +# 'tag' uses a composite PK since the same tag name can exist for multiple users. +LEGACY_TABLES = { + 'auth': ['id'], + 'chat': ['id'], + 'chatidtag': ['id'], + 'document': ['id'], + 'file': ['id'], + 'function': ['id'], + 'memory': ['id'], + 'model': ['id'], + 'prompt': ['id'], + 'tag': ['id', 'user_id'], + 'tool': ['id'], + 'user': ['id'], +} + + +def upgrade() -> None: + conn = op.get_bind() + inspector = sa.inspect(conn) + existing_tables = set(inspector.get_table_names()) + + for table_name, pk_columns in LEGACY_TABLES.items(): + if table_name not in existing_tables: + continue + + pk = inspector.get_pk_constraint(table_name) + pk_cols = pk.get('constrained_columns', []) + + # Already has the correct PK — nothing to do + if sorted(pk_cols) == sorted(pk_columns): + continue + + # Check that all PK columns exist + columns = {c['name'] for c in inspector.get_columns(table_name)} + if not all(c in columns for c in pk_columns): + continue + + print(f"Promoting UNIQUE(id) -> PRIMARY KEY({', '.join(pk_columns)}) for '{table_name}'") + + conn.execute(sa.text(f'DROP TABLE IF EXISTS _alembic_tmp_{table_name}')) + with op.batch_alter_table(table_name) as batch_op: + # Drop existing PK if any (e.g. on wrong column) + if pk_cols and pk.get('name'): + batch_op.drop_constraint(pk['name'], type_='primary') + + batch_op.create_primary_key(f'pk_{table_name}', pk_columns) + + +def downgrade() -> None: + # Downgrade is a no-op — we don't want to remove PKs + pass 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 index 91e0dce0be..e1e3b32635 100644 --- a/backend/open_webui/migrations/versions/4ace53fd72c8_update_folder_table_datetime.py +++ b/backend/open_webui/migrations/versions/4ace53fd72c8_update_folder_table_datetime.py @@ -6,8 +6,8 @@ Create Date: 2024-10-23 03:00:00.000000 """ -from alembic import op import sqlalchemy as sa +from alembic import op revision = '4ace53fd72c8' down_revision = 'af906e964978' @@ -16,6 +16,18 @@ depends_on = None def upgrade(): + conn = op.get_bind() + inspector = sa.inspect(conn) + columns = {c['name']: c for c in inspector.get_columns('folder')} + + created_at_col = columns.get('created_at') + if not created_at_col: + return + + # Only convert if still DateTime — skip if already BigInteger + if isinstance(created_at_col['type'], sa.BigInteger): + return + # 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 @@ -48,20 +60,24 @@ def upgrade(): def downgrade(): - # Downgrade: Convert columns back to DateTime and restore defaults + # Convert columns back to DateTime and restore defaults. Mirrors the + # upgrade's postgresql_using cast — without it, Postgres can't + # auto-cast BigInteger → timestamp and aborts with DatatypeMismatch. 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 + server_default=sa.func.now(), + postgresql_using='to_timestamp(created_at)::timestamp without time zone', ) 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 + server_default=sa.func.now(), + onupdate=sa.func.now(), + postgresql_using='to_timestamp(updated_at)::timestamp without time zone', ) diff --git a/backend/open_webui/migrations/versions/4de81c2a3af1_add_pinned_note_table.py b/backend/open_webui/migrations/versions/4de81c2a3af1_add_pinned_note_table.py index 858c9b1541..1ec11a0419 100644 --- a/backend/open_webui/migrations/versions/4de81c2a3af1_add_pinned_note_table.py +++ b/backend/open_webui/migrations/versions/4de81c2a3af1_add_pinned_note_table.py @@ -8,10 +8,9 @@ Create Date: 2026-05-09 04:29:27.651341 from typing import Sequence, Union -from alembic import op -import sqlalchemy as sa import open_webui.internal.db - +import sqlalchemy as sa +from alembic import op # revision identifiers, used by Alembic. revision: str = '4de81c2a3af1' @@ -20,46 +19,55 @@ branch_labels: Union[str, Sequence[str], None] = None depends_on: Union[str, Sequence[str], None] = None -import uuid import time -from sqlalchemy import select, update, insert -from sqlalchemy.sql import table, column +import uuid + +from sqlalchemy import insert, select, update +from sqlalchemy.sql import column, table def upgrade() -> None: - op.create_table( - 'pinned_note', - sa.Column('id', sa.Text(), nullable=False), - sa.Column('user_id', sa.Text(), nullable=False), - sa.Column('note_id', sa.Text(), sa.ForeignKey('note.id', ondelete='CASCADE'), nullable=False), - sa.Column('created_at', sa.BigInteger(), nullable=False), - sa.PrimaryKeyConstraint('id'), - sa.UniqueConstraint('user_id', 'note_id', name='uq_pinned_note'), - ) - conn = op.get_bind() + inspector = sa.inspect(conn) + existing_tables = set(inspector.get_table_names()) - note_table = table('note', column('id', sa.Text), column('user_id', sa.Text), column('is_pinned', sa.Boolean)) - - pinned_note_table = table( - 'pinned_note', - column('id', sa.Text), - column('user_id', sa.Text), - column('note_id', sa.Text), - column('created_at', sa.BigInteger), - ) - - notes = conn.execute(select(note_table.c.id, note_table.c.user_id).where(note_table.c.is_pinned == True)).fetchall() - - if notes: - now = int(time.time_ns()) - conn.execute( - insert(pinned_note_table), - [{'id': str(uuid.uuid4()), 'user_id': note[1], 'note_id': note[0], 'created_at': now} for note in notes], + if 'pinned_note' not in existing_tables: + op.create_table( + 'pinned_note', + sa.Column('id', sa.Text(), nullable=False), + sa.Column('user_id', sa.Text(), nullable=False), + sa.Column('note_id', sa.Text(), sa.ForeignKey('note.id', ondelete='CASCADE'), nullable=False), + sa.Column('created_at', sa.BigInteger(), nullable=False), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('user_id', 'note_id', name='uq_pinned_note'), ) - with op.batch_alter_table('note', schema=None) as batch_op: - batch_op.drop_column('is_pinned') + note_table = table('note', column('id', sa.Text), column('user_id', sa.Text), column('is_pinned', sa.Boolean)) + + pinned_note_table = table( + 'pinned_note', + column('id', sa.Text), + column('user_id', sa.Text), + column('note_id', sa.Text), + column('created_at', sa.BigInteger), + ) + + notes = conn.execute( + select(note_table.c.id, note_table.c.user_id).where(note_table.c.is_pinned == True) + ).fetchall() + + if notes: + now = int(time.time_ns()) + conn.execute( + insert(pinned_note_table), + [ + {'id': str(uuid.uuid4()), 'user_id': note[1], 'note_id': note[0], 'created_at': now} + for note in notes + ], + ) + + with op.batch_alter_table('note', schema=None) as batch_op: + batch_op.drop_column('is_pinned') def downgrade() -> None: diff --git a/backend/open_webui/migrations/versions/56359461a091_add_calendar_tables.py b/backend/open_webui/migrations/versions/56359461a091_add_calendar_tables.py index e556440f56..4796233604 100644 --- a/backend/open_webui/migrations/versions/56359461a091_add_calendar_tables.py +++ b/backend/open_webui/migrations/versions/56359461a091_add_calendar_tables.py @@ -8,9 +8,8 @@ Create Date: 2026-04-19 16:20:58.162045 from typing import Sequence, Union -from alembic import op import sqlalchemy as sa - +from alembic import op # revision identifiers, used by Alembic. revision: str = '56359461a091' @@ -19,58 +18,86 @@ branch_labels: Union[str, Sequence[str], None] = None depends_on: Union[str, Sequence[str], None] = None +def _index_exists(inspector, index_name, table_name): + """Check if an index already exists on the given table.""" + indexes = inspector.get_indexes(table_name) + return any(idx['name'] == index_name for idx in indexes) + + 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) + conn = op.get_bind() + inspector = sa.inspect(conn) + tables = inspector.get_table_names() - 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) + if 'calendar' not in tables: + 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_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) + inspector.clear_cache() + if 'calendar' in inspector.get_table_names(): + if not _index_exists(inspector, 'ix_calendar_user', 'calendar'): + op.create_index('ix_calendar_user', 'calendar', ['user_id'], unique=False) + + if 'calendar_event' not in tables: + 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'), + ) + + inspector.clear_cache() + if 'calendar_event' in inspector.get_table_names(): + if not _index_exists(inspector, 'ix_calendar_event_calendar', 'calendar_event'): + op.create_index('ix_calendar_event_calendar', 'calendar_event', ['calendar_id', 'start_at'], unique=False) + if not _index_exists(inspector, 'ix_calendar_event_user_date', 'calendar_event'): + op.create_index('ix_calendar_event_user_date', 'calendar_event', ['user_id', 'start_at'], unique=False) + + if 'calendar_event_attendee' not in tables: + 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'), + ) + + inspector.clear_cache() + if 'calendar_event_attendee' in inspector.get_table_names(): + if not _index_exists(inspector, 'ix_calendar_event_attendee_user', 'calendar_event_attendee'): + op.create_index( + 'ix_calendar_event_attendee_user', 'calendar_event_attendee', ['user_id', 'status'], unique=False + ) def downgrade() -> None: diff --git a/backend/open_webui/migrations/versions/57c599a3cb57_add_channel_table.py b/backend/open_webui/migrations/versions/57c599a3cb57_add_channel_table.py index 79f0e8827e..4db23c2ace 100644 --- a/backend/open_webui/migrations/versions/57c599a3cb57_add_channel_table.py +++ b/backend/open_webui/migrations/versions/57c599a3cb57_add_channel_table.py @@ -6,8 +6,8 @@ Create Date: 2024-12-22 03:00:00.000000 """ -from alembic import op import sqlalchemy as sa +from alembic import op revision = '57c599a3cb57' down_revision = '922e7a387820' @@ -16,30 +16,36 @@ 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), - ) + conn = op.get_bind() + inspector = sa.inspect(conn) + existing_tables = set(inspector.get_table_names()) - 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), - ) + if 'channel' not in existing_tables: + 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), + ) + + if 'message' not in existing_tables: + 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(): 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 index 2bd2d9fd60..aa1e52062b 100644 --- a/backend/open_webui/migrations/versions/6283dc0e4d8d_add_channel_file_table.py +++ b/backend/open_webui/migrations/versions/6283dc0e4d8d_add_channel_file_table.py @@ -8,9 +8,9 @@ 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 +import sqlalchemy as sa +from alembic import op # revision identifiers, used by Alembic. revision: str = '6283dc0e4d8d' @@ -20,31 +20,38 @@ 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 - ) + conn = op.get_bind() + inspector = sa.inspect(conn) + existing_tables = set(inspector.get_table_names()) + + if 'channel_file' not in existing_tables: + 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: diff --git a/backend/open_webui/migrations/versions/6a39f3d8e55c_add_knowledge_table.py b/backend/open_webui/migrations/versions/6a39f3d8e55c_add_knowledge_table.py index c65ca01415..b1fd2ab40a 100644 --- a/backend/open_webui/migrations/versions/6a39f3d8e55c_add_knowledge_table.py +++ b/backend/open_webui/migrations/versions/6a39f3d8e55c_add_knowledge_table.py @@ -6,11 +6,12 @@ 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 +import sqlalchemy as sa +from alembic import op +from sqlalchemy.sql import column, select, table + revision = '6a39f3d8e55c' down_revision = 'c0fbf31ca0db' branch_labels = None @@ -18,62 +19,67 @@ 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), - ) + conn = op.get_bind() + inspector = sa.inspect(conn) + existing_tables = set(inspector.get_table_names()) - 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, + if 'knowledge' not in existing_tables: + # 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), ) - ) - # 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 + 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 = conn.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: + conn.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, + ) + ) + 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 index 4211c6642e..b3313e3762 100644 --- a/backend/open_webui/migrations/versions/7826ab40b532_update_file_table.py +++ b/backend/open_webui/migrations/versions/7826ab40b532_update_file_table.py @@ -6,8 +6,8 @@ Create Date: 2024-12-23 03:00:00.000000 """ -from alembic import op import sqlalchemy as sa +from alembic import op revision = '7826ab40b532' down_revision = '57c599a3cb57' @@ -16,10 +16,15 @@ depends_on = None def upgrade(): - op.add_column( - 'file', - sa.Column('access_control', sa.JSON(), nullable=True), - ) + conn = op.get_bind() + inspector = sa.inspect(conn) + file_cols = {c['name'] for c in inspector.get_columns('file')} + + if 'access_control' not in file_cols: + op.add_column( + 'file', + sa.Column('access_control', sa.JSON(), nullable=True), + ) def downgrade(): diff --git a/backend/open_webui/migrations/versions/7e5b5dc7342b_init.py b/backend/open_webui/migrations/versions/7e5b5dc7342b_init.py index 39f488d72e..ca75fff7d1 100644 --- a/backend/open_webui/migrations/versions/7e5b5dc7342b_init.py +++ b/backend/open_webui/migrations/versions/7e5b5dc7342b_init.py @@ -1,44 +1,34 @@ -"""init - -Revision ID: 7e5b5dc7342b -Revises: -Create Date: 2024-06-24 13:15:33.808998 - -""" - -from typing import Sequence, Union - +# Initial bootstrap migration version. +# Revision ID: 7e5b5dc7342b +# Revises: (none) +# Created on: 2024-06-24 13:15:33.808998 +from __future__ import annotations +from typing import Sequence +import open_webui.internal.db # noqa: F401 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', +down_revision: str | None = None +branch_labels: str | Sequence[str] | None = None +depends_on: str | Sequence[str] | None = None +# Initial schema table declarations +_INITIAL_TABLES: list[tuple[str, list[sa.Column], list]] = [ + ( + '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.PrimaryKeyConstraint('id')], + ), + ( + 'chat', + [ sa.Column('id', sa.String(), nullable=False), sa.Column('user_id', sa.String(), nullable=True), sa.Column('title', sa.Text(), nullable=True), @@ -47,24 +37,23 @@ def upgrade() -> None: 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.PrimaryKeyConstraint('id'), sa.UniqueConstraint('share_id')], + ), + ( + '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.PrimaryKeyConstraint('id')], + ), + ( + 'document', + [ sa.Column('collection_name', sa.String(), nullable=False), sa.Column('name', sa.String(), nullable=True), sa.Column('title', sa.Text(), nullable=True), @@ -72,24 +61,23 @@ def upgrade() -> None: 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.PrimaryKeyConstraint('collection_name'), sa.UniqueConstraint('name')], + ), + ( + '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.PrimaryKeyConstraint('id')], + ), + ( + 'function', + [ sa.Column('id', sa.String(), nullable=False), sa.Column('user_id', sa.String(), nullable=True), sa.Column('name', sa.Text(), nullable=True), @@ -101,23 +89,23 @@ def upgrade() -> None: 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.PrimaryKeyConstraint('id')], + ), + ( + '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.PrimaryKeyConstraint('id')], + ), + ( + '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), @@ -126,33 +114,33 @@ def upgrade() -> None: 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.PrimaryKeyConstraint('id')], + ), + ( + '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.PrimaryKeyConstraint('command')], + ), + ( + '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.PrimaryKeyConstraint('id')], + ), + ( + 'tool', + [ sa.Column('id', sa.String(), nullable=False), sa.Column('user_id', sa.String(), nullable=True), sa.Column('name', sa.Text(), nullable=True), @@ -162,12 +150,12 @@ def upgrade() -> None: 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.PrimaryKeyConstraint('id')], + ), + ( + 'user', + [ sa.Column('id', sa.String(), nullable=False), sa.Column('name', sa.String(), nullable=True), sa.Column('email', sa.String(), nullable=True), @@ -180,25 +168,25 @@ def upgrade() -> None: 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 ### +# --- migration execution --- +def upgrade() -> None: # deploy initial schema tables + existing_tables = set(get_existing_tables()) + for name, columns, constraints in _INITIAL_TABLES: + if name not in existing_tables: + op.create_table(name, *columns, *constraints) + + +# --- rollback function --- +def downgrade() -> None: # rollback initial schema tables + for table_name, _, _ in reversed(_INITIAL_TABLES): + op.drop_table(table_name) diff --git a/backend/open_webui/migrations/versions/81cc2ce44d79_update_channel_file_and_knowledge_table.py b/backend/open_webui/migrations/versions/81cc2ce44d79_update_channel_file_and_knowledge_table.py index e45a2443df..2294f89562 100644 --- 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 @@ -8,9 +8,9 @@ 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 +import sqlalchemy as sa +from alembic import op # revision identifiers, used by Alembic. revision: str = '81cc2ce44d79' @@ -20,20 +20,27 @@ depends_on: Union[str, Sequence[str], None] = None def upgrade() -> None: + conn = op.get_bind() + inspector = sa.inspect(conn) + # 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, + cf_cols = {c['name'] for c in inspector.get_columns('channel_file')} + if 'message_id' not in cf_cols: + 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)) + k_cols = {c['name'] for c in inspector.get_columns('knowledge')} + if 'data' not in k_cols: + 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: 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 index 3254b57858..e247e5788e 100644 --- a/backend/open_webui/migrations/versions/8452d01d26d7_add_chat_message_table.py +++ b/backend/open_webui/migrations/versions/8452d01d26d7_add_chat_message_table.py @@ -6,13 +6,13 @@ Create Date: 2026-02-01 04:00:00.000000 """ -import time import json import logging +import time from typing import Sequence, Union -from alembic import op import sqlalchemy as sa +from alembic import op log = logging.getLogger(__name__) @@ -56,6 +56,13 @@ def _flush_batch(conn, table, batch): def upgrade() -> None: + conn = op.get_bind() + inspector = sa.inspect(conn) + existing_tables = set(inspector.get_table_names()) + + if 'chat_message' in existing_tables: + return # Already created — skip everything + # Step 1: Create table op.create_table( 'chat_message', @@ -85,8 +92,6 @@ def upgrade() -> None: 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()), 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 index 9d115b1e5c..5936667624 100644 --- 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 @@ -8,9 +8,9 @@ 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 +import sqlalchemy as sa +from alembic import op # revision identifiers, used by Alembic. revision: str = '90ef40d4714e' @@ -20,42 +20,53 @@ depends_on: Union[str, Sequence[str], None] = None def upgrade() -> None: + conn = op.get_bind() + inspector = sa.inspect(conn) + existing_tables = set(inspector.get_table_names()) + # 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)) + channel_cols = {c['name'] for c in inspector.get_columns('channel')} + if 'is_private' not in channel_cols: + op.add_column('channel', sa.Column('is_private', sa.Boolean(), nullable=True)) + if 'archived_at' not in channel_cols: + op.add_column('channel', sa.Column('archived_at', sa.BigInteger(), nullable=True)) + if 'archived_by' not in channel_cols: + op.add_column('channel', sa.Column('archived_by', sa.Text(), nullable=True)) + if 'deleted_at' not in channel_cols: + op.add_column('channel', sa.Column('deleted_at', sa.BigInteger(), nullable=True)) + if 'deleted_by' not in channel_cols: + op.add_column('channel', sa.Column('deleted_by', sa.Text(), nullable=True)) + if 'updated_by' not in channel_cols: + 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)) + cm_cols = {c['name'] for c in inspector.get_columns('channel_member')} + if 'role' not in cm_cols: + op.add_column('channel_member', sa.Column('role', sa.Text(), nullable=True)) + if 'invited_by' not in cm_cols: + op.add_column('channel_member', sa.Column('invited_by', sa.Text(), nullable=True)) + if 'invited_at' not in cm_cols: + 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 + if 'channel_webhook' not in existing_tables: + 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), + ) def downgrade() -> None: @@ -74,5 +85,3 @@ def downgrade() -> None: # 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 index 5e617be1e6..4150f5ffdf 100644 --- a/backend/open_webui/migrations/versions/922e7a387820_add_group_table.py +++ b/backend/open_webui/migrations/versions/922e7a387820_add_group_table.py @@ -6,8 +6,8 @@ Create Date: 2024-11-14 03:00:00.000000 """ -from alembic import op import sqlalchemy as sa +from alembic import op revision = '922e7a387820' down_revision = '4ace53fd72c8' @@ -16,70 +16,55 @@ 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), - ) + conn = op.get_bind() + inspector = sa.inspect(conn) + existing_tables = set(inspector.get_table_names()) + + if 'group' not in existing_tables: + 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(), - ), - ) + model_cols = {c['name'] for c in inspector.get_columns('model')} + if 'access_control' not in model_cols: + op.add_column('model', sa.Column('access_control', sa.JSON(), nullable=True)) + if 'is_active' not in model_cols: + 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), - ) + knowledge_cols = {c['name'] for c in inspector.get_columns('knowledge')} + if 'access_control' not in knowledge_cols: + 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), - ) + prompt_cols = {c['name'] for c in inspector.get_columns('prompt')} + if 'access_control' not in prompt_cols: + 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), - ) + tool_cols = {c['name'] for c in inspector.get_columns('tool')} + if 'access_control' not in tool_cols: + 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 index c75db04ca5..ae096e0808 100644 --- a/backend/open_webui/migrations/versions/9f0c9cd09105_add_note_table.py +++ b/backend/open_webui/migrations/versions/9f0c9cd09105_add_note_table.py @@ -6,8 +6,8 @@ Create Date: 2025-05-03 03:00:00.000000 """ -from alembic import op import sqlalchemy as sa +from alembic import op revision = '9f0c9cd09105' down_revision = '3781e22d8b01' @@ -16,17 +16,22 @@ 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), - ) + conn = op.get_bind() + inspector = sa.inspect(conn) + existing_tables = set(inspector.get_table_names()) + + if 'note' not in existing_tables: + 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(): diff --git a/backend/open_webui/migrations/versions/a0b1c2d3e4f5_add_memory_user_id_index.py b/backend/open_webui/migrations/versions/a0b1c2d3e4f5_add_memory_user_id_index.py index a52ade7711..b73e654149 100644 --- a/backend/open_webui/migrations/versions/a0b1c2d3e4f5_add_memory_user_id_index.py +++ b/backend/open_webui/migrations/versions/a0b1c2d3e4f5_add_memory_user_id_index.py @@ -6,6 +6,7 @@ Create Date: 2025-09-15 03:00:00.000000 """ +import sqlalchemy as sa from alembic import op revision = 'a0b1c2d3e4f5' @@ -15,7 +16,12 @@ depends_on = None def upgrade(): - op.create_index('ix_memory_user_id', 'memory', ['user_id']) + conn = op.get_bind() + inspector = sa.inspect(conn) + existing_indexes = {idx['name'] for idx in inspector.get_indexes('memory')} + + if 'ix_memory_user_id' not in existing_indexes: + op.create_index('ix_memory_user_id', 'memory', ['user_id']) def downgrade(): diff --git a/backend/open_webui/migrations/versions/a1b2c3d4e5f6_add_skill_table.py b/backend/open_webui/migrations/versions/a1b2c3d4e5f6_add_skill_table.py index f11f7d8d1b..1ffce041e1 100644 --- a/backend/open_webui/migrations/versions/a1b2c3d4e5f6_add_skill_table.py +++ b/backend/open_webui/migrations/versions/a1b2c3d4e5f6_add_skill_table.py @@ -8,9 +8,8 @@ Create Date: 2026-02-11 09:30:00.000000 from typing import Sequence, Union -from alembic import op import sqlalchemy as sa - +from alembic import op from open_webui.migrations.util import get_existing_tables revision: str = 'a1b2c3d4e5f6' 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 index 20a3152cfe..5ccff52312 100644 --- 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 @@ -8,8 +8,8 @@ Create Date: 2026-03-29 22:15:00.000000 from typing import Sequence, Union -from alembic import op import sqlalchemy as sa +from alembic import op # revision identifiers, used by Alembic. revision: str = 'a3dd5bedd151' @@ -19,8 +19,14 @@ 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)) + conn = op.get_bind() + inspector = sa.inspect(conn) + columns = [col['name'] for col in inspector.get_columns('chat')] + + if 'tasks' not in columns: + op.add_column('chat', sa.Column('tasks', sa.JSON(), nullable=True)) + if 'summary' not in columns: + op.add_column('chat', sa.Column('summary', sa.Text(), nullable=True)) def downgrade() -> None: 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 index 29157baa07..0108e44614 100644 --- 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 @@ -8,8 +8,8 @@ Create Date: 2025-09-27 02:24:18.058455 from typing import Sequence, Union -from alembic import op import sqlalchemy as sa +from alembic import op # revision identifiers, used by Alembic. revision: str = 'a5c220713937' @@ -19,16 +19,18 @@ depends_on: Union[str, Sequence[str], None] = None def upgrade() -> None: + conn = op.get_bind() + inspector = sa.inspect(conn) + msg_cols = {c['name'] for c in inspector.get_columns('message')} + # 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 + if 'reply_to_id' not in msg_cols: + op.add_column( + 'message', + sa.Column('reply_to_id', sa.Text(), nullable=True), + ) 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 index 4d8fd63e80..65dcf6f5f9 100644 --- a/backend/open_webui/migrations/versions/af906e964978_add_feedback_table.py +++ b/backend/open_webui/migrations/versions/af906e964978_add_feedback_table.py @@ -6,8 +6,8 @@ Create Date: 2024-10-20 17:02:35.241684 """ -from alembic import op import sqlalchemy as sa +from alembic import op # Revision identifiers, used by Alembic. revision = 'af906e964978' @@ -17,23 +17,28 @@ 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) - ) + conn = op.get_bind() + inspector = sa.inspect(conn) + existing_tables = set(inspector.get_table_names()) + + if 'feedback' not in existing_tables: + # ### 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(): diff --git a/backend/open_webui/migrations/versions/b10670c03dd5_update_user_table.py b/backend/open_webui/migrations/versions/b10670c03dd5_update_user_table.py index 623289d885..ba2ab26cca 100644 --- a/backend/open_webui/migrations/versions/b10670c03dd5_update_user_table.py +++ b/backend/open_webui/migrations/versions/b10670c03dd5_update_user_table.py @@ -6,15 +6,13 @@ 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 +from typing import Sequence, Union + +import open_webui.internal.db +import sqlalchemy as sa +from alembic import op # revision identifiers, used by Alembic. revision: str = 'b10670c03dd5' @@ -22,20 +20,49 @@ down_revision: Union[str, None] = '2f1211949ecc' branch_labels: Union[str, Sequence[str], None] = None depends_on: Union[str, Sequence[str], None] = None +# ── Ad-hoc table references for Core DML ───────────────────────────────── +# These are lightweight table() / column() references used only inside this +# migration for SELECT / UPDATE / INSERT — they do NOT create or alter +# anything on disk. + +_user = sa.table( + 'user', + sa.column('id', sa.Text), + sa.column('oauth_sub', sa.Text), + sa.column('oauth', sa.JSON), + sa.column('api_key', sa.Text), + sa.column('info', sa.Text), + sa.column('settings', sa.Text), +) + +_api_key = sa.table( + 'api_key', + sa.column('id', sa.Text), + sa.column('user_id', sa.Text), + sa.column('key', sa.Text), + sa.column('created_at', sa.BigInteger), + sa.column('updated_at', sa.BigInteger), +) + 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. + SQLite requires manual removal of any user-created indexes referencing + a column before ALTER TABLE ... DROP COLUMN can succeed. + + NOTE: PRAGMAs have no Core equivalent — raw text is unavoidable here. """ 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 + # Skip system-managed autoindexes (PK / UNIQUE constraints) — they + # cannot be dropped directly and will disappear when the column is + # removed via batch_alter_table. + if index_name.startswith('sqlite_autoindex_'): + continue idx_info = conn.execute(sa.text(f"PRAGMA index_info('{index_name}')")).fetchall() - - indexed_cols = [row[2] for row in idx_info] # col names + indexed_cols = [row[2] for row in idx_info] if column_name in indexed_cols: conn.execute(sa.text(f'DROP INDEX IF EXISTS {index_name}')) @@ -44,33 +71,31 @@ def _convert_column_to_json(table: str, column: str): conn = op.get_bind() dialect = conn.dialect.name + t = sa.table(table, sa.column('id', sa.Text), sa.column(column, sa.Text)) + t_json = sa.column(f'{column}_json', sa.JSON) + # 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() + rows = conn.execute(sa.select(t.c.id, t.c[column])).fetchall() - for row in rows: - uid, raw = row + for uid, raw in rows: if raw is None: parsed = None else: try: parsed = json.loads(raw) except Exception: - parsed = None # fallback safe behavior + parsed = None conn.execute( - sa.text(f'UPDATE "{table}" SET {column}_json = :val WHERE id = :id'), - {'val': json.dumps(parsed) if parsed else None, 'id': uid}, + sa.update(sa.table(table, sa.column('id'), t_json)) + .where(sa.column('id') == uid) + .values({f'{column}_json': json.dumps(parsed) if parsed else None}) ) - # 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: @@ -87,15 +112,19 @@ def _convert_column_to_text(table: str, column: str): conn = op.get_bind() dialect = conn.dialect.name + t = sa.table(table, sa.column('id', sa.Text), sa.column(column)) + t_text = sa.column(f'{column}_text', sa.Text) + 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() + rows = conn.execute(sa.select(t.c.id, t.c[column])).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}, + sa.update(sa.table(table, sa.column('id'), t_text)) + .where(sa.column('id') == uid) + .values({f'{column}_text': json.dumps(raw) if raw else None}) ) op.drop_column(table, column) @@ -111,88 +140,93 @@ def _convert_column_to_text(table: str, column: str): 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() + inspector = sa.inspect(conn) + existing_tables = set(inspector.get_table_names()) + user_columns = {c['name'] for c in inspector.get_columns('user')} - 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 + # ── Add new columns (idempotent) ────────────────────────────────── + for col_name, col_type in [ + ('profile_banner_image_url', sa.Text()), + ('timezone', sa.String()), + ('presence_state', sa.String()), + ('status_emoji', sa.String()), + ('status_message', sa.Text()), + ('status_expires_at', sa.BigInteger()), + ('oauth', sa.JSON()), + ]: + if col_name not in user_columns: + op.add_column('user', sa.Column(col_name, col_type, nullable=True)) - oauth_json = json.dumps({provider: {'sub': sub}}) - conn.execute( - sa.text('UPDATE "user" SET oauth = :oauth WHERE id = :id'), - {'oauth': oauth_json, 'id': uid}, - ) + # Convert info (TEXT/JSONField) → JSON (skip if already JSON) + user_col_types = {c['name']: c['type'] for c in inspector.get_columns('user')} + if isinstance(user_col_types.get('info'), sa.Text): + _convert_column_to_json('user', 'info') + # Convert settings (TEXT/JSONField) → JSON (skip if already JSON) + if isinstance(user_col_types.get('settings'), sa.Text): + _convert_column_to_json('user', 'settings') - users_with_keys = conn.execute(sa.text('SELECT id, api_key FROM "user" WHERE api_key IS NOT NULL')).fetchall() - now = int(time.time()) + # ── Create api_key table (idempotent) ───────────────────────────── + if 'api_key' not in existing_tables: + 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), + ) - 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, - }, - ) + # ── Migrate oauth_sub → oauth JSON (only if old column still exists) + if 'oauth_sub' in user_columns: + rows = conn.execute(sa.select(_user.c.id, _user.c.oauth_sub).where(_user.c.oauth_sub.is_not(None))).fetchall() - if conn.dialect.name == 'sqlite': - _drop_sqlite_indexes_for_column('user', 'api_key', conn) - _drop_sqlite_indexes_for_column('user', 'oauth_sub', conn) + for uid, oauth_sub in rows: + if oauth_sub: + provider, sub = oauth_sub.split('@', 1) if '@' in oauth_sub else ('oidc', oauth_sub) + conn.execute( + sa.update(_user).where(_user.c.id == uid).values(oauth=json.dumps({provider: {'sub': sub}})) + ) - with op.batch_alter_table('user') as batch_op: - batch_op.drop_column('api_key') - batch_op.drop_column('oauth_sub') + # ── Migrate api_key column → api_key table (only if old column still exists) + if 'api_key' in user_columns: + rows = conn.execute(sa.select(_user.c.id, _user.c.api_key).where(_user.c.api_key.is_not(None))).fetchall() + now = int(time.time()) + + for uid, key_val in rows: + if key_val: + conn.execute( + sa.insert(_api_key).values( + id=f'key_{uid}', + user_id=uid, + key=key_val, + created_at=now, + updated_at=now, + ) + ) + + # ── Drop legacy columns (idempotent) ────────────────────────────── + cols_to_drop = {'api_key', 'oauth_sub'} & user_columns + if cols_to_drop: + if conn.dialect.name == 'sqlite': + for col in cols_to_drop: + _drop_sqlite_indexes_for_column('user', col, conn) + + with op.batch_alter_table('user') as batch_op: + for col in cols_to_drop: + batch_op.drop_column(col) 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() + rows = conn.execute(sa.select(_user.c.id, _user.c.oauth).where(_user.c.oauth.is_not(None))).fetchall() - for uid, oauth in users: + for uid, oauth in rows: try: data = json.loads(oauth) provider = list(data.keys())[0] @@ -201,25 +235,17 @@ def downgrade() -> None: 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}, - ) + conn.execute(sa.update(_user).where(_user.c.id == uid).values(oauth_sub=oauth_sub)) op.drop_column('user', 'oauth') - # --- 2. Restore api_key field --- + # --- 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() + keys = conn.execute(sa.select(_api_key.c.user_id, _api_key.c.key)).fetchall() for uid, key in keys: - conn.execute( - sa.text('UPDATE "user" SET api_key = :key WHERE id = :id'), - {'key': key, 'id': uid}, - ) + conn.execute(sa.update(_user).where(_user.c.id == uid).values(api_key=key)) - # Drop new table op.drop_table('api_key') with op.batch_alter_table('user') as batch_op: 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 index e3668d3b6e..cf942ec8c0 100644 --- 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 @@ -8,8 +8,8 @@ Create Date: 2026-02-13 14:19:00.000000 from typing import Sequence, Union -from alembic import op import sqlalchemy as sa +from alembic import op # revision identifiers, used by Alembic. revision: str = 'b2c3d4e5f6a7' @@ -19,7 +19,12 @@ depends_on: Union[str, Sequence[str], None] = None def upgrade() -> None: - op.add_column('user', sa.Column('scim', sa.JSON(), nullable=True)) + conn = op.get_bind() + inspector = sa.inspect(conn) + user_cols = {c['name'] for c in inspector.get_columns('user')} + + if 'scim' not in user_cols: + op.add_column('user', sa.Column('scim', sa.JSON(), nullable=True)) def downgrade() -> None: 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 index fb254432f6..fd298b7929 100644 --- 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 @@ -6,9 +6,8 @@ Create Date: 2026-04-01 04:00:00.000000 """ -from alembic import op import sqlalchemy as sa - +from alembic import op # revision identifiers, used by Alembic. revision = 'b7c8d9e0f1a2' @@ -18,9 +17,14 @@ 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') + conn = op.get_bind() + inspector = sa.inspect(conn) + columns = [col['name'] for col in inspector.get_columns('chat')] + + if 'last_read_at' not in columns: + 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(): diff --git a/backend/open_webui/migrations/versions/c0fbf31ca0db_update_file_table.py b/backend/open_webui/migrations/versions/c0fbf31ca0db_update_file_table.py index 709b644150..75eada2d3f 100644 --- a/backend/open_webui/migrations/versions/c0fbf31ca0db_update_file_table.py +++ b/backend/open_webui/migrations/versions/c0fbf31ca0db_update_file_table.py @@ -19,10 +19,17 @@ depends_on: Union[str, Sequence[str], None] = None def upgrade(): + conn = op.get_bind() + inspector = sa.inspect(conn) + file_cols = {c['name'] for c in inspector.get_columns('file')} + # ### 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)) + if 'hash' not in file_cols: + op.add_column('file', sa.Column('hash', sa.Text(), nullable=True)) + if 'data' not in file_cols: + op.add_column('file', sa.Column('data', sa.JSON(), nullable=True)) + if 'updated_at' not in file_cols: + op.add_column('file', sa.Column('updated_at', sa.BigInteger(), nullable=True)) def downgrade(): 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 index 2451f50ae2..771f7195b7 100644 --- a/backend/open_webui/migrations/versions/c1d2e3f4a5b6_add_shared_chat_table.py +++ b/backend/open_webui/migrations/versions/c1d2e3f4a5b6_add_shared_chat_table.py @@ -9,8 +9,8 @@ Create Date: 2026-04-16 23:00:00.000000 import time import uuid -from alembic import op import sqlalchemy as sa +from alembic import op revision = 'c1d2e3f4a5b6' down_revision = 'e1f2a3b4c5d6' @@ -61,18 +61,21 @@ access_grant_t = sa.table( def upgrade(): conn = op.get_bind() + inspector = sa.inspect(conn) + tables = inspector.get_table_names() - # 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), - ) + # 1. Create shared_chat table (idempotent) + if 'shared_chat' not in tables: + 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( @@ -96,31 +99,51 @@ def upgrade(): 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, - ) - ) + # Check if shared_chat record already exists (idempotent) + existing_shared = conn.execute( + sa.select(shared_chat_t.c.id).where(shared_chat_t.c.id == share_token) + ).fetchone() - # 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()), + if not existing_shared: + # 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, + ) + ) + + # Check if access_grant record already exists (idempotent) + existing_grant = conn.execute( + sa.select(access_grant_t.c.id).where( + sa.and_( + access_grant_t.c.resource_type == 'shared_chat', + access_grant_t.c.resource_id == original_chat_id, + access_grant_t.c.principal_type == 'user', + access_grant_t.c.principal_id == '*', + access_grant_t.c.permission == 'read', + ) + ) + ).fetchone() + + if not existing_grant: + # 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( 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 index 37fe63ef15..a08485f87f 100644 --- a/backend/open_webui/migrations/versions/c29facfe716b_update_file_table_path.py +++ b/backend/open_webui/migrations/versions/c29facfe716b_update_file_table_path.py @@ -6,11 +6,12 @@ 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_ + +import sqlalchemy as sa +from alembic import op +from sqlalchemy import JSON, String, Text, and_ +from sqlalchemy.sql import column, table revision = 'c29facfe716b' down_revision = 'c69f45358db4' @@ -19,8 +20,13 @@ depends_on = None def upgrade(): + conn = op.get_bind() + inspector = sa.inspect(conn) + file_cols = {c['name'] for c in inspector.get_columns('file')} + # 1. Add the `path` column to the "file" table. - op.add_column('file', sa.Column('path', sa.Text(), nullable=True)) + if 'path' not in file_cols: + 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. 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 index 0eae928b91..b1d0859fe1 100644 --- a/backend/open_webui/migrations/versions/c440947495f3_add_chat_file_table.py +++ b/backend/open_webui/migrations/versions/c440947495f3_add_chat_file_table.py @@ -8,8 +8,8 @@ Create Date: 2025-12-21 20:27:41.694897 from typing import Sequence, Union -from alembic import op import sqlalchemy as sa +from alembic import op # revision identifiers, used by Alembic. revision: str = 'c440947495f3' @@ -19,36 +19,39 @@ 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 + conn = op.get_bind() + inspector = sa.inspect(conn) + existing_tables = set(inspector.get_table_names()) + + if 'chat_file' not in existing_tables: + 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 + ) 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 index c9572fe7a3..646074427e 100644 --- a/backend/open_webui/migrations/versions/c69f45358db4_add_folder_table.py +++ b/backend/open_webui/migrations/versions/c69f45358db4_add_folder_table.py @@ -6,8 +6,8 @@ Create Date: 2024-10-16 02:02:35.241684 """ -from alembic import op import sqlalchemy as sa +from alembic import op revision = 'c69f45358db4' down_revision = '3ab32c4b8f59' @@ -16,30 +16,37 @@ 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'), - ) + conn = op.get_bind() + inspector = sa.inspect(conn) + existing_tables = set(inspector.get_table_names()) - op.add_column( - 'chat', - sa.Column('folder_id', sa.Text(), nullable=True), - ) + if 'folder' not in existing_tables: + 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'), + ) + + chat_cols = {c['name'] for c in inspector.get_columns('chat')} + if 'folder_id' not in chat_cols: + op.add_column( + 'chat', + sa.Column('folder_id', sa.Text(), nullable=True), + ) def downgrade(): diff --git a/backend/open_webui/migrations/versions/ca81bd47c050_add_config_table.py b/backend/open_webui/migrations/versions/ca81bd47c050_add_config_table.py index 5fdf933dd6..ff54232bdd 100644 --- a/backend/open_webui/migrations/versions/ca81bd47c050_add_config_table.py +++ b/backend/open_webui/migrations/versions/ca81bd47c050_add_config_table.py @@ -1,9 +1,8 @@ -"""Add config table +"""Add config table. Revision ID: ca81bd47c050 Revises: 7e5b5dc7342b Create Date: 2024-08-25 15:26:35.241684 - """ from typing import Sequence, Union @@ -11,29 +10,40 @@ 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 upgrade() -> None: + """Create a key-value config table with versioning.""" + conn = op.get_bind() + inspector = sa.inspect(conn) + existing_tables = set(inspector.get_table_names()) + + if 'config' not in existing_tables: + 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(): +def downgrade() -> None: + """Drop the config table.""" 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 index 444e131db7..60c2e6e9e0 100644 --- a/backend/open_webui/migrations/versions/d31026856c01_update_folder_table_data.py +++ b/backend/open_webui/migrations/versions/d31026856c01_update_folder_table_data.py @@ -6,8 +6,8 @@ Create Date: 2025-07-13 03:00:00.000000 """ -from alembic import op import sqlalchemy as sa +from alembic import op revision = 'd31026856c01' down_revision = '9f0c9cd09105' @@ -16,7 +16,12 @@ depends_on = None def upgrade(): - op.add_column('folder', sa.Column('data', sa.JSON(), nullable=True)) + conn = op.get_bind() + inspector = sa.inspect(conn) + folder_cols = {c['name'] for c in inspector.get_columns('folder')} + + if 'data' not in folder_cols: + op.add_column('folder', sa.Column('data', sa.JSON(), nullable=True)) def downgrade(): diff --git a/backend/open_webui/migrations/versions/d4e5f6a7b8c9_add_automation_tables.py b/backend/open_webui/migrations/versions/d4e5f6a7b8c9_add_automation_tables.py index fc90dc417f..1df1cf7092 100644 --- a/backend/open_webui/migrations/versions/d4e5f6a7b8c9_add_automation_tables.py +++ b/backend/open_webui/migrations/versions/d4e5f6a7b8c9_add_automation_tables.py @@ -7,8 +7,8 @@ Create Date: 2026-03-30 from typing import Union -from alembic import op import sqlalchemy as sa +from alembic import op revision: str = 'd4e5f6a7b8c9' down_revision: Union[str, None] = 'a3dd5bedd151' @@ -16,36 +16,56 @@ 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']) +def _index_exists(inspector, index_name, table_name): + """Check if an index already exists on the given table (works for both SQLite and PostgreSQL).""" + indexes = inspector.get_indexes(table_name) + return any(idx['name'] == index_name for idx in indexes) - 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 upgrade(): + conn = op.get_bind() + inspector = sa.inspect(conn) + tables = inspector.get_table_names() + + if 'automation' not in tables: + 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), + ) + + inspector.clear_cache() + if 'automation' in inspector.get_table_names(): + if not _index_exists(inspector, 'ix_automation_next_run', 'automation'): + op.create_index('ix_automation_next_run', 'automation', ['next_run_at']) + + if 'automation_run' not in tables: + 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), + ) + + inspector.clear_cache() + if 'automation_run' in inspector.get_table_names(): + if not _index_exists(inspector, 'ix_automation_run_automation_id', 'automation_run'): + op.create_index( + 'ix_automation_run_automation_id', + 'automation_run', + ['automation_id'], + ) def downgrade(): 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 index 0d80558746..7232b11b83 100644 --- 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 @@ -6,8 +6,8 @@ Create Date: 2026-04-14 22:00:00.000000 """ -from alembic import op import sqlalchemy as sa +from alembic import op revision = 'e1f2a3b4c5d6' down_revision = 'b7c8d9e0f1a2' @@ -16,7 +16,12 @@ depends_on = None def upgrade(): - op.add_column('note', sa.Column('is_pinned', sa.Boolean(), nullable=True)) + conn = op.get_bind() + inspector = sa.inspect(conn) + columns = [col['name'] for col in inspector.get_columns('note')] + + if 'is_pinned' not in columns: + op.add_column('note', sa.Column('is_pinned', sa.Boolean(), nullable=True)) def downgrade(): 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 index 5ed572cf7a..6a33d087b5 100644 --- a/backend/open_webui/migrations/versions/f1e2d3c4b5a6_add_access_grant_table.py +++ b/backend/open_webui/migrations/versions/f1e2d3c4b5a6_add_access_grant_table.py @@ -11,13 +11,12 @@ Access control semantics: - {read: {...}, write: {...}}: Custom permissions -> insert specific grants """ -from typing import Sequence, Union import time import uuid +from typing import Sequence, Union -from alembic import op import sqlalchemy as sa - +from alembic import op from open_webui.migrations.util import get_existing_tables revision: str = 'f1e2d3c4b5a6' @@ -81,13 +80,18 @@ def upgrade() -> None: 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: + # Check if access_control and id columns exist (may already be dropped on re-run, + # or table may have been rebuilt without id during intermediate migration states) + insp = sa.inspect(conn) + insp.clear_cache() # Ensure fresh metadata after prior migrations that rebuild tables + table_cols = {c['name'] for c in insp.get_columns(table_name)} + if 'access_control' not in table_cols or 'id' not in table_cols: continue + # Query all rows + result = conn.execute(sa.text(f'SELECT id, access_control FROM "{table_name}"')) + rows = result.fetchall() + for row in rows: resource_id = row[0] access_control_json = row[1] @@ -208,15 +212,15 @@ def upgrade() -> None: except Exception: pass - # Drop access_control columns from resource tables + # Drop access_control columns from resource tables (only if column still exists) + inspector = sa.inspect(conn) for table_name, _ in resource_tables: if table_name not in existing_tables: continue - try: + cols = {c['name'] for c in inspector.get_columns(table_name)} + if 'access_control' in cols: with op.batch_alter_table(table_name) as batch: batch.drop_column('access_control') - except Exception: - pass def downgrade() -> None: diff --git a/backend/open_webui/models/access_grants.py b/backend/open_webui/models/access_grants.py index f031495912..1c86dc08e7 100644 --- a/backend/open_webui/models/access_grants.py +++ b/backend/open_webui/models/access_grants.py @@ -3,13 +3,11 @@ 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 import BigInteger, Column, Text, UniqueConstraint, and_, delete, or_, select from sqlalchemy.dialects.postgresql import JSONB +from sqlalchemy.ext.asyncio import AsyncSession log = logging.getLogger(__name__) @@ -623,8 +621,8 @@ class AccessGrantsTable: 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 + from open_webui.models.users import UserModel, Users async with get_async_db_context(db) as db: result = await db.execute( diff --git a/backend/open_webui/models/auths.py b/backend/open_webui/models/auths.py index 2c8c6ba99f..5e363352bd 100644 --- a/backend/open_webui/models/auths.py +++ b/backend/open_webui/models/auths.py @@ -1,50 +1,50 @@ +"""Auth credential models and data-access layer.""" + +from __future__ import annotations + 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 +from sqlalchemy import Boolean, Column, String, Text, delete, select, update +from sqlalchemy.ext.asyncio import AsyncSession log = logging.getLogger(__name__) -#################### -# DB MODEL -#################### +class Auth(Base): # credential ↔ user linkage + """Maps a user ID to an email/password pair with an active flag.""" -class Auth(Base): __tablename__ = 'auth' - id = Column(String, primary_key=True, unique=True) - email = Column(String) - password = Column(Text) - active = Column(Boolean) + id = Column(String, primary_key=True, unique=True) # mirrors User.id + email = Column(String) # login address, kept in sync with User.email + password = Column(Text) # argon2 / bcrypt hash + active = Column(Boolean) # account soft-disable toggle class AuthModel(BaseModel): + """Pydantic mirror of the ``auth`` table row.""" + id: str email: str password: str active: bool = True -#################### -# Forms -#################### - - class Token(BaseModel): + """JWT bearer-token response wrapper.""" + token: str token_type: str class ApiKey(BaseModel): - api_key: Optional[str] = None + api_key: str | None = None class SigninResponse(Token, UserProfileImageResponse): @@ -74,21 +74,26 @@ class SignupForm(BaseModel): name: str email: str password: str - profile_image_url: Optional[str] = '/user.png' + profile_image_url: str | None = '/user.png' @field_validator('profile_image_url') @classmethod - def check_profile_image_url(cls, v: Optional[str]) -> Optional[str]: + def check_profile_image_url(cls, v: str | None) -> str | None: if v is not None: return validate_profile_image_url(v) return v class AddUserForm(SignupForm): - role: Optional[str] = 'pending' + role: str | None = 'pending' + + +# --- data-access layer --- class AuthsTable: + """Provides CRUD operations for the Auth ↔ User lifecycle.""" + async def insert_new_auth( self, email: str, @@ -96,117 +101,131 @@ class AuthsTable: 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: + oauth: dict | None = None, + db: AsyncSession | None = None, + ) -> UserModel | None: + """Create an Auth + User pair inside a single transaction.""" + async with get_async_db_context(db) as session: log.info('insert_new_auth') - id = str(uuid.uuid4()) + new_id = str(uuid.uuid4()) - auth = AuthModel(**{'id': id, 'email': email, 'password': password, 'active': True}) - result = Auth(**auth.model_dump()) - db.add(result) + credential = Auth( + id=new_id, + email=email, + password=password, + active=True, + ) + session.add(credential) - 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 + created_user = await Users.insert_new_user( + new_id, + name, + email, + profile_image_url, + role, + oauth=oauth, + db=session, + ) + # persist both records and reload generated defaults + await session.commit() + await session.refresh(credential) + return created_user if credential and created_user else 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 + self, + email: str, + verify_password: callable, + db: AsyncSession | None = None, + ) -> UserModel | None: + """Verify email + password credentials and return the matching user.""" + log.info('authenticate_user: %s', email) + resolved = await Users.get_user_by_email(email, db=db) + if not resolved: + return + # load the credential row and verify the password hash + async with get_async_db_context(db) as session: + credential = await session.get(Auth, resolved.id) + if not credential or not credential.active: + return + if not verify_password(credential.password): + return + return resolved 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 + self, + api_key: str, + db: AsyncSession | None = None, + ) -> UserModel | None: + """Look up the user that owns the given API key.""" + log.info('authenticate_user_by_api_key') if not api_key: - return None + return + # delegate to the Users model for the actual lookup + return await Users.get_user_by_api_key(api_key, db=db) - 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: AsyncSession | None = None, + ) -> UserModel | None: + """Single-query auth via JOIN on Auth ↔ User, filtered by active flag.""" + log.info('authenticate_user_by_email: %s', email) + # single JOIN avoids N+1 — returns (Auth, User) tuple or None + async with get_async_db_context(db) as session: + joined_query = ( + select(Auth, User).join(User, Auth.id == User.id).where(Auth.email == email, Auth.active.is_(True)) + ) + match = (await session.execute(joined_query)).first() + if not match: + return + _, found_user = match + return UserModel.model_validate(found_user) - 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 + async def update_email_by_id( + self, + user_id: str, + email: str, + db: AsyncSession | None = None, + ) -> bool: + """Set a new email on the auth record and propagate to the user row.""" + async with get_async_db_context(db) as session: + auth_row = await session.get(Auth, user_id) + if auth_row is None: return False - except Exception: - return False + auth_row.email = email + await session.commit() + await Users.update_user_by_id(user_id, {'email': email}, db=session) + return True + # --- password modification --- - 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) + async def update_user_password_by_id( + self, + user_id: str, + new_password: str, + db: AsyncSession | None = None, + ) -> bool: + """Set a new password hash for an existing user.""" + async with get_async_db_context(db) as session: + auth_row = await session.get(Auth, user_id) + if auth_row is None: + return False + auth_row.password = new_password + await session.commit() + return True - if result: - await db.execute(delete(Auth).filter_by(id=id)) - await db.commit() - - return True - else: - return False - except Exception: - return False + async def delete_auth_by_id( + self, + id: str, + db: AsyncSession | None = None, + ) -> bool: + """Remove a user and their auth credential in one transaction.""" + async with get_async_db_context(db) as session: + if not await Users.delete_user_by_id(id, db=session): + return False + await session.execute(delete(Auth).where(Auth.id == id)) + await session.commit() + return True -Auths = AuthsTable() +Auths = AuthsTable() # singleton — module-level instance diff --git a/backend/open_webui/models/automations.py b/backend/open_webui/models/automations.py index 05f449ad13..4038a3bdbe 100644 --- a/backend/open_webui/models/automations.py +++ b/backend/open_webui/models/automations.py @@ -1,13 +1,12 @@ -import time import logging +import time 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 +from pydantic import BaseModel, ConfigDict +from sqlalchemy import JSON, BigInteger, Boolean, Column, Index, String, Text, cast, delete, func, or_, select, update +from sqlalchemy.ext.asyncio import AsyncSession log = logging.getLogger(__name__) diff --git a/backend/open_webui/models/calendar.py b/backend/open_webui/models/calendar.py index 48b28d8e9a..2067ccfab5 100644 --- a/backend/open_webui/models/calendar.py +++ b/backend/open_webui/models/calendar.py @@ -1,30 +1,29 @@ -import time import logging +import time 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 +from pydantic import BaseModel, ConfigDict, Field +from sqlalchemy import ( + JSON, + BigInteger, + Boolean, + Column, + Index, + Text, + UniqueConstraint, + delete, + exists, + func, + or_, + select, + update, +) +from sqlalchemy.ext.asyncio import AsyncSession log = logging.getLogger(__name__) @@ -696,12 +695,19 @@ class CalendarEventTable: self, now_ns: int, default_lookahead_ns: int, + grace_ns: int = 0, 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. + + *grace_ns* widens the SQL lower bound so that events whose start_at + is up to *grace_ns* nanoseconds in the past are still fetched. This + ensures "At time of event" alerts (alert_minutes=0) are not missed + when the scheduler polls a few seconds after the event's exact start + time. """ from open_webui.models.users import User as UserRow @@ -716,7 +722,7 @@ class CalendarEventTable: .outerjoin(UserRow, UserRow.id == CalendarEvent.user_id) .filter( CalendarEvent.is_cancelled == False, - CalendarEvent.start_at >= now_ns, + CalendarEvent.start_at >= now_ns - grace_ns, CalendarEvent.start_at <= upper, ) ) diff --git a/backend/open_webui/models/channels.py b/backend/open_webui/models/channels.py index adeaeaf9da..9d5f130355 100644 --- a/backend/open_webui/models/channels.py +++ b/backend/open_webui/models/channels.py @@ -4,31 +4,33 @@ import time import uuid from typing import Optional -from open_webui.utils.validate import validate_profile_image_url - -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 open_webui.models.groups import Groups +from open_webui.utils.validate import validate_profile_image_url from pydantic import BaseModel, ConfigDict, Field, field_validator -from sqlalchemy.dialects.postgresql import JSONB - - from sqlalchemy import ( + JSON, BigInteger, Boolean, Column, ForeignKey, String, Text, - JSON, UniqueConstraint, + and_, + case, + delete, + func, + or_, + select, + update, ) +from sqlalchemy.dialects.postgresql import JSONB +from sqlalchemy.ext.asyncio import AsyncSession #################### # Channel DB Schema diff --git a/backend/open_webui/models/chat_messages.py b/backend/open_webui/models/chat_messages.py index a7d875c9dc..8660e1e987 100644 --- a/backend/open_webui/models/chat_messages.py +++ b/backend/open_webui/models/chat_messages.py @@ -3,21 +3,24 @@ 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 ( + JSON, BigInteger, Boolean, Column, ForeignKey, - Text, - JSON, Index, + Integer, + Text, + cast, + delete, + func, + select, ) +from sqlalchemy.ext.asyncio import AsyncSession #################### # Helpers @@ -169,7 +172,7 @@ class ChatMessageTable: # Update existing if 'role' in data: existing.role = data['role'] - if 'parent_id' in data: + if 'parent_id' in data or 'parentId' in data: existing.parent_id = data.get('parent_id') or data.get('parentId') if 'content' in data: existing.content = data.get('content') @@ -391,6 +394,24 @@ class ChatMessageTable: await db.commit() return True + async def delete_message_ids_by_chat_id( + self, + chat_id: str, + message_ids: set[str], + db: Optional[AsyncSession] = None, + ) -> bool: + """Delete specific ``chat_message`` rows by their original message IDs.""" + if not message_ids: + return True + async with get_async_db_context(db) as db: + await db.execute( + delete(ChatMessage) + .where(ChatMessage.chat_id == chat_id) + .where(ChatMessage.id.in_({f'{chat_id}-{mid}' for mid in message_ids})) + ) + await db.commit() + return True + # Analytics methods async def get_message_count_by_model( self, @@ -579,6 +600,7 @@ class ChatMessageTable: """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( diff --git a/backend/open_webui/models/chats.py b/backend/open_webui/models/chats.py index 957492d817..f237ce58f7 100644 --- a/backend/open_webui/models/chats.py +++ b/backend/open_webui/models/chats.py @@ -1,55 +1,58 @@ -import logging +"""Chat models, forms, and database operations.""" + +from __future__ import annotations + import json +import logging 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 +# local imports 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.models.chat_messages import ChatMessage, ChatMessages +from open_webui.models.folders import Folders +from open_webui.models.tags import Tag, TagModel, Tags from open_webui.utils.misc import sanitize_data_for_db, sanitize_text_for_db - from pydantic import BaseModel, ConfigDict from sqlalchemy import ( + JSON, BigInteger, Boolean, Column, ForeignKey, + Index, String, Text, - JSON, - Index, UniqueConstraint, + and_, + delete, + func, + or_, + select, + text, + update, ) - -#################### -# 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. -#################### +from sqlalchemy.ext.asyncio import AsyncSession +from sqlalchemy.sql import exists +from sqlalchemy.sql.expression import bindparam log = logging.getLogger(__name__) -class Chat(Base): +class Chat(Base): # database table mapping for chat entity __tablename__ = 'chat' id = Column(String, primary_key=True, unique=True) - user_id = Column(String) - title = Column(Text) + user_id = Column(String, index=True) # owner user id + title = Column(Text) # user-visible conversation title chat = Column(JSON) - created_at = Column(BigInteger) - updated_at = Column(BigInteger) + created_at = Column(BigInteger, index=True) # conversation creation timestamp + updated_at = Column(BigInteger, index=True) # conversation modification timestamp - share_id = Column(Text, unique=True, nullable=True) - archived = Column(Boolean, default=False) + share_id = Column(Text, unique=True, nullable=True) # public share link token + archived = Column(Boolean, default=False) # hidden from main chat list pinned = Column(Boolean, default=False, nullable=True) meta = Column(JSON, server_default='{}') @@ -71,8 +74,7 @@ class Chat(Base): class ChatModel(BaseModel): - model_config = ConfigDict(from_attributes=True) - + model_config = ConfigDict(from_attributes=True) # allows ORM model binding id: str user_id: str title: str @@ -81,17 +83,17 @@ class ChatModel(BaseModel): created_at: int # timestamp in epoch updated_at: int # timestamp in epoch - share_id: Optional[str] = None + share_id: str | None = None archived: bool = False - pinned: Optional[bool] = False + pinned: bool | None = False meta: dict = {} - folder_id: Optional[str] = None + folder_id: str | None = None - tasks: Optional[list] = None - summary: Optional[str] = None + tasks: list | None = None + summary: str | None = None - last_read_at: Optional[int] = None + last_read_at: int | None = None class ChatFile(Base): @@ -115,7 +117,7 @@ class ChatFileModel(BaseModel): user_id: str chat_id: str - message_id: Optional[str] = None + message_id: str | None = None file_id: str created_at: int @@ -131,14 +133,14 @@ class ChatFileModel(BaseModel): class ChatForm(BaseModel): chat: dict - folder_id: Optional[str] = None + folder_id: str | None = None class ChatImportForm(ChatForm): - meta: Optional[dict] = {} - pinned: Optional[bool] = False - created_at: Optional[int] = None - updated_at: Optional[int] = None + meta: dict | None = {} + pinned: bool | None = False + created_at: int | None = None + updated_at: int | None = None class ChatsImportForm(BaseModel): @@ -161,14 +163,14 @@ class ChatResponse(BaseModel): 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 + share_id: str | None = None # id of the chat to be shared archived: bool - pinned: Optional[bool] = False + pinned: bool | None = False meta: dict = {} - folder_id: Optional[str] = None + folder_id: str | None = None - tasks: Optional[list] = None - summary: Optional[str] = None + tasks: list | None = None + summary: str | None = None class ChatTitleIdResponse(BaseModel): @@ -176,13 +178,13 @@ class ChatTitleIdResponse(BaseModel): title: str updated_at: int created_at: int - last_read_at: Optional[int] = None + last_read_at: int | None = None class SharedChatResponse(BaseModel): id: str title: str - share_id: Optional[str] = None + share_id: str | None = None updated_at: int created_at: int @@ -225,17 +227,17 @@ class ChatUsageStatsListResponse(BaseModel): class MessageStats(BaseModel): id: str role: str - model: Optional[str] = None + model: str | None = 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 + token_count: int | None = None + timestamp: int | None = None + rating: int | None = None # Derived from message.annotation.rating + tags: list[str | None] = None # Derived from message.annotation.tags class ChatHistoryStats(BaseModel): messages: dict[str, MessageStats] - currentId: Optional[str] = None + currentId: str | None = None class ChatBody(BaseModel): @@ -293,9 +295,9 @@ class ChatTable: 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: + self, id: str, user_id: str, form_data: ChatForm, db: AsyncSession | None = None + ) -> ChatModel | None: + async with get_async_db_context(db) as session: chat = ChatModel( **{ 'id': id, @@ -311,9 +313,9 @@ class ChatTable: ) chat_item = Chat(**chat.model_dump()) - db.add(chat_item) - await db.commit() - await db.refresh(chat_item) + session.add(chat_item) + await session.commit() + await session.refresh(chat_item) # Dual-write initial messages to chat_message table try: @@ -353,17 +355,32 @@ class ChatTable: self, user_id: str, chat_import_forms: list[ChatImportForm], - db: Optional[AsyncSession] = None, + db: AsyncSession | None = None, ) -> list[ChatModel]: - async with get_async_db_context(db) as db: + async with get_async_db_context(db) as session: + # Validate folder_id references — clear any that don't exist + folder_ids = {f.folder_id for f in chat_import_forms if f.folder_id} + existing = set() + for fid in folder_ids: + if await Folders.get_folder_by_id_and_user_id(fid, user_id, db=session): + existing.add(fid) + + cleared = 0 + for form in chat_import_forms: + if form.folder_id and form.folder_id not in existing: + form.folder_id = None + cleared += 1 + if cleared: + log.info('Import: cleared %d dangling folder_id(s) for user %s', cleared, user_id) + 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() + session.add_all(chats) + await session.commit() # Dual-write messages to chat_message table for form_data, chat_obj in zip(chat_import_forms, chats): @@ -383,52 +400,61 @@ class ChatTable: 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) + async def update_chat_by_id( + self, + id: str, + chat: dict, + db: AsyncSession | None = None, + ) -> ChatModel | None: + """Persist updated chat content, sanitizing null bytes.""" + try: # load the chat record for in-place mutation + async with get_async_db_context(db) as session: + chat_item = await session.get(Chat, id) + if chat_item is None: + return None + 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() + await session.commit() return ChatModel.model_validate(chat_item) except Exception: - return None + return - async def update_chat_last_read_at_by_id(self, id: str, user_id: str, db: Optional[AsyncSession] = None) -> bool: + async def update_chat_last_read_at_by_id(self, id: str, user_id: str, db: AsyncSession | None = None) -> bool: try: - async with get_async_db_context(db) as db: - chat = await db.get(Chat, id) + async with get_async_db_context(db) as session: + chat = await session.get(Chat, id) if chat and chat.user_id == user_id: chat.last_read_at = int(time.time()) - await db.commit() + await session.commit() return True return False except Exception: return False - async def update_chat_title_by_id(self, id: str, title: str) -> Optional[ChatModel]: + async def update_chat_title_by_id(self, id: str, title: str) -> ChatModel | None: try: - async with get_async_db_context() as db: - chat_item = await db.get(Chat, id) + async with get_async_db_context() as session: + chat_item = await session.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) + await session.commit() + await session.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) + async def update_chat_tags_by_id(self, id: str, tags: list[str], user) -> ChatModel | None: + async with get_async_db_context() as session: + chat = await session.get(Chat, id) if chat is None: return None @@ -438,22 +464,22 @@ class ChatTable: # Single meta update chat.meta = {**chat.meta, 'tags': new_tag_ids} - await db.commit() - await db.refresh(chat) + await session.commit() + await session.refresh(chat) # Batch-create any missing tag rows - await Tags.ensure_tags_exist(new_tags, user.id, db=db) + await Tags.ensure_tags_exist(new_tags, user.id, db=session) # 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) + await self.delete_orphan_tags_for_user(list(removed), user.id, db=session) 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)) + async def get_chat_title_by_id(self, id: str) -> str | None: + async with get_async_db_context() as session: + result = await session.execute(select(Chat.title).filter_by(id=id)) row = result.first() if row is None: return None @@ -488,7 +514,25 @@ class ChatTable: except Exception as e: log.warning('Backfill failed for message %s in chat %s: %s', message_id, chat_id, e) - async def get_messages_map_by_chat_id(self, id: str) -> Optional[dict]: + async def reconcile_messages_by_chat_id(self, chat_id: str, user_id: str, messages: dict[str, dict]) -> None: + """Sync ``chat_message`` rows with the committed JSON blob. + + Upserts current messages via ``backfill_messages_by_chat_id`` + and deletes orphaned rows whose message_id no longer appears + in the blob. Best-effort: errors are logged but never raised. + """ + try: + await self.backfill_messages_by_chat_id(chat_id, user_id, messages) + + existing_map = await ChatMessages.get_messages_map_by_chat_id(chat_id) + if existing_map is not None: + orphaned_ids = set(existing_map.keys()) - set(messages.keys()) + if orphaned_ids: + await ChatMessages.delete_message_ids_by_chat_id(chat_id, orphaned_ids) + except Exception as e: + log.warning('Failed to reconcile chat_message rows for chat %s: %s', chat_id, e) + + async def get_messages_map_by_chat_id(self, id: str) -> dict | None: """Message map for walking history (see ``get_message_list``). Prefer ``chat_message`` rows to avoid loading the large embedded @@ -541,7 +585,7 @@ class ChatTable: return history_messages - async def get_message_by_id_and_message_id(self, id: str, message_id: str) -> Optional[dict]: + async def get_message_by_id_and_message_id(self, id: str, message_id: str) -> dict | None: chat = await self.get_chat_by_id(id) if chat is None: return None @@ -550,7 +594,7 @@ class ChatTable: async def upsert_message_to_chat_by_id_and_message_id( self, id: str, message_id: str, message: dict - ) -> Optional[ChatModel]: + ) -> ChatModel | None: chat = await self.get_chat_by_id(id) if chat is None: return None @@ -590,7 +634,7 @@ class ChatTable: async def add_message_status_to_chat_by_id_and_message_id( self, id: str, message_id: str, status: dict - ) -> Optional[ChatModel]: + ) -> ChatModel | None: chat = await self.get_chat_by_id(id) if chat is None: return None @@ -607,8 +651,8 @@ class ChatTable: 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) + async with get_async_db_context() as session: + chat = await self.get_chat_by_id(id, db=session) if chat is None: return None @@ -623,52 +667,51 @@ class ChatTable: history['messages'][message_id]['files'] = message_files chat['history'] = history - await self.update_chat_by_id(id, chat, db=db) + await self.update_chat_by_id(id, chat, db=session) return message_files - async def insert_shared_chat_by_chat_id( - self, chat_id: str, db: Optional[AsyncSession] = None - ) -> Optional[ChatModel]: + async def insert_shared_chat_by_chat_id(self, chat_id: str, db: AsyncSession | None = None) -> ChatModel | None: """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) + async with get_async_db_context(db) as session: + chat = await session.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) + return await self.update_shared_chat_by_chat_id(chat_id, db=session) - shared = await SharedChats.create(chat_id, chat.user_id, db=db) + shared = await SharedChats.create(chat_id, chat.user_id, db=session) 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) + await session.commit() + await session.refresh(chat) + return ChatModel.model_validate(chat) # return the updated original + # refresh helper 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.""" + self, + chat_id: str, + db: AsyncSession | None = None, + ) -> ChatModel | None: + """Refresh the shared snapshot with current chat content.""" 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) + async with get_async_db_context(db) as session: + record = await session.get(Chat, chat_id) + if not record or not record.share_id: + return await self.insert_shared_chat_by_chat_id(chat_id, db=session) + await SharedChats.update(record.share_id, db=session) + return ChatModel.model_validate(record) + # unreachable — context manager above always returns + return - 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: + async def delete_shared_chat_by_chat_id(self, chat_id: str, db: AsyncSession | None = None) -> bool: """Delete shared snapshot for a chat.""" from open_webui.models.shared_chats import SharedChats @@ -677,58 +720,58 @@ class ChatTable: except Exception: return False - async def unarchive_all_chats_by_user_id(self, user_id: str, db: Optional[AsyncSession] = None) -> bool: + async def unarchive_all_chats_by_user_id(self, user_id: str, db: AsyncSession | None = 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() + async with get_async_db_context(db) as session: + await session.execute(update(Chat).filter_by(user_id=user_id).values(archived=False)) + await session.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]: + self, id: str, share_id: str | None, db: AsyncSession | None = None + ) -> ChatModel | None: try: - async with get_async_db_context(db) as db: - chat = await db.get(Chat, id) + async with get_async_db_context(db) as session: + chat = await session.get(Chat, id) chat.share_id = share_id - await db.commit() - await db.refresh(chat) + await session.commit() + await session.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]: + async def toggle_chat_pinned_by_id(self, id: str, db: AsyncSession | None = None) -> ChatModel | None: try: - async with get_async_db_context(db) as db: - chat = await db.get(Chat, id) + async with get_async_db_context(db) as session: + chat = await session.get(Chat, id) chat.pinned = not chat.pinned chat.updated_at = int(time.time()) - await db.commit() - await db.refresh(chat) + await session.commit() + await session.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]: + async def toggle_chat_archive_by_id(self, id: str, db: AsyncSession | None = None) -> ChatModel | None: try: - async with get_async_db_context(db) as db: - chat = await db.get(Chat, id) + async with get_async_db_context(db) as session: + chat = await session.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) + await session.commit() + await session.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: + async def archive_all_chats_by_user_id(self, user_id: str, db: AsyncSession | None = 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() + async with get_async_db_context(db) as session: + await session.execute(update(Chat).filter_by(user_id=user_id).values(archived=True)) + await session.commit() return True except Exception: return False @@ -736,12 +779,12 @@ class ChatTable: async def get_archived_chat_list_by_user_id( self, user_id: str, - filter: Optional[dict] = None, + filter: dict | None = None, skip: int = 0, limit: int = 50, - db: Optional[AsyncSession] = None, + db: AsyncSession | None = None, ) -> list[ChatTitleIdResponse]: - async with get_async_db_context(db) as db: + async with get_async_db_context(db) as session: stmt = select(Chat.id, Chat.title, Chat.updated_at, Chat.created_at).filter_by( user_id=user_id, archived=True ) @@ -772,7 +815,7 @@ class ChatTable: if limit: stmt = stmt.limit(limit) - result = await db.execute(stmt) + result = await session.execute(stmt) all_chats = result.all() return [ ChatTitleIdResponse.model_validate( @@ -789,10 +832,10 @@ class ChatTable: async def get_shared_chat_list_by_user_id( self, user_id: str, - filter: Optional[dict] = None, + filter: dict | None = None, skip: int = 0, limit: int = 50, - db: Optional[AsyncSession] = None, + db: AsyncSession | None = None, ) -> list[SharedChatResponse]: """Delegate to SharedChats for listing shared chats by user.""" from open_webui.models.shared_chats import SharedChats @@ -803,12 +846,12 @@ class ChatTable: self, user_id: str, include_archived: bool = False, - filter: Optional[dict] = None, + filter: dict | None = None, skip: int = 0, limit: int = 50, - db: Optional[AsyncSession] = None, + db: AsyncSession | None = None, ) -> list[ChatTitleIdResponse]: - async with get_async_db_context(db) as db: + async with get_async_db_context(db) as session: stmt = select(Chat.id, Chat.title, Chat.updated_at, Chat.created_at, Chat.last_read_at).filter_by( user_id=user_id ) @@ -838,7 +881,7 @@ class ChatTable: if limit: stmt = stmt.limit(limit) - result = await db.execute(stmt) + result = await session.execute(stmt) all_chats = result.all() return [ ChatTitleIdResponse.model_validate( @@ -859,11 +902,11 @@ class ChatTable: include_archived: bool = False, include_folders: bool = False, include_pinned: bool = False, - skip: Optional[int] = None, - limit: Optional[int] = None, - db: Optional[AsyncSession] = None, + skip: int | None = None, + limit: int | None = None, + db: AsyncSession | None = None, ) -> list[ChatTitleIdResponse]: - async with get_async_db_context(db) as db: + async with get_async_db_context(db) as session: stmt = select(Chat.id, Chat.title, Chat.updated_at, Chat.created_at, Chat.last_read_at).filter_by( user_id=user_id ) @@ -884,7 +927,7 @@ class ChatTable: if limit: stmt = stmt.limit(limit) - result = await db.execute(stmt) + result = await session.execute(stmt) all_chats = result.all() return [ @@ -905,31 +948,37 @@ class ChatTable: chat_ids: list[str], skip: int = 0, limit: int = 50, - db: Optional[AsyncSession] = None, + db: AsyncSession | None = None, ) -> list[ChatModel]: - async with get_async_db_context(db) as db: - result = await db.execute( + async with get_async_db_context(db) as session: + result = await session.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]: + # retrieve conversation + async def get_chat_by_id( + self, + id: str, + db: AsyncSession | None = None, + ) -> ChatModel | None: + """Fetch a chat by PK, auto-sanitizing null bytes on read.""" try: - async with get_async_db_context(db) as db: - chat_item = await db.get(Chat, id) + async with get_async_db_context(db) as session: + chat_item = await session.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) + await session.commit() + await session.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]: + async def get_chat_by_share_id(self, id: str, db: AsyncSession | None = None) -> ChatModel | None: """Look up a shared chat snapshot by its share token.""" from open_webui.models.shared_chats import SharedChats @@ -951,56 +1000,57 @@ class ChatTable: return None async def get_chat_by_id_and_user_id( - self, id: str, user_id: str, db: Optional[AsyncSession] = None - ) -> Optional[ChatModel]: + self, id: str, user_id: str, db: AsyncSession | None = None + ) -> ChatModel | None: try: - async with get_async_db_context(db) as db: - result = await db.execute(select(Chat).filter_by(id=id, user_id=user_id)) + async with get_async_db_context(db) as session: + result = await session.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: + async def is_chat_owner(self, id: str, user_id: str, db: AsyncSession | None = 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)))) + async with get_async_db_context(db) as session: + result = await session.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]: + async def get_chat_folder_id(self, id: str, user_id: str, db: AsyncSession | None = None) -> str | None: """ 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)) + async with get_async_db_context(db) as session: + result = await session.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())) + async def get_chats(self, skip: int = 0, limit: int = 50, db: AsyncSession | None = None) -> list[ChatModel]: + async with get_async_db_context(db) as session: + result = await session.execute(select(Chat).order_by(Chat.updated_at.desc())) all_chats = result.scalars().all() return [ChatModel.model_validate(chat) for chat in all_chats] + # list user conversations 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, + filter: dict | None = None, + skip: int | None = None, + limit: int | None = None, + db: AsyncSession | None = None, ) -> ChatListResponse: - async with get_async_db_context(db) as db: + async with get_async_db_context(db) as session: stmt = select(Chat).filter_by(user_id=user_id) if filter: @@ -1022,7 +1072,7 @@ class ChatTable: else: stmt = stmt.order_by(Chat.updated_at.desc(), Chat.id) - count_result = await db.execute(select(func.count()).select_from(stmt.subquery())) + count_result = await session.execute(select(func.count()).select_from(stmt.subquery())) total = count_result.scalar() if skip is not None: @@ -1030,7 +1080,7 @@ class ChatTable: if limit is not None: stmt = stmt.limit(limit) - result = await db.execute(stmt) + result = await session.execute(stmt) all_chats = result.scalars().all() return ChatListResponse( @@ -1040,11 +1090,12 @@ class ChatTable: } ) + # list pinned chats async def get_pinned_chats_by_user_id( - self, user_id: str, db: Optional[AsyncSession] = None + self, user_id: str, db: AsyncSession | None = None ) -> list[ChatTitleIdResponse]: - async with get_async_db_context(db) as db: - result = await db.execute( + async with get_async_db_context(db) as session: + result = await session.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()) @@ -1063,13 +1114,14 @@ class ChatTable: 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( + async def get_archived_chats_by_user_id(self, user_id: str, db: AsyncSession | None = None) -> list[ChatModel]: + async with get_async_db_context(db) as session: + result = await session.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()] + # search user conversations async def get_chats_by_user_id_and_search_text( self, user_id: str, @@ -1077,7 +1129,7 @@ class ChatTable: include_archived: bool = False, skip: int = 0, limit: int = 60, - db: Optional[AsyncSession] = None, + db: AsyncSession | None = None, ) -> list[ChatModel]: """ Filters chats based on a search query using Python, allowing pagination using skip and limit. @@ -1135,7 +1187,7 @@ class ChatTable: search_text = ' '.join(search_text_words) - async with get_async_db_context(db) as db: + async with get_async_db_context(db) as session: stmt = select(Chat).filter(Chat.user_id == user_id) if is_archived is not None: @@ -1158,7 +1210,7 @@ class ChatTable: stmt = stmt.order_by(Chat.updated_at.desc(), Chat.id) # Check if the database dialect is either 'sqlite' or 'postgresql' - bind = await db.connection() + bind = await session.connection() dialect_name = bind.dialect.name if dialect_name == 'sqlite': # SQLite case: using JSON1 extension for JSON searching @@ -1256,7 +1308,7 @@ class ChatTable: # Perform pagination at the SQL level stmt = stmt.offset(skip).limit(limit) - result = await db.execute(stmt) + result = await session.execute(stmt) all_chats = result.scalars().all() log.info(f'The number of chats: {len(all_chats)}') @@ -1270,9 +1322,9 @@ class ChatTable: user_id: str, skip: int = 0, limit: int = 60, - db: Optional[AsyncSession] = None, + db: AsyncSession | None = None, ) -> list[ChatTitleIdResponse]: - async with get_async_db_context(db) as db: + async with get_async_db_context(db) as session: 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) @@ -1286,7 +1338,7 @@ class ChatTable: if limit: stmt = stmt.limit(limit) - result = await db.execute(stmt) + result = await session.execute(stmt) all_chats = result.all() return [ ChatTitleIdResponse.model_validate( @@ -1302,9 +1354,9 @@ class ChatTable: ] async def get_chats_by_folder_ids_and_user_id( - self, folder_ids: list[str], user_id: str, db: Optional[AsyncSession] = None + self, folder_ids: list[str], user_id: str, db: AsyncSession | None = None ) -> list[ChatModel]: - async with get_async_db_context(db) as db: + async with get_async_db_context(db) as session: stmt = ( select(Chat) .filter(Chat.folder_id.in_(folder_ids), Chat.user_id == user_id) @@ -1313,34 +1365,34 @@ class ChatTable: .order_by(Chat.updated_at.desc()) ) - result = await db.execute(stmt) + result = await session.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]: + self, id: str, user_id: str, folder_id: str, db: AsyncSession | None = None + ) -> ChatModel | None: try: - async with get_async_db_context(db) as db: - chat = await db.get(Chat, id) + async with get_async_db_context(db) as session: + chat = await session.get(Chat, id) chat.folder_id = folder_id chat.updated_at = int(time.time()) chat.pinned = False - await db.commit() - await db.refresh(chat) + await session.commit() + await session.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 + self, id: str, user_id: str, db: AsyncSession | None = None ) -> list[TagModel]: - async with get_async_db_context(db) as db: + async with get_async_db_context(db) as session: stmt = select(Chat.meta).where(Chat.id == id) - result = await db.execute(stmt) + result = await session.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) + return await Tags.get_tags_by_ids_and_user_id(tag_ids, user_id, db=session) async def get_chat_list_by_user_id_and_tag_name( self, @@ -1348,15 +1400,15 @@ class ChatTable: tag_name: str, skip: int = 0, limit: int = 50, - db: Optional[AsyncSession] = None, + db: AsyncSession | None = None, ) -> list[ChatTitleIdResponse]: - async with get_async_db_context(db) as db: + async with get_async_db_context(db) as session: 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() + bind = await session.connection() dialect_name = bind.dialect.name log.info(f'DB dialect name: {dialect_name}') if dialect_name == 'sqlite': @@ -1377,7 +1429,7 @@ class ChatTable: if limit: stmt = stmt.limit(limit) - result = await db.execute(stmt) + result = await session.execute(stmt) all_chats = result.all() return [ ChatTitleIdResponse.model_validate( @@ -1393,32 +1445,32 @@ class ChatTable: ] 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]: + self, id: str, user_id: str, tag_name: str, db: AsyncSession | None = None + ) -> ChatModel | None: 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) + async with get_async_db_context(db) as session: + chat = await session.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) + await session.commit() + await session.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 + self, tag_name: str, user_id: str, db: AsyncSession | None = None ) -> int: - async with get_async_db_context(db) as db: + async with get_async_db_context(db) as session: stmt = select(func.count(Chat.id)).filter_by(user_id=user_id, archived=False) tag_id = tag_name.replace(' ', '_').lower() - bind = await db.connection() + bind = await session.connection() dialect_name = bind.dialect.name if dialect_name == 'sqlite': stmt = stmt.filter( @@ -1431,7 +1483,7 @@ class ChatTable: else: raise NotImplementedError(f'Unsupported dialect: {dialect_name}') - result = await db.execute(stmt) + result = await session.execute(stmt) return result.scalar() async def delete_orphan_tags_for_user( @@ -1439,7 +1491,7 @@ class ChatTable: tag_ids: list[str], user_id: str, threshold: int = 0, - db: Optional[AsyncSession] = None, + db: AsyncSession | None = 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 @@ -1451,30 +1503,30 @@ class ChatTable: """ if not tag_ids: return - async with get_async_db_context(db) as db: + async with get_async_db_context(db) as session: orphans = [] for tag_id in tag_ids: - count = await self.count_chats_by_tag_name_and_user_id(tag_id, user_id, db=db) + count = await self.count_chats_by_tag_name_and_user_id(tag_id, user_id, db=session) if count <= threshold: orphans.append(tag_id) - await Tags.delete_tags_by_ids_and_user_id(orphans, user_id, db=db) + await Tags.delete_tags_by_ids_and_user_id(orphans, user_id, db=session) async def count_chats_by_folder_id_and_user_id( - self, folder_id: str, user_id: str, db: Optional[AsyncSession] = None + self, folder_id: str, user_id: str, db: AsyncSession | None = 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)) + async with get_async_db_context(db) as session: + result = await session.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 + self, id: str, user_id: str, tag_name: str, db: AsyncSession | None = None ) -> bool: try: - async with get_async_db_context(db) as db: - chat = await db.get(Chat, id) + async with get_async_db_context(db) as session: + chat = await session.get(Chat, id) tags = chat.meta.get('tags', []) tag_id = tag_name.replace(' ', '_').lower() @@ -1483,82 +1535,68 @@ class ChatTable: **chat.meta, 'tags': list(set(tags)), } - await db.commit() + await session.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: + async def delete_chat_by_id(self, id: str, db: AsyncSession | None = 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() + async with get_async_db_context(db) as session: + await session.execute(update(AutomationRun).filter_by(chat_id=id).values(chat_id=None)) + await session.execute(delete(ChatMessage).filter_by(chat_id=id)) + await session.execute(delete(Chat).filter_by(id=id)) + await session.commit() - return True + return True and await self.delete_shared_chat_by_chat_id(id, db=session) except Exception: return False - async def delete_chat_by_id(self, id: str, db: Optional[AsyncSession] = None) -> bool: + async def delete_chat_by_id_and_user_id(self, id: str, user_id: str, db: AsyncSession | None = 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() + async with get_async_db_context(db) as session: + await session.execute(update(AutomationRun).filter_by(chat_id=id).values(chat_id=None)) + await session.execute(delete(ChatMessage).filter_by(chat_id=id)) + await session.execute(delete(Chat).filter_by(id=id, user_id=user_id)) + await session.commit() - return True and await self.delete_shared_chat_by_chat_id(id, db=db) + return True and await self.delete_shared_chat_by_chat_id(id, db=session) except Exception: return False - async def delete_chat_by_id_and_user_id(self, id: str, user_id: str, db: Optional[AsyncSession] = None) -> bool: + async def delete_chats_by_user_id(self, user_id: str, db: AsyncSession | None = 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) + async with get_async_db_context(db) as session: + await self.delete_shared_chats_by_user_id(user_id, db=session) chat_id_subquery = select(Chat.id).filter_by(user_id=user_id).scalar_subquery() - await db.execute( + await session.execute( update(AutomationRun) .filter(AutomationRun.chat_id.in_(select(Chat.id).filter_by(user_id=user_id))) .values(chat_id=None) ) - await db.execute( + await session.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() + await session.execute(delete(Chat).filter_by(user_id=user_id)) + await session.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 + self, user_id: str, folder_id: str, db: AsyncSession | None = None ) -> bool: try: - async with get_async_db_context(db) as db: + async with get_async_db_context(db) as session: chat_ids_stmt = select(Chat.id).filter_by(user_id=user_id, folder_id=folder_id) - await db.execute( + await session.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() + await session.execute(delete(ChatMessage).filter(ChatMessage.chat_id.in_(chat_ids_stmt))) + await session.execute(delete(Chat).filter_by(user_id=user_id, folder_id=folder_id)) + await session.commit() return True except Exception: @@ -1568,32 +1606,33 @@ class ChatTable: self, user_id: str, folder_id: str, - new_folder_id: Optional[str], - db: Optional[AsyncSession] = None, + new_folder_id: str | None, + db: AsyncSession | None = None, ) -> bool: try: - async with get_async_db_context(db) as db: - await db.execute( + async with get_async_db_context(db) as session: + await session.execute( update(Chat).filter_by(user_id=user_id, folder_id=folder_id).values(folder_id=new_folder_id) ) - await db.commit() + await session.commit() return True except Exception: return False - async def delete_shared_chats_by_user_id(self, user_id: str, db: Optional[AsyncSession] = None) -> bool: + async def delete_shared_chats_by_user_id(self, user_id: str, db: AsyncSession | None = None) -> bool: """Delete all shared chat snapshots created by a user.""" - from open_webui.models.shared_chats import SharedChats, SharedChat as SharedChatTable + from open_webui.models.shared_chats import SharedChat as SharedChatTable + from open_webui.models.shared_chats import SharedChats try: - async with get_async_db_context(db) as db: + async with get_async_db_context(db) as session: # Delete shared_chat rows for this user's chats - await db.execute(delete(SharedChatTable).filter_by(user_id=user_id)) + await session.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() + await session.execute(update(Chat).filter_by(user_id=user_id).values(share_id=None)) + await session.commit() return True except Exception: @@ -1605,8 +1644,8 @@ class ChatTable: message_id: str, file_ids: list[str], user_id: str, - db: Optional[AsyncSession] = None, - ) -> Optional[list[ChatFileModel]]: + db: AsyncSession | None = None, + ) -> list[ChatFileModel | None]: if not file_ids: return None @@ -1618,8 +1657,29 @@ class ChatTable: if not file_ids: return None + # Only link files the caller can read; blocks forging a chat_file row to another user's file. + from open_webui.models.files import Files + from open_webui.models.users import Users + from open_webui.utils.access_control.files import has_access_to_file + + user = await Users.get_user_by_id(user_id, db=db) + accessible_file_ids = [] + for file_id in file_ids: + file = await Files.get_file_by_id(file_id, db=db) + if not file: + continue + if ( + file.user_id == user_id + or (user and user.role == 'admin') + or (user and await has_access_to_file(file_id, 'read', user, db=db)) + ): + accessible_file_ids.append(file_id) + file_ids = accessible_file_ids + if not file_ids: + return None + try: - async with get_async_db_context(db) as db: + async with get_async_db_context(db) as session: now = int(time.time()) chat_files = [ @@ -1637,64 +1697,64 @@ class ChatTable: results = [ChatFile(**chat_file.model_dump()) for chat_file in chat_files] - db.add_all(results) - await db.commit() + session.add_all(results) + await session.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 + self, chat_id: str, message_id: str, db: AsyncSession | None = None ) -> list[ChatFileModel]: - async with get_async_db_context(db) as db: - result = await db.execute( + async with get_async_db_context(db) as session: + result = await session.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: + async def delete_chat_file(self, chat_id: str, file_id: str, db: AsyncSession | None = 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() + async with get_async_db_context(db) as session: + await session.execute(delete(ChatFile).filter_by(chat_id=chat_id, file_id=file_id)) + await session.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]: + async def get_shared_chat_ids_by_file_id(self, file_id: str, db: AsyncSession | None = 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( + async with get_async_db_context(db) as session: + result = await session.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]: + async def update_chat_tasks_by_id(self, id: str, tasks: list[dict]) -> ChatModel | None: """Update the tasks list on a chat.""" try: - async with get_async_db_context() as db: - chat = await db.get(Chat, id) + async with get_async_db_context() as session: + chat = await session.get(Chat, id) if chat is None: return None chat.tasks = tasks - await db.commit() - await db.refresh(chat) + await session.commit() + await session.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)) + async with get_async_db_context() as session: + result = await session.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() +Chats = ChatTable() # singleton chats repository diff --git a/backend/open_webui/models/feedbacks.py b/backend/open_webui/models/feedbacks.py index d8ae4dc9b1..d288980501 100644 --- a/backend/open_webui/models/feedbacks.py +++ b/backend/open_webui/models/feedbacks.py @@ -3,13 +3,11 @@ 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 +from sqlalchemy import JSON, BigInteger, Boolean, Column, Text, delete, func, select +from sqlalchemy.ext.asyncio import AsyncSession log = logging.getLogger(__name__) @@ -319,8 +317,8 @@ class FeedbackTable: 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 + from datetime import datetime, timedelta async with get_async_db_context(db) as db: if days == 0: @@ -382,10 +380,39 @@ class FeedbackTable: 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 def get_feedbacks_by_user_id( + self, + user_id: str, + skip: int = 0, + limit: int = 30, + db: Optional[AsyncSession] = None, + ) -> FeedbackListResponse: 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()] + stmt = ( + select(Feedback, User) + .join(User, Feedback.user_id == User.id) + .filter(Feedback.user_id == user_id) + .order_by(Feedback.updated_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() + + 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 update_feedback_by_id( self, id: str, form_data: FeedbackForm, db: Optional[AsyncSession] = None diff --git a/backend/open_webui/models/files.py b/backend/open_webui/models/files.py index cfdcfbc2d9..7fcc62558b 100644 --- a/backend/open_webui/models/files.py +++ b/backend/open_webui/models/files.py @@ -1,36 +1,33 @@ +"""File upload models, forms, and database operations.""" + +from __future__ import annotations + import logging import time -from typing import Optional -from sqlalchemy import select, delete, func -from sqlalchemy.ext.asyncio import AsyncSession +# local imports 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 +from sqlalchemy import JSON, BigInteger, Column, String, Text, delete, func, select +from sqlalchemy.ext.asyncio import AsyncSession 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): +class File(Base): # uploaded file record __tablename__ = 'file' id = Column(String, primary_key=True, unique=True) - user_id = Column(String) + user_id = Column(String, index=True) # owner user id hash = Column(Text, nullable=True) - filename = Column(Text) + filename = Column(Text) # original upload filename path = Column(Text, nullable=True) data = Column(JSON, nullable=True) meta = Column(JSON, nullable=True) - created_at = Column(BigInteger) + created_at = Column(BigInteger, index=True) # upload timestamp updated_at = Column(BigInteger) @@ -39,27 +36,23 @@ class FileModel(BaseModel): id: str user_id: str - hash: Optional[str] = None + hash: str | None = None filename: str - path: Optional[str] = None + path: str | None = None - data: Optional[dict] = None - meta: Optional[dict] = None + data: dict | None = None + meta: dict | None = None - created_at: Optional[int] # timestamp in epoch - updated_at: Optional[int] # timestamp in epoch - - -#################### -# Forms -#################### + created_at: int | None # timestamp in epoch + updated_at: int | None # timestamp in epoch +# --- metadata structures --- class FileMeta(BaseModel): - name: Optional[str] = None - content_type: Optional[str] = None - size: Optional[int] = None + name: str | None = None + content_type: str | None = None + size: int | None = None model_config = ConfigDict(extra='allow') @@ -84,22 +77,22 @@ class FileMeta(BaseModel): class FileModelResponse(BaseModel): id: str user_id: str - hash: Optional[str] = None + hash: str | None = None filename: str - data: Optional[dict] = None - meta: Optional[FileMeta] = None + data: dict | None = None + meta: FileMeta | None = None created_at: int # timestamp in epoch - updated_at: Optional[int] = None # timestamp in epoch, optional for legacy files + updated_at: int | None = 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 + hash: str | None = None + meta: dict | None = None created_at: int # timestamp in epoch updated_at: int # timestamp in epoch @@ -111,7 +104,7 @@ class FileListResponse(BaseModel): class FileForm(BaseModel): id: str - hash: Optional[str] = None + hash: str | None = None filename: str path: str data: dict = {} @@ -119,15 +112,15 @@ class FileForm(BaseModel): class FileUpdateForm(BaseModel): - hash: Optional[str] = None - data: Optional[dict] = None - meta: Optional[dict] = None + hash: str | None = None + data: dict | None = None + meta: dict | None = None class FilesTable: async def insert_new_file( - self, user_id: str, form_data: FileForm, db: Optional[AsyncSession] = None - ) -> Optional[FileModel]: + self, user_id: str, form_data: FileForm, db: AsyncSession | None = None + ) -> FileModel | None: async with get_async_db_context(db) as db: file_data = form_data.model_dump() @@ -156,22 +149,26 @@ class FilesTable: return None except Exception as e: log.exception(f'Error inserting a new file: {e}') - return None + return None # insertion failed - async def get_file_by_id(self, id: str, db: Optional[AsyncSession] = None) -> Optional[FileModel]: + async def get_file_by_id( + self, + id: str, + db: AsyncSession | None = None, + ) -> FileModel | None: + """Look up a file by its primary key.""" 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: + file = await db.get(File, id) + if not file: return None + return FileModel.model_validate(file) 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]: + self, id: str, user_id: str, db: AsyncSession | None = None + ) -> FileModel | None: async with get_async_db_context(db) as db: try: result = await db.execute(select(File).filter_by(id=id, user_id=user_id)) @@ -183,9 +180,7 @@ class FilesTable: except Exception: return None - async def get_file_metadata_by_id( - self, id: str, db: Optional[AsyncSession] = None - ) -> Optional[FileMetadataResponse]: + async def get_file_metadata_by_id(self, id: str, db: AsyncSession | None = None) -> FileMetadataResponse | None: async with get_async_db_context(db) as db: try: file = await db.get(File, id) @@ -201,12 +196,12 @@ class FilesTable: except Exception: return None - async def get_files(self, db: Optional[AsyncSession] = None) -> list[FileModel]: + async def get_files(self, db: AsyncSession | None = 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: + async def check_access_by_user_id(self, id, user_id, permission='write', db: AsyncSession | None = None) -> bool: file = await self.get_file_by_id(id, db=db) if not file: return False @@ -215,13 +210,13 @@ class FilesTable: # 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 def get_files_by_ids(self, ids: list[str], db: AsyncSession | None = 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 + self, ids: list[str], db: AsyncSession | None = None ) -> list[FileMetadataResponse]: async with get_async_db_context(db) as db: result = await db.execute( @@ -240,17 +235,17 @@ class FilesTable: for row in result.all() ] - async def get_files_by_user_id(self, user_id: str, db: Optional[AsyncSession] = None) -> list[FileModel]: + async def get_files_by_user_id(self, user_id: str, db: AsyncSession | None = 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, + user_id: str | None = None, skip: int = 0, limit: int = 50, - db: Optional[AsyncSession] = None, + db: AsyncSession | None = None, ) -> 'FileListResponse': async with get_async_db_context(db) as db: stmt = select(File) @@ -290,11 +285,11 @@ class FilesTable: async def search_files( self, - user_id: Optional[str] = None, + user_id: str | None = None, filename: str = '*', skip: int = 0, limit: int = 100, - db: Optional[AsyncSession] = None, + db: AsyncSession | None = None, ) -> list[FileModel]: """ Search files with glob pattern matching, optional user filter, and pagination. @@ -323,8 +318,8 @@ class FilesTable: 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]: + self, id: str, form_data: FileUpdateForm, db: AsyncSession | None = None + ) -> FileModel | None: async with get_async_db_context(db) as db: try: result = await db.execute(select(File).filter_by(id=id)) @@ -347,8 +342,8 @@ class FilesTable: return None async def update_file_hash_by_id( - self, id: str, hash: Optional[str], db: Optional[AsyncSession] = None - ) -> Optional[FileModel]: + self, id: str, hash: str | None, db: AsyncSession | None = None + ) -> FileModel | None: async with get_async_db_context(db) as db: try: result = await db.execute(select(File).filter_by(id=id)) @@ -361,9 +356,7 @@ class FilesTable: except Exception: return None - async def update_file_data_by_id( - self, id: str, data: dict, db: Optional[AsyncSession] = None - ) -> Optional[FileModel]: + async def update_file_data_by_id(self, id: str, data: dict, db: AsyncSession | None = None) -> FileModel | None: async with get_async_db_context(db) as db: try: result = await db.execute(select(File).filter_by(id=id)) @@ -375,9 +368,7 @@ class FilesTable: 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 def update_file_metadata_by_id(self, id: str, meta: dict, db: AsyncSession | None = None) -> FileModel | None: async with get_async_db_context(db) as db: try: result = await db.execute(select(File).filter_by(id=id)) @@ -389,7 +380,58 @@ class FilesTable: except Exception: return None - async def delete_file_by_id(self, id: str, db: Optional[AsyncSession] = None) -> bool: + async def update_file_name_by_id(self, id: str, name: str, db: AsyncSession | None = None) -> FileModel | None: + 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.filename = name + file.meta = {**(file.meta if file.meta else {}), 'name': name} + file.updated_at = int(time.time()) + await db.commit() + return FileModel.model_validate(file) + except Exception: + return None + + async def get_pending_files_for_knowledge( + self, knowledge_id: str, db: AsyncSession | None = None + ) -> list[FileModelResponse]: + """Return files still being processed for this knowledge base. + + These are files uploaded with ``meta.data.knowledge_id`` set, whose + ``data.status`` is still ``pending`` or ``processing``, and which + have not yet been added to the ``knowledge_file`` join table. + + The JSON subscript syntax (``Column['key']['subkey'].as_string()``) + is supported by both SQLite (``json_extract``) and PostgreSQL + (``->>``/``->``). + """ + async with get_async_db_context(db) as db: + try: + # Lazy import to avoid circular dependency + from open_webui.models.knowledge import KnowledgeFile + + # Subquery: file IDs already linked to this knowledge base + linked_ids = ( + select(KnowledgeFile.file_id).filter(KnowledgeFile.knowledge_id == knowledge_id).correlate(None) + ) + + stmt = ( + select(File) + .filter( + File.meta['data']['knowledge_id'].as_string() == knowledge_id, + File.data['status'].as_string().in_(['pending', 'processing']), + File.id.notin_(linked_ids), + ) + .order_by(File.created_at.desc()) + ) + result = await db.execute(stmt) + return [FileModelResponse.model_validate(f, from_attributes=True) for f in result.scalars().all()] + except Exception as e: + log.warning(f'Error fetching pending files for knowledge {knowledge_id}: {e}') + return [] + + async def delete_file_by_id(self, id: str, db: AsyncSession | None = None) -> bool: async with get_async_db_context(db) as db: try: await db.execute(delete(File).filter_by(id=id)) @@ -399,7 +441,7 @@ class FilesTable: except Exception: return False - async def delete_all_files(self, db: Optional[AsyncSession] = None) -> bool: + async def delete_all_files(self, db: AsyncSession | None = None) -> bool: async with get_async_db_context(db) as db: try: await db.execute(delete(File)) @@ -410,4 +452,4 @@ class FilesTable: return False -Files = FilesTable() +Files = FilesTable() # singleton files repository diff --git a/backend/open_webui/models/folders.py b/backend/open_webui/models/folders.py index c553239482..1688b8bd46 100644 --- a/backend/open_webui/models/folders.py +++ b/backend/open_webui/models/folders.py @@ -1,15 +1,13 @@ import logging +import re 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 +from pydantic import BaseModel, ConfigDict +from sqlalchemy import JSON, BigInteger, Boolean, Column, Text, delete, func, select +from sqlalchemy.ext.asyncio import AsyncSession log = logging.getLogger(__name__) diff --git a/backend/open_webui/models/functions.py b/backend/open_webui/models/functions.py index ddac317863..c419dd3f93 100644 --- a/backend/open_webui/models/functions.py +++ b/backend/open_webui/models/functions.py @@ -1,44 +1,41 @@ +"""Function (filter/action/pipe) models, forms, and database operations.""" + +from __future__ import annotations + import logging import time -from typing import Optional -from sqlalchemy import select, delete, update -from sqlalchemy.ext.asyncio import AsyncSession +# local imports from open_webui.internal.db import Base, JSONField, get_async_db_context -from open_webui.models.users import Users, UserModel, UserResponse +from open_webui.models.users import UserModel, UserResponse, Users from pydantic import BaseModel, ConfigDict -from sqlalchemy import BigInteger, Boolean, Column, String, Text, Index +from sqlalchemy import BigInteger, Boolean, Column, Index, String, Text, delete, select, update +from sqlalchemy.ext.asyncio import AsyncSession 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): +class Function(Base): # database table mapping __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) + user_id = Column(String, index=True) # creator user id + name = Column(Text, nullable=False) # function identifier + type = Column(Text, nullable=False) # function type (pipe, filter, etc.) + content = Column(Text, nullable=True) # Python source code + meta = Column(JSONField, nullable=True) # function metadata + valves = Column(JSONField, nullable=True) # function configuration valves + is_active = Column(Boolean, default=False) # function activation status + is_global = Column(Boolean) # if True, applied to every chat automatically + updated_at = Column(BigInteger) # epoch seconds + created_at = Column(BigInteger) # epoch seconds - __table_args__ = (Index('is_global_idx', 'is_global'),) + __table_args__ = (Index('is_global_idx', 'is_global'),) # speed up global-function lookups class FunctionMeta(BaseModel): - description: Optional[str] = None - manifest: Optional[dict] = {} + description: str | None = None + manifest: dict | None = {} model_config = ConfigDict(extra='allow') @@ -54,9 +51,10 @@ class FunctionModel(BaseModel): updated_at: int # timestamp in epoch created_at: int # timestamp in epoch - model_config = ConfigDict(from_attributes=True) + model_config = ConfigDict(from_attributes=True) # allows ORM model binding +# --- form / schema definitions --- class FunctionWithValvesModel(BaseModel): id: str user_id: str @@ -64,7 +62,7 @@ class FunctionWithValvesModel(BaseModel): type: str content: str meta: FunctionMeta - valves: Optional[dict] = None + valves: dict | None = None is_active: bool = False is_global: bool = False updated_at: int # timestamp in epoch @@ -93,7 +91,7 @@ class FunctionResponse(BaseModel): class FunctionUserResponse(FunctionResponse): - user: Optional[UserResponse] = None + user: UserResponse | None = None class FunctionForm(BaseModel): @@ -104,7 +102,7 @@ class FunctionForm(BaseModel): class FunctionValves(BaseModel): - valves: Optional[dict] = None + valves: dict | None = None class FunctionsTable: @@ -113,8 +111,8 @@ class FunctionsTable: user_id: str, type: str, form_data: FunctionForm, - db: Optional[AsyncSession] = None, - ) -> Optional[FunctionModel]: + db: AsyncSession | None = None, + ) -> FunctionModel | None: function = FunctionModel( **{ **form_data.model_dump(), @@ -143,7 +141,7 @@ class FunctionsTable: self, user_id: str, functions: list[FunctionWithValvesModel], - db: Optional[AsyncSession] = None, + db: AsyncSession | None = None, ) -> list[FunctionWithValvesModel]: # Synchronize functions for a user by updating existing ones, inserting new ones, and removing those that are no longer present. try: @@ -191,7 +189,7 @@ class FunctionsTable: 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]: + async def get_function_by_id(self, id: str, db: AsyncSession | None = None) -> FunctionModel | None: try: async with get_async_db_context(db) as db: function = await db.get(Function, id) @@ -199,7 +197,7 @@ class FunctionsTable: except Exception: return None - async def get_functions_by_ids(self, ids: list[str], db: Optional[AsyncSession] = None) -> list[FunctionModel]: + async def get_functions_by_ids(self, ids: list[str], db: AsyncSession | None = 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). @@ -218,7 +216,7 @@ class FunctionsTable: return [] async def get_functions( - self, active_only=False, include_valves=False, db: Optional[AsyncSession] = None + self, active_only=False, include_valves=False, db: AsyncSession | None = None ) -> list[FunctionModel | FunctionWithValvesModel]: async with get_async_db_context(db) as db: if active_only: @@ -233,7 +231,7 @@ class FunctionsTable: else: return [FunctionModel.model_validate(function) for function in functions] - async def get_function_list(self, db: Optional[AsyncSession] = None) -> list[FunctionUserResponse]: + async def get_function_list(self, db: AsyncSession | None = 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() @@ -262,7 +260,7 @@ class FunctionsTable: ] async def get_functions_by_type( - self, type: str, active_only=False, db: Optional[AsyncSession] = None + self, type: str, active_only=False, db: AsyncSession | None = None ) -> list[FunctionModel]: async with get_async_db_context(db) as db: if active_only: @@ -271,17 +269,17 @@ class FunctionsTable: 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 def get_global_filter_functions(self, db: AsyncSession | None = 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 def get_global_action_functions(self, db: AsyncSession | None = 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 def get_function_valves_by_id(self, id: str, db: AsyncSession | None = None) -> dict | None: async with get_async_db_context(db) as db: try: function = await db.get(Function, id) @@ -290,7 +288,7 @@ class FunctionsTable: 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]: + async def get_function_valves_by_ids(self, ids: list[str], db: AsyncSession | None = None) -> dict[str, dict]: """ Batch fetch valves for multiple functions in a single query. Returns a dict mapping function_id -> valves dict. @@ -308,8 +306,8 @@ class FunctionsTable: return {} async def update_function_valves_by_id( - self, id: str, valves: dict, db: Optional[AsyncSession] = None - ) -> Optional[FunctionValves]: + self, id: str, valves: dict, db: AsyncSession | None = None + ) -> FunctionValves | None: async with get_async_db_context(db) as db: try: function = await db.get(Function, id) @@ -322,8 +320,8 @@ class FunctionsTable: return None async def update_function_metadata_by_id( - self, id: str, metadata: dict, db: Optional[AsyncSession] = None - ) -> Optional[FunctionModel]: + self, id: str, metadata: dict, db: AsyncSession | None = None + ) -> FunctionModel | None: async with get_async_db_context(db) as db: try: function = await db.get(Function, id) @@ -345,8 +343,8 @@ class FunctionsTable: return None async def get_user_valves_by_id_and_user_id( - self, id: str, user_id: str, db: Optional[AsyncSession] = None - ) -> Optional[dict]: + self, id: str, user_id: str, db: AsyncSession | None = None + ) -> dict | None: try: user = await Users.get_user_by_id(user_id, db=db) user_settings = user.settings.model_dump() if user.settings else {} @@ -363,8 +361,8 @@ class FunctionsTable: 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]: + self, id: str, user_id: str, valves: dict, db: AsyncSession | None = None + ) -> dict | None: try: user = await Users.get_user_by_id(user_id, db=db) user_settings = user.settings.model_dump() if user.settings else {} @@ -386,8 +384,8 @@ class FunctionsTable: return None async def update_function_by_id( - self, id: str, updated: dict, db: Optional[AsyncSession] = None - ) -> Optional[FunctionModel]: + self, id: str, updated: dict, db: AsyncSession | None = None + ) -> FunctionModel | None: async with get_async_db_context(db) as db: try: await db.execute( @@ -404,7 +402,7 @@ class FunctionsTable: except Exception: return None - async def deactivate_all_functions(self, db: Optional[AsyncSession] = None) -> Optional[bool]: + async def deactivate_all_functions(self, db: AsyncSession | None = None) -> bool | None: async with get_async_db_context(db) as db: try: await db.execute( @@ -418,7 +416,7 @@ class FunctionsTable: except Exception: return None - async def delete_function_by_id(self, id: str, db: Optional[AsyncSession] = None) -> bool: + async def delete_function_by_id(self, id: str, db: AsyncSession | None = None) -> bool: async with get_async_db_context(db) as db: try: await db.execute(delete(Function).filter_by(id=id)) @@ -429,4 +427,4 @@ class FunctionsTable: return False -Functions = FunctionsTable() +Functions = FunctionsTable() # singleton functions engine diff --git a/backend/open_webui/models/groups.py b/backend/open_webui/models/groups.py index bc199fac5b..66785794a7 100644 --- a/backend/open_webui/models/groups.py +++ b/backend/open_webui/models/groups.py @@ -1,25 +1,29 @@ import json import logging import time -from typing import Optional import uuid +from typing import Optional -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.internal.db import Base, JSONField, get_async_db_context from open_webui.models.files import FileMetadataResponse - - from pydantic import BaseModel, ConfigDict from sqlalchemy import ( + JSON, BigInteger, Column, - Text, - JSON, ForeignKey, + String, + Text, + and_, + cast, + delete, + func, + or_, + select, + update, ) +from sqlalchemy.ext.asyncio import AsyncSession log = logging.getLogger(__name__) diff --git a/backend/open_webui/models/knowledge.py b/backend/open_webui/models/knowledge.py index e08e626981..84cf4b7ae8 100644 --- a/backend/open_webui/models/knowledge.py +++ b/backend/open_webui/models/knowledge.py @@ -1,34 +1,36 @@ import json import logging import time -from typing import Optional import uuid +from typing import Optional -from sqlalchemy import select, delete, update, or_, func, cast -from sqlalchemy.ext.asyncio import AsyncSession from open_webui.internal.db import Base, JSONField, get_async_db_context - +from open_webui.models.access_grants import AccessGrantModel, AccessGrants from open_webui.models.files import ( File, - FileModel, FileMetadataResponse, + FileModel, 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 open_webui.models.users import User, UserModel, UserResponse, Users from pydantic import BaseModel, ConfigDict, Field from sqlalchemy import ( + JSON, BigInteger, Column, ForeignKey, + Index, String, Text, - JSON, UniqueConstraint, + delete, + func, + or_, + select, + update, ) +from sqlalchemy.ext.asyncio import AsyncSession log = logging.getLogger(__name__) @@ -54,6 +56,25 @@ class Knowledge(Base): updated_at = Column(BigInteger) +class KnowledgeDirectory(Base): + __tablename__ = 'knowledge_directory' + + id = Column(Text, unique=True, primary_key=True) + knowledge_id = Column(Text, ForeignKey('knowledge.id', ondelete='CASCADE'), nullable=False) + parent_id = Column(Text, ForeignKey('knowledge_directory.id', ondelete='CASCADE'), nullable=True) + name = Column(Text, 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', 'parent_id', 'name', name='uq_knowledge_directory_knowledge_parent_name'), + Index('ix_knowledge_directory_knowledge_id', 'knowledge_id'), + Index('ix_knowledge_directory_parent_id', 'parent_id'), + ) + + class KnowledgeModel(BaseModel): model_config = ConfigDict(from_attributes=True) @@ -78,18 +99,23 @@ class KnowledgeFile(Base): knowledge_id = Column(Text, ForeignKey('knowledge.id', ondelete='CASCADE'), nullable=False) file_id = Column(Text, ForeignKey('file.id', ondelete='CASCADE'), nullable=False) + directory_id = Column(Text, ForeignKey('knowledge_directory.id', ondelete='SET NULL'), nullable=True) 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'),) + __table_args__ = ( + UniqueConstraint('knowledge_id', 'file_id', name='uq_knowledge_file_knowledge_file'), + Index('ix_knowledge_file_directory_id', 'directory_id'), + ) class KnowledgeFileModel(BaseModel): id: str knowledge_id: str file_id: str + directory_id: Optional[str] = None user_id: str created_at: int # timestamp in epoch @@ -98,6 +124,24 @@ class KnowledgeFileModel(BaseModel): model_config = ConfigDict(from_attributes=True) +class KnowledgeDirectoryModel(BaseModel): + model_config = ConfigDict(from_attributes=True) + + id: str + knowledge_id: str + parent_id: Optional[str] = None + name: str + user_id: str + + created_at: int # timestamp in epoch + updated_at: int # timestamp in epoch + + +class KnowledgeDirectoryForm(BaseModel): + name: str + parent_id: Optional[str] = None + + #################### # Forms #################### @@ -130,6 +174,8 @@ class KnowledgeListResponse(BaseModel): class KnowledgeFileListResponse(BaseModel): items: list[FileUserResponse] + directories: list[KnowledgeDirectoryModel] = Field(default_factory=list) + breadcrumbs: list[KnowledgeDirectoryModel] = Field(default_factory=list) total: int @@ -314,21 +360,44 @@ class KnowledgeTable: ) # Apply filename / content search + search_filter = None if filter: q = filter.get('query') if q: - stmt = stmt.filter( - or_( + if filter.get('include_content'): + # Use ->> (as_string) instead of CAST(-> AS TEXT) + # to avoid PostgreSQL "invalid memory alloc request + # size" on large extracted-content rows (#24670). + content_text = File.data['content'].as_string() + search_filter = or_( File.filename.ilike(f'%{q}%'), - cast(File.data['content'], Text).ilike(f'%{q}%'), + content_text.ilike(f'%{q}%'), ) - ) + else: + search_filter = File.filename.ilike(f'%{q}%') + stmt = stmt.filter(search_filter) # 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())) + # Lightweight count: avoid selecting File.data and ORDER BY + count_stmt = ( + select(func.count(File.id)) + .select_from(File) + .join(KnowledgeFile, File.id == KnowledgeFile.file_id) + .join(Knowledge, KnowledgeFile.knowledge_id == Knowledge.id) + ) + count_stmt = AccessGrants.has_permission_filter( + db=db, + query=count_stmt, + DocumentModel=Knowledge, + filter=filter, + resource_type='knowledge', + permission='read', + ) + if search_filter is not None: + count_stmt = count_stmt.filter(search_filter) + count_result = await db.execute(count_stmt) total = count_result.scalar() if skip: @@ -466,18 +535,33 @@ class KnowledgeTable: .filter(KnowledgeFile.knowledge_id == knowledge_id) ) + # Filter by directory_id (None = root level) + directory_id = filter.get('directory_id') if filter else None + if directory_id: + stmt = stmt.filter(KnowledgeFile.directory_id == directory_id) + elif filter and 'directory_id' in filter: + # Explicit None = root level only + stmt = stmt.filter(KnowledgeFile.directory_id.is_(None)) + # 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}%'), - cast(File.data['content'], Text).ilike(f'%{query_key}%'), + if filter.get('include_content'): + # Use ->> (as_string) instead of CAST(-> AS TEXT) + # to avoid PostgreSQL memory allocation failures on + # large content (#24670). + content_text = File.data['content'].as_string() + stmt = stmt.filter( + or_( + File.filename.ilike(f'%{query_key}%'), + content_text.ilike(f'%{query_key}%'), + ) ) - ) + else: + stmt = stmt.filter(File.filename.ilike(f'%{query_key}%')) view_option = filter.get('view_option') if view_option == 'created': @@ -520,7 +604,19 @@ class KnowledgeTable: ) ) - return KnowledgeFileListResponse(items=files, total=total) + return KnowledgeFileListResponse( + items=files, + directories=await self.get_directories( + knowledge_id, + parent_id=filter.get('directory_id') if filter else None, + db=db, + ), + breadcrumbs=await self.get_directory_breadcrumbs( + filter.get('directory_id') if filter else None, + db=db, + ), + total=total, + ) except Exception as e: print(e) return KnowledgeFileListResponse(items=[], total=0) @@ -552,6 +648,7 @@ class KnowledgeTable: knowledge_id: str, file_id: str, user_id: str, + directory_id: Optional[str] = None, db: Optional[AsyncSession] = None, ) -> Optional[KnowledgeFileModel]: async with get_async_db_context(db) as db: @@ -560,6 +657,7 @@ class KnowledgeTable: 'id': str(uuid.uuid4()), 'knowledge_id': knowledge_id, 'file_id': file_id, + 'directory_id': directory_id, 'user_id': user_id, 'created_at': int(time.time()), 'updated_at': int(time.time()), @@ -600,11 +698,18 @@ class KnowledgeTable: except Exception: return False - async def reset_knowledge_by_id(self, id: str, db: Optional[AsyncSession] = None) -> Optional[KnowledgeModel]: + async def reset_knowledge_by_id( + self, id: str, include_directories: bool = True, 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)) + + # Delete all directories if requested + if include_directories: + await db.execute(delete(KnowledgeDirectory).filter_by(knowledge_id=id)) + await db.commit() # Update the knowledge entry's updated_at timestamp @@ -684,5 +789,277 @@ class KnowledgeTable: except Exception: return False + # ── Directory CRUD ──────────────────────────────────────────────── + + async def create_directory( + self, + knowledge_id: str, + name: str, + user_id: str, + parent_id: Optional[str] = None, + db: Optional[AsyncSession] = None, + ) -> Optional[KnowledgeDirectoryModel]: + async with get_async_db_context(db) as db: + try: + now = int(time.time()) + directory = KnowledgeDirectory( + id=str(uuid.uuid4()), + knowledge_id=knowledge_id, + parent_id=parent_id, + name=name, + user_id=user_id, + created_at=now, + updated_at=now, + ) + db.add(directory) + await db.commit() + await db.refresh(directory) + return KnowledgeDirectoryModel.model_validate(directory) + except Exception as e: + log.exception(e) + return None + + async def get_directories( + self, + knowledge_id: str, + parent_id: Optional[str] = None, + db: Optional[AsyncSession] = None, + ) -> list[KnowledgeDirectoryModel]: + """List directories at a given level (parent_id=None for root).""" + async with get_async_db_context(db) as db: + stmt = select(KnowledgeDirectory).filter(KnowledgeDirectory.knowledge_id == knowledge_id) + if parent_id: + stmt = stmt.filter(KnowledgeDirectory.parent_id == parent_id) + else: + stmt = stmt.filter(KnowledgeDirectory.parent_id.is_(None)) + + stmt = stmt.order_by(KnowledgeDirectory.name.asc()) + result = await db.execute(stmt) + return [KnowledgeDirectoryModel.model_validate(d) for d in result.scalars().all()] + + async def get_all_directories( + self, + knowledge_id: str, + db: Optional[AsyncSession] = None, + ) -> list[KnowledgeDirectoryModel]: + """Get ALL directories for a KB (no parent filter). Used for tree building.""" + async with get_async_db_context(db) as db: + stmt = ( + select(KnowledgeDirectory) + .filter(KnowledgeDirectory.knowledge_id == knowledge_id) + .order_by(KnowledgeDirectory.name.asc()) + ) + result = await db.execute(stmt) + return [KnowledgeDirectoryModel.model_validate(d) for d in result.scalars().all()] + + async def get_files_with_directory_ids( + self, + knowledge_id: str, + db: Optional[AsyncSession] = None, + ) -> list[tuple[FileModel, Optional[str]]]: + """Get all files in a KB with their directory_id from KnowledgeFile.""" + try: + async with get_async_db_context(db) as db: + result = await db.execute( + select(File, KnowledgeFile.directory_id) + .join(KnowledgeFile, File.id == KnowledgeFile.file_id) + .filter(KnowledgeFile.knowledge_id == knowledge_id) + ) + return [(FileModel.model_validate(file), dir_id) for file, dir_id in result.all()] + except Exception: + return [] + + async def get_directory_by_id( + self, directory_id: str, db: Optional[AsyncSession] = None + ) -> Optional[KnowledgeDirectoryModel]: + async with get_async_db_context(db) as db: + result = await db.execute(select(KnowledgeDirectory).filter_by(id=directory_id)) + directory = result.scalars().first() + return KnowledgeDirectoryModel.model_validate(directory) if directory else None + + async def get_directory_breadcrumbs( + self, + directory_id: Optional[str], + db: Optional[AsyncSession] = None, + ) -> list[KnowledgeDirectoryModel]: + """Walk up the parent chain to build breadcrumbs (root first).""" + if not directory_id: + return [] + + async with get_async_db_context(db) as db: + breadcrumbs = [] + current_id = directory_id + seen = set() + + while current_id and current_id not in seen: + seen.add(current_id) + result = await db.execute(select(KnowledgeDirectory).filter_by(id=current_id)) + directory = result.scalars().first() + if not directory: + break + breadcrumbs.append(KnowledgeDirectoryModel.model_validate(directory)) + current_id = directory.parent_id + + breadcrumbs.reverse() # root first + return breadcrumbs + + async def rename_directory( + self, + directory_id: str, + name: str, + db: Optional[AsyncSession] = None, + ) -> Optional[KnowledgeDirectoryModel]: + async with get_async_db_context(db) as db: + try: + await db.execute( + update(KnowledgeDirectory).filter_by(id=directory_id).values(name=name, updated_at=int(time.time())) + ) + await db.commit() + return await self.get_directory_by_id(directory_id, db=db) + except Exception as e: + log.exception(e) + return None + + async def move_directory( + self, + directory_id: str, + new_parent_id: Optional[str], + db: Optional[AsyncSession] = None, + ) -> Optional[KnowledgeDirectoryModel]: + """Move a directory to a new parent, with cycle detection.""" + async with get_async_db_context(db) as db: + try: + # Cycle detection: walk up from new_parent_id to ensure + # we don't encounter directory_id + if new_parent_id: + current = new_parent_id + seen = set() + while current and current not in seen: + if current == directory_id: + return None # Would create a cycle + seen.add(current) + result = await db.execute(select(KnowledgeDirectory.parent_id).filter_by(id=current)) + row = result.first() + current = row[0] if row else None + + await db.execute( + update(KnowledgeDirectory) + .filter_by(id=directory_id) + .values(parent_id=new_parent_id, updated_at=int(time.time())) + ) + await db.commit() + return await self.get_directory_by_id(directory_id, db=db) + except Exception as e: + log.exception(e) + return None + + async def update_directory( + self, + directory_id: str, + name: Optional[str] = None, + parent_id: Optional[str] = '__unset__', + db: Optional[AsyncSession] = None, + ) -> Optional[KnowledgeDirectoryModel]: + """Update directory name and/or parent. Pass parent_id=None to move to root.""" + # Handle move if parent_id is being changed + if parent_id != '__unset__': + result = await self.move_directory(directory_id, parent_id, db=db) + if result is None: + return None # Cycle detected or error + + if name is not None: + return await self.rename_directory(directory_id, name, db=db) + + return await self.get_directory_by_id(directory_id, db=db) + + async def delete_directory( + self, + directory_id: str, + move_files_to_parent: bool = True, + db: Optional[AsyncSession] = None, + ) -> bool: + """ + Delete a directory. + - If move_files_to_parent=True: files move to parent dir (or root) + - If move_files_to_parent=False: files are also deleted + """ + async with get_async_db_context(db) as db: + try: + # Get the directory to find its parent + result = await db.execute(select(KnowledgeDirectory).filter_by(id=directory_id)) + directory = result.scalars().first() + if not directory: + return False + + parent_id = directory.parent_id + + if move_files_to_parent: + # Move files in this directory to its parent (or root) + await db.execute( + update(KnowledgeFile).filter_by(directory_id=directory_id).values(directory_id=parent_id) + ) + # Recursively move files from all subdirectories too + await self._move_files_from_subtree(directory_id, parent_id, db=db) + else: + # Delete files in this directory and all subdirectories + await self._delete_files_in_subtree(directory_id, db=db) + + # CASCADE on parent_id will handle deleting subdirectories + await db.execute(delete(KnowledgeDirectory).filter_by(id=directory_id)) + await db.commit() + return True + except Exception as e: + log.exception(e) + return False + + async def _move_files_from_subtree( + self, + directory_id: str, + target_directory_id: Optional[str], + db: AsyncSession, + ) -> None: + """Recursively move all files from a directory subtree to the target.""" + result = await db.execute(select(KnowledgeDirectory.id).filter_by(parent_id=directory_id)) + child_ids = [row[0] for row in result.all()] + + for child_id in child_ids: + await db.execute( + update(KnowledgeFile).filter_by(directory_id=child_id).values(directory_id=target_directory_id) + ) + await self._move_files_from_subtree(child_id, target_directory_id, db=db) + + async def _delete_files_in_subtree( + self, + directory_id: str, + db: AsyncSession, + ) -> None: + """Recursively delete all files from a directory subtree.""" + await db.execute(delete(KnowledgeFile).filter_by(directory_id=directory_id)) + result = await db.execute(select(KnowledgeDirectory.id).filter_by(parent_id=directory_id)) + child_ids = [row[0] for row in result.all()] + for child_id in child_ids: + await self._delete_files_in_subtree(child_id, db=db) + + async def move_file_to_directory( + self, + knowledge_id: str, + file_id: str, + directory_id: Optional[str] = None, + db: Optional[AsyncSession] = None, + ) -> bool: + """Move a file to a different directory within the same KB.""" + async with get_async_db_context(db) as db: + try: + await db.execute( + update(KnowledgeFile) + .filter_by(knowledge_id=knowledge_id, file_id=file_id) + .values(directory_id=directory_id, updated_at=int(time.time())) + ) + await db.commit() + return True + except Exception as e: + log.exception(e) + return False + Knowledges = KnowledgeTable() diff --git a/backend/open_webui/models/memories.py b/backend/open_webui/models/memories.py index 1ec52eeb6a..2337371f12 100644 --- a/backend/open_webui/models/memories.py +++ b/backend/open_webui/models/memories.py @@ -1,43 +1,38 @@ +"""Long-term memory storage for per-user context recall.""" + +from __future__ import annotations + 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. -#################### +from sqlalchemy import BigInteger, Column, String, Text, delete, select +from sqlalchemy.ext.asyncio import AsyncSession -class Memory(Base): +class Memory(Base): # user memory store + """Stores user-created memory entries linked to a vector collection.""" + __tablename__ = 'memory' id = Column(String, primary_key=True, unique=True) user_id = Column(String, index=True) - content = Column(Text) - updated_at = Column(BigInteger) - created_at = Column(BigInteger) + content = Column(Text) # free-form text learned from conversation + updated_at = Column(BigInteger) # epoch seconds + created_at = Column(BigInteger) # epoch seconds class MemoryModel(BaseModel): + """Pydantic mirror of the Memory table row.""" + 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 -#################### + model_config = ConfigDict(from_attributes=True) # allows ORM mapping class MemoriesTable: @@ -45,36 +40,30 @@ class MemoriesTable: self, user_id: str, content: str, - db: Optional[AsyncSession] = None, - ) -> Optional[MemoryModel]: + db: AsyncSession | None = None, + ) -> MemoryModel | None: + """Persist a new memory entry and return the created model.""" 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()), - } + now = int(time.time()) + record = Memory( + id=str(uuid.uuid4()), + user_id=user_id, + content=content, + created_at=now, + updated_at=now, ) - result = Memory(**memory.model_dump()) - db.add(result) + db.add(record) await db.commit() - await db.refresh(result) - if result: - return MemoryModel.model_validate(result) - else: - return None + await db.refresh(record) + return MemoryModel.model_validate(record) if record else None async def update_memory_by_id_and_user_id( self, id: str, user_id: str, content: str, - db: Optional[AsyncSession] = None, - ) -> Optional[MemoryModel]: + db: AsyncSession | None = None, + ) -> MemoryModel | None: async with get_async_db_context(db) as db: try: memory = await db.get(Memory, id) @@ -90,7 +79,7 @@ class MemoriesTable: except Exception: return None - async def get_memories(self, db: Optional[AsyncSession] = None) -> list[MemoryModel]: + async def get_memories(self, db: AsyncSession | None = None) -> list[MemoryModel]: async with get_async_db_context(db) as db: try: result = await db.execute(select(Memory)) @@ -99,7 +88,7 @@ class MemoriesTable: except Exception: return None - async def get_memories_by_user_id(self, user_id: str, db: Optional[AsyncSession] = None) -> list[MemoryModel]: + async def get_memories_by_user_id(self, user_id: str, db: AsyncSession | None = 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)) @@ -108,7 +97,7 @@ class MemoriesTable: except Exception: return None - async def get_memory_by_id(self, id: str, db: Optional[AsyncSession] = None) -> Optional[MemoryModel]: + async def get_memory_by_id(self, id: str, db: AsyncSession | None = None) -> MemoryModel | None: async with get_async_db_context(db) as db: try: memory = await db.get(Memory, id) @@ -116,7 +105,7 @@ class MemoriesTable: except Exception: return None - async def delete_memory_by_id(self, id: str, db: Optional[AsyncSession] = None) -> bool: + async def delete_memory_by_id(self, id: str, db: AsyncSession | None = None) -> bool: async with get_async_db_context(db) as db: try: await db.execute(delete(Memory).filter_by(id=id)) @@ -127,7 +116,7 @@ class MemoriesTable: except Exception: return False - async def delete_memories_by_user_id(self, user_id: str, db: Optional[AsyncSession] = None) -> bool: + async def delete_memories_by_user_id(self, user_id: str, db: AsyncSession | None = None) -> bool: async with get_async_db_context(db) as db: try: await db.execute(delete(Memory).filter_by(user_id=user_id)) @@ -137,20 +126,18 @@ class MemoriesTable: 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 def delete_memory_by_id_and_user_id(self, id: str, user_id: str, db: AsyncSession | None = 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 + return False - # Delete the memory await db.delete(memory) await db.commit() - return True except Exception: return False -Memories = MemoriesTable() +Memories = MemoriesTable() # user memory registry diff --git a/backend/open_webui/models/messages.py b/backend/open_webui/models/messages.py index 7f33a72eff..342abed2f8 100644 --- a/backend/open_webui/models/messages.py +++ b/backend/open_webui/models/messages.py @@ -3,17 +3,13 @@ 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 open_webui.models.channels import ChannelMember, Channels +from open_webui.models.tags import Tag, TagModel, Tags +from open_webui.models.users import User, UserNameResponse, Users 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 import JSON, BigInteger, Boolean, Column, String, Text, and_, delete, func, or_, select, text +from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.sql import exists #################### diff --git a/backend/open_webui/models/models.py b/backend/open_webui/models/models.py index 79c13153ac..0fab100c46 100755 --- a/backend/open_webui/models/models.py +++ b/backend/open_webui/models/models.py @@ -1,51 +1,61 @@ +from __future__ import annotations + 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 open_webui.models.groups import Groups +from open_webui.models.users import User, UserModel, UserResponse, Users +from open_webui.utils.validate import validate_profile_image_url +from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator +from sqlalchemy import BigInteger, Boolean, Column, String, Text, cast, delete, func, or_, select, update from sqlalchemy.dialects.postgresql import JSONB -from sqlalchemy import BigInteger, Column, Text, Boolean +from sqlalchemy.ext.asyncio import AsyncSession 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. -#################### +# Track invalid profile_image_url values we've already warned about so we +# don't flood the logs on every DB read (the validator fires per-row). +_warned_profile_urls: set[str] = set() + + +# --- Models DB Schema --- -# ModelParams is a model for the data stored in the params field of the Model table class ModelParams(BaseModel): + """Parameters for model inference (temperature, top_p, etc.).""" + 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' + """Metadata for a workspace model entry (profile, description, tags, capabilities).""" - description: Optional[str] = None - """ - User-facing description of the model. - """ - - capabilities: Optional[dict] = None + profile_image_url: str | None = None + description: str | None = Field(default=None, description='User-facing description of the model.') + capabilities: dict | None = None model_config = ConfigDict(extra='allow') + @field_validator('profile_image_url', mode='before') + @classmethod + def check_profile_image_url(cls, v: str | None) -> str | None: + if v is None: + return v + try: + return validate_profile_image_url(v) + except ValueError: + if v not in _warned_profile_urls: + _warned_profile_urls.add(v) + log.warning( + 'Clearing invalid profile_image_url stored in DB (likely a legacy SVG data-URI): %.80s…', + v, + ) + return None + @model_validator(mode='before') @classmethod def normalize_tags(cls, data): @@ -63,44 +73,25 @@ class ModelMeta(BaseModel): class Model(Base): + """Workspace model entry — wraps an upstream LLM with custom params and metadata.""" + __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) + id = Column(Text, primary_key=True, unique=True) # API model identifier; overrides built-in when matching + user_id = Column(Text) # owner + base_model_id = Column(Text, nullable=True) # actual upstream model for proxied requests + name = Column(Text) # human-readable display name + params = Column(JSONField) # see ModelParams + meta = Column(JSONField) # see ModelMeta + is_active = Column(Boolean, default=True) # soft-disable toggle + updated_at = Column(BigInteger) # epoch seconds + created_at = Column(BigInteger) # epoch seconds class ModelModel(BaseModel): id: str user_id: str - base_model_id: Optional[str] = None + base_model_id: str | None = None name: str params: ModelParams @@ -112,20 +103,17 @@ class ModelModel(BaseModel): updated_at: int # timestamp in epoch created_at: int # timestamp in epoch - model_config = ConfigDict(from_attributes=True) - - -#################### -# Forms -#################### + model_config = ConfigDict( + from_attributes=True, + ) class ModelUserResponse(ModelModel): - user: Optional[UserResponse] = None + user: UserResponse | None = None class ModelAccessResponse(ModelUserResponse): - write_access: Optional[bool] = False + write_access: bool | None = False class ModelResponse(ModelModel): @@ -146,23 +134,23 @@ class ModelForm(BaseModel): model_config = ConfigDict(extra='ignore') id: str - base_model_id: Optional[str] = None + base_model_id: str | None = None name: str meta: ModelMeta params: ModelParams - access_grants: Optional[list[dict]] = None + access_grants: list[dict | None] = None is_active: bool = True class ModelsTable: - async def _get_access_grants(self, model_id: str, db: Optional[AsyncSession] = None) -> list[AccessGrantModel]: + async def _get_access_grants(self, model_id: str, db: AsyncSession | None = 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, + access_grants: list[AccessGrantModel | None] = None, + db: AsyncSession | None = None, ) -> ModelModel: model_data = ModelModel.model_validate(model).model_dump(exclude={'access_grants'}) model_data['access_grants'] = ( @@ -171,8 +159,8 @@ class ModelsTable: return ModelModel.model_validate(model_data) async def insert_new_model( - self, form_data: ModelForm, user_id: str, db: Optional[AsyncSession] = None - ) -> Optional[ModelModel]: + self, form_data: ModelForm, user_id: str, db: AsyncSession | None = None + ) -> ModelModel | None: try: async with get_async_db_context(db) as db: result = Model( @@ -196,18 +184,21 @@ class ModelsTable: 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 def get_all_models(self, db: AsyncSession | None = 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 - ] + models: list[ModelModel] = [] + for model in all_models: + try: + models.append(await self._to_model_model(model, access_grants=grants_map.get(model.id, []), db=db)) + except Exception as exc: + log.error('Skipping model %r during get_all_models due to error: %s', model.id, exc) + return models - async def get_models(self, db: Optional[AsyncSession] = None) -> list[ModelUserResponse]: + async def get_models(self, db: AsyncSession | None = 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() @@ -238,7 +229,7 @@ class ModelsTable: ) return models - async def get_base_models(self, db: Optional[AsyncSession] = None) -> list[ModelModel]: + async def get_base_models(self, db: AsyncSession | None = 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() @@ -250,7 +241,7 @@ class ModelsTable: ] async def get_models_by_user_id( - self, user_id: str, permission: str = 'write', db: Optional[AsyncSession] = None + self, user_id: str, permission: str = 'write', db: AsyncSession | None = None ) -> list[ModelUserResponse]: models = await self.get_models(db=db) user_groups = await Groups.get_groups_by_member_id(user_id, db=db) @@ -287,7 +278,7 @@ class ModelsTable: filter: dict = {}, skip: int = 0, limit: int = 30, - db: Optional[AsyncSession] = None, + db: AsyncSession | None = None, ) -> ModelListResponse: async with get_async_db_context(db) as db: stmt = select(Model, User).outerjoin(User, User.id == Model.user_id) @@ -391,7 +382,7 @@ class ModelsTable: return ModelListResponse(items=models, total=total) - async def get_model_meta_by_id(self, id: str, db: Optional[AsyncSession] = None) -> Optional[tuple[dict, int]]: + async def get_model_meta_by_id(self, id: str, db: AsyncSession | None = None) -> tuple[dict, int | None]: """Return (meta, updated_at) for a model, skipping access grant resolution.""" try: async with get_async_db_context(db) as db: @@ -404,7 +395,7 @@ class ModelsTable: self, user_id: str, is_admin: bool = False, - db: Optional[AsyncSession] = None, + db: AsyncSession | None = None, ) -> set[str]: """Extract unique tag names from model meta, querying only the meta column.""" async with get_async_db_context(db) as db: @@ -437,7 +428,7 @@ class ModelsTable: return tags_set - async def get_model_by_id(self, id: str, db: Optional[AsyncSession] = None) -> Optional[ModelModel]: + async def get_model_by_id(self, id: str, db: AsyncSession | None = None) -> ModelModel | None: try: async with get_async_db_context(db) as db: model = await db.get(Model, id) @@ -445,7 +436,7 @@ class ModelsTable: except Exception: return None - async def get_models_by_ids(self, ids: list[str], db: Optional[AsyncSession] = None) -> list[ModelModel]: + async def get_models_by_ids(self, ids: list[str], db: AsyncSession | None = None) -> list[ModelModel]: try: async with get_async_db_context(db) as db: result = await db.execute(select(Model).filter(Model.id.in_(ids))) @@ -463,7 +454,7 @@ class ModelsTable: except Exception: return [] - async def toggle_model_by_id(self, id: str, db: Optional[AsyncSession] = None) -> Optional[ModelModel]: + async def toggle_model_by_id(self, id: str, db: AsyncSession | None = None) -> ModelModel | None: async with get_async_db_context(db) as db: try: result = await db.execute(select(Model).filter_by(id=id)) @@ -480,9 +471,7 @@ class ModelsTable: except Exception: return None - async def update_model_by_id( - self, id: str, model: ModelForm, db: Optional[AsyncSession] = None - ) -> Optional[ModelModel]: + async def update_model_by_id(self, id: str, model: ModelForm, db: AsyncSession | None = None) -> ModelModel | None: try: async with get_async_db_context(db) as db: # update only the fields that are present in the model @@ -499,7 +488,7 @@ class ModelsTable: 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]: + async def update_model_updated_at_by_id(self, id: str, db: AsyncSession | None = None) -> ModelModel | None: try: async with get_async_db_context(db) as db: result = await db.execute(select(Model).filter_by(id=id)) @@ -514,7 +503,7 @@ class ModelsTable: 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: + async def delete_model_by_id(self, id: str, db: AsyncSession | None = None) -> bool: try: async with get_async_db_context(db) as db: await AccessGrants.revoke_all_access('model', id, db=db) @@ -525,7 +514,7 @@ class ModelsTable: except Exception: return False - async def delete_all_models(self, db: Optional[AsyncSession] = None) -> bool: + async def delete_all_models(self, db: AsyncSession | None = None) -> bool: try: async with get_async_db_context(db) as db: result = await db.execute(select(Model.id)) @@ -540,7 +529,7 @@ class ModelsTable: return False async def sync_models( - self, user_id: str, models: list[ModelModel], db: Optional[AsyncSession] = None + self, user_id: str, models: list[ModelModel], db: AsyncSession | None = None ) -> list[ModelModel]: try: async with get_async_db_context(db) as db: @@ -600,4 +589,4 @@ class ModelsTable: return [] -Models = ModelsTable() +Models = ModelsTable() # singleton model registry diff --git a/backend/open_webui/models/notes.py b/backend/open_webui/models/notes.py index f651d226ca..1cddd2f8fa 100644 --- a/backend/open_webui/models/notes.py +++ b/backend/open_webui/models/notes.py @@ -1,19 +1,16 @@ import json import time import uuid -from typing import Optional from functools import lru_cache +from typing import Optional -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 open_webui.models.groups import Groups +from open_webui.models.users import User, UserModel, UserResponse, Users from pydantic import BaseModel, ConfigDict, Field -from sqlalchemy import BigInteger, Column, Text, JSON, ForeignKey +from sqlalchemy import JSON, BigInteger, Boolean, Column, ForeignKey, Text, delete, func, or_, select, update +from sqlalchemy.ext.asyncio import AsyncSession #################### # Note DB Schema @@ -183,7 +180,7 @@ class NoteTable: or_( func.replace(func.replace(Note.title, '-', ''), ' ', '').ilike(f'%{word}%'), func.replace( - func.replace(cast(Note.data['content']['md'], Text), '-', ''), + func.replace(Note.data['content']['md'].as_string(), '-', ''), ' ', '', ).ilike(f'%{word}%'), diff --git a/backend/open_webui/models/oauth_sessions.py b/backend/open_webui/models/oauth_sessions.py index c43567f670..0619bd574a 100644 --- a/backend/open_webui/models/oauth_sessions.py +++ b/backend/open_webui/models/oauth_sessions.py @@ -1,20 +1,17 @@ -import time -import logging -import uuid -from typing import Optional, List import base64 import hashlib import json +import logging +import time +import uuid +from typing import List, Optional 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 open_webui.internal.db import Base, get_async_db_context from pydantic import BaseModel, ConfigDict -from sqlalchemy import BigInteger, Column, String, Text, Index +from sqlalchemy import BigInteger, Column, Index, String, Text, delete, select, update +from sqlalchemy.ext.asyncio import AsyncSession log = logging.getLogger(__name__) diff --git a/backend/open_webui/models/prompt_history.py b/backend/open_webui/models/prompt_history.py index 5d0f4a65b2..bb27657032 100644 --- a/backend/open_webui/models/prompt_history.py +++ b/backend/open_webui/models/prompt_history.py @@ -1,18 +1,16 @@ """Prompt history model for version tracking.""" +import difflib +import json 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 open_webui.models.users import UserResponse, Users from pydantic import BaseModel, ConfigDict -from sqlalchemy import BigInteger, Column, Text, JSON, Index +from sqlalchemy import JSON, BigInteger, Column, Index, Text, delete, func, select +from sqlalchemy.ext.asyncio import AsyncSession #################### # PromptHistory DB Schema @@ -153,13 +151,19 @@ class PromptHistoryTable: self, from_id: str, to_id: str, + prompt_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)) + # Bind both entries to the authorized prompt; an unbound id reads another prompt's snapshot. + result_from = await db.execute( + select(PromptHistory).filter(PromptHistory.id == from_id, PromptHistory.prompt_id == prompt_id) + ) from_entry = result_from.scalars().first() - result_to = await db.execute(select(PromptHistory).filter(PromptHistory.id == to_id)) + result_to = await db.execute( + select(PromptHistory).filter(PromptHistory.id == to_id, PromptHistory.prompt_id == prompt_id) + ) to_entry = result_to.scalars().first() if not from_entry or not to_entry: @@ -205,11 +209,13 @@ class PromptHistoryTable: async def delete_history_entry( self, history_id: str, + prompt_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)) + # Bind to the authorized prompt; an unbound id deletes another prompt's history. + result = await db.execute(select(PromptHistory).filter_by(id=history_id, prompt_id=prompt_id)) entry = result.scalars().first() if not entry: return False diff --git a/backend/open_webui/models/prompts.py b/backend/open_webui/models/prompts.py index 5a3e35d23d..23a5017acf 100644 --- a/backend/open_webui/models/prompts.py +++ b/backend/open_webui/models/prompts.py @@ -1,37 +1,37 @@ +"""Prompt template models, forms, and database operations.""" + +from __future__ import annotations + import json +import logging 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 +log = logging.getLogger(__name__) + 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 open_webui.models.groups import Groups +from open_webui.models.prompt_history import PromptHistories +from open_webui.models.users import User, UserModel, UserResponse, Users 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. -#################### +from sqlalchemy import JSON, BigInteger, Boolean, Column, String, Text, cast, delete, func, or_, select, text, update +from sqlalchemy.ext.asyncio import AsyncSession -class Prompt(Base): +class Prompt(Base): # versioned template + """Slash-command prompt with history tracking and access control.""" + __tablename__ = 'prompt' id = Column(Text, primary_key=True) command = Column(String, unique=True, index=True) - user_id = Column(String) + user_id = Column(String, index=True) # owner user id name = Column(Text) - content = Column(Text) - data = Column(JSON, nullable=True) - meta = Column(JSON, nullable=True) + content = Column(Text) # the prompt template body + data = Column(JSON, nullable=True) # structured prompt parameters + meta = Column(JSON, nullable=True) # freeform metadata (description, etc.) tags = Column(JSON, nullable=True) is_active = Column(Boolean, default=True) version_id = Column(Text, nullable=True) # Points to active history entry @@ -40,34 +40,34 @@ class Prompt(Base): class PromptModel(BaseModel): - id: Optional[str] = None + id: str | None = 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 + data: dict | None = None + meta: dict | None = None + tags: list[str | None] = None + is_active: bool | None = True + version_id: str | None = None + created_at: int | None = None + updated_at: int | None = None access_grants: list[AccessGrantModel] = Field(default_factory=list) - model_config = ConfigDict(from_attributes=True) + model_config = ConfigDict(from_attributes=True) # allows ORM model binding -#################### +# --- form / schema definitions --- # Forms #################### class PromptUserResponse(PromptModel): - user: Optional[UserResponse] = None + user: UserResponse | None = None class PromptAccessResponse(PromptUserResponse): - write_access: Optional[bool] = False + write_access: bool | None = False class PromptListResponse(BaseModel): @@ -84,24 +84,24 @@ 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 + data: dict | None = None + meta: dict | None = None + tags: list[str | None] = None + access_grants: list[dict | None] = None + version_id: str | None = None # Active version + commit_message: str | None = None # For history tracking + is_production: bool | None = 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]: + async def _get_access_grants(self, prompt_id: str, db: AsyncSession | None = 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, + access_grants: list[AccessGrantModel | None] = None, + db: AsyncSession | None = None, ) -> PromptModel: prompt_data = PromptModel.model_validate(prompt).model_dump(exclude={'access_grants'}) prompt_data['access_grants'] = ( @@ -110,106 +110,117 @@ class PromptsTable: return PromptModel.model_validate(prompt_data) async def insert_new_prompt( - self, user_id: str, form_data: PromptForm, db: Optional[AsyncSession] = None - ) -> Optional[PromptModel]: + self, user_id: str, form_data: PromptForm, db: AsyncSession | None = None + ) -> PromptModel | None: 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, - ) + async with get_async_db_context(db) as session: + try: + record = Prompt( + 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 [], + is_active=True, + created_at=now, + updated_at=now, + ) + session.add(record) + await session.commit() + await session.refresh(record) # populate generated defaults - 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) + await AccessGrants.set_access_grants( + 'prompt', + prompt_id, + form_data.access_grants, + db=session, + ) # persist sharing rules - 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: + if not record: # shouldn't happen, but guard anyway 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) + # Build the initial version snapshot. + grants = await self._get_access_grants(prompt_id, db=session) + 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': [g.model_dump() for g in grants], + } + + history_entry = await PromptHistories.create_history_entry( + prompt_id=prompt_id, + snapshot=snapshot, + user_id=user_id, + parent_id=None, + commit_message=form_data.commit_message or 'Initial version', + db=session, + ) # creates the first version entry + + # Pin the initial history entry as the production version. + if history_entry: + record.version_id = history_entry.id + await session.commit() + await session.refresh(record) # re-read version_id + + return await self._to_prompt_model(record, db=session) + except Exception as e: + log.exception('Error creating prompt: %s', e) return None - except Exception: - return None - async def get_prompt_by_command(self, command: str, db: Optional[AsyncSession] = None) -> Optional[PromptModel]: + async def get_prompt_by_id(self, prompt_id: str, db: AsyncSession | None = None) -> PromptModel | None: 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 with get_async_db_context(db) as session: + result = await session.execute( + select(Prompt).filter_by(id=prompt_id), + ) + prompt = result.scalars().first() # None when not found + if not prompt: + return None + return await self._to_prompt_model(prompt, db=session) + except Exception: # connection / integrity error + return - 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()) + async def get_prompt_by_command(self, command: str, db: AsyncSession | None = None) -> PromptModel | None: + """Look up a prompt by its unique slash-command string.""" + async with get_async_db_context(db) as session: + match = (await session.execute(select(Prompt).where(Prompt.command == command))).scalars().first() + if match is None: + return + return await self._to_prompt_model(match, db=session) + # --- context manager always returns above --- + return + + async def get_prompts(self, db: AsyncSession | None = None) -> list[PromptUserResponse]: + """Return all active prompts ordered by most recently updated.""" + async with get_async_db_context(db) as session: + active = ( + ( + await session.execute( + select(Prompt).where(Prompt.is_active.is_(True)).order_by(Prompt.updated_at.desc()) + ) + ) + .scalars() + .all() ) - 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] + user_ids = list(set(p.user_id for p in active)) + prompt_ids = [p.id for p in active] - 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) + users = await Users.get_users_by_user_ids(user_ids, db=session) if user_ids else [] + users_dict = {u.id: u for u in users} + grants_map = await AccessGrants.get_grants_by_resources('prompt', prompt_ids, db=session) prompts = [] - for prompt in all_prompts: + for prompt in active: user = users_dict.get(prompt.user_id) prompts.append( PromptUserResponse.model_validate( @@ -218,7 +229,7 @@ class PromptsTable: await self._to_prompt_model( prompt, access_grants=grants_map.get(prompt.id, []), - db=db, + db=session, ) ).model_dump(), 'user': user.model_dump() if user else None, @@ -229,10 +240,10 @@ class PromptsTable: return prompts async def get_prompts_by_user_id( - self, user_id: str, permission: str = 'write', db: Optional[AsyncSession] = None + self, user_id: str, permission: str = 'write', db: AsyncSession | None = None ) -> list[PromptUserResponse]: - async with get_async_db_context(db) as db: - user_groups = await Groups.get_groups_by_member_id(user_id, db=db) + async with get_async_db_context(db) as session: + user_groups = await Groups.get_groups_by_member_id(user_id, db=session) user_group_ids = [group.id for group in user_groups] query = select(Prompt).filter(Prompt.is_active == True).order_by(Prompt.updated_at.desc()) @@ -245,7 +256,7 @@ class PromptsTable: permission=permission, ) - result = await db.execute(query) + result = await session.execute(query) accessible_prompts = result.scalars().all() if not accessible_prompts: @@ -254,9 +265,9 @@ class PromptsTable: prompt_ids = [p.id for p in accessible_prompts] owner_ids = list({p.user_id for p in accessible_prompts}) - users = await Users.get_users_by_user_ids(owner_ids, db=db) + users = await Users.get_users_by_user_ids(owner_ids, db=session) users_dict = {u.id: u for u in users} - grants_map = await AccessGrants.get_grants_by_resources('prompt', prompt_ids, db=db) + grants_map = await AccessGrants.get_grants_by_resources('prompt', prompt_ids, db=session) results = [] for prompt in accessible_prompts: @@ -283,9 +294,9 @@ class PromptsTable: filter: dict = {}, skip: int = 0, limit: int = 30, - db: Optional[AsyncSession] = None, + db: AsyncSession | None = None, ) -> PromptListResponse: - async with get_async_db_context(db) as db: + async with get_async_db_context(db) as session: # Join with User table for user filtering and sorting query = select(Prompt, User).outerjoin(User, User.id == Prompt.user_id) @@ -320,7 +331,7 @@ class PromptsTable: tag = filter.get('tag') if tag: - bind = await db.connection() + bind = await session.connection() dialect_name = bind.dialect.name tag_lower = tag.lower() @@ -368,7 +379,7 @@ class PromptsTable: query = query.order_by(Prompt.updated_at.desc()) # Count BEFORE pagination - count_result = await db.execute(select(func.count()).select_from(query.subquery())) + count_result = await session.execute(select(func.count()).select_from(query.subquery())) total = count_result.scalar() if skip: @@ -376,11 +387,11 @@ class PromptsTable: if limit: query = query.limit(limit) - result = await db.execute(query) + result = await session.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) + grants_map = await AccessGrants.get_grants_by_resources('prompt', prompt_ids, db=session) prompts = [] for prompt, user in items: @@ -404,18 +415,20 @@ class PromptsTable: 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)) + db: AsyncSession | None = None, + ) -> PromptModel | None: + if not command: + return None + try: # database transaction + async with get_async_db_context(db) as session: + result = await session.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) + latest_history = await PromptHistories.get_latest_history_entry(prompt.id, db=session) parent_id = latest_history.id if latest_history else None - current_access_grants = await self._get_access_grants(prompt.id, db=db) + current_access_grants = await self._get_access_grants(prompt.id, db=session) # Check if content changed to decide on history creation content_changed = ( @@ -431,10 +444,10 @@ class PromptsTable: 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 AccessGrants.set_access_grants('prompt', prompt.id, form_data.access_grants, db=session) + current_access_grants = await self._get_access_grants(prompt.id, db=session) - await db.commit() + await session.commit() # Create history entry only if content changed if content_changed: @@ -459,9 +472,9 @@ class PromptsTable: # 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() + await session.commit() - return await self._to_prompt_model(prompt, db=db) + return await self._to_prompt_model(prompt, db=session) except Exception: return None @@ -470,18 +483,18 @@ class PromptsTable: prompt_id: str, form_data: PromptForm, user_id: str, - db: Optional[AsyncSession] = None, - ) -> Optional[PromptModel]: + db: AsyncSession | None = None, + ) -> PromptModel | None: try: - async with get_async_db_context(db) as db: - result = await db.execute(select(Prompt).filter_by(id=prompt_id)) + async with get_async_db_context(db) as session: + result = await session.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) + latest_history = await PromptHistories.get_latest_history_entry(prompt.id, db=session) parent_id = latest_history.id if latest_history else None - current_access_grants = await self._get_access_grants(prompt.id, db=db) + current_access_grants = await self._get_access_grants(prompt.id, db=session) # Check if content changed to decide on history creation content_changed = ( @@ -503,12 +516,12 @@ class PromptsTable: 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) + await AccessGrants.set_access_grants('prompt', prompt.id, form_data.access_grants, db=session) + current_access_grants = await self._get_access_grants(prompt.id, db=session) prompt.updated_at = int(time.time()) - await db.commit() + await session.commit() # Create history entry only if content changed if content_changed: @@ -534,9 +547,9 @@ class PromptsTable: # 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() + await session.commit() - return await self._to_prompt_model(prompt, db=db) + return await self._to_prompt_model(prompt, db=session) except Exception: return None @@ -545,13 +558,13 @@ class PromptsTable: prompt_id: str, name: str, command: str, - tags: Optional[list[str]] = None, - db: Optional[AsyncSession] = None, - ) -> Optional[PromptModel]: + tags: list[str | None] = None, + db: AsyncSession | None = None, + ) -> PromptModel | None: """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)) + async with get_async_db_context(db) as session: + result = await session.execute(select(Prompt).filter_by(id=prompt_id)) prompt = result.scalars().first() if not prompt: return None @@ -563,9 +576,9 @@ class PromptsTable: prompt.tags = tags prompt.updated_at = int(time.time()) - await db.commit() + await session.commit() - return await self._to_prompt_model(prompt, db=db) + return await self._to_prompt_model(prompt, db=session) except Exception: return None @@ -573,19 +586,20 @@ class PromptsTable: self, prompt_id: str, version_id: str, - db: Optional[AsyncSession] = None, - ) -> Optional[PromptModel]: + db: AsyncSession | None = None, + ) -> PromptModel | None: """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)) + async with get_async_db_context(db) as session: + result = await session.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) + history_entry = await PromptHistories.get_history_entry_by_id(version_id, db=session) - if not history_entry: + # Reject a version_id from another prompt; restoring it would copy a foreign snapshot in. + if not history_entry or history_entry.prompt_id != prompt_id: return None # Restore prompt content from the snapshot @@ -600,66 +614,74 @@ class PromptsTable: prompt.version_id = version_id prompt.updated_at = int(time.time()) - await db.commit() + await session.commit() - return await self._to_prompt_model(prompt, db=db) - except Exception: + return await self._to_prompt_model(prompt, db=session) + except Exception as e: # connection error + log.error(f'Failed to restore prompt version: {e}') + return None # restoration failed + + async def toggle_prompt_active( + self, + prompt_id: str, + db: AsyncSession | None = None, + ) -> PromptModel | None: + """Flip the is_active flag on a prompt.""" + if not prompt_id: 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)) + try: # activation state toggle + async with get_async_db_context(db) as session: + result = await session.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) + await session.commit() + await session.refresh(prompt) + return await self._to_prompt_model(prompt, db=session) return None except Exception: return None - async def delete_prompt_by_command(self, command: str, db: Optional[AsyncSession] = None) -> bool: + async def delete_prompt_by_command(self, command: str, db: AsyncSession | None = 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)) + async with get_async_db_context(db) as session: + result = await session.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 PromptHistories.delete_history_by_prompt_id(prompt.id, db=session) + await AccessGrants.revoke_all_access('prompt', prompt.id, db=session) - await db.delete(prompt) - await db.commit() + await session.delete(prompt) + await session.commit() return True return False except Exception: return False - async def delete_prompt_by_id(self, prompt_id: str, db: Optional[AsyncSession] = None) -> bool: + async def delete_prompt_by_id(self, prompt_id: str, db: AsyncSession | None = 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)) + async with get_async_db_context(db) as session: + result = await session.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 PromptHistories.delete_history_by_prompt_id(prompt.id, db=session) + await AccessGrants.revoke_all_access('prompt', prompt.id, db=session) - await db.delete(prompt) - await db.commit() + await session.delete(prompt) + await session.commit() return True return False - except Exception: - return False + except Exception as err: + log.error(f'Failed to delete prompt: {err}') + return False # deletion failed - async def get_tags(self, db: Optional[AsyncSession] = None) -> list[str]: + async def get_tags(self, db: AsyncSession | None = None) -> list[str]: try: - async with get_async_db_context(db) as db: - result = await db.execute(select(Prompt.tags).filter(Prompt.is_active == True)) + async with get_async_db_context(db) as session: + result = await session.execute(select(Prompt.tags).filter(Prompt.is_active == True)) tags = set() for (tag_list,) in result.all(): if tag_list: @@ -670,10 +692,10 @@ class PromptsTable: except Exception: return [] - async def get_tags_by_user_id(self, user_id: str, db: Optional[AsyncSession] = None) -> list[str]: + async def get_tags_by_user_id(self, user_id: str, db: AsyncSession | None = None) -> list[str]: try: - async with get_async_db_context(db) as db: - user_groups = await Groups.get_groups_by_member_id(user_id, db=db) + async with get_async_db_context(db) as session: + user_groups = await Groups.get_groups_by_member_id(user_id, db=session) user_group_ids = [group.id for group in user_groups] query = select(Prompt.tags).filter(Prompt.is_active == True) @@ -686,7 +708,7 @@ class PromptsTable: permission='read', ) - result = await db.execute(query) + result = await session.execute(query) tags = set() for (tag_list,) in result.all(): if tag_list: @@ -698,4 +720,4 @@ class PromptsTable: return [] -Prompts = PromptsTable() +Prompts = PromptsTable() # singleton prompts registry diff --git a/backend/open_webui/models/shared_chats.py b/backend/open_webui/models/shared_chats.py index 37a3fea852..9132f11301 100644 --- a/backend/open_webui/models/shared_chats.py +++ b/backend/open_webui/models/shared_chats.py @@ -3,12 +3,10 @@ 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 +from sqlalchemy import JSON, BigInteger, Column, ForeignKey, Text, delete, select +from sqlalchemy.ext.asyncio import AsyncSession log = logging.getLogger(__name__) diff --git a/backend/open_webui/models/skills.py b/backend/open_webui/models/skills.py index 0fc6dfc52d..5bc8b54efc 100644 --- a/backend/open_webui/models/skills.py +++ b/backend/open_webui/models/skills.py @@ -2,15 +2,13 @@ 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 open_webui.models.groups import Groups +from open_webui.models.users import User, UserModel, UserResponse, Users from pydantic import BaseModel, ConfigDict, Field -from sqlalchemy import JSON, BigInteger, Boolean, Column, String, Text, func +from sqlalchemy import JSON, BigInteger, Boolean, Column, String, Text, delete, func, or_, select, update +from sqlalchemy.ext.asyncio import AsyncSession log = logging.getLogger(__name__) diff --git a/backend/open_webui/models/tags.py b/backend/open_webui/models/tags.py index ee2baefc01..87f6bac7e3 100644 --- a/backend/open_webui/models/tags.py +++ b/backend/open_webui/models/tags.py @@ -1,29 +1,25 @@ +"""Tag models and database operations.""" + +from __future__ import annotations + import logging import time import uuid -from typing import Optional -from sqlalchemy import select, delete -from sqlalchemy.ext.asyncio import AsyncSession +# local imports 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 +from sqlalchemy import JSON, BigInteger, Column, Index, PrimaryKeyConstraint, String, delete, select +from sqlalchemy.ext.asyncio import AsyncSession 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): +class Tag(Base): # database table mapping for tag entity __tablename__ = 'tag' id = Column(String) - name = Column(String) - user_id = Column(String) + name = Column(String, index=True) # tag label + user_id = Column(String, index=True) # user identifier meta = Column(JSON, nullable=True) __table_args__ = ( @@ -39,11 +35,11 @@ class TagModel(BaseModel): id: str name: str user_id: str - meta: Optional[dict] = None - model_config = ConfigDict(from_attributes=True) + meta: dict | None = None + model_config = ConfigDict(from_attributes=True) # allows ORM model binding -#################### +# --- tag schema forms --- # Forms #################### @@ -54,26 +50,28 @@ class TagChatIdForm(BaseModel): class TagTable: - async def insert_new_tag(self, name: str, user_id: str, db: Optional[AsyncSession] = None) -> Optional[TagModel]: + async def insert_new_tag( + self, + name: str, + user_id: str, + db: AsyncSession | None = None, + ) -> TagModel | None: + """Create a new tag, deriving the id from the name.""" async with get_async_db_context(db) as db: - id = name.replace(' ', '_').lower() - tag = TagModel(**{'id': id, 'user_id': user_id, 'name': name}) + tag_id = name.replace(' ', '_').lower() try: - result = Tag(**tag.model_dump()) - db.add(result) + record = Tag(id=tag_id, user_id=user_id, name=name) + db.add(record) await db.commit() - await db.refresh(result) - if result: - return TagModel.model_validate(result) - else: - return None + await db.refresh(record) + return TagModel.model_validate(record) if record else None except Exception as e: - log.exception(f'Error inserting a new tag: {e}') - return None + log.exception('Error inserting tag %r: %s', name, e) + return None # insertion failed async def get_tag_by_name_and_user_id( - self, name: str, user_id: str, db: Optional[AsyncSession] = None - ) -> Optional[TagModel]: + self, name: str, user_id: str, db: AsyncSession | None = None + ) -> TagModel | None: try: id = name.replace(' ', '_').lower() async with get_async_db_context(db) as db: @@ -83,19 +81,19 @@ class TagTable: except Exception: return None - async def get_tags_by_user_id(self, user_id: str, db: Optional[AsyncSession] = None) -> list[TagModel]: + async def get_tags_by_user_id(self, user_id: str, db: AsyncSession | None = 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 + self, ids: list[str], user_id: str, db: AsyncSession | None = 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: + async def delete_tag_by_name_and_user_id(self, name: str, user_id: str, db: AsyncSession | None = None) -> bool: try: async with get_async_db_context(db) as db: id = name.replace(' ', '_').lower() @@ -108,7 +106,7 @@ class TagTable: return False async def delete_tags_by_ids_and_user_id( - self, ids: list[str], user_id: str, db: Optional[AsyncSession] = None + self, ids: list[str], user_id: str, db: AsyncSession | None = None ) -> bool: """Delete all tags whose id is in *ids* for the given user, in one query.""" if not ids: @@ -122,7 +120,7 @@ class TagTable: 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: + async def ensure_tags_exist(self, names: list[str], user_id: str, db: AsyncSession | None = None) -> None: """Create tag rows for any *names* that don't already exist for *user_id*.""" if not names: return @@ -138,4 +136,4 @@ class TagTable: await db.commit() -Tags = TagTable() +Tags = TagTable() # singleton tag repository diff --git a/backend/open_webui/models/tools.py b/backend/open_webui/models/tools.py index 70035121aa..d575cc1439 100644 --- a/backend/open_webui/models/tools.py +++ b/backend/open_webui/models/tools.py @@ -1,44 +1,40 @@ +"""Tool models, forms, and database operations.""" + +from __future__ import annotations + import logging import time -from typing import Optional -from sqlalchemy import select, delete, update -from sqlalchemy.ext.asyncio import AsyncSession +# local imports 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 open_webui.models.groups import Groups +from open_webui.models.users import UserResponse, Users from pydantic import BaseModel, ConfigDict, Field -from sqlalchemy import BigInteger, Column, String, Text +from sqlalchemy import BigInteger, Column, String, Text, delete, select, update +from sqlalchemy.ext.asyncio import AsyncSession 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): +class Tool(Base): # database table definition __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) + user_id = Column(String, index=True) # owner user id + name = Column(Text) # human-readable label + content = Column(Text) # Python source code + specs = Column(JSONField) # OpenAPI-style function specs + meta = Column(JSONField) # description, manifest, etc. + valves = Column(JSONField) # admin-configurable runtime parameters - updated_at = Column(BigInteger) - created_at = Column(BigInteger) + updated_at = Column(BigInteger, nullable=False) # modification timestamp + created_at = Column(BigInteger, index=True) # creation timestamp class ToolMeta(BaseModel): - description: Optional[str] = None - manifest: Optional[dict] = {} + description: str | None = None + manifest: dict | None = {} class ToolModel(BaseModel): @@ -53,16 +49,16 @@ class ToolModel(BaseModel): updated_at: int # timestamp in epoch created_at: int # timestamp in epoch - model_config = ConfigDict(from_attributes=True) + model_config = ConfigDict(from_attributes=True) # enables ORM mapping -#################### +# --- tool request forms --- # Forms #################### class ToolUserModel(ToolModel): - user: Optional[UserResponse] = None + user: UserResponse | None = None class ToolResponse(BaseModel): @@ -76,13 +72,13 @@ class ToolResponse(BaseModel): class ToolUserResponse(ToolResponse): - user: Optional[UserResponse] = None + user: UserResponse | None = None model_config = ConfigDict(extra='allow') class ToolAccessResponse(ToolUserResponse): - write_access: Optional[bool] = False + write_access: bool | None = False class ToolForm(BaseModel): @@ -90,22 +86,22 @@ class ToolForm(BaseModel): name: str content: str meta: ToolMeta - access_grants: Optional[list[dict]] = None + access_grants: list[dict | None] = None class ToolValves(BaseModel): - valves: Optional[dict] = None + valves: dict | None = None class ToolsTable: - async def _get_access_grants(self, tool_id: str, db: Optional[AsyncSession] = None) -> list[AccessGrantModel]: + async def _get_access_grants(self, tool_id: str, db: AsyncSession | None = 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, + access_grants: list[AccessGrantModel | None] = None, + db: AsyncSession | None = None, ) -> ToolModel: tool_data = ToolModel.model_validate(tool).model_dump(exclude={'access_grants'}) tool_data['access_grants'] = ( @@ -118,8 +114,8 @@ class ToolsTable: user_id: str, form_data: ToolForm, specs: list[dict], - db: Optional[AsyncSession] = None, - ) -> Optional[ToolModel]: + db: AsyncSession | None = None, + ) -> ToolModel | None: async with get_async_db_context(db) as db: try: result = Tool( @@ -141,17 +137,37 @@ class ToolsTable: return None except Exception as e: log.exception(f'Error creating a new tool: {e}') - return None + return None # creation failed - 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 + async def get_tool_by_id( + self, + id: str, + db: AsyncSession | None = None, + ) -> ToolModel | None: + """Fetch a single tool by primary key, including access grants.""" + try: # single PK lookup + access grants + async with get_async_db_context(db) as session: + tool = await session.get(Tool, id) + if not tool: + return None + return await self._to_tool_model(tool, db=session) except Exception: return None - async def get_tools(self, defer_content: bool = False, db: Optional[AsyncSession] = None) -> list[ToolUserModel]: + async def get_tools_by_ids(self, tool_ids: list[str], db: AsyncSession | None = None) -> dict[str, ToolModel]: + """Batch-fetch multiple tools by ID, returning a dict keyed by tool ID.""" + if not tool_ids: + return {} + async with get_async_db_context(db) as db: + result = await db.execute(select(Tool).where(Tool.id.in_(tool_ids))) + tools = result.scalars().all() + grants_map = await AccessGrants.get_grants_by_resources('tool', [tool.id for tool in tools], db=db) + return { + tool.id: await self._to_tool_model(tool, access_grants=grants_map.get(tool.id, []), db=db) + for tool in tools + } + + async def get_tools(self, defer_content: bool = False, db: AsyncSession | None = None) -> list[ToolUserModel]: async with get_async_db_context(db) as db: stmt = select(Tool).order_by(Tool.updated_at.desc()) if defer_content: @@ -190,7 +206,7 @@ class ToolsTable: user_id: str, permission: str = 'write', defer_content: bool = False, - db: Optional[AsyncSession] = None, + db: AsyncSession | None = 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) @@ -211,7 +227,7 @@ class ToolsTable: result.append(tool) return result - async def get_tool_valves_by_id(self, id: str, db: Optional[AsyncSession] = None) -> Optional[dict]: + async def get_tool_valves_by_id(self, id: str, db: AsyncSession | None = None) -> dict | None: try: async with get_async_db_context(db) as db: tool = await db.get(Tool, id) @@ -221,8 +237,8 @@ class ToolsTable: return None async def update_tool_valves_by_id( - self, id: str, valves: dict, db: Optional[AsyncSession] = None - ) -> Optional[ToolValves]: + self, id: str, valves: dict, db: AsyncSession | None = None + ) -> ToolValves | None: 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()))) @@ -232,8 +248,8 @@ class ToolsTable: return None async def get_user_valves_by_id_and_user_id( - self, id: str, user_id: str, db: Optional[AsyncSession] = None - ) -> Optional[dict]: + self, id: str, user_id: str, db: AsyncSession | None = None + ) -> dict | None: try: user = await Users.get_user_by_id(user_id, db=db) user_settings = user.settings.model_dump() if user.settings else {} @@ -250,8 +266,8 @@ class ToolsTable: 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]: + self, id: str, user_id: str, valves: dict, db: AsyncSession | None = None + ) -> dict | None: try: user = await Users.get_user_by_id(user_id, db=db) user_settings = user.settings.model_dump() if user.settings else {} @@ -272,7 +288,7 @@ class ToolsTable: 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]: + async def update_tool_by_id(self, id: str, updated: dict, db: AsyncSession | None = None) -> ToolModel | None: try: async with get_async_db_context(db) as db: access_grants = updated.pop('access_grants', None) @@ -287,7 +303,7 @@ class ToolsTable: except Exception: return None - async def delete_tool_by_id(self, id: str, db: Optional[AsyncSession] = None) -> bool: + async def delete_tool_by_id(self, id: str, db: AsyncSession | None = None) -> bool: try: async with get_async_db_context(db) as db: await AccessGrants.revoke_all_access('tool', id, db=db) @@ -299,4 +315,4 @@ class ToolsTable: return False -Tools = ToolsTable() +Tools = ToolsTable() # singleton tool registry diff --git a/backend/open_webui/models/users.py b/backend/open_webui/models/users.py index 025e79bd8a..bd64887ad8 100644 --- a/backend/open_webui/models/users.py +++ b/backend/open_webui/models/users.py @@ -1,29 +1,34 @@ -import time -from typing import Optional +"""User models, Pydantic schemas, and database access layer.""" -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 +from __future__ import annotations import datetime +import time +from typing import Optional +from open_webui.env import DATABASE_USER_ACTIVE_STATUS_UPDATE_INTERVAL +from open_webui.internal.db import Base, JSONField, get_async_db_context +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 ( + JSON, + BigInteger, + Boolean, + Column, + Date, + String, + Text, + case, + cast, + delete, + exists, + func, + or_, + select, + update, +) +from sqlalchemy.dialects.postgresql import JSONB +from sqlalchemy.ext.asyncio import AsyncSession #################### # User DB Schema @@ -33,83 +38,92 @@ import datetime class UserSettings(BaseModel): - ui: Optional[dict] = {} + ui: dict | None = {} model_config = ConfigDict(extra='allow') pass -class User(Base): - __tablename__ = 'user' +class User(Base): # identity & profile + """One row per registered account — profile, role, and settings.""" - id = Column(String, primary_key=True, unique=True) - email = Column(String) - username = Column(String(50), nullable=True) - role = Column(String) + __tablename__: str = 'user' # Identity & Credentials + id = Column(String, primary_key=True, unique=True) # unique user id + email = Column(String, unique=True) # user email address + username = Column(String(50), nullable=True) # custom handle + role = Column(String, default='pending') # permissions role + name = Column(String, nullable=False) # display name - name = Column(String) - - profile_image_url = Column(Text) + # Profile + profile_image_url = Column(Text) # data-uri, path, or external URL 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) + # Online status 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) + # Metadata info = Column(JSON, nullable=True) settings = Column(JSON, nullable=True) - oauth = Column(JSON, nullable=True) scim = Column(JSON, nullable=True) + # Timestamps (epoch seconds) last_active_at = Column(BigInteger) updated_at = Column(BigInteger) created_at = Column(BigInteger) +_DEFAULT_PROFILE_IMAGE_URL = '/api/v1/users/{user_id}/profile/image' + + class UserModel(BaseModel): id: str email: str - username: Optional[str] = None + username: str | None = None role: str = 'pending' name: str - profile_image_url: Optional[str] = None - profile_banner_image_url: Optional[str] = None + profile_image_url: str | None = None + profile_banner_image_url: str | None = None - bio: Optional[str] = None - gender: Optional[str] = None - date_of_birth: Optional[datetime.date] = None - timezone: Optional[str] = None + bio: str | None = None + gender: str | None = None + date_of_birth: datetime.date | None = None + timezone: str | None = None - presence_state: Optional[str] = None - status_emoji: Optional[str] = None - status_message: Optional[str] = None - status_expires_at: Optional[int] = None + presence_state: str | None = None + status_emoji: str | None = None + status_message: str | None = None + status_expires_at: int | None = None - info: Optional[dict] = None - settings: Optional[UserSettings] = None + info: dict | None = None + settings: UserSettings | None = None - oauth: Optional[dict] = None - scim: Optional[dict] = None + oauth: dict | None = None + scim: dict | None = 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_config = ConfigDict( + from_attributes=True, + ) + # validation schema logic + # --- model validators --- @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' + def _ensure_profile_image(self) -> 'UserModel': + """Assign a generated avatar when no profile image is provided.""" + self.profile_image_url = self.profile_image_url or _DEFAULT_PROFILE_IMAGE_URL.format(user_id=self.id) return self @@ -136,9 +150,9 @@ 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 + data: dict | None = None + expires_at: int | None = None + last_used_at: int | None = None created_at: int # timestamp in epoch updated_at: int # timestamp in epoch @@ -153,9 +167,9 @@ class ApiKeyModel(BaseModel): class UpdateProfileForm(BaseModel): profile_image_url: str name: str - bio: Optional[str] = None - gender: Optional[str] = None - date_of_birth: Optional[datetime.date] = None + bio: str | None = None + gender: str | None = None + date_of_birth: datetime.date | None = None @field_validator('profile_image_url') @classmethod @@ -182,9 +196,9 @@ class UserGroupIdsListResponse(BaseModel): class UserStatus(BaseModel): - status_emoji: Optional[str] = None - status_message: Optional[str] = None - status_expires_at: Optional[int] = None + status_emoji: str | None = None + status_message: str | None = None + status_expires_at: int | None = None class UserInfoResponse(UserStatus): @@ -192,8 +206,8 @@ class UserInfoResponse(UserStatus): name: str email: str role: str - bio: Optional[str] = None - groups: Optional[list] = [] + bio: str | None = None + groups: list | None = [] is_active: bool = False @@ -205,7 +219,7 @@ class UserIdNameResponse(BaseModel): class UserIdNameStatusResponse(UserStatus): id: str name: str - is_active: Optional[bool] = None + is_active: bool | None = None class UserInfoListResponse(BaseModel): @@ -239,15 +253,15 @@ class UserRoleUpdateForm(BaseModel): 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 + role: str | None = None + name: str | None = None + email: str | None = None + profile_image_url: str | None = None + password: str | None = None @field_validator('profile_image_url', mode='before') @classmethod - def check_profile_image_url(cls, v: Optional[str]) -> Optional[str]: + def check_profile_image_url(cls, v: str | None) -> str | None: if v is None: return v return validate_profile_image_url(v) @@ -261,11 +275,11 @@ class UsersTable: 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: + username: str | None = None, + oauth: dict | None = None, + db: AsyncSession | None = None, + ) -> UserModel | None: + async with get_async_db_context(db) as session: user = UserModel( **{ 'id': id, @@ -281,93 +295,104 @@ class UsersTable: } ) result = User(**user.model_dump()) - db.add(result) - await db.commit() - await db.refresh(result) - if result: - return user - else: - return None + session.add(result) + await session.commit() + await session.refresh(result) + return user if result else 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 + # database read methods + # --- read / lookup operations --- + async def get_user_by_id( + self, + id: str, + db: AsyncSession | None = None, + ) -> UserModel | None: + """Fetch a single user by primary key.""" + async with get_async_db_context(db) as session: + user = await session.get(User, id) + return UserModel.model_validate(user) if user else 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 + # api key auth helper + async def get_user_by_api_key( + self, + api_key: str, + db: AsyncSession | None = None, + ) -> UserModel | None: + """Resolve a user from their API key via a JOIN on the api_key table.""" + async with get_async_db_context(db) as session: + result = await session.execute( + select(User).join(ApiKey, User.id == ApiKey.user_id).where(ApiKey.key == api_key), + ) + user = result.scalars().first() + return UserModel.model_validate(user) if user else 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_email( + self, + email: str, + db: AsyncSession | None = None, + ) -> UserModel | None: + """Case-insensitive email lookup using SQL lower().""" + async with get_async_db_context(db) as session: + email_filter = func.lower(User.email) == email.lower() + query = select(User).where(email_filter) + match = (await session.execute(query)).scalars().first() + if match is None: + return + return UserModel.model_validate(match) + # --- context manager above always returns --- + return + # --- oauth & integrations --- 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 + self, + provider: str, + sub: str, + db: AsyncSession | None = None, + ) -> UserModel | None: + """Look up a user by OAuth provider + subject claim (dialect-aware JSON filter).""" + async with get_async_db_context(db) as session: + dialect = session.bind.dialect.name + query = select(User) + if dialect == 'sqlite': + oauth_match = User.oauth.contains({provider: {'sub': sub}}) + query = query.where(oauth_match) + elif dialect == 'postgresql': + oauth_match = User.oauth[provider].cast(JSONB)['sub'].astext == sub + query = query.where(oauth_match) + row = (await session.execute(query)).scalars().first() + return UserModel.model_validate(row) if row else 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 + self, + provider: str, + external_id: str, + db: AsyncSession | None = None, + ) -> UserModel | None: + """Look up a user by SCIM provider + external ID (dialect-aware JSON filter).""" + async with get_async_db_context(db) as session: + dialect = session.bind.dialect.name + query = select(User) + if dialect == 'sqlite': + scim_match = User.scim.contains({provider: {'external_id': external_id}}) + query = query.where(scim_match) + elif dialect == 'postgresql': + scim_match = User.scim[provider].cast(JSONB)['external_id'].astext == external_id + query = query.where(scim_match) + row = (await session.execute(query)).scalars().first() + return UserModel.model_validate(row) if row else None async def get_users( self, - filter: Optional[dict] = None, - skip: Optional[int] = None, - limit: Optional[int] = None, - db: Optional[AsyncSession] = None, + filter: dict | None = None, + skip: int | None = None, + limit: int | None = None, + db: AsyncSession | None = None, ) -> dict: - async with get_async_db_context(db) as db: - # Import here to avoid circular imports - from open_webui.models.groups import GroupMember + """Paginated user listing with optional filters for role, group, and channel.""" + async with get_async_db_context(db) as session: + # Deferred imports to avoid circular dependencies from open_webui.models.channels import ChannelMember + from open_webui.models.groups import GroupMember # Join GroupMember so we can order by group_id when requested stmt = select(User) @@ -485,7 +510,7 @@ class UsersTable: stmt = stmt.order_by(User.created_at.desc()) # Count BEFORE pagination - count_result = await db.execute(select(func.count()).select_from(stmt.subquery())) + count_result = await session.execute(select(func.count()).select_from(stmt.subquery())) total = count_result.scalar() # correct pagination logic @@ -494,321 +519,226 @@ class UsersTable: if limit is not None: stmt = stmt.limit(limit) - result = await db.execute(stmt) + result = await session.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: + async def get_users_by_group_id(self, group_id: str, db: AsyncSession | None = None) -> list[UserModel]: + async with get_async_db_context(db) as session: from open_webui.models.groups import GroupMember - result = await db.execute( + result = await session.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))) + async def get_users_by_user_ids(self, user_ids: list[str], db: AsyncSession | None = None) -> list[UserStatusModel]: + async with get_async_db_context(db) as session: + result = await session.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)) + # count registered accounts + async def get_num_users(self, db: AsyncSession | None = None) -> int | None: + async with get_async_db_context(db) as session: + result = await session.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)))) + # check user existence + async def has_users(self, db: AsyncSession | None = None) -> bool: + async with get_async_db_context(db) as session: + result = await session.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: + async def get_first_user(self, db: AsyncSession | None = None) -> UserModel | None: + """Return the earliest-created user (bootstrap admin detection).""" + async with get_async_db_context(db) as session: + stmt = select(User).order_by(User.created_at).limit(1) + row = (await session.execute(stmt)).scalars().first() + return UserModel.model_validate(row) if row else None + + async def get_user_webhook_url_by_id(self, id: str, db: AsyncSession | None = None) -> str | None: + async with get_async_db_context(db) as session: + user = await session.get(User, id) + if user and user.settings: + return user.settings.get('ui', {}).get('notifications', {}).get('webhook_url', None) 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()) + async def get_num_users_active_today(self, db: AsyncSession | None = None) -> int | None: + async with get_async_db_context(db) as session: + current_timestamp = int(time.time()) 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) + result = await session.execute( + select(func.count()).select_from(User).where(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_role_by_id(self, id: str, role: str, db: AsyncSession | None = None) -> UserModel | None: + async with get_async_db_context(db) as session: + user = await session.get(User, id) + if not user: + return None + user.role = role + await session.commit() + await session.refresh(user) + return UserModel.model_validate(user) 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 + self, id: str, form_data: UserStatus, db: AsyncSession | None = None + ) -> UserModel | None: + async with get_async_db_context(db) as session: + user = await session.get(User, id) + if not user: + return None + for key, value in form_data.model_dump(exclude_none=True).items(): + setattr(user, key, value) + await session.commit() + await session.refresh(user) + return UserModel.model_validate(user) 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 + self, + id: str, + profile_image_url: str, + db: AsyncSession | None = None, + ) -> UserModel | None: + async with get_async_db_context(db) as session: + user = await session.get(User, id) + if user is None: + return None + user.profile_image_url = profile_image_url + await session.commit() + await session.refresh(user) + return UserModel.model_validate(user) @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_last_active_by_id(self, id: str, db: AsyncSession | None = None) -> None: + async with get_async_db_context(db) as session: + await session.execute(update(User).where(User.id == id).values(last_active_at=int(time.time()))) + await session.commit() 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 + self, id: str, provider: str, sub: str, db: AsyncSession | None = None + ) -> UserModel | None: + """Update or insert an OAuth provider/sub pair into the user's oauth JSON field.""" + async with get_async_db_context(db) as session: + user = await session.get(User, id) + if not user: + return None + oauth = dict(user.oauth or {}) + oauth[provider] = {'sub': sub} + user.oauth = oauth + await session.commit() + await session.refresh(user) + return UserModel.model_validate(user) 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: + db: AsyncSession | None = None, + ) -> UserModel | None: + """Update or insert a SCIM provider/external_id pair into the user's scim JSON field.""" + async with get_async_db_context(db) as session: + user = await session.get(User, id) + if not user: return None + scim = dict(user.scim or {}) + scim[provider] = {'external_id': external_id} + user.scim = scim + await session.commit() + await session.refresh(user) + return UserModel.model_validate(user) - async def get_active_user_count(self, db: Optional[AsyncSession] = None) -> int: - async with get_async_db_context(db) as db: + async def update_user_by_id(self, id: str, updated: dict, db: AsyncSession | None = None) -> UserModel | None: + async with get_async_db_context(db) as session: + user = await session.get(User, id) + if not user: + return None + for key, value in updated.items(): + setattr(user, key, value) + await session.commit() + await session.refresh(user) + return UserModel.model_validate(user) + + # settings update helper + async def update_user_settings_by_id( + self, id: str, updated: dict, db: AsyncSession | None = None + ) -> UserModel | None: + async with get_async_db_context(db) as session: + user = await session.get(User, id) + if not user: + return None + user_settings = dict(user.settings or {}) + user_settings.update(updated) + user.settings = user_settings + await session.commit() + await session.refresh(user) + return UserModel.model_validate(user) + + async def delete_user_by_id(self, id: str, db: AsyncSession | None = None) -> bool: + from open_webui.models.chats import Chats + from open_webui.models.groups import Groups + + # Remove User from Groups + await Groups.remove_user_from_all_groups(id) + + # Delete User Chats + async with get_async_db_context(db) as session: + deleted_chats = await Chats.delete_chats_by_user_id(id, db=session) + if not deleted_chats: + return False # chats deletion failed + await session.execute(delete(User).where(User.id == id)) + await session.commit() + return True + + async def get_user_api_key_by_id(self, id: str, db: AsyncSession | None = None) -> str | None: + async with get_async_db_context(db) as session: + api_key = (await session.execute(select(ApiKey).where(ApiKey.user_id == id))).scalars().first() + return api_key.key if api_key else None + + async def update_user_api_key_by_id(self, id: str, api_key: str, db: AsyncSession | None = None) -> bool: + async with get_async_db_context(db) as session: + await session.execute(delete(ApiKey).where(ApiKey.user_id == id)) + now_ts = int(time.time()) + new_key = ApiKey( + id=f'key_{id}', + user_id=id, + key=api_key, + created_at=now_ts, + updated_at=now_ts, + ) + session.add(new_key) + await session.commit() + return True + + async def delete_user_api_key_by_id(self, id: str, db: AsyncSession | None = None) -> bool: + async with get_async_db_context(db) as session: + await session.execute(delete(ApiKey).where(ApiKey.user_id == id)) + await session.commit() + return True + + async def get_valid_user_ids(self, user_ids: list[str], db: AsyncSession | None = None) -> list[str]: + async with get_async_db_context(db) as session: + result = await session.execute(select(User).where(User.id.in_(user_ids))) + return [u.id for u in result.scalars().all()] + + async def get_super_admin_user(self, db: AsyncSession | None = None) -> UserModel | None: + async with get_async_db_context(db) as session: + row = (await session.execute(select(User).where(User.role == 'admin').limit(1))).scalars().first() + return UserModel.model_validate(row) if row else None + + async def get_active_user_count(self, db: AsyncSession | None = None) -> int: + async with get_async_db_context(db) as session: # 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) + result = await session.execute( + select(func.count()).select_from(User).where(User.last_active_at >= three_minutes_ago) ) return result.scalar() @@ -820,10 +750,9 @@ class UsersTable: 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() + async def is_user_active(self, user_id: str, db: AsyncSession | None = None) -> bool: + async with get_async_db_context(db) as session: + user = await session.get(User, user_id) 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 @@ -831,4 +760,4 @@ class UsersTable: return False -Users = UsersTable() +Users = UsersTable() # singleton user repository diff --git a/backend/open_webui/retrieval/loaders/datalab_marker.py b/backend/open_webui/retrieval/loaders/datalab_marker.py index dd4a763b70..be8cb9baaa 100644 --- a/backend/open_webui/retrieval/loaders/datalab_marker.py +++ b/backend/open_webui/retrieval/loaders/datalab_marker.py @@ -1,11 +1,12 @@ +import json +import logging import os import time -import requests -import logging -import json from typing import List, Optional -from langchain_core.documents import Document + +import requests from fastapi import HTTPException, status +from langchain_core.documents import Document log = logging.getLogger(__name__) diff --git a/backend/open_webui/retrieval/loaders/external_document.py b/backend/open_webui/retrieval/loaders/external_document.py index 77b1abfcd8..ddafc3124b 100644 --- a/backend/open_webui/retrieval/loaders/external_document.py +++ b/backend/open_webui/retrieval/loaders/external_document.py @@ -1,8 +1,9 @@ -import requests -import logging, os +import logging +import os from typing import Iterator, List, Union from urllib.parse import quote +import requests from langchain_core.document_loaders import BaseLoader from langchain_core.documents import Document from open_webui.utils.headers import include_user_info_headers diff --git a/backend/open_webui/retrieval/loaders/external_web.py b/backend/open_webui/retrieval/loaders/external_web.py index 64248427b3..e3fd0b2614 100644 --- a/backend/open_webui/retrieval/loaders/external_web.py +++ b/backend/open_webui/retrieval/loaders/external_web.py @@ -1,7 +1,7 @@ -import requests import logging from typing import Iterator, List, Union +import requests from langchain_core.document_loaders import BaseLoader from langchain_core.documents import Document diff --git a/backend/open_webui/retrieval/loaders/main.py b/backend/open_webui/retrieval/loaders/main.py index 2daa641bf2..ee4166d120 100644 --- a/backend/open_webui/retrieval/loaders/main.py +++ b/backend/open_webui/retrieval/loaders/main.py @@ -1,10 +1,10 @@ import asyncio -import requests -import logging -import ftfy -import sys import json +import logging +import sys +import ftfy +import requests from azure.identity import DefaultAzureCredential from langchain_community.document_loaders import ( AzureAIDocumentIntelligenceLoader, @@ -17,16 +17,13 @@ from langchain_community.document_loaders import ( 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.env import AIOHTTP_CLIENT_SESSION_SSL, GLOBAL_LOG_LEVEL, REQUESTS_VERIFY from open_webui.retrieval.loaders.datalab_marker import DatalabMarkerLoader +from open_webui.retrieval.loaders.external_document import ExternalDocumentLoader from open_webui.retrieval.loaders.mineru import MinerULoader +from open_webui.retrieval.loaders.mistral import MistralLoader 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__) @@ -237,7 +234,6 @@ class Loader: 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]: @@ -260,6 +256,140 @@ class Loader: and not file_content_type.find('html') >= 0 ) + def _detect_text_encoding(self, file_path: str) -> str: + """Detect the encoding of a text file with CJK-aware fallbacks. + + Langchain's ``TextLoader`` uses chardet internally when + ``autodetect_encoding=True``, but chardet frequently misidentifies + CJK encodings (e.g. GB18030 detected as GB2312 or even Cyrillic). + This method replaces that by: + + 1. Trying UTF-8 first (fast path for the vast majority of files). + 2. Using chardet as a *hint* to prioritise the right CJK codec + family, but mapping subset names to their superset + (e.g. GB2312 → gb18030). + 3. Validating that decoded text actually contains CJK characters, + guarding against codecs that "succeed" but produce garbage. + 4. Falling back to latin-1 (always valid, ftfy fixes mojibake later). + """ + try: + with open(file_path, 'rb') as f: + raw = f.read() + except OSError: + return 'utf-8' + + if not raw: + return 'utf-8' + + # Fast path: most files are UTF-8 + try: + raw.decode('utf-8') + return 'utf-8' + except UnicodeDecodeError: + pass + + # Use chardet as a hint, not as ground truth + import chardet + + detected = chardet.detect(raw) + detected_enc = (detected.get('encoding') or '').lower().replace('-', '').replace('_', '') + + # Map chardet's detected encoding to the correct superset codec. + # chardet often reports GB2312 for content that is actually GB18030; + # GB18030 is a strict superset of both GB2312 and GBK. + _ENC_FAMILY = { + 'gb2312': 'gb18030', + 'gb18030': 'gb18030', + 'gbk': 'gb18030', + 'big5': 'big5', + 'euckr': 'euc-kr', + 'eucjp': 'euc-jp', + 'iso2022jp': 'euc-jp', + 'shiftjis': 'shift_jis', + } + + # Build priority list: chardet-hinted codec first, then remaining CJK + base_order = ['gb18030', 'big5', 'euc-kr', 'euc-jp'] + hinted = _ENC_FAMILY.get(detected_enc) + if hinted and hinted in base_order: + ordered = [hinted] + [e for e in base_order if e != hinted] + else: + ordered = base_order + + for enc in ordered: + try: + text = raw.decode(enc) + if text.strip() and self._has_cjk_characters(text): + log.info( + 'Detected encoding %s for %s (chardet guessed %s)', + enc, + file_path, + detected.get('encoding'), + ) + return enc + except (UnicodeDecodeError, LookupError): + continue + + # If chardet gave a non-CJK answer that isn't in our family map, + # try it directly — it might be a valid Western encoding. + chardet_encoding = detected.get('encoding') + if chardet_encoding: + try: + raw.decode(chardet_encoding) + log.info( + 'Using chardet-detected encoding %s for %s', + chardet_encoding, + file_path, + ) + return chardet_encoding + except (UnicodeDecodeError, LookupError): + pass + + # latin-1 is the ultimate fallback: every byte 0x00–0xFF is valid. + # ftfy.fix_text() (applied downstream) repairs most mojibake that + # results from treating Windows-1252 content as Latin-1. + log.info('Falling back to latin-1 encoding for %s', file_path) + return 'latin-1' + + @staticmethod + def _has_cjk_characters(text: str, threshold: float = 0.05) -> bool: + """Check if decoded text contains a meaningful proportion of CJK characters. + + This guards against codecs that technically "succeed" but decode the + bytes into wrong Unicode codepoints (e.g. PUA chars, random symbols). + A genuine CJK document should have at least ``threshold`` fraction of + its non-whitespace characters in CJK Unicode blocks. + """ + if not text: + return False + + cjk_count = 0 + total = 0 + for ch in text: + if ch.isspace(): + continue + total += 1 + cp = ord(ch) + if ( + 0x4E00 <= cp <= 0x9FFF # CJK Unified Ideographs + or 0x3400 <= cp <= 0x4DBF # CJK Extension A + or 0x20000 <= cp <= 0x2A6DF # CJK Extension B + or 0x2A700 <= cp <= 0x2B73F # CJK Extension C + or 0x2B740 <= cp <= 0x2B81F # CJK Extension D + or 0xF900 <= cp <= 0xFAFF # CJK Compatibility Ideographs + or 0x3000 <= cp <= 0x303F # CJK Symbols and Punctuation + or 0x3040 <= cp <= 0x309F # Hiragana + or 0x30A0 <= cp <= 0x30FF # Katakana + or 0xAC00 <= cp <= 0xD7AF # Hangul Syllables + or 0xFF00 <= cp <= 0xFFEF # Halfwidth and Fullwidth Forms + ): + cjk_count += 1 + + if total == 0: + return False + + return (cjk_count / total) >= threshold + def _get_loader(self, filename: str, file_content_type: str, file_path: str): file_ext = filename.split('.')[-1].lower() @@ -277,7 +407,7 @@ class Loader: ) 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) + loader = TextLoader(file_path, encoding=self._detect_text_encoding(file_path)) else: loader = TikaLoader( url=self.kwargs.get('TIKA_SERVER_URL'), @@ -329,7 +459,7 @@ class Loader: ) 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) + loader = TextLoader(file_path, encoding=self._detect_text_encoding(file_path)) else: # Build params for DoclingLoader params = self.kwargs.get('DOCLING_PARAMS', {}) @@ -374,7 +504,7 @@ class Loader: 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 + elif self.engine == 'mineru' and file_ext in self.kwargs.get('MINERU_FILE_EXTENSIONS', ['pdf']): mineru_timeout = self.kwargs.get('MINERU_API_TIMEOUT', 300) if mineru_timeout: try: @@ -414,7 +544,7 @@ class Loader: mode=self.kwargs.get('PDF_LOADER_MODE', 'page'), ) elif file_ext == 'csv': - loader = CSVLoader(file_path, autodetect_encoding=True) + loader = CSVLoader(file_path, encoding=self._detect_text_encoding(file_path)) elif file_ext == 'rst': try: from langchain_community.document_loaders import UnstructuredRSTLoader @@ -426,7 +556,7 @@ class Loader: 'Falling back to plain text loading for .rst file. ' 'Install it with: pip install unstructured' ) - loader = TextLoader(file_path, autodetect_encoding=True) + loader = TextLoader(file_path, encoding=self._detect_text_encoding(file_path)) elif file_ext == 'xml': try: from langchain_community.document_loaders import UnstructuredXMLLoader @@ -438,11 +568,11 @@ class Loader: 'Falling back to plain text loading for .xml file. ' 'Install it with: pip install unstructured' ) - loader = TextLoader(file_path, autodetect_encoding=True) + loader = TextLoader(file_path, encoding=self._detect_text_encoding(file_path)) 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) + loader = TextLoader(file_path, encoding=self._detect_text_encoding(file_path)) elif file_content_type == 'application/epub+zip': try: from langchain_community.document_loaders import UnstructuredEPubLoader @@ -458,6 +588,16 @@ class Loader: or file_ext == 'docx' ): loader = Docx2txtLoader(file_path) + elif file_ext == 'doc' or file_content_type == 'application/msword': + try: + from langchain_community.document_loaders import UnstructuredWordDocumentLoader + + loader = UnstructuredWordDocumentLoader(file_path) + except ImportError: + raise ValueError( + "Processing .doc files requires the 'unstructured' package. " + 'Install it with: pip install unstructured' + ) elif file_content_type in [ 'application/vnd.ms-excel', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', @@ -501,8 +641,8 @@ class Loader: 'Install it with: pip install unstructured' ) elif self._is_text_file(file_ext, file_content_type): - loader = TextLoader(file_path, autodetect_encoding=True) + loader = TextLoader(file_path, encoding=self._detect_text_encoding(file_path)) else: - loader = TextLoader(file_path, autodetect_encoding=True) + loader = TextLoader(file_path, encoding=self._detect_text_encoding(file_path)) return loader diff --git a/backend/open_webui/retrieval/loaders/mineru.py b/backend/open_webui/retrieval/loaders/mineru.py index 1f0848a613..63608f9bf9 100644 --- a/backend/open_webui/retrieval/loaders/mineru.py +++ b/backend/open_webui/retrieval/loaders/mineru.py @@ -1,12 +1,13 @@ -import os -import time -import requests import logging +import os import tempfile +import time import zipfile from typing import List, Optional -from langchain_core.documents import Document + +import requests from fastapi import HTTPException, status +from langchain_core.documents import Document log = logging.getLogger(__name__) diff --git a/backend/open_webui/retrieval/loaders/mistral.py b/backend/open_webui/retrieval/loaders/mistral.py index b3d274ee7c..465ea5d91e 100644 --- a/backend/open_webui/retrieval/loaders/mistral.py +++ b/backend/open_webui/retrieval/loaders/mistral.py @@ -1,15 +1,15 @@ -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 typing import Any, Dict, List +import aiohttp +import requests from langchain_core.documents import Document -from open_webui.env import GLOBAL_LOG_LEVEL, AIOHTTP_CLIENT_SESSION_SSL +from open_webui.env import AIOHTTP_CLIENT_SESSION_SSL, GLOBAL_LOG_LEVEL logging.basicConfig(stream=sys.stdout, level=GLOBAL_LOG_LEVEL) log = logging.getLogger(__name__) diff --git a/backend/open_webui/retrieval/loaders/paddleocr_vl.py b/backend/open_webui/retrieval/loaders/paddleocr_vl.py index b89369b2a4..40c185eab6 100644 --- a/backend/open_webui/retrieval/loaders/paddleocr_vl.py +++ b/backend/open_webui/retrieval/loaders/paddleocr_vl.py @@ -1,10 +1,10 @@ import base64 -import os -import requests import logging +import os import sys from typing import List +import requests from langchain_core.documents import Document from open_webui.env import GLOBAL_LOG_LEVEL diff --git a/backend/open_webui/retrieval/loaders/tavily.py b/backend/open_webui/retrieval/loaders/tavily.py index 742ac499cf..bdf70830e4 100644 --- a/backend/open_webui/retrieval/loaders/tavily.py +++ b/backend/open_webui/retrieval/loaders/tavily.py @@ -1,7 +1,7 @@ -import requests import logging from typing import Iterator, List, Literal, Union +import requests from langchain_core.document_loaders import BaseLoader from langchain_core.documents import Document diff --git a/backend/open_webui/retrieval/loaders/youtube.py b/backend/open_webui/retrieval/loaders/youtube.py index 34a1d20740..ad4c3524ac 100644 --- a/backend/open_webui/retrieval/loaders/youtube.py +++ b/backend/open_webui/retrieval/loaders/youtube.py @@ -1,8 +1,8 @@ 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 xml.etree.ElementTree import ParseError + from langchain_core.documents import Document log = logging.getLogger(__name__) @@ -98,7 +98,7 @@ class YoutubeLoader: try: transcript_list = transcript_api.list(self.video_id) except Exception as e: - log.exception('Loading YouTube transcript failed') + log.warning(f'Loading YouTube transcript failed: {e}') return [] # Try each language in order of priority diff --git a/backend/open_webui/retrieval/models/base_reranker.py b/backend/open_webui/retrieval/models/base_reranker.py index 6be7a5649b..78002e087a 100644 --- a/backend/open_webui/retrieval/models/base_reranker.py +++ b/backend/open_webui/retrieval/models/base_reranker.py @@ -1,5 +1,5 @@ from abc import ABC, abstractmethod -from typing import Optional, List, Tuple +from typing import List, Optional, Tuple class BaseReranker(ABC): diff --git a/backend/open_webui/retrieval/models/colbert.py b/backend/open_webui/retrieval/models/colbert.py index ceb41824e3..d11bde1b09 100644 --- a/backend/open_webui/retrieval/models/colbert.py +++ b/backend/open_webui/retrieval/models/colbert.py @@ -1,11 +1,10 @@ -import os import logging -import torch +import os + import numpy as np +import torch from colbert.infra import ColBERTConfig from colbert.modeling.checkpoint import Checkpoint - - from open_webui.retrieval.models.base_reranker import BaseReranker log = logging.getLogger(__name__) diff --git a/backend/open_webui/retrieval/models/external.py b/backend/open_webui/retrieval/models/external.py index f04583b965..6d2849eb88 100644 --- a/backend/open_webui/retrieval/models/external.py +++ b/backend/open_webui/retrieval/models/external.py @@ -1,9 +1,8 @@ import logging -import requests -from typing import Optional, List, Tuple +from typing import List, Optional, Tuple from urllib.parse import quote - +import requests 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 diff --git a/backend/open_webui/retrieval/utils.py b/backend/open_webui/retrieval/utils.py index 8e672b7a8f..2db9f47c53 100644 --- a/backend/open_webui/retrieval/utils.py +++ b/backend/open_webui/retrieval/utils.py @@ -1,16 +1,17 @@ -import logging -import os -from typing import Awaitable, Optional, Union +from __future__ import annotations -import requests -import aiohttp import asyncio import hashlib -from concurrent.futures import ThreadPoolExecutor -import time +import logging +import os import re - +import time +from concurrent.futures import ThreadPoolExecutor +from typing import Awaitable, Optional, Union from urllib.parse import quote + +import aiohttp +import requests from huggingface_hub import snapshot_download from langchain_classic.retrievers import ( ContextualCompressionRetriever, @@ -18,41 +19,35 @@ from langchain_classic.retrievers import ( ) 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, - AIOHTTP_CLIENT_ALLOW_REDIRECTS, - 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, + RAG_EMBEDDING_QUERY_PREFIX, + VECTOR_DB, ) +from open_webui.env import ( + AIOHTTP_CLIENT_ALLOW_REDIRECTS, + AIOHTTP_CLIENT_SESSION_SSL, + AIOHTTP_CLIENT_TIMEOUT, + BYPASS_RETRIEVAL_ACCESS_CONTROL, + ENABLE_FORWARD_USER_INFO_HEADERS, + ENABLE_RETRIEVAL_UNSCOPED_COLLECTIONS, + OFFLINE_MODE, +) +from open_webui.models.access_grants import AccessGrants +from open_webui.models.chats import Chats +from open_webui.models.files import Files +from open_webui.models.knowledge import Knowledges +from open_webui.models.notes import Notes +from open_webui.models.users import UserModel +from open_webui.retrieval.loaders.youtube import YoutubeLoader +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.retrieval.vector.main import GetResult +from open_webui.retrieval.web.utils import get_web_loader +from open_webui.utils.access_control.files import has_access_to_file +from open_webui.utils.headers import include_user_info_headers +from open_webui.utils.misc import get_message_list log = logging.getLogger(__name__) @@ -122,6 +117,7 @@ def build_loader_from_config(request): MINERU_API_KEY=config.MINERU_API_KEY, MINERU_API_TIMEOUT=config.MINERU_API_TIMEOUT, MINERU_PARAMS=config.MINERU_PARAMS, + MINERU_FILE_EXTENSIONS=config.MINERU_FILE_EXTENSIONS, ) @@ -180,6 +176,18 @@ def get_content_from_url(request, url: str) -> str: # Validate URL before making any request (blocks private IPs, non-HTTP, filter list) validate_url(url) + # YouTube URLs (including youtu.be short links) should go straight to + # YoutubeLoader, which uses youtube-transcript-api and never needs the + # HTTP response body. Probing the URL first is harmful for short URLs: + # youtu.be returns a 303 redirect with Content-Type: application/binary + # when allow_redirects=False, causing the binary-content path to run + # and produce empty docs → HTTP 400. + if is_youtube_url(url): + loader = get_loader(request, url) + docs = loader.load() + content = ' '.join([doc.page_content for doc in docs]) + return content, docs + # Streamed GET to check Content-Type without downloading the body. # allow_redirects=False prevents redirect-based SSRF: validate_url() above is # called on the originally-submitted URL only; following 3xx redirects without @@ -919,6 +927,13 @@ def get_embedding_function( concurrent_requests=0, ) -> Awaitable: if embedding_engine == '': + if embedding_function is None: + raise ValueError( + 'No embedding model is loaded. Set RAG_EMBEDDING_MODEL to a valid ' + 'SentenceTransformer model name, or configure an external ' + 'RAG_EMBEDDING_ENGINE (ollama, openai, azure_openai).' + ) + # Sentence transformers: CPU-bound sync operation async def async_embedding_function(query, prefix=None, user=None): return await asyncio.to_thread( @@ -1058,6 +1073,16 @@ def get_reranking_function(reranking_engine, reranking_model, reranking_function ) +# UUIDs, SHA-256 digests, and prefixed variants thereof all fit [A-Za-z0-9_-]. +# Anything else cannot be a real Open WebUI collection and could break out of +# a Milvus expression literal. +_SAFE_COLLECTION_NAME_RE = re.compile(r'^[A-Za-z0-9_-]{1,255}$') + + +def _is_safe_collection_name(name: str) -> bool: + return isinstance(name, str) and bool(_SAFE_COLLECTION_NAME_RE.match(name)) + + async def filter_accessible_collections( collection_names: set[str], user: UserModel, @@ -1067,20 +1092,33 @@ async def filter_accessible_collections( Return only the collection names the user is allowed to access. Admins bypass all checks. For non-admins the policy is: + - any name with characters outside [A-Za-z0-9_-] → rejected - 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 + such KB exists, denied by default. When + ENABLE_RETRIEVAL_UNSCOPED_COLLECTIONS is True, + the name is treated as a legacy/ephemeral + collection and allowed. """ + # Applied before the admin bypass — malformed names should never reach the vector store. + safe_names = {n for n in collection_names if _is_safe_collection_name(n)} + rejected = collection_names - safe_names + if rejected: + log.warning( + 'filter_accessible_collections: rejected %d collection name(s) with unsafe characters (user_id=%s)', + len(rejected), + getattr(user, 'id', ''), + ) + if user.role == 'admin': - return collection_names + return safe_names validated = set() - for name in collection_names: + for name in safe_names: if name == 'knowledge-bases': # System meta-collection — never exposed to non-admins. continue @@ -1099,11 +1137,13 @@ async def filter_accessible_collections( 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. + # exists, the behaviour depends on + # ENABLE_RETRIEVAL_UNSCOPED_COLLECTIONS: + # False (default) — deny (closes the unscoped namespace) + # True — allow (preserves legacy behaviour) 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): + elif ENABLE_RETRIEVAL_UNSCOPED_COLLECTIONS and not await Knowledges.get_knowledge_by_id(name): # Not a KB at all — legacy/ephemeral collection, allow validated.add(name) return validated @@ -1121,7 +1161,7 @@ async def get_sources_from_items( hybrid_bm25_weight, hybrid_search, full_context=False, - user: Optional[UserModel] = None, + user: UserModel | None = None, ): log.debug(f'items: {items} {queries} {embedding_function} {reranking_function} {full_context}') @@ -1247,11 +1287,27 @@ async def get_sources_from_items( ], } else: - # Fallback to collection names - if item.get('legacy'): - collection_names.append(f'{item["id"]}') - else: - collection_names.append(f'file-{item["id"]}') + # Chunked-retrieval fallback — verify read access before + # exposing the file's vector collection (same posture as the + # full-context branch above). + file_id = item.get('id') + if file_id: + if BYPASS_RETRIEVAL_ACCESS_CONTROL: + if item.get('legacy'): + collection_names.append(f'{file_id}') + else: + collection_names.append(f'file-{file_id}') + else: + file_object = await Files.get_file_by_id(file_id) + if file_object and ( + user.role == 'admin' + or file_object.user_id == user.id + or await has_access_to_file(file_id, 'read', user) + ): + if item.get('legacy'): + collection_names.append(f'{file_id}') + else: + collection_names.append(f'file-{file_id}') elif item.get('type') == 'collection': # Manual Full Mode Toggle for Collection @@ -1297,9 +1353,18 @@ async def get_sources_from_items( 'metadatas': [metadatas], } else: - # Fallback to collection names if item.get('legacy'): - collection_names = item.get('collection_names', []) + if BYPASS_RETRIEVAL_ACCESS_CONTROL: + collection_names = item.get('collection_names', []) + else: + # Legacy KB: item.collection_names is client-supplied. + # Validate against the KB's actual files to prevent + # cross-tenant collection name substitution. + files = await Knowledges.get_files_by_id(knowledge_base.id) + owned_names = {f'file-{f.id}' for f in files} + owned_names.add(knowledge_base.id) + valid_names = [n for n in (item.get('collection_names') or []) if n in owned_names] + collection_names = valid_names if valid_names else [knowledge_base.id] else: collection_names.append(item['id']) @@ -1310,11 +1375,20 @@ async def get_sources_from_items( 'metadatas': [[doc.get('metadata') for doc in item.get('docs')]], } elif item.get('collection_name'): - # Direct Collection Name - collection_names.append(item['collection_name']) + if BYPASS_RETRIEVAL_ACCESS_CONTROL: + collection_names.append(item['collection_name']) + else: + log.debug( + "get_sources_from_items: ignoring untrusted direct collection_name '%s' on item without type", + item.get('collection_name'), + ) elif item.get('collection_names'): - # Collection Names List - collection_names.extend(item['collection_names']) + if BYPASS_RETRIEVAL_ACCESS_CONTROL: + collection_names.extend(item['collection_names']) + else: + log.debug( + 'get_sources_from_items: ignoring untrusted direct collection_names on item without type', + ) # If query_result is None # Fallback to collection names and vector search the collections @@ -1433,7 +1507,7 @@ class RerankCompressor(BaseDocumentCompressor): self, documents: Sequence[Document], query: str, - callbacks: Optional[Callbacks] = None, + callbacks: Callbacks | None = None, ) -> Sequence[Document]: """Compress retrieved documents given the query context. @@ -1452,7 +1526,7 @@ class RerankCompressor(BaseDocumentCompressor): self, documents: Sequence[Document], query: str, - callbacks: Optional[Callbacks] = None, + callbacks: Callbacks | None = None, ) -> Sequence[Document]: reranking = self.reranking_function is not None @@ -1460,13 +1534,12 @@ class RerankCompressor(BaseDocumentCompressor): if reranking: scores = await asyncio.to_thread(self.reranking_function, query, documents) else: - from sentence_transformers import util + from sentence_transformers import util as st_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] + doc_texts = [doc.page_content for doc in documents] + document_embedding = await self.embedding_function(doc_texts, RAG_EMBEDDING_CONTENT_PREFIX) + scores = st_util.cos_sim(query_embedding, document_embedding)[0] if scores is not None: docs_with_scores = list( diff --git a/backend/open_webui/retrieval/vector/dbs/chroma.py b/backend/open_webui/retrieval/vector/dbs/chroma.py index 4ace732b2d..cd0a59eeaf 100755 --- a/backend/open_webui/retrieval/vector/dbs/chroma.py +++ b/backend/open_webui/retrieval/vector/dbs/chroma.py @@ -1,29 +1,27 @@ -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 - +import chromadb +from chromadb import Settings +from chromadb.utils.batch_utils import create_batches from open_webui.config import ( + CHROMA_CLIENT_AUTH_CREDENTIALS, + CHROMA_CLIENT_AUTH_PROVIDER, CHROMA_DATA_PATH, + CHROMA_DATABASE, + CHROMA_HTTP_HEADERS, CHROMA_HTTP_HOST, CHROMA_HTTP_PORT, - CHROMA_HTTP_HEADERS, CHROMA_HTTP_SSL, CHROMA_TENANT, - CHROMA_DATABASE, - CHROMA_CLIENT_AUTH_PROVIDER, - CHROMA_CLIENT_AUTH_CREDENTIALS, ) +from open_webui.retrieval.vector.main import ( + GetResult, + SearchResult, + VectorDBBase, + VectorItem, +) +from open_webui.retrieval.vector.utils import process_metadata log = logging.getLogger(__name__) diff --git a/backend/open_webui/retrieval/vector/dbs/elasticsearch.py b/backend/open_webui/retrieval/vector/dbs/elasticsearch.py index 201a5e1706..da3e6e93c8 100644 --- a/backend/open_webui/retrieval/vector/dbs/elasticsearch.py +++ b/backend/open_webui/retrieval/vector/dbs/elasticsearch.py @@ -2,28 +2,28 @@ 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 typing import Optional -from open_webui.retrieval.vector.utils import process_metadata -from open_webui.retrieval.vector.main import ( - VectorDBBase, - VectorItem, - SearchResult, - GetResult, -) +from elasticsearch import BadRequestError, Elasticsearch +from elasticsearch.helpers import bulk, scan from open_webui.config import ( - ELASTICSEARCH_URL, - ELASTICSEARCH_CA_CERTS, ELASTICSEARCH_API_KEY, - ELASTICSEARCH_USERNAME, - ELASTICSEARCH_PASSWORD, + ELASTICSEARCH_CA_CERTS, ELASTICSEARCH_CLOUD_ID, ELASTICSEARCH_INDEX_PREFIX, + ELASTICSEARCH_PASSWORD, + ELASTICSEARCH_URL, + ELASTICSEARCH_USERNAME, SSL_ASSERT_FINGERPRINT, ) +from open_webui.retrieval.vector.main import ( + GetResult, + SearchResult, + VectorDBBase, + VectorItem, +) +from open_webui.retrieval.vector.utils import process_metadata class ElasticsearchClient(VectorDBBase): diff --git a/backend/open_webui/retrieval/vector/dbs/mariadb_vector.py b/backend/open_webui/retrieval/vector/dbs/mariadb_vector.py index 1cb3563382..a8cf62f7b1 100644 --- a/backend/open_webui/retrieval/vector/dbs/mariadb_vector.py +++ b/backend/open_webui/retrieval/vector/dbs/mariadb_vector.py @@ -13,18 +13,15 @@ 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, + MARIADB_VECTOR_POOL_SIZE, + MARIADB_VECTOR_POOL_TIMEOUT, ) from open_webui.retrieval.vector.main import ( GetResult, @@ -33,6 +30,8 @@ from open_webui.retrieval.vector.main import ( VectorItem, ) from open_webui.retrieval.vector.utils import process_metadata +from sqlalchemy import create_engine +from sqlalchemy.pool import NullPool, QueuePool log = logging.getLogger(__name__) diff --git a/backend/open_webui/retrieval/vector/dbs/milvus.py b/backend/open_webui/retrieval/vector/dbs/milvus.py index 2f3d8f3890..9b356b1c69 100644 --- a/backend/open_webui/retrieval/vector/dbs/milvus.py +++ b/backend/open_webui/retrieval/vector/dbs/milvus.py @@ -2,33 +2,31 @@ 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, + MILVUS_HNSW_EFCONSTRUCTION, + MILVUS_HNSW_M, + MILVUS_INDEX_TYPE, + MILVUS_IVF_FLAT_NLIST, + MILVUS_METRIC_TYPE, + MILVUS_TOKEN, + MILVUS_URI, ) +from open_webui.retrieval.vector.main import ( + GetResult, + SearchResult, + VectorDBBase, + VectorItem, +) +from open_webui.retrieval.vector.utils import process_metadata +from pymilvus import Collection, DataType, FieldSchema, connections +from pymilvus import MilvusClient as Client log = logging.getLogger(__name__) diff --git a/backend/open_webui/retrieval/vector/dbs/milvus_multitenancy.py b/backend/open_webui/retrieval/vector/dbs/milvus_multitenancy.py index 93b4a8cbc4..af64919b6e 100644 --- a/backend/open_webui/retrieval/vector/dbs/milvus_multitenancy.py +++ b/backend/open_webui/retrieval/vector/dbs/milvus_multitenancy.py @@ -3,18 +3,19 @@ NOTE: This vector database integration is community-supported and maintained on """ import logging -from typing import Optional, Tuple, List, Dict, Any +import re +from typing import Any, Dict, List, Optional, Tuple from open_webui.config import ( - MILVUS_URI, - MILVUS_TOKEN, - MILVUS_DB, MILVUS_COLLECTION_PREFIX, - MILVUS_INDEX_TYPE, - MILVUS_METRIC_TYPE, - MILVUS_HNSW_M, + MILVUS_DB, MILVUS_HNSW_EFCONSTRUCTION, + MILVUS_HNSW_M, + MILVUS_INDEX_TYPE, MILVUS_IVF_FLAT_NLIST, + MILVUS_METRIC_TYPE, + MILVUS_TOKEN, + MILVUS_URI, ) from open_webui.retrieval.vector.main import ( GetResult, @@ -23,18 +24,42 @@ from open_webui.retrieval.vector.main import ( VectorItem, ) from pymilvus import ( - connections, - utility, Collection, CollectionSchema, - FieldSchema, DataType, + FieldSchema, + connections, + utility, ) log = logging.getLogger(__name__) RESOURCE_ID_FIELD = 'resource_id' +# Milvus expressions are SQL-like strings with no parameterized-query API; +# values get interpolated into single-quoted literals. Reject anything that +# can't be a legitimate Open WebUI collection name. +_SAFE_RESOURCE_ID_RE = re.compile(r'^[A-Za-z0-9_-]{1,255}$') +_SAFE_METADATA_KEY_RE = re.compile(r'^[A-Za-z_][A-Za-z0-9_]{0,63}$') + + +def _validate_resource_id(resource_id: str) -> str: + if not isinstance(resource_id, str) or not _SAFE_RESOURCE_ID_RE.match(resource_id): + raise ValueError(f'Invalid Milvus resource_id (collection name): {resource_id!r}') + return resource_id + + +def _validate_metadata_key(key: str) -> str: + if not isinstance(key, str) or not _SAFE_METADATA_KEY_RE.match(key): + raise ValueError(f'Invalid Milvus metadata filter key: {key!r}') + return key + + +def _escape_milvus_string(value: str) -> str: + if not isinstance(value, str): + raise TypeError(f'Expected str for Milvus expression value, got {type(value).__name__}') + return value.replace('\\', '\\\\').replace("'", "\\'") + class MilvusClient(VectorDBBase): def __init__(self): @@ -126,6 +151,7 @@ class MilvusClient(VectorDBBase): def has_collection(self, collection_name: str) -> bool: mt_collection, resource_id = self._get_collection_and_resource_id(collection_name) + _validate_resource_id(resource_id) if not utility.has_collection(mt_collection): return False @@ -138,6 +164,7 @@ class MilvusClient(VectorDBBase): if not items: return mt_collection, resource_id = self._get_collection_and_resource_id(collection_name) + _validate_resource_id(resource_id) dimension = len(items[0]['vector']) self._ensure_collection(mt_collection, dimension) collection = Collection(mt_collection) @@ -165,6 +192,7 @@ class MilvusClient(VectorDBBase): return None mt_collection, resource_id = self._get_collection_and_resource_id(collection_name) + _validate_resource_id(resource_id) if not utility.has_collection(mt_collection): return None @@ -203,21 +231,22 @@ class MilvusClient(VectorDBBase): filter: Optional[Dict[str, Any]] = None, ): mt_collection, resource_id = self._get_collection_and_resource_id(collection_name) + _validate_resource_id(resource_id) 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]) + id_list_str = ', '.join([f"'{_escape_milvus_string(str(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}'") + _validate_metadata_key(key) + expr.append(f"metadata['{key}'] == '{_escape_milvus_string(str(value))}'") collection.delete(' and '.join(expr)) @@ -228,6 +257,7 @@ class MilvusClient(VectorDBBase): def delete_collection(self, collection_name: str): mt_collection, resource_id = self._get_collection_and_resource_id(collection_name) + _validate_resource_id(resource_id) if not utility.has_collection(mt_collection): return @@ -236,6 +266,7 @@ class MilvusClient(VectorDBBase): 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) + _validate_resource_id(resource_id) if not utility.has_collection(mt_collection): return None @@ -245,10 +276,15 @@ class MilvusClient(VectorDBBase): expr = [f"{RESOURCE_ID_FIELD} == '{resource_id}'"] if filter: for key, value in filter.items(): + _validate_metadata_key(key) if isinstance(value, str): - expr.append(f"metadata['{key}'] == '{value}'") - else: + expr.append(f"metadata['{key}'] == '{_escape_milvus_string(value)}'") + elif isinstance(value, bool): + expr.append(f"metadata['{key}'] == {str(value).lower()}") + elif isinstance(value, (int, float)): expr.append(f"metadata['{key}'] == {value}") + else: + raise TypeError(f'Unsupported Milvus filter value type for key {key!r}: {type(value).__name__}') iterator = collection.query_iterator( expr=' and '.join(expr), diff --git a/backend/open_webui/retrieval/vector/dbs/opengauss.py b/backend/open_webui/retrieval/vector/dbs/opengauss.py index ac97cf01fa..1c9d35253c 100644 --- a/backend/open_webui/retrieval/vector/dbs/opengauss.py +++ b/backend/open_webui/retrieval/vector/dbs/opengauss.py @@ -2,37 +2,36 @@ NOTE: This vector database integration is community-supported and maintained on a best-effort basis. """ -from typing import Optional, List, Dict, Any +import json import logging import re -import json +from typing import Any, Dict, List, Optional + +from pgvector.sqlalchemy import Vector from sqlalchemy import ( - func, - literal, + Column, + Integer, + LargeBinary, + MetaData, + Table, + Text, cast, column, create_engine, - Column, - Integer, - MetaData, - LargeBinary, + func, + literal, 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 +from sqlalchemy.dialects.postgresql import JSONB, array +from sqlalchemy.dialects.postgresql.psycopg2 import PGDialect_psycopg2 +from sqlalchemy.exc import NoSuchTableError +from sqlalchemy.ext.mutable import MutableDict +from sqlalchemy.orm import declarative_base, scoped_session, sessionmaker +from sqlalchemy.pool import NullPool, QueuePool +from sqlalchemy.sql import true class OpenGaussDialect(PGDialect_psycopg2): @@ -56,23 +55,22 @@ class OpenGaussDialect(PGDialect_psycopg2): # 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, + OPENGAUSS_POOL_SIZE, + OPENGAUSS_POOL_TIMEOUT, ) - from open_webui.env import SRC_LOG_LEVELS +from open_webui.retrieval.vector.main import ( + GetResult, + SearchResult, + VectorDBBase, + VectorItem, +) +from open_webui.retrieval.vector.utils import process_metadata VECTOR_LENGTH = OPENGAUSS_INITIALIZE_MAX_VECTOR_LENGTH Base = declarative_base() diff --git a/backend/open_webui/retrieval/vector/dbs/opensearch.py b/backend/open_webui/retrieval/vector/dbs/opensearch.py index a08dca7865..798802cd54 100644 --- a/backend/open_webui/retrieval/vector/dbs/opensearch.py +++ b/backend/open_webui/retrieval/vector/dbs/opensearch.py @@ -2,24 +2,24 @@ 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.config import ( + OPENSEARCH_CERT_VERIFY, + OPENSEARCH_PASSWORD, + OPENSEARCH_SSL, + OPENSEARCH_URI, + OPENSEARCH_USERNAME, +) from open_webui.retrieval.vector.main import ( + GetResult, + SearchResult, VectorDBBase, VectorItem, - SearchResult, - GetResult, -) -from open_webui.config import ( - OPENSEARCH_URI, - OPENSEARCH_SSL, - OPENSEARCH_CERT_VERIFY, - OPENSEARCH_USERNAME, - OPENSEARCH_PASSWORD, ) +from open_webui.retrieval.vector.utils import process_metadata +from opensearchpy import OpenSearch +from opensearchpy.helpers import bulk class OpenSearchClient(VectorDBBase): diff --git a/backend/open_webui/retrieval/vector/dbs/oracle23ai.py b/backend/open_webui/retrieval/vector/dbs/oracle23ai.py index 9a5bd638d9..b09eacb81d 100644 --- a/backend/open_webui/retrieval/vector/dbs/oracle23ai.py +++ b/backend/open_webui/retrieval/vector/dbs/oracle23ai.py @@ -28,34 +28,33 @@ ORACLE_DB_POOL_MAX = 10 ORACLE_DB_POOL_INCREMENT = 1 """ -from typing import Optional, List, Dict, Any, Union -from decimal import Decimal +import array +import json import logging import os import threading import time -import json -import array +from decimal import Decimal +from typing import Any, Dict, List, Optional, Union + import oracledb - -from open_webui.retrieval.vector.main import ( - VectorDBBase, - VectorItem, - SearchResult, - GetResult, -) - from open_webui.config import ( + ORACLE_DB_DSN, + ORACLE_DB_PASSWORD, + ORACLE_DB_POOL_INCREMENT, + ORACLE_DB_POOL_MAX, + ORACLE_DB_POOL_MIN, ORACLE_DB_USE_WALLET, ORACLE_DB_USER, - ORACLE_DB_PASSWORD, - ORACLE_DB_DSN, + ORACLE_VECTOR_LENGTH, ORACLE_WALLET_DIR, ORACLE_WALLET_PASSWORD, - ORACLE_VECTOR_LENGTH, - ORACLE_DB_POOL_MIN, - ORACLE_DB_POOL_MAX, - ORACLE_DB_POOL_INCREMENT, +) +from open_webui.retrieval.vector.main import ( + GetResult, + SearchResult, + VectorDBBase, + VectorItem, ) log = logging.getLogger(__name__) diff --git a/backend/open_webui/retrieval/vector/dbs/pgvector.py b/backend/open_webui/retrieval/vector/dbs/pgvector.py index 90e65b9ad0..861d49bc1b 100644 --- a/backend/open_webui/retrieval/vector/dbs/pgvector.py +++ b/backend/open_webui/retrieval/vector/dbs/pgvector.py @@ -1,56 +1,54 @@ -from typing import Optional, List, Dict, Any, Tuple -import logging import json +import logging +from typing import Any, Dict, List, Optional, Tuple + +from open_webui.config import ( + PGVECTOR_CREATE_EXTENSION, + PGVECTOR_DB_URL, + PGVECTOR_HNSW_EF_CONSTRUCTION, + PGVECTOR_HNSW_M, + PGVECTOR_INDEX_METHOD, + PGVECTOR_INITIALIZE_MAX_VECTOR_LENGTH, + PGVECTOR_IVFFLAT_LISTS, + PGVECTOR_PGCRYPTO, + PGVECTOR_PGCRYPTO_KEY, + PGVECTOR_POOL_MAX_OVERFLOW, + PGVECTOR_POOL_RECYCLE, + PGVECTOR_POOL_SIZE, + PGVECTOR_POOL_TIMEOUT, + PGVECTOR_USE_HALFVEC, +) +from open_webui.retrieval.vector.main import ( + GetResult, + SearchResult, + VectorDBBase, + VectorItem, +) +from open_webui.retrieval.vector.utils import process_metadata +from open_webui.utils.misc import sanitize_text_for_db +from pgvector.sqlalchemy import HALFVEC, Vector from sqlalchemy import ( - func, - literal, + Column, + Integer, + LargeBinary, + MetaData, + Table, + Text, cast, column, create_engine, - Column, - Integer, - MetaData, - LargeBinary, + func, + literal, 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, -) +from sqlalchemy.ext.mutable import MutableDict +from sqlalchemy.orm import declarative_base, scoped_session, sessionmaker +from sqlalchemy.pool import NullPool, QueuePool +from sqlalchemy.sql import true VECTOR_LENGTH = PGVECTOR_INITIALIZE_MAX_VECTOR_LENGTH USE_HALFVEC = PGVECTOR_USE_HALFVEC diff --git a/backend/open_webui/retrieval/vector/dbs/pinecone.py b/backend/open_webui/retrieval/vector/dbs/pinecone.py index 6469ac9172..7e2b6e2dfa 100644 --- a/backend/open_webui/retrieval/vector/dbs/pinecone.py +++ b/backend/open_webui/retrieval/vector/dbs/pinecone.py @@ -2,9 +2,10 @@ 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 typing import Any, Dict, List, Optional, Union + from pinecone import Pinecone, ServerlessSpec # Add gRPC support for better performance (Pinecone best practice) @@ -16,24 +17,23 @@ 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 functools # for partial binding in async tasks 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_CLOUD, + PINECONE_DIMENSION, PINECONE_ENVIRONMENT, PINECONE_INDEX_NAME, - PINECONE_DIMENSION, PINECONE_METRIC, - PINECONE_CLOUD, +) +from open_webui.retrieval.vector.main import ( + GetResult, + SearchResult, + VectorDBBase, + VectorItem, ) from open_webui.retrieval.vector.utils import process_metadata diff --git a/backend/open_webui/retrieval/vector/dbs/qdrant.py b/backend/open_webui/retrieval/vector/dbs/qdrant.py index f050bebeb5..7156a16c53 100644 --- a/backend/open_webui/retrieval/vector/dbs/qdrant.py +++ b/backend/open_webui/retrieval/vector/dbs/qdrant.py @@ -2,31 +2,30 @@ NOTE: This vector database integration is community-supported and maintained on a best-effort basis. """ -from typing import Optional import logging +from typing import Optional from urllib.parse import urlparse +from open_webui.config import ( + QDRANT_API_KEY, + QDRANT_COLLECTION_PREFIX, + QDRANT_GRPC_PORT, + QDRANT_HNSW_M, + QDRANT_ON_DISK, + QDRANT_PREFER_GRPC, + QDRANT_TIMEOUT, + QDRANT_URI, +) +from open_webui.retrieval.vector.main import ( + GetResult, + SearchResult, + VectorDBBase, + VectorItem, +) 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__) @@ -217,28 +216,23 @@ class QdrantClient(VectorDBBase): ids: Optional[list[str]] = None, filter: Optional[dict] = None, ): - # Delete the items from the collection based on the ids. - field_conditions = [] - + # Delete by point ID: the point ID is the item's id (see _create_points). + # Filtering on metadata.id silently misses points whose payload omits an + # id (e.g. memories), leaving orphaned vectors behind. if ids: - for id_value in ids: - ( - field_conditions.append( - models.FieldCondition( - key='metadata.id', - match=models.MatchValue(value=id_value), - ), - ), - ) - elif filter: + return self.client.delete( + collection_name=f'{self.collection_prefix}_{collection_name}', + points_selector=models.PointIdsList(points=ids), + ) + + field_conditions = [] + if filter: for key, value in filter.items(): - ( - field_conditions.append( - models.FieldCondition( - key=f'metadata.{key}', - match=models.MatchValue(value=value), - ), - ), + field_conditions.append( + models.FieldCondition( + key=f'metadata.{key}', + match=models.MatchValue(value=value), + ) ) return self.client.delete( diff --git a/backend/open_webui/retrieval/vector/dbs/qdrant_multitenancy.py b/backend/open_webui/retrieval/vector/dbs/qdrant_multitenancy.py index c3c2ba41d0..9b717644c5 100644 --- a/backend/open_webui/retrieval/vector/dbs/qdrant_multitenancy.py +++ b/backend/open_webui/retrieval/vector/dbs/qdrant_multitenancy.py @@ -3,19 +3,19 @@ NOTE: This vector database integration is community-supported and maintained on """ import logging -from typing import Optional, Tuple, List, Dict, Any +from typing import Any, Dict, List, Optional, Tuple from urllib.parse import urlparse import grpc from open_webui.config import ( QDRANT_API_KEY, + QDRANT_COLLECTION_PREFIX, QDRANT_GRPC_PORT, + QDRANT_HNSW_M, QDRANT_ON_DISK, QDRANT_PREFER_GRPC, - QDRANT_URI, - QDRANT_COLLECTION_PREFIX, QDRANT_TIMEOUT, - QDRANT_HNSW_M, + QDRANT_URI, ) from open_webui.retrieval.vector.main import ( GetResult, @@ -228,15 +228,17 @@ class QdrantClient(VectorDBBase): return None must_conditions = [_tenant_filter(tenant_id)] - should_conditions = [] if ids: - should_conditions = [_metadata_filter('id', id_value) for id_value in ids] + # Delete by point ID within the tenant. The point ID is the item's id + # (see _create_points); filtering on metadata.id silently misses points + # whose payload omits an id (e.g. memories), leaving orphaned vectors. + must_conditions.append(models.HasIdCondition(has_id=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)), + points_selector=models.FilterSelector(filter=models.Filter(must=must_conditions)), ) def search( diff --git a/backend/open_webui/retrieval/vector/dbs/s3vector.py b/backend/open_webui/retrieval/vector/dbs/s3vector.py index 8877d206e6..e0b156931c 100644 --- a/backend/open_webui/retrieval/vector/dbs/s3vector.py +++ b/backend/open_webui/retrieval/vector/dbs/s3vector.py @@ -2,17 +2,18 @@ NOTE: This vector database integration is community-supported and maintained on a best-effort basis. """ -from open_webui.retrieval.vector.utils import process_metadata +import logging +from typing import Any, Dict, List, Optional, Union + +import boto3 +from open_webui.config import S3_VECTOR_BUCKET_NAME, S3_VECTOR_REGION from open_webui.retrieval.vector.main import ( - VectorDBBase, - VectorItem, GetResult, SearchResult, + VectorDBBase, + VectorItem, ) -from open_webui.config import S3_VECTOR_BUCKET_NAME, S3_VECTOR_REGION -from typing import List, Optional, Dict, Any, Union -import logging -import boto3 +from open_webui.retrieval.vector.utils import process_metadata log = logging.getLogger(__name__) diff --git a/backend/open_webui/retrieval/vector/dbs/valkey.py b/backend/open_webui/retrieval/vector/dbs/valkey.py new file mode 100644 index 0000000000..1db10281a4 --- /dev/null +++ b/backend/open_webui/retrieval/vector/dbs/valkey.py @@ -0,0 +1,765 @@ +# NOTE: This vector database integration is community-supported and maintained on a best-effort basis. +# Requires Valkey core >= 9.0.1 with the valkey-search module >= 1.2.0 loaded. + +import atexit +import json +import logging +import re +import struct +from urllib.parse import urlparse + +from open_webui.config import ( + VALKEY_COLLECTION_PREFIX, + VALKEY_DISTANCE_METRIC, + VALKEY_HNSW_EF_CONSTRUCTION, + VALKEY_HNSW_EF_RUNTIME, + VALKEY_HNSW_M, + VALKEY_INDEX_TYPE, + VALKEY_URL, +) +from open_webui.retrieval.vector.main import ( + GetResult, + SearchResult, + VectorDBBase, + VectorItem, +) +from open_webui.retrieval.vector.utils import process_metadata + +log = logging.getLogger(__name__) + + +def _import_glide(): + """Lazily import glide_sync so the module can be loaded without valkey-glide-sync installed.""" + try: + from glide_sync import ( + Batch, + DataType, + DistanceMetricType, + FtCreateOptions, + FtSearchLimit, + FtSearchOptions, + GlideClient, + GlideClientConfiguration, + NodeAddress, + RequestError, + ReturnField, + TagField, + TextField, + VectorAlgorithm, + VectorField, + VectorFieldAttributesFlat, + VectorFieldAttributesHnsw, + VectorType, + ) + from glide_sync import ( + ft as glide_ft, + ) + except ImportError as e: + raise ImportError( + 'valkey-glide-sync is required when VECTOR_DB=valkey. Install it with: pip install valkey-glide-sync==2.3.1' + ) from e + return { + 'Batch': Batch, + 'DataType': DataType, + 'DistanceMetricType': DistanceMetricType, + 'FtCreateOptions': FtCreateOptions, + 'FtSearchLimit': FtSearchLimit, + 'FtSearchOptions': FtSearchOptions, + 'GlideClient': GlideClient, + 'GlideClientConfiguration': GlideClientConfiguration, + 'NodeAddress': NodeAddress, + 'RequestError': RequestError, + 'ReturnField': ReturnField, + 'TagField': TagField, + 'TextField': TextField, + 'VectorAlgorithm': VectorAlgorithm, + 'VectorField': VectorField, + 'VectorFieldAttributesFlat': VectorFieldAttributesFlat, + 'VectorFieldAttributesHnsw': VectorFieldAttributesHnsw, + 'VectorType': VectorType, + 'glide_ft': glide_ft, + } + + +# valkey-search 1.2.0 requires Valkey core 9.0.1+ per upstream release notes. +# Unlike RediSearch (dialects 1-4), valkey-search only implements DIALECT 2 — GLIDE's +# FtSearchOptions doesn't expose a dialect parameter because it's always dialect 2. +MIN_VALKEY_VERSION = (9, 0, 1) +MIN_SEARCH_MODULE_VERSION = (1, 2, 0) + +_VALID_DISTANCE_METRICS = {'COSINE', 'L2', 'IP'} +_NEVER_MATCH_SENTINEL = '__open_webui_valkey_never_match__' + +# Compile once at module load — includes `?` which is a single-char wildcard in TAG queries. +_TAG_SPECIAL_RE = re.compile(r'([,.<>{}\[\]"\':;!@#$%^&*()\-+=~?\\/| \t\n\r])') + + +_SAFE_FIELD_RE = re.compile(r'^[a-zA-Z_][a-zA-Z0-9_]*$') + + +def _vector_to_bytes(vector: list[float | int]) -> bytes: + """Pack a list of floats as a float32 little-endian binary blob.""" + return struct.pack(f'<{len(vector)}f', *vector) + + +def _escape_tag_value(value: str) -> str: + """Escape special characters for RediSearch/Valkey-Search TAG field queries.""" + return _TAG_SPECIAL_RE.sub(r'\\\1', str(value)) + + +def _build_filter_expression(filter: dict) -> str: + """Translate a Chroma-style filter dict into a valkey-search filter expression. + + Supports simple equality, $in, $ne, and $eq. Multiple keys are ANDed together. + Raises ValueError on unsupported operators rather than silently matching nothing. + """ + parts = [] + for key, value in filter.items(): + if not _SAFE_FIELD_RE.match(key): + raise ValueError( + f'Invalid filter field name: {key!r}. ' + 'Field names must start with a letter or underscore and contain only alphanumerics/underscores.' + ) + if isinstance(value, dict): + for op, operand in value.items(): + if op == '$in' and isinstance(operand, list): + if not operand: + # Empty $in → match nothing, not "match all". + parts.append(f'@{key}:{{{_NEVER_MATCH_SENTINEL}}}') + continue + escaped = [_escape_tag_value(str(v)) for v in operand] + parts.append(f'@{key}:{{{"|".join(escaped)}}}') + elif op in ('$eq', '$ne'): + prefix = '-' if op == '$ne' else '' + parts.append(f'{prefix}@{key}:{{{_escape_tag_value(str(operand))}}}') + else: + raise ValueError( + f'Unsupported filter operator {op!r} for key {key!r}. Supported operators: $in, $ne, $eq.' + ) + else: + parts.append(f'@{key}:{{{_escape_tag_value(str(value))}}}') + return ' '.join(parts) + + +def _decode(value) -> str: + """Decode bytes to str; pass through str unchanged.""" + if isinstance(value, (bytes, bytearray)): + return value.decode() + return str(value) if value is not None else '' + + +class ValkeyClient(VectorDBBase): + def __init__(self): + if not VALKEY_URL: + raise ValueError( + 'VALKEY_URL is required when VECTOR_DB=valkey. ' + 'Set it to your Valkey server URL (e.g., valkey://localhost:6379).' + ) + + # Lazily import glide_sync — only needed when this backend is actually used. + self._g = _import_glide() + + # Validate distance metric at init — invalid values pass through to FT.CREATE + # and fail with a cryptic server error. + metric = VALKEY_DISTANCE_METRIC.upper() + if metric not in _VALID_DISTANCE_METRICS: + raise ValueError( + f'Invalid VALKEY_DISTANCE_METRIC={VALKEY_DISTANCE_METRIC!r}. ' + f'Must be one of: {", ".join(sorted(_VALID_DISTANCE_METRICS))}.' + ) + + DistanceMetricType = self._g['DistanceMetricType'] + self._distance_metric_map = { + 'COSINE': DistanceMetricType.COSINE, + 'L2': DistanceMetricType.L2, + 'IP': DistanceMetricType.IP, + } + + self.collection_prefix = VALKEY_COLLECTION_PREFIX + self.index_type = VALKEY_INDEX_TYPE + self.distance_metric = metric + + parsed = urlparse(VALKEY_URL) + host = parsed.hostname or 'localhost' + port = parsed.port or 6379 + db = int(parsed.path.lstrip('/') or 0) + + GlideClientConfiguration = self._g['GlideClientConfiguration'] + NodeAddress = self._g['NodeAddress'] + GlideClient = self._g['GlideClient'] + + config = GlideClientConfiguration( + addresses=[NodeAddress(host=host, port=port)], + database_id=db if db else None, + request_timeout=5000, + client_name='open_webui_vector_store_client', + ) + try: + self.client = GlideClient.create(config) + except Exception as e: + raise ConnectionError(f'Failed to connect to Valkey at {host}:{port}: {e}') from e + + # Separate client for batch writes — large HSET payloads on the multiplexed + # connection can starve concurrent reads. + batch_config = GlideClientConfiguration( + addresses=[NodeAddress(host=host, port=port)], + database_id=db if db else None, + request_timeout=10000, # 10s — HNSW indexing can take 1-4s per vector + client_name='open_webui_vector_store_batch_client', + ) + try: + self.batch_client = GlideClient.create(batch_config) + except Exception as e: + raise ConnectionError(f'Failed to create batch write client for Valkey at {host}:{port}: {e}') from e + + try: + self.client.ping() + except Exception as e: + raise ConnectionError(f'Failed to ping Valkey at {host}:{port}: {e}') from e + + # Catch misconfigured deployments at startup (e.g., valkey-bundle:9.0.1 ships + # valkey-search 1.0.0 which lacks TEXT fields and filter-only FT.SEARCH). + self._check_core_version() + self._check_search_module() + + atexit.register(self.close) + + def close(self) -> None: + """Close both GLIDE clients, flushing in-flight requests.""" + try: + self.client.close() + except Exception: + pass + try: + self.batch_client.close() + except Exception: + pass + + # ----- version checks ---------------------------------------------------- + + @staticmethod + def _parse_semver(version_str: str) -> tuple[int, int, int] | None: + if not version_str: + return None + m = re.match(r'^(\d+)\.(\d+)\.(\d+)', version_str) + return (int(m.group(1)), int(m.group(2)), int(m.group(3))) if m else None + + @staticmethod + def _format_version(v: tuple[int, int, int]) -> str: + return f'{v[0]}.{v[1]}.{v[2]}' + + def _check_core_version(self) -> None: + try: + info_raw = self.client.info() + except Exception as e: + log.warning(f'Could not fetch Valkey INFO for version check, proceeding: {e}') + return + + raw = None + text = _decode(info_raw) if info_raw else '' + redis_fallback = None + for line in text.splitlines(): + if line.startswith('valkey_version:'): + raw = line.split(':', 1)[1].strip() + break + if line.startswith('redis_version:') and redis_fallback is None: + redis_fallback = line.split(':', 1)[1].strip() + if raw is None: + raw = redis_fallback + version = self._parse_semver(raw) if raw else None + + if version is None: + log.warning( + f'Could not determine Valkey core version (raw={raw!r}); proceeding but ' + f'minimum {self._format_version(MIN_VALKEY_VERSION)} is required.' + ) + elif version < MIN_VALKEY_VERSION: + raise RuntimeError( + f'Valkey core {self._format_version(version)} is below the minimum required version ' + f'{self._format_version(MIN_VALKEY_VERSION)}. valkey-search 1.2.0 requires Valkey core ' + '9.0.1 or later. Upgrade your server or use valkey-bundle:9.1.0-rc2+.' + ) + log.info(f'Valkey core version: {self._format_version(version) if version else "unknown"}') + + def _check_search_module(self) -> None: + try: + modules = self.client.custom_command(['MODULE', 'LIST']) + except Exception as e: + log.warning( + f'Could not list modules on the Valkey server ({e}); proceeding but ' + f'valkey-search >= {self._format_version(MIN_SEARCH_MODULE_VERSION)} is required.' + ) + return + + # MODULE LIST returns [{b'name': b'search', b'ver': 66048, ...}] + # ver encoding: major*10000 + minor*100 + patch + search_version: tuple[int, int, int] | None = None + module_present = False + raw_ver = None + for entry in modules or []: + if isinstance(entry, dict): + name = _decode(entry.get(b'name') or entry.get('name') or '') + raw_ver = entry.get(b'ver') or entry.get('ver', 0) + else: + parsed = self._decode_kv_pairs(entry) + name = parsed.get('name', '') + raw_ver = parsed.get('ver', 0) + if name.lower() == 'search': + module_present = True + try: + ver_int = int(raw_ver) + search_version = (ver_int // 10000, (ver_int % 10000) // 100, ver_int % 100) + except (TypeError, ValueError): + search_version = None + break + + if not module_present: + raise RuntimeError( + 'The valkey-search module is not loaded on the Valkey server. ' + f'This backend requires valkey-search >= {self._format_version(MIN_SEARCH_MODULE_VERSION)}. ' + 'Use valkey-bundle:9.1.0-rc2+ or load libsearch.so via --loadmodule on a Valkey 9.0.1+ server.' + ) + if search_version is None: + log.warning( + f'valkey-search module is loaded but version could not be parsed (raw={raw_ver!r}); ' + f'proceeding but minimum {self._format_version(MIN_SEARCH_MODULE_VERSION)} is required.' + ) + elif search_version < MIN_SEARCH_MODULE_VERSION: + raise RuntimeError( + f'valkey-search {self._format_version(search_version)} is below the minimum required ' + f'version {self._format_version(MIN_SEARCH_MODULE_VERSION)}. Earlier versions lack the ' + 'TEXT field type and filter-only FT.SEARCH support required by this backend. ' + 'Upgrade to valkey-bundle:9.1.0-rc2+ or load valkey-search 1.2.0+ as a module.' + ) + log.info(f'valkey-search version: {self._format_version(search_version) if search_version else "unknown"}') + + def _index_name(self, collection_name: str) -> str: + return f'idx:{self.collection_prefix}:{collection_name}' + + def _key_prefix(self, collection_name: str) -> str: + return f'{self.collection_prefix}:{collection_name}:' + + def _item_key(self, collection_name: str, item_id: str) -> str: + return f'{self.collection_prefix}:{collection_name}:{item_id}' + + def _create_index(self, collection_name: str, dimension: int) -> None: + """Create an FT index for a collection with the given vector dimension.""" + g = self._g + index_name = self._index_name(collection_name) + prefix = self._key_prefix(collection_name) + distance_metric = self._distance_metric_map[self.distance_metric] + + if self.index_type == 'HNSW': + vector_attrs = g['VectorFieldAttributesHnsw']( + dimensions=dimension, + distance_metric=distance_metric, + type=g['VectorType'].FLOAT32, + number_of_edges=VALKEY_HNSW_M, + vectors_examined_on_construction=VALKEY_HNSW_EF_CONSTRUCTION, + vectors_examined_on_runtime=VALKEY_HNSW_EF_RUNTIME, + ) + algo = g['VectorAlgorithm'].HNSW + else: + if self.index_type != 'FLAT': + log.warning(f'Unrecognized VALKEY_INDEX_TYPE={self.index_type!r}; falling back to FLAT.') + vector_attrs = g['VectorFieldAttributesFlat']( + dimensions=dimension, + distance_metric=distance_metric, + type=g['VectorType'].FLOAT32, + ) + algo = g['VectorAlgorithm'].FLAT + + schema = [ + g['VectorField'](name='vector', algorithm=algo, attributes=vector_attrs), + g['TextField'](name='text'), + g['TagField'](name='id'), + g['TextField'](name='metadata_json'), + g['TagField'](name='hash'), + g['TagField'](name='file_id'), + g['TagField'](name='source'), + g['TagField'](name='knowledge_base_id'), + ] + + options = g['FtCreateOptions'](data_type=g['DataType'].HASH, prefixes=[prefix]) + + try: + g['glide_ft'].create(self.client, index_name, schema, options) + log.info( + f'Created Valkey index {index_name} with dimension={dimension}, ' + f'type={self.index_type}, metric={self.distance_metric}' + ) + except g['RequestError'] as e: + if 'already exists' in str(e).lower(): + log.debug(f'Index {index_name} already exists, skipping creation.') + else: + raise + + def _verify_collection_dimension(self, collection_name: str, dimension: int) -> None: + index_name = self._index_name(collection_name) + try: + info = self._g['glide_ft'].info(self.client, index_name) + except Exception as e: + log.warning(f'Could not FT.INFO {index_name} for dimension check, skipping: {e}') + return + + # ft.info response has nested structure: b'attributes' → list of fields, + # each field is [k1, v1, ...] with a nested 'index' sub-list containing 'dimensions'. + existing = None + attrs = None + if isinstance(info, dict): + attrs = info.get(b'attributes') or info.get('attributes') + elif isinstance(info, (list, tuple)): + attrs = self._find_in_kv_pairs(info, 'attributes', case_insensitive=True) + + for attr in attrs or []: + if not isinstance(attr, (list, tuple)): + continue + field_type = self._find_in_kv_pairs(attr, 'type', case_insensitive=True) + if _decode(field_type).upper() != 'VECTOR': + continue + index_params = self._find_in_kv_pairs(attr, 'index', case_insensitive=True) + if index_params and isinstance(index_params, (list, tuple)): + dim_raw = self._find_in_kv_pairs(index_params, 'dimensions', case_insensitive=True) + if dim_raw is not None: + try: + existing = int(dim_raw) + except (ValueError, TypeError): + pass + break + + if existing is None: + log.warning( + f'Could not determine vector dimension for {index_name} from FT.INFO response, ' + 'skipping dimension check.' + ) + return + if existing != dimension: + raise ValueError( + f'Collection {collection_name!r} was created with dim={existing}, refusing to ' + f'insert vectors with dim={dimension}. Recreate the collection (e.g., via ' + 'VECTOR_DB_CLIENT.delete_collection) if you intend to switch embedding models.' + ) + + def has_collection(self, collection_name: str) -> bool: + index_name = self._index_name(collection_name) + try: + self._g['glide_ft'].info(self.client, index_name) + return True + except self._g['RequestError'] as e: + msg = str(e).lower() + if 'no such index' in msg or 'unknown index' in msg or 'not found in database' in msg: + return False + log.warning(f'Unexpected FT.INFO response for collection {collection_name}: {e}') + raise + + def delete_collection(self, collection_name: str): + index_name = self._index_name(collection_name) + try: + self._g['glide_ft'].dropindex(self.client, index_name) + log.info(f'Dropped index {index_name}') + except self._g['RequestError'] as e: + log.debug(f'Could not drop index {index_name}: {e}') + + self._delete_keys_by_prefix(self._key_prefix(collection_name)) + + def insert(self, collection_name: str, items: list[VectorItem]): + if not items: + return + + dimension = len(items[0]['vector']) + if not self.has_collection(collection_name): + self._create_index(collection_name, dimension) + else: + self._verify_collection_dimension(collection_name, dimension) + + # Individual HSET rather than Batch.exec() — each command gets its own timeout. + # HNSW indexing can take 1-4s per vector (ef_construction=200), and Batch.exec() + # applies a single timeout to ALL commands, causing all-or-nothing failures on + # large inserts. + for item in items: + metadata = process_metadata(item['metadata']) if item.get('metadata') else {} + mapping = { + 'id': item['id'], + 'vector': _vector_to_bytes(item['vector']), + 'text': item['text'], + 'metadata_json': json.dumps(metadata), + # `or ''` prevents indexing literal 'None' as a TAG value, which would + # poison $ne / equality queries. + 'hash': str(metadata.get('hash') or ''), + 'file_id': str(metadata.get('file_id') or ''), + 'source': str(metadata.get('source') or ''), + 'knowledge_base_id': str(metadata.get('knowledge_base_id') or ''), + } + self.batch_client.hset(self._item_key(collection_name, item['id']), mapping) + + log.debug(f'Inserted {len(items)} items into collection {collection_name}') + + def upsert(self, collection_name: str, items: list[VectorItem]): + self.insert(collection_name, items) + + def search( + self, + collection_name: str, + vectors: list[list[float | int]], + filter: dict | None = None, + limit: int = 10, + ) -> SearchResult | None: + if not vectors: + return None + if not self.has_collection(collection_name): + return None + + filter_expr = _build_filter_expression(filter) if filter else '' + query_str = ( + f'({filter_expr})=>[KNN {limit} @vector $query_vec]' + if filter_expr + else f'*=>[KNN {limit} @vector $query_vec]' + ) + + g = self._g + try: + opts = g['FtSearchOptions']( + params={'query_vec': _vector_to_bytes(vectors[0])}, + limit=g['FtSearchLimit'](offset=0, count=limit), + ) + result = g['glide_ft'].search(self.client, self._index_name(collection_name), query_str, opts) + except g['RequestError'] as e: + log.error(f'Valkey search error on collection {collection_name}: {e}') + return None + + return self._parse_glide_search_response(result, include_score=True) + + def query(self, collection_name: str, filter: dict, limit: int | None = None) -> GetResult | None: + if not self.has_collection(collection_name): + return None + if not filter: + return self.get(collection_name, limit=limit) + + query_str = _build_filter_expression(filter) + if not query_str: + return self.get(collection_name, limit=limit) + + # Hard cap when no limit provided — FT.SEARCH requires a finite count. + effective_limit = limit if limit and limit > 0 else 10000 + if not (limit and limit > 0): + log.warning( + f'query() called without a limit on collection {collection_name}; ' + f'capping at {effective_limit} results. Pass an explicit limit to avoid silent truncation.' + ) + + g = self._g + try: + opts = g['FtSearchOptions']( + return_fields=[ + g['ReturnField'](field_identifier='id'), + g['ReturnField'](field_identifier='text'), + g['ReturnField'](field_identifier='metadata_json'), + ], + limit=g['FtSearchLimit'](offset=0, count=effective_limit), + ) + result = g['glide_ft'].search(self.client, self._index_name(collection_name), query_str, opts) + except g['RequestError'] as e: + log.error(f'Valkey query error on collection {collection_name}: {e}') + return None + + return self._parse_glide_search_response(result, include_score=False) + + def get(self, collection_name: str, limit: int | None = None) -> GetResult | None: + if not self.has_collection(collection_name): + return None + + # FT.SEARCH "*" wildcard not yet in a tagged valkey-search release (tracked in #957). + # SCAN fallback is acceptable here — get() is not on the hot search path. + prefix = self._key_prefix(collection_name) + ids, documents, metadatas = [], [], [] + cursor = '0' + while True: + scan_result = self.client.scan(cursor=cursor, match=f'{prefix}*', count=500) + cursor = _decode(scan_result[0]) + keys = scan_result[1] + if keys: + batch = self._g['Batch'](is_atomic=False) + for key in keys: + batch.hgetall(key) + results = self.client.exec(batch, raise_on_error=False) or [] + for fields in results: + if not fields: + continue + ids.append(_decode(fields.get(b'id', b''))) + documents.append(_decode(fields.get(b'text', b''))) + try: + metadatas.append(json.loads(_decode(fields.get(b'metadata_json', b'{}')))) + except (json.JSONDecodeError, TypeError): + metadatas.append({}) + if limit is not None and limit > 0 and len(ids) >= limit: + return GetResult(ids=[ids], documents=[documents], metadatas=[metadatas]) + if cursor == '0': + break + + return GetResult(ids=[ids], documents=[documents], metadatas=[metadatas]) + + def delete( + self, + collection_name: str, + ids: list[str] | None = None, + filter: dict | None = None, + ): + if ids: + keys = [self._item_key(collection_name, item_id) for item_id in ids] + try: + self.batch_client.delete(keys) + except self._g['RequestError'] as e: + log.error(f'Valkey delete error on collection {collection_name}: {e}') + return + + if not filter: + return + + filter_expr = _build_filter_expression(filter) + if not filter_expr: + return + + index_name = self._index_name(collection_name) + page_size = 10000 + g = self._g + while True: + try: + opts = g['FtSearchOptions']( + return_fields=[g['ReturnField'](field_identifier='id')], + limit=g['FtSearchLimit'](offset=0, count=page_size), + ) + result = g['glide_ft'].search(self.client, index_name, filter_expr, opts) + except g['RequestError'] as e: + log.error(f'Valkey delete-by-filter error on collection {collection_name}: {e}') + return + + if not result or result[0] == 0: + return + + keys_map = result[1] if len(result) > 1 else {} + keys = [_decode(k) for k in keys_map.keys()] if isinstance(keys_map, dict) else [] + if not keys: + return + self.batch_client.delete(keys) + if len(keys) < page_size: + return + + def reset(self): + glide_ft = self._g['glide_ft'] + collections: list[str] = [] + try: + indexes = glide_ft.list(self.client) or [] + idx_prefix = f'idx:{self.collection_prefix}:' + for idx in indexes: + name = _decode(idx) + if name.startswith(idx_prefix): + collections.append(name[len(idx_prefix) :]) + try: + glide_ft.dropindex(self.client, idx) + log.info(f'Dropped index: {name}') + except Exception as e: + log.error(f'Error dropping index {name}: {e}') + except Exception as e: + log.error(f'Error listing indexes during reset: {e}') + + for collection in collections: + self._delete_keys_by_prefix(self._key_prefix(collection)) + log.info(f'Valkey vector store reset complete (prefix: {self.collection_prefix})') + + def _delete_keys_by_prefix(self, prefix: str) -> None: + cursor = '0' + while True: + scan_result = self.client.scan(cursor=cursor, match=f'{prefix}*', count=500) + cursor = _decode(scan_result[0]) + keys = scan_result[1] + if keys: + self.batch_client.delete(keys) + if cursor == '0': + break + + @staticmethod + def _decode_kv_pairs(fields) -> dict: + """Decode a flat [k1, v1, k2, v2, ...] wire array into a dict.""" + if not fields: + return {} + if len(fields) % 2 != 0: + fields = fields[:-1] + out = {} + for k, v in zip(fields[::2], fields[1::2]): + key = _decode(k) + if isinstance(v, (bytes, bytearray)): + try: + val = v.decode() + except UnicodeDecodeError: + val = v + else: + val = v + out[key] = val + return out + + @staticmethod + def _find_in_kv_pairs(pairs, target: str, case_insensitive: bool = False): + """Look up a value in a flat [k1, v1, k2, v2, ...] array or dict.""" + if isinstance(pairs, dict): + needle = target.lower() if case_insensitive else target + for k, v in pairs.items(): + key = _decode(k) + if (key.lower() if case_insensitive else key) == needle: + return v + return None + if not isinstance(pairs, (list, tuple)) or len(pairs) < 2: + return None + needle = target.lower() if case_insensitive else target + for j in range(0, len(pairs) - 1, 2): + key = _decode(pairs[j]) + if (key.lower() if case_insensitive else key) == needle: + return pairs[j + 1] + return None + + def _parse_glide_search_response(self, result, include_score: bool) -> SearchResult | GetResult | None: + """Parse ft.search response: [total_count, {key: {field: value, ...}, ...}]""" + empty_search = SearchResult(ids=[[]], distances=[[]], documents=[[]], metadatas=[[]]) + empty_get = GetResult(ids=[[]], documents=[[]], metadatas=[[]]) + if not result or result[0] == 0: + return empty_search if include_score else empty_get + + docs_map = result[1] if len(result) > 1 else {} + if not isinstance(docs_map, dict): + return empty_search if include_score else empty_get + + ids, documents, metadatas, distances = [], [], [], [] + for _key, fields in docs_map.items(): + if not isinstance(fields, dict): + continue + ids.append(_decode(fields.get(b'id', b''))) + documents.append(_decode(fields.get(b'text', b''))) + try: + metadatas.append(json.loads(_decode(fields.get(b'metadata_json', b'{}')))) + except (json.JSONDecodeError, TypeError): + metadatas.append({}) + + if include_score: + try: + raw_score = _decode(fields.get(b'__vector_score', b'0')) + distances.append(self._normalize_score(float(raw_score))) + except (ValueError, TypeError): + distances.append(0.0) + + if not include_score: + return GetResult(ids=[ids], documents=[documents], metadatas=[metadatas]) + + return SearchResult(ids=[ids], distances=[distances], documents=[documents], metadatas=[metadatas]) + + def _normalize_score(self, score: float) -> float: + """Convert valkey-search __vector_score (a distance, lower = more similar) to [0, 1] similarity. + + All metrics return distance: COSINE/IP in [0, 2] for unit vectors, L2 in [0, ∞). + """ + if self.distance_metric == 'COSINE': + # COSINE distance: 0 (identical) → 2 (opposite). Map to similarity [1, -1], clamp [0, 1]. + return max(0.0, min(1.0, 1.0 - score)) + if self.distance_metric == 'L2': + # L2 distance: 0 (identical) → ∞. + return 1.0 / (1.0 + score) + # IP: distance = 1 - inner_product + return max(0.0, min(1.0, 1.0 - score)) diff --git a/backend/open_webui/retrieval/vector/dbs/weaviate.py b/backend/open_webui/retrieval/vector/dbs/weaviate.py index 2cf4c135c5..a896d8ed5e 100644 --- a/backend/open_webui/retrieval/vector/dbs/weaviate.py +++ b/backend/open_webui/retrieval/vector/dbs/weaviate.py @@ -2,28 +2,28 @@ 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 +import weaviate 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_HOST, + WEAVIATE_GRPC_PORT, WEAVIATE_GRPC_SECURE, + WEAVIATE_HTTP_HOST, + WEAVIATE_HTTP_PORT, + WEAVIATE_HTTP_SECURE, WEAVIATE_SKIP_INIT_CHECKS, ) +from open_webui.retrieval.vector.main import ( + GetResult, + SearchResult, + VectorDBBase, + VectorItem, +) +from open_webui.retrieval.vector.utils import process_metadata def _convert_uuids_to_strings(obj: Any) -> Any: diff --git a/backend/open_webui/retrieval/vector/factory.py b/backend/open_webui/retrieval/vector/factory.py index 8c0208fd4f..3080956163 100644 --- a/backend/open_webui/retrieval/vector/factory.py +++ b/backend/open_webui/retrieval/vector/factory.py @@ -1,10 +1,10 @@ +from open_webui.config import ( + ENABLE_MILVUS_MULTITENANCY_MODE, + ENABLE_QDRANT_MULTITENANCY_MODE, + VECTOR_DB, +) 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: @@ -80,6 +80,10 @@ class Vector: from open_webui.retrieval.vector.dbs.weaviate import WeaviateClient return WeaviateClient() + case VectorType.VALKEY: + from open_webui.retrieval.vector.dbs.valkey import ValkeyClient + + return ValkeyClient() case _: raise ValueError(f'Unsupported vector type: {vector_type}') diff --git a/backend/open_webui/retrieval/vector/main.py b/backend/open_webui/retrieval/vector/main.py index f7904baa20..38ea699514 100644 --- a/backend/open_webui/retrieval/vector/main.py +++ b/backend/open_webui/retrieval/vector/main.py @@ -1,7 +1,8 @@ -from pydantic import BaseModel from abc import ABC, abstractmethod from typing import Any, Dict, List, Optional, Union +from pydantic import BaseModel + class VectorItem(BaseModel): id: str diff --git a/backend/open_webui/retrieval/vector/type.py b/backend/open_webui/retrieval/vector/type.py index 999aee9c54..15e6da0f9e 100644 --- a/backend/open_webui/retrieval/vector/type.py +++ b/backend/open_webui/retrieval/vector/type.py @@ -14,3 +14,4 @@ class VectorType(StrEnum): S3VECTOR = 's3vector' WEAVIATE = 'weaviate' OPENGAUSS = 'opengauss' + VALKEY = 'valkey' diff --git a/backend/open_webui/retrieval/web/azure.py b/backend/open_webui/retrieval/web/azure.py index 4f74ecc982..5ba57f11c5 100644 --- a/backend/open_webui/retrieval/web/azure.py +++ b/backend/open_webui/retrieval/web/azure.py @@ -1,5 +1,6 @@ import logging from typing import Optional + from open_webui.retrieval.web.main import SearchResult, get_filtered_results log = logging.getLogger(__name__) diff --git a/backend/open_webui/retrieval/web/bing.py b/backend/open_webui/retrieval/web/bing.py index b7cfea89de..00449f9091 100644 --- a/backend/open_webui/retrieval/web/bing.py +++ b/backend/open_webui/retrieval/web/bing.py @@ -1,10 +1,11 @@ +import argparse 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__) """ @@ -63,5 +64,12 @@ def main(): args = parser.parse_args() - results = search_bing(args.locale, args.query, args.count, args.filter) + results = search_bing( + os.environ.get('BING_SEARCH_V7_SUBSCRIPTION_KEY', ''), + os.environ.get('BING_SEARCH_V7_ENDPOINT', 'https://api.bing.microsoft.com/v7.0/search'), + 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 index 3557dcffb9..cb94646310 100644 --- a/backend/open_webui/retrieval/web/bocha.py +++ b/backend/open_webui/retrieval/web/bocha.py @@ -1,8 +1,8 @@ +import json 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__) diff --git a/backend/open_webui/retrieval/web/brave.py b/backend/open_webui/retrieval/web/brave.py index 9e663c2684..f785cbe779 100644 --- a/backend/open_webui/retrieval/web/brave.py +++ b/backend/open_webui/retrieval/web/brave.py @@ -1,19 +1,26 @@ -import logging -import time -from typing import Optional +from __future__ import annotations + +import asyncio +import logging -import requests from open_webui.retrieval.web.main import SearchResult, get_filtered_results +from open_webui.utils.session_pool import get_session log = logging.getLogger(__name__) +# Brave free-tier rate limit: 1 request per second. +_RATE_LIMIT_RETRY_DELAY = 1.0 -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 +async def search_brave( + api_key: str, + query: str, + count: int, + filter_list: list[str | None] | None = None, +) -> list[SearchResult]: + """Query the Brave Web Search API and return normalised results. + + Retries once on HTTP 429 (rate-limit) after a short delay. """ url = 'https://api.search.brave.com/res/v1/web/search' headers = { @@ -23,27 +30,27 @@ def search_brave(api_key: str, query: str, count: int, filter_list: Optional[lis } params = {'q': query, 'count': count} - response = requests.get(url, headers=headers, params=params) + session = await get_session() + async with session.get(url, headers=headers, params=params) as response: + if response.status == 429: + log.info('Brave Search rate-limited (429); retrying after %.1fs', _RATE_LIMIT_RETRY_DELAY) + await asyncio.sleep(_RATE_LIMIT_RETRY_DELAY) + async with session.get(url, headers=headers, params=params) as retry_resp: + retry_resp.raise_for_status() + payload = await retry_resp.json() + else: + response.raise_for_status() + payload = await response.json() - # 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', []) + web_results = payload.get('web', {}).get('results', []) if filter_list: - results = get_filtered_results(results, filter_list) + web_results = get_filtered_results(web_results, filter_list) return [ SearchResult( - link=result['url'], - title=result.get('title'), - snippet=result.get('description'), + link=item.get('url', ''), + title=item.get('title'), + snippet=item.get('description'), ) - for result in results[:count] + for item in web_results[:count] ] diff --git a/backend/open_webui/retrieval/web/duckduckgo.py b/backend/open_webui/retrieval/web/duckduckgo.py index 5b2f076227..27d56f6934 100644 --- a/backend/open_webui/retrieval/web/duckduckgo.py +++ b/backend/open_webui/retrieval/web/duckduckgo.py @@ -1,10 +1,11 @@ +from __future__ import annotations + import logging import urllib.request -from typing import Optional -from open_webui.retrieval.web.main import SearchResult, get_filtered_results from ddgs import DDGS from ddgs.exceptions import RatelimitException +from open_webui.retrieval.web.main import SearchResult, get_filtered_results log = logging.getLogger(__name__) @@ -12,9 +13,9 @@ 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', + filter_list: list[str | None] = None, + concurrent_requests: int | None = None, + backend: str | None = 'auto', ) -> list[SearchResult]: """ Search using DuckDuckGo's Search API and return the results as a list of SearchResult objects. diff --git a/backend/open_webui/retrieval/web/external.py b/backend/open_webui/retrieval/web/external.py index 7f5a2bf2af..4db37cb645 100644 --- a/backend/open_webui/retrieval/web/external.py +++ b/backend/open_webui/retrieval/web/external.py @@ -1,14 +1,11 @@ import logging -from typing import Optional, List +from typing import List, Optional import requests - from fastapi import Request - - +from open_webui.env import FORWARD_SESSION_INFO_HEADER_CHAT_ID 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__) diff --git a/backend/open_webui/retrieval/web/firecrawl.py b/backend/open_webui/retrieval/web/firecrawl.py index 4bbd4f212b..baffb6207d 100644 --- a/backend/open_webui/retrieval/web/firecrawl.py +++ b/backend/open_webui/retrieval/web/firecrawl.py @@ -197,8 +197,11 @@ def search_firecrawl( }, timeout=count * 3 + 10, ) + # Firecrawl /search has historically returned both `{"data": [...]}` + # (flat list, what v1 did and what frost19k reported under #23966) + # and `{"data": {"web": [...]}}` (current v2). Accept either. data = response.get('data') or {} - results = data.get('web') or [] + results = data if isinstance(data, list) else (data.get('web') or []) if filter_list: from open_webui.retrieval.web.main import get_filtered_results diff --git a/backend/open_webui/retrieval/web/google_pse.py b/backend/open_webui/retrieval/web/google_pse.py index bb0a852658..3b5fe0d685 100644 --- a/backend/open_webui/retrieval/web/google_pse.py +++ b/backend/open_webui/retrieval/web/google_pse.py @@ -1,70 +1,66 @@ -import logging -from typing import Optional +from __future__ import annotations + +import logging -import requests from open_webui.retrieval.web.main import SearchResult, get_filtered_results +from open_webui.utils.session_pool import get_session log = logging.getLogger(__name__) -def search_google_pse( +async 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, + filter_list: list[str | None] | None = None, + referer: str | None = 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. + """Query Google Programmable Search Engine with automatic pagination. - 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. + The PSE API returns at most 10 results per request, so this function + issues multiple requests when ``count > 10``. """ url = 'https://www.googleapis.com/customsearch/v1' - - headers = {'Content-Type': 'application/json'} + headers: dict[str, str] = {'Content-Type': 'application/json'} if referer: headers['Referer'] = referer - all_results = [] - start_index = 1 # Google PSE start parameter is 1-based + all_items: list[dict] = [] + start_index = 1 # PSE uses 1-based pagination - while count > 0: - num_results_this_page = min(count, 10) # Google PSE max results per page is 10 + session = await get_session() + remaining = count + while remaining > 0: + page_size = min(remaining, 10) params = { 'cx': search_engine_id, 'q': query, 'key': api_key, - 'num': num_results_this_page, - 'start': start_index, + 'num': str(page_size), + 'start': str(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 + + async with session.get(url, headers=headers, params=params) as response: + response.raise_for_status() + payload = await response.json() + + items = payload.get('items', []) + if not items: + break + + all_items.extend(items) + remaining -= len(items) + start_index += 10 if filter_list: - all_results = get_filtered_results(all_results, filter_list) + all_items = get_filtered_results(all_items, filter_list) return [ SearchResult( - link=result['link'], - title=result.get('title'), - snippet=result.get('snippet'), + link=item.get('link', ''), + title=item.get('title'), + snippet=item.get('snippet'), ) - for result in all_results + for item in all_items ] diff --git a/backend/open_webui/retrieval/web/jina_search.py b/backend/open_webui/retrieval/web/jina_search.py index b3266c47d0..830beec702 100644 --- a/backend/open_webui/retrieval/web/jina_search.py +++ b/backend/open_webui/retrieval/web/jina_search.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import logging import requests @@ -17,9 +19,9 @@ def search_jina(api_key: str, query: str, count: int, base_url: str = '') -> lis base_url (str): Optional custom base URL for the Jina API Returns: - list[SearchResult]: A list of search results + A list of SearchResult objects. """ - jina_search_endpoint = base_url if base_url else 'https://s.jina.ai/' + jina_search_endpoint = base_url or 'https://s.jina.ai/' headers = { 'Accept': 'application/json', diff --git a/backend/open_webui/retrieval/web/kagi.py b/backend/open_webui/retrieval/web/kagi.py index e6ed570011..cb35ccbb22 100644 --- a/backend/open_webui/retrieval/web/kagi.py +++ b/backend/open_webui/retrieval/web/kagi.py @@ -17,21 +17,20 @@ def search_kagi(api_key: str, query: str, count: int, filter_list: Optional[list query (str): The query to search for count (int): The number of results to return """ - url = 'https://kagi.com/api/v0/search' + url = 'https://kagi.com/api/v1/search' headers = { - 'Authorization': f'Bot {api_key}', + 'Authorization': f'Bearer {api_key}', } - params = {'q': query, 'limit': count} + params = {'query': query, 'limit': count} - response = requests.get(url, headers=headers, params=params) + response = requests.post(url, headers=headers, json=params) response.raise_for_status() json_response = response.json() - search_results = json_response.get('data', []) + search_results = json_response.get('data', {}).get('search', []) results = [ SearchResult(link=result['url'], title=result['title'], snippet=result.get('snippet')) for result in search_results - if result['t'] == 0 ] print(results) diff --git a/backend/open_webui/retrieval/web/linkup.py b/backend/open_webui/retrieval/web/linkup.py new file mode 100644 index 0000000000..b62ec9ed53 --- /dev/null +++ b/backend/open_webui/retrieval/web/linkup.py @@ -0,0 +1,74 @@ +import logging +from typing import Optional + +import requests + +from open_webui.retrieval.web.main import SearchResult, get_filtered_results + +log = logging.getLogger(__name__) + +DEFAULT_LINKUP_PARAMS = { + 'url': 'https://api.linkup.so/v1/search', + 'depth': 'standard', + 'outputType': 'sourcedAnswer', +} + + +def search_linkup( + api_key: str, + query: str, + count: int, + filter_list: Optional[list[str]] = None, + params: Optional[dict] = None, +) -> list[SearchResult]: + """Search using the Linkup Search API. + + ``params`` is forwarded almost verbatim as the JSON body; only ``q`` + and ``maxResults`` are injected automatically. The special key + ``url`` (default ``https://api.linkup.so/v1/search``) is popped and + used as the endpoint. + """ + if hasattr(api_key, '__str__'): + api_key = str(api_key) + + merged = {**DEFAULT_LINKUP_PARAMS, **(params or {})} + api_url = str(merged.pop('url', DEFAULT_LINKUP_PARAMS['url'])) + + payload = {**merged, 'q': query, 'maxResults': count} + + try: + response = requests.post( + api_url, + headers={ + 'Authorization': f'Bearer {api_key}', + 'Content-Type': 'application/json', + }, + json=payload, + timeout=30, + ) + response.raise_for_status() + json_response = response.json() + + output_type = merged.get('outputType', 'sourcedAnswer') + search_results = ( + json_response.get('sources', []) if output_type == 'sourcedAnswer' else json_response.get('results', []) + ) + + if filter_list: + search_results = get_filtered_results(search_results, filter_list) + + return [ + SearchResult( + link=r.get('url', ''), + title=r.get('name') or r.get('title'), + snippet=r.get('content') or r.get('text') or r.get('snippet'), + ) + for r in search_results + ][:count] + + except requests.exceptions.RequestException as e: + log.error(f'Linkup API request failed: {e}') + raise Exception(f'Linkup search failed: {str(e)}') + except Exception as e: + log.error(f'Error searching Linkup: {e}') + raise Exception(f'Linkup search error: {str(e)}') diff --git a/backend/open_webui/retrieval/web/main.py b/backend/open_webui/retrieval/web/main.py index 3a8fed52dd..a55c62c8b5 100644 --- a/backend/open_webui/retrieval/web/main.py +++ b/backend/open_webui/retrieval/web/main.py @@ -1,12 +1,11 @@ -import validators +from __future__ import annotations -from typing import Optional from urllib.parse import urlparse -from pydantic import BaseModel - +import validators from open_webui.retrieval.web.utils import resolve_hostname from open_webui.utils.misc import is_string_allowed +from pydantic import BaseModel def get_filtered_results(results, filter_list): @@ -42,5 +41,5 @@ def get_filtered_results(results, filter_list): class SearchResult(BaseModel): link: str - title: Optional[str] - snippet: Optional[str] + title: str | None + snippet: str | None diff --git a/backend/open_webui/retrieval/web/mojeek.py b/backend/open_webui/retrieval/web/mojeek.py index a094ef6fc8..da24c2a87c 100644 --- a/backend/open_webui/retrieval/web/mojeek.py +++ b/backend/open_webui/retrieval/web/mojeek.py @@ -1,5 +1,6 @@ +from __future__ import annotations + import logging -from typing import Optional import requests from open_webui.retrieval.web.main import SearchResult, get_filtered_results @@ -7,7 +8,7 @@ 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]: +def search_mojeek(api_key: str, query: str, count: int, filter_list: list[str | None] = None) -> list[SearchResult]: """Search using Mojeek's Search API and return the results as a list of SearchResult objects. Args: diff --git a/backend/open_webui/retrieval/web/perplexity.py b/backend/open_webui/retrieval/web/perplexity.py index 8b3a9d3b08..05f2d5d51c 100644 --- a/backend/open_webui/retrieval/web/perplexity.py +++ b/backend/open_webui/retrieval/web/perplexity.py @@ -1,7 +1,8 @@ import logging -from typing import Optional, Literal -import requests +from typing import Literal, Optional +import requests +from open_webui.env import VERSION from open_webui.retrieval.web.main import SearchResult, get_filtered_results MODELS = Literal[ @@ -37,7 +38,7 @@ def search_perplexity( """ - # Handle PersistentConfig object + # Handle ConfigVar object if hasattr(api_key, '__str__'): api_key = str(api_key) @@ -64,6 +65,7 @@ def search_perplexity( headers = { 'Authorization': f'Bearer {api_key}', 'Content-Type': 'application/json', + 'X-Pplx-Integration': f'open-webui/{VERSION}', } # Make the API request diff --git a/backend/open_webui/retrieval/web/perplexity_search.py b/backend/open_webui/retrieval/web/perplexity_search.py index 9cbec049d9..f3284f9586 100644 --- a/backend/open_webui/retrieval/web/perplexity_search.py +++ b/backend/open_webui/retrieval/web/perplexity_search.py @@ -1,7 +1,8 @@ import logging -from typing import Optional, Literal -import requests +from typing import Literal, Optional +import requests +from open_webui.env import VERSION from open_webui.retrieval.web.main import SearchResult, get_filtered_results from open_webui.utils.headers import include_user_info_headers @@ -28,7 +29,7 @@ def search_perplexity_search( """ - # Handle PersistentConfig object + # Handle ConfigVar object if hasattr(api_key, '__str__'): api_key = str(api_key) @@ -47,6 +48,7 @@ def search_perplexity_search( headers = { 'Authorization': f'Bearer {api_key}', 'Content-Type': 'application/json', + 'X-Pplx-Integration': f'open-webui/{VERSION}', } # Forward user info headers if user is provided diff --git a/backend/open_webui/retrieval/web/searxng.py b/backend/open_webui/retrieval/web/searxng.py index 2b7bd04895..9c48b0f1b3 100644 --- a/backend/open_webui/retrieval/web/searxng.py +++ b/backend/open_webui/retrieval/web/searxng.py @@ -1,87 +1,66 @@ -import logging -from typing import Optional +from __future__ import annotations + +import logging -import requests from open_webui.retrieval.web.main import SearchResult, get_filtered_results +from open_webui.utils.session_pool import get_session log = logging.getLogger(__name__) +# SearXNG request headers — identifies the bot to instance operators. +_SEARXNG_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', +} -def search_searxng( + +async def search_searxng( query_url: str, query: str, count: int, - filter_list: Optional[list[str]] = None, + filter_list: list[str | None] | None = None, **kwargs, ) -> list[SearchResult]: + """Query a SearXNG instance and return results sorted by relevance score. + + Optional keyword arguments (language, safesearch, time_range, categories) + are forwarded directly as SearXNG query parameters. """ - 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').strip().rstrip(',') - safesearch = kwargs.get('safesearch', '1') - time_range = kwargs.get('time_range', '') - categories = ''.join(kwargs.get('categories', [])) + # Normalise legacy ````-style URLs by stripping any query string. + if '' in query_url: + query_url = query_url.split('?')[0] params = { 'q': query, 'format': 'json', 'pageno': 1, - 'safesearch': safesearch, - 'language': language, - 'time_range': time_range, - 'categories': categories, + 'safesearch': kwargs.get('safesearch', '1'), + 'language': kwargs.get('language', 'all').strip().rstrip(','), + 'time_range': kwargs.get('time_range', ''), + 'categories': ''.join(kwargs.get('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('searching %s', query_url) - log.debug(f'searching {query_url}') + session = await get_session() + async with session.get(query_url, headers=_SEARXNG_HEADERS, params=params) as response: + response.raise_for_status() + payload = await response.json() - 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) + results = sorted(payload.get('results', []), key=lambda x: x.get('score', 0), reverse=True) if filter_list: - sorted_results = get_filtered_results(sorted_results, 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 sorted_results[:count] + SearchResult( + link=item.get('url', ''), + title=item.get('title'), + snippet=item.get('content'), + ) + for item in results[:count] ] diff --git a/backend/open_webui/retrieval/web/serper.py b/backend/open_webui/retrieval/web/serper.py index 9f1a8e1b3a..1304529404 100644 --- a/backend/open_webui/retrieval/web/serper.py +++ b/backend/open_webui/retrieval/web/serper.py @@ -1,37 +1,41 @@ +from __future__ import annotations + import json import logging -from typing import Optional -import requests from open_webui.retrieval.web.main import SearchResult, get_filtered_results +from open_webui.utils.session_pool import get_session 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. +async def search_serper( + api_key: str, + query: str, + count: int, + filter_list: list[str | None] | None = None, +) -> list[SearchResult]: + """Query the serper.dev Google Search API and return normalised results. - Args: - api_key (str): A serper.dev API key - query (str): The query to search for + Results are sorted by their position field before truncation. """ 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() + session = await get_session() + async with session.post(url, headers=headers, data=json.dumps({'q': query})) as response: + response.raise_for_status() + payload = await response.json() - json_response = response.json() - results = sorted(json_response.get('organic', []), key=lambda x: x.get('position', 0)) + organic = sorted(payload.get('organic', []), key=lambda item: item.get('position', 0)) if filter_list: - results = get_filtered_results(results, filter_list) + organic = get_filtered_results(organic, filter_list) + return [ SearchResult( - link=result['link'], - title=result.get('title'), - snippet=result.get('snippet'), + link=item.get('link', ''), + title=item.get('title'), + snippet=item.get('snippet'), ) - for result in results[:count] + for item in organic[:count] ] diff --git a/backend/open_webui/retrieval/web/serply.py b/backend/open_webui/retrieval/web/serply.py index f245392b75..c4909c14e0 100644 --- a/backend/open_webui/retrieval/web/serply.py +++ b/backend/open_webui/retrieval/web/serply.py @@ -1,5 +1,6 @@ +from __future__ import annotations + import logging -from typing import Optional from urllib.parse import urlencode import requests @@ -16,7 +17,7 @@ def search_serply( limit: int = 10, device_type: str = 'desktop', proxy_location: str = 'US', - filter_list: Optional[list[str]] = None, + filter_list: list[str | None] = None, ) -> list[SearchResult]: """Search using serper.dev's API and return the results as a list of SearchResult objects. diff --git a/backend/open_webui/retrieval/web/serpstack.py b/backend/open_webui/retrieval/web/serpstack.py index 28a4956645..532246c11a 100644 --- a/backend/open_webui/retrieval/web/serpstack.py +++ b/backend/open_webui/retrieval/web/serpstack.py @@ -1,42 +1,42 @@ -import logging -from typing import Optional +from __future__ import annotations + +import logging -import requests from open_webui.retrieval.web.main import SearchResult, get_filtered_results +from open_webui.utils.session_pool import get_session log = logging.getLogger(__name__) -def search_serpstack( +async def search_serpstack( api_key: str, query: str, count: int, - filter_list: Optional[list[str]] = None, + filter_list: list[str | None] | None = None, https_enabled: bool = True, ) -> list[SearchResult]: - """Search using serpstack.com's and return the results as a list of SearchResult objects. + """Query the serpstack.com API and return normalised results. - 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 + Uses HTTPS by default; set ``https_enabled=False`` for free-tier HTTP access. """ - url = f'{"https" if https_enabled else "http"}://api.serpstack.com/search' + scheme = 'https' if https_enabled else 'http' + url = f'{scheme}://api.serpstack.com/search' + params = {'access_key': api_key, 'query': query} - headers = {'Content-Type': 'application/json'} - params = { - 'access_key': api_key, - 'query': query, - } + session = await get_session() + async with session.get(url, params=params) as response: + response.raise_for_status() + payload = await response.json() - 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)) + organic = sorted(payload.get('organic_results', []), key=lambda x: x.get('position', 0)) if filter_list: - results = get_filtered_results(results, filter_list) + organic = get_filtered_results(organic, filter_list) + return [ - SearchResult(link=result['url'], title=result.get('title'), snippet=result.get('snippet')) - for result in results[:count] + SearchResult( + link=item.get('url', ''), + title=item.get('title'), + snippet=item.get('snippet'), + ) + for item in organic[:count] ] diff --git a/backend/open_webui/retrieval/web/sougou.py b/backend/open_webui/retrieval/web/sougou.py index b267374d79..3d12e2a57b 100644 --- a/backend/open_webui/retrieval/web/sougou.py +++ b/backend/open_webui/retrieval/web/sougou.py @@ -1,7 +1,6 @@ -import logging import json -from typing import Optional, List - +import logging +from typing import List, Optional from open_webui.retrieval.web.main import SearchResult, get_filtered_results @@ -15,8 +14,8 @@ def search_sougou( 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.common_client import CommonClient from tencentcloud.common.exception.tencent_cloud_sdk_exception import ( TencentCloudSDKException, ) diff --git a/backend/open_webui/retrieval/web/tavily.py b/backend/open_webui/retrieval/web/tavily.py index 6b52bbb45b..419bebb05e 100644 --- a/backend/open_webui/retrieval/web/tavily.py +++ b/backend/open_webui/retrieval/web/tavily.py @@ -1,5 +1,6 @@ +from __future__ import annotations + import logging -from typing import Optional import requests from open_webui.retrieval.web.main import SearchResult, get_filtered_results @@ -11,7 +12,7 @@ def search_tavily( api_key: str, query: str, count: int, - filter_list: Optional[list[str]] = None, + filter_list: list[str | None] = None, # **kwargs, ) -> list[SearchResult]: """Search using Tavily's Search API and return the results as a list of SearchResult objects. @@ -22,7 +23,7 @@ def search_tavily( count (int): The maximum number of results to return Returns: - list[SearchResult]: A list of search results + A list of SearchResult objects. """ url = 'https://api.tavily.com/search' headers = { diff --git a/backend/open_webui/retrieval/web/testdata/bing.json b/backend/open_webui/retrieval/web/testdata/bing.json deleted file mode 100644 index 80324f3b40..0000000000 --- a/backend/open_webui/retrieval/web/testdata/bing.json +++ /dev/null @@ -1,58 +0,0 @@ -{ - "_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 deleted file mode 100644 index 0cc72109ef..0000000000 --- a/backend/open_webui/retrieval/web/testdata/brave.json +++ /dev/null @@ -1,998 +0,0 @@ -{ - "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 deleted file mode 100644 index 15da9729cd..0000000000 --- a/backend/open_webui/retrieval/web/testdata/google_pse.json +++ /dev/null @@ -1,442 +0,0 @@ -{ - "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 deleted file mode 100644 index fa3d1c3d74..0000000000 --- a/backend/open_webui/retrieval/web/testdata/searchapi.json +++ /dev/null @@ -1,357 +0,0 @@ -{ - "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 deleted file mode 100644 index 0e6952baa8..0000000000 --- a/backend/open_webui/retrieval/web/testdata/searxng.json +++ /dev/null @@ -1,476 +0,0 @@ -{ - "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 deleted file mode 100644 index b269eaf5b3..0000000000 --- a/backend/open_webui/retrieval/web/testdata/serper.json +++ /dev/null @@ -1,190 +0,0 @@ -{ - "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 deleted file mode 100644 index 0fc2a31e4d..0000000000 --- a/backend/open_webui/retrieval/web/testdata/serply.json +++ /dev/null @@ -1,206 +0,0 @@ -{ - "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 deleted file mode 100644 index a82f689d8b..0000000000 --- a/backend/open_webui/retrieval/web/testdata/serpstack.json +++ /dev/null @@ -1,276 +0,0 @@ -{ - "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 index c2ce6bdbd1..afa73a9e0e 100644 --- a/backend/open_webui/retrieval/web/utils.py +++ b/backend/open_webui/retrieval/web/utils.py @@ -5,8 +5,6 @@ import socket import ssl import urllib.parse import urllib.request - -import requests from datetime import datetime, time, timedelta from typing import ( Any, @@ -14,41 +12,45 @@ from typing import ( Dict, Iterator, List, + Literal, Optional, Sequence, Union, - Literal, ) -from fastapi.concurrency import run_in_threadpool import aiohttp +import aiohttp.resolver import certifi +import requests +import urllib3.connection +import urllib3.connectionpool import validators +from requests.adapters import HTTPAdapter +from fastapi.concurrency import run_in_threadpool 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, + EXTERNAL_WEB_LOADER_API_KEY, + EXTERNAL_WEB_LOADER_URL, FIRECRAWL_API_BASE_URL, FIRECRAWL_API_KEY, FIRECRAWL_TIMEOUT, + PLAYWRIGHT_TIMEOUT, + PLAYWRIGHT_WS_URL, TAVILY_API_KEY, TAVILY_EXTRACT_DEPTH, - EXTERNAL_WEB_LOADER_URL, - EXTERNAL_WEB_LOADER_API_KEY, WEB_FETCH_FILTER_LIST, + WEB_LOADER_ENGINE, + WEB_LOADER_TIMEOUT, ) +from open_webui.constants import ERROR_MESSAGES +from open_webui.env import AIOHTTP_CLIENT_ALLOW_REDIRECTS, AIOHTTP_CLIENT_SESSION_SSL, USER_AGENT +from open_webui.retrieval.loaders.external_web import ExternalWebLoader +from open_webui.retrieval.loaders.tavily import TavilyLoader +from open_webui.retrieval.web.firecrawl import scrape_firecrawl_url from open_webui.utils.misc import is_string_allowed -from open_webui.env import AIOHTTP_CLIENT_SESSION_SSL, AIOHTTP_CLIENT_ALLOW_REDIRECTS log = logging.getLogger(__name__) @@ -96,7 +98,7 @@ def validate_url(url: Union[str, Sequence[str]]): # 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 + # DNS rebinding is mitigated at the connection layer; see _SSRFSafeResolver / _SSRFSafeAdapter for ip in ipv4_addresses + ipv6_addresses: addr = ipaddress.ip_address(ip) if not addr.is_global: @@ -120,6 +122,81 @@ def safe_validate_urls(url: Sequence[str]) -> Sequence[str]: return valid_urls +def _ssrf_safe_new_conn(self): + """Resolve DNS, validate all IPs are global, connect to validated IP. + + Replaces urllib3's _new_conn so the DNS lookup that feeds the actual TCP + connect is the same one we validate — no second resolution, no rebinding + window. + """ + host = getattr(self, '_dns_host', self.host) + port = self.port + infos = socket.getaddrinfo(host, port, 0, socket.SOCK_STREAM) + if not infos: + raise OSError(f'getaddrinfo for {host!r} returned empty list') + if not ENABLE_RAG_LOCAL_WEB_FETCH: + for _, _, _, _, sa in infos: + if not ipaddress.ip_address(sa[0]).is_global: + raise ValueError(ERROR_MESSAGES.INVALID_URL) + err = None + for fam, typ, proto, _, sa in infos: + sock = None + try: + sock = socket.socket(fam, typ, proto) + if self.timeout is not socket._GLOBAL_DEFAULT_TIMEOUT: + sock.settimeout(self.timeout) + if getattr(self, 'source_address', None): + sock.bind(self.source_address) + for opt in getattr(self, 'socket_options', None) or (): + sock.setsockopt(*opt) + sock.connect(sa) + return sock + except OSError as exc: + err = exc + if sock is not None: + sock.close() + raise err or OSError(f'connect to {host!r}:{port} failed') + + +class _SafeHTTPConn(urllib3.connection.HTTPConnection): + _new_conn = _ssrf_safe_new_conn + + +class _SafeHTTPSConn(urllib3.connection.HTTPSConnection): + _new_conn = _ssrf_safe_new_conn + + +class _SafeHTTPPool(urllib3.connectionpool.HTTPConnectionPool): + ConnectionCls = _SafeHTTPConn + + +class _SafeHTTPSPool(urllib3.connectionpool.HTTPSConnectionPool): + ConnectionCls = _SafeHTTPSConn + + +class _SSRFSafeAdapter(HTTPAdapter): + """requests transport adapter that validates resolved IPs at connect time.""" + + def init_poolmanager(self, *args, **kwargs): + super().init_poolmanager(*args, **kwargs) + self.poolmanager.pool_classes_by_scheme = { + 'http': _SafeHTTPPool, + 'https': _SafeHTTPSPool, + } + + +class _SSRFSafeResolver(aiohttp.resolver.DefaultResolver): + """aiohttp resolver that rejects non-global IPs unless local fetch is on.""" + + async def resolve(self, host, port=0, family=socket.AF_INET): + results = await super().resolve(host, port, family) + if not ENABLE_RAG_LOCAL_WEB_FETCH: + for entry in results: + if not ipaddress.ip_address(entry['host']).is_global: + raise ValueError(ERROR_MESSAGES.INVALID_URL) + return results + + def extract_metadata(soup, url): metadata = {'source': url} if title := soup.find('title'): @@ -423,6 +500,62 @@ class SafePlaywrightURLLoader(PlaywrightURLLoader, RateLimitMixin, URLProcessing self.trust_env = trust_env self.playwright_timeout = playwright_timeout + def _intercept_navigation_sync(self, route, request=None): + req = request or route.request + + if req.resource_type != 'document': + route.continue_() + return + + try: + validate_url(req.url) + except Exception: + route.abort() + return + + if AIOHTTP_CLIENT_ALLOW_REDIRECTS: + resp = route.fetch() + else: + try: + resp = route.fetch(max_redirects=0) + except TypeError: + route.abort() + return + + if 300 <= resp.status < 400: + route.abort() + return + + route.fulfill(response=resp) + + async def _intercept_navigation(self, route, request=None): + req = request or route.request + + if req.resource_type != 'document': + await route.continue_() + return + + try: + await run_in_threadpool(validate_url, req.url) + except Exception: + await route.abort() + return + + if AIOHTTP_CLIENT_ALLOW_REDIRECTS: + resp = await route.fetch() + else: + try: + resp = await route.fetch(max_redirects=0) + except TypeError: + await route.abort() + return + + if 300 <= resp.status < 400: + await route.abort() + return + + await route.fulfill(response=resp) + def lazy_load(self) -> Iterator[Document]: """Safely load URLs synchronously with support for remote browser.""" from playwright.sync_api import sync_playwright @@ -438,6 +571,7 @@ class SafePlaywrightURLLoader(PlaywrightURLLoader, RateLimitMixin, URLProcessing try: self._safe_process_url_sync(url) page = browser.new_page() + page.route('**/*', self._intercept_navigation_sync) response = page.goto(url, timeout=self.playwright_timeout) if response is None: raise ValueError(f'page.goto() returned None for url {url}') @@ -467,6 +601,7 @@ class SafePlaywrightURLLoader(PlaywrightURLLoader, RateLimitMixin, URLProcessing try: await self._safe_process_url(url) page = await browser.new_page() + await page.route('**/*', self._intercept_navigation) response = await page.goto(url, timeout=self.playwright_timeout) if response is None: raise ValueError(f'page.goto() returned None for url {url}') @@ -493,6 +628,15 @@ class SafeWebBaseLoader(WebBaseLoader): """ super().__init__(*args, **kwargs) self.trust_env = trust_env + + # Propagate USER_AGENT env var so that both the sync _scrape() and + # async _fetch() paths present a real UA instead of python-requests/2.x + # which gets blocked by Cloudflare, Wikipedia, and similar bot-detection. + # _fetch() forwards self.session.headers to the aiohttp session, so + # setting it here covers both code-paths. + if USER_AGENT: + self.session.headers['User-Agent'] = USER_AGENT + # Prevent redirect-based SSRF on the synchronous _scrape() path. # validate_url() is called once on the originally-submitted URL, but the # parent WebBaseLoader's _scrape() invokes self.session.get(url, **self.requests_kwargs) @@ -505,8 +649,12 @@ class SafeWebBaseLoader(WebBaseLoader): 'allow_redirects': AIOHTTP_CLIENT_ALLOW_REDIRECTS, } + self.session.mount('http://', _SSRFSafeAdapter()) + self.session.mount('https://', _SSRFSafeAdapter()) + 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: + connector = aiohttp.TCPConnector(resolver=_SSRFSafeResolver()) + async with aiohttp.ClientSession(trust_env=self.trust_env, connector=connector) as session: for i in range(retries): try: kwargs: Dict = dict( @@ -521,7 +669,6 @@ class SafeWebBaseLoader(WebBaseLoader): async with session.get( url, **(self.requests_kwargs | kwargs), - allow_redirects=AIOHTTP_CLIENT_ALLOW_REDIRECTS, ) as response: if self.raise_for_status: response.raise_for_status() diff --git a/backend/open_webui/retrieval/web/yacy.py b/backend/open_webui/retrieval/web/yacy.py index 32ca04f531..969acb7398 100644 --- a/backend/open_webui/retrieval/web/yacy.py +++ b/backend/open_webui/retrieval/web/yacy.py @@ -2,8 +2,8 @@ import logging from typing import Optional import requests -from requests.auth import HTTPDigestAuth from open_webui.retrieval.web.main import SearchResult, get_filtered_results +from requests.auth import HTTPDigestAuth log = logging.getLogger(__name__) diff --git a/backend/open_webui/retrieval/web/yandex.py b/backend/open_webui/retrieval/web/yandex.py index 1fffac8f61..d338db11a3 100644 --- a/backend/open_webui/retrieval/web/yandex.py +++ b/backend/open_webui/retrieval/web/yandex.py @@ -3,19 +3,16 @@ 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 typing import List, Optional from xml.etree import ElementTree as ET from xml.etree.ElementTree import Element +import requests +from fastapi import Request +from open_webui.env import FORWARD_SESSION_INFO_HEADER_CHAT_ID +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__) @@ -122,8 +119,8 @@ def search_yandex( if __name__ == '__main__': - from starlette.datastructures import Headers from fastapi import FastAPI + from starlette.datastructures import Headers result = search_yandex( Request( diff --git a/backend/open_webui/retrieval/web/ydc.py b/backend/open_webui/retrieval/web/ydc.py index 21059d8b03..446fa5f16d 100644 --- a/backend/open_webui/retrieval/web/ydc.py +++ b/backend/open_webui/retrieval/web/ydc.py @@ -1,5 +1,5 @@ import logging -from typing import Optional, List +from typing import List, Optional import requests from open_webui.retrieval.web.main import SearchResult, get_filtered_results diff --git a/backend/open_webui/routers/analytics.py b/backend/open_webui/routers/analytics.py index fd045f79e7..fcef30342c 100644 --- a/backend/open_webui/routers/analytics.py +++ b/backend/open_webui/routers/analytics.py @@ -1,17 +1,17 @@ -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 collections import defaultdict +from datetime import datetime, timedelta +from typing import Optional -from open_webui.models.chat_messages import ChatMessages, ChatMessageModel +from fastapi import APIRouter, Depends, Query +from open_webui.internal.db import get_async_session +from open_webui.models.chat_messages import ChatMessageModel, ChatMessages from open_webui.models.chats import Chats +from open_webui.models.feedbacks import Feedbacks 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 pydantic import BaseModel from sqlalchemy.ext.asyncio import AsyncSession log = logging.getLogger(__name__) diff --git a/backend/open_webui/routers/audio.py b/backend/open_webui/routers/audio.py index 6366f0e72b..441915972d 100644 --- a/backend/open_webui/routers/audio.py +++ b/backend/open_webui/routers/audio.py @@ -1,91 +1,77 @@ +"""Audio router — TTS speech synthesis and STT transcription endpoints.""" + import asyncio -import io +import base64 import hashlib +import html +import io import json import logging +import mimetypes 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 fnmatch import fnmatch +from pathlib import Path from typing import Optional -from fnmatch import fnmatch -import aiohttp import aiofiles -import requests -import mimetypes - +import aiohttp from fastapi import ( + APIRouter, 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 pydub import AudioSegment +from pydub.silence import split_on_silence +from pydub.utils import mediainfo - -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, + WHISPER_COMPUTE_TYPE, + WHISPER_LANGUAGE, + WHISPER_MODEL_AUTO_UPDATE, + WHISPER_MODEL_DIR, + WHISPER_MULTILINGUAL, + WHISPER_VAD_FILTER, ) - 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, + ENV, ) - -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 +from open_webui.utils.access_control import has_permission +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.misc import strict_match_mime_type +from open_webui.utils.session_pool import get_session log = logging.getLogger(__name__) +router = APIRouter() + +# --- Constants --- + +MAX_FILE_SIZE_MB: int = 20 +MAX_FILE_SIZE: int = MAX_FILE_SIZE_MB * 1024 * 1024 +AZURE_MAX_FILE_SIZE_MB: int = 200 +AZURE_MAX_FILE_SIZE: int = AZURE_MAX_FILE_SIZE_MB * 1024 * 1024 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. @@ -199,13 +185,6 @@ def set_faster_whisper_model(model: str, auto_update: bool = False): return whisper_model -########################################## -# -# Audio API -# -########################################## - - class TTSConfigForm(BaseModel): OPENAI_API_BASE_URL: str OPENAI_API_KEY: str @@ -287,6 +266,7 @@ async def get_audio_config(request: Request, user=Depends(get_admin_user)): @router.post('/config/update') async def update_audio_config(request: Request, form_data: AudioConfigUpdateForm, user=Depends(get_admin_user)): + # TTS settings 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 @@ -301,13 +281,13 @@ async def update_audio_config(request: Request, form_data: AudioConfigUpdateForm 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 + # STT settings 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.STT_ALLOWED_EXTENSIONS = form_data.stt.ALLOWED_EXTENSIONS - 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 @@ -364,8 +344,8 @@ async def update_audio_config(request: Request, form_data: AudioConfigUpdateForm def load_speech_pipeline(request): - from transformers import pipeline from datasets import load_dataset + from transformers import pipeline if request.app.state.speech_synthesiser is None: request.app.state.speech_synthesiser = pipeline('text-to-speech', 'microsoft/speecht5_tts') @@ -376,9 +356,230 @@ def load_speech_pipeline(request): ) +async def _raise_tts_error(exc: Exception, r=None) -> None: + """Raise a standardised HTTPException from a TTS provider failure.""" + code = r.status if r is not None else 500 + detail = 'Open WebUI: Server Connection Error' + if r is not None: + try: + res = await r.json() + if 'error' in res: + msg = res['error'] + detail = f'External: {msg.get("message", msg) if isinstance(msg, dict) else msg}' + elif 'message' in res: + detail = f'External: {res["message"]}' + except Exception: + detail = f'External: {exc}' + raise HTTPException(status_code=code, detail=detail) + + +async def _write_tts_cache( + file_path: Path, + audio: bytes, + body_path: Path, + payload: dict, +) -> None: + """Persist audio + request metadata to the speech cache.""" + async with aiofiles.open(file_path, 'wb') as f: + await f.write(audio) + async with aiofiles.open(body_path, 'w') as f: + await f.write(json.dumps(payload)) + + +async def _tts_openai(request, payload, file_path, file_body_path, user): + """Generate speech via an OpenAI-compatible TTS endpoint.""" + payload['model'] = request.app.state.config.TTS_MODEL + if not payload.get('voice'): + payload['voice'] = request.app.state.config.TTS_VOICE + 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 = None + try: + session = await get_session() + 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() + + audio_data = await r.read() + content_type = r.headers.get('Content-Type', 'audio/mpeg') + + if not await asyncio.to_thread(transcode_audio_to_mp3, audio_data, content_type, file_path): + async with aiofiles.open(file_path, 'wb') as f: + await f.write(audio_data) + + async with aiofiles.open(file_body_path, 'w') as f: + await f.write(json.dumps(payload)) + + return FileResponse(file_path) + except Exception as exc: + log.exception(exc) + await _raise_tts_error(exc, r) + + +async def _tts_elevenlabs(request, payload, file_path, file_body_path, user): + """Generate speech via the ElevenLabs TTS API.""" + voice_id = payload.get('voice', '') + if voice_id not in await get_available_voices(request): + raise HTTPException(status_code=400, detail='Invalid voice id') + + r = None + try: + session = await get_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() + await _write_tts_cache(file_path, await r.read(), file_body_path, payload) + return FileResponse(file_path) + except Exception as exc: + log.exception(exc) + await _raise_tts_error(exc, r) + + +async def _tts_azure(request, payload, file_path, file_body_path, user): + """Generate speech via Azure Cognitive Services TTS.""" + az_region = request.app.state.config.TTS_AZURE_SPEECH_REGION or 'eastus' + az_base = request.app.state.config.TTS_AZURE_SPEECH_BASE_URL + language = payload.get('voice') or request.app.state.config.TTS_VOICE + locale = '-'.join(language.split('-')[:2]) + output_format = request.app.state.config.TTS_AZURE_SPEECH_OUTPUT_FORMAT + + ssml = ( + f'' + f'{html.escape(payload["input"])}' + f'' + ) + + r = None + try: + session = await get_session() + async with session.post( + (az_base or f'https://{az_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=ssml, + ssl=AIOHTTP_CLIENT_SESSION_SSL, + ) as r: + r.raise_for_status() + await _write_tts_cache(file_path, await r.read(), file_body_path, payload) + return FileResponse(file_path) + except Exception as exc: + log.exception(exc) + await _raise_tts_error(exc, r) + + +async def _tts_transformers(request, payload, file_path, file_body_path, user): + """Generate speech via the local HuggingFace SpeechT5 pipeline (thread-offloaded).""" + import soundfile as sf + import torch + + load_speech_pipeline(request) + + embeddings = request.app.state.speech_speaker_embeddings_dataset + model_name = request.app.state.config.TTS_MODEL + + idx = 6799 + try: + idx = embeddings['filename'].index(model_name) + except (ValueError, KeyError): + log.debug(f'Speaker embedding not found for {model_name}, using default index {idx}') + + def _run_pipeline(): + speaker_embedding = torch.tensor(embeddings[idx]['xvector']).unsqueeze(0) + wav = request.app.state.speech_synthesiser( + payload['input'], # raw text to synthesize + forward_params={ + 'speaker_embeddings': speaker_embedding, + }, + ) + sf.write(str(file_path), wav['audio'], samplerate=wav['sampling_rate']) + + await asyncio.to_thread(_run_pipeline) + + # Audio file already written by sf.write; just persist the request metadata. + async with aiofiles.open(file_body_path, 'w') as f: + await f.write(json.dumps(payload)) + return FileResponse(file_path) + + +async def _tts_mistral(request, payload, file_path, file_body_path, user): + """Generate speech via the Mistral TTS API.""" + 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') + + r = None + try: + session = await get_session() + r = await session.post( + url=f'{api_base_url}/audio/speech', + json={ + 'input': payload.get('input', ''), # text to synthesize + 'model': request.app.state.config.TTS_MODEL or 'voxtral-mini-tts-2603', + 'voice_id': payload.get('voice', ''), + 'response_format': 'mp3', + }, + headers={ + 'Content-Type': 'application/json', + 'Authorization': f'Bearer {api_key}', + }, + ssl=AIOHTTP_CLIENT_SESSION_SSL, + ) + r.raise_for_status() + + res = await r.json() + audio_b64 = res.get('audio_data', '') + if not audio_b64: + raise ValueError('No audio_data in Mistral TTS response') + + await _write_tts_cache(file_path, base64.b64decode(audio_b64), file_body_path, payload) + return FileResponse(file_path) + except Exception as exc: + log.exception(exc) + await _raise_tts_error(exc, r) + + +# Dispatcher map: engine name -> handler +_TTS_ENGINES = { + 'openai': _tts_openai, + 'elevenlabs': _tts_elevenlabs, + 'azure': _tts_azure, + 'transformers': _tts_transformers, + 'mistral': _tts_mistral, +} + + @router.post('/speech') async def speech(request: Request, user=Depends(get_verified_user)): - if request.app.state.config.TTS_ENGINE == '': + engine = request.app.state.config.TTS_ENGINE + if engine == '': raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND, @@ -394,320 +595,36 @@ async def speech(request: Request, user=Depends(get_verified_user)): 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') + body + str(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 + # Return cached result if available 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) + payload = json.loads(body) + except Exception as exc: + log.exception(exc) 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 + handler = _TTS_ENGINES.get(engine) + if handler is None: + raise HTTPException(status_code=400, detail=f'Unsupported TTS engine: {engine}') - 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() - - audio_data = await r.read() - content_type_header = r.headers.get('Content-Type', 'audio/mpeg') - - if not transcode_audio_to_mp3(audio_data, content_type_header, file_path): - async with aiofiles.open(file_path, 'wb') as f: - await f.write(audio_data) - - 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, - ) + return await handler(request, payload, file_path, file_body_path, user) -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] +async def _transcribe_whisper(request, file_path, languages, file_dir, id): + 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) - metadata = metadata or {} + model = request.app.state.faster_whisper_model - 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 + def _run(): segments, info = model.transcribe( file_path, beam_size=5, @@ -716,158 +633,150 @@ def transcription_handler(request, file_path, metadata, user=None): multilingual=WHISPER_MULTILINGUAL, ) log.info("Detected language '%s' with probability %f" % (info.language, info.language_probability)) + return ''.join([segment.text for segment in list(segments)]) - transcript = ''.join([segment.text for segment in list(segments)]) - data = {'text': transcript.strip()} + transcript = await asyncio.to_thread(_run) + 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) + async with aiofiles.open(os.path.join(file_dir, f'{id}.json'), 'w') as f: + await f.write(json.dumps(data)) - 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, - } + log.debug(data) + return data - 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) +async def _transcribe_openai(request, file_path, filename, languages, file_dir, id, user=None): + """Transcribe audio via an OpenAI-compatible STT endpoint.""" + r = None + try: + session = await get_session() + for language in languages: + payload = {'model': request.app.state.config.STT_MODEL} + if language: + payload['language'] = language - 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, - ) + 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) - if r.status_code == 200: - # Successful transcription - break + form_data = aiohttp.FormData() + for key, value in payload.items(): + form_data.add_field(key, str(value)) + form_data.add_field('file', open(file_path, 'rb'), filename=filename) - 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", + r = await session.post( + url=f'{request.app.state.config.STT_OPENAI_API_BASE_URL}/audio/transcriptions', + headers=headers, + data=form_data, + ssl=AIOHTTP_CLIENT_SESSION_SSL, ) + if r.status == 200: + break - 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 + r.raise_for_status() + data = await r.json() - # IF NO LOCALES, USE DEFAULTS - if len(locales) < 2: - locales = [ + async with aiofiles.open(os.path.join(file_dir, f'{id}.json'), 'w') as f: + await f.write(json.dumps(data)) + return data + 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"].get("message", "")}' + except Exception: + detail = f'External: {e}' + raise Exception(detail if detail else 'Open WebUI: Server Connection Error') + + +async def _transcribe_deepgram(request, file_path, languages, file_dir, id): + """Transcribe audio via the Deepgram listen API with language fallback.""" + content_type = mimetypes.guess_type(file_path)[0] or 'audio/wav' + + async with aiofiles.open(file_path, 'rb') as f: + audio_bytes = await f.read() + + api_key = request.app.state.config.DEEPGRAM_API_KEY + stt_model = request.app.state.config.STT_MODEL + + r = None + try: + session = await get_session() + for lang in languages: + query: dict = {'smart_format': 'true'} + if stt_model: + query['model'] = stt_model + if lang: + query['language'] = lang + + r = await session.post( + 'https://api.deepgram.com/v1/listen', + headers={'Authorization': f'Token {api_key}', 'Content-Type': content_type}, + params=query, + data=audio_bytes, + ssl=AIOHTTP_CLIENT_SESSION_SSL, + ) + if r.status == 200: + break + + r.raise_for_status() + body = await r.json() + + # Parse the Deepgram response structure + try: + transcript = body['results']['channels'][0]['alternatives'][0].get('transcript', '').strip() + except (KeyError, IndexError) as exc: + log.error(f'Malformed Deepgram response: {exc}') + raise Exception('Failed to parse Deepgram response') from exc + + data = {'text': transcript} + async with aiofiles.open(os.path.join(file_dir, f'{id}.json'), 'w') as f: + await f.write(json.dumps(data)) + return data + + except Exception as e: + log.exception(e) + detail = 'Open WebUI: Server Connection Error' + if r is not None: + try: + res = await r.json() + msg = ( + res.get('error', {}).get('message', '') + if isinstance(res.get('error'), dict) + else str(res.get('error', '')) + ) + if msg: + detail = f'External: {msg}' + except Exception: + detail = f'External: {e}' + raise Exception(detail) + + +async def _transcribe_azure(request, file_path, filename, file_dir, id): + """Transcribe audio via Azure Cognitive Services batch transcription.""" + if not os.path.isfile(file_path): + raise HTTPException(status_code=400, detail='Audio file not found') + + audio_size = os.path.getsize(file_path) + if audio_size > AZURE_MAX_FILE_SIZE: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=f'File size ({audio_size // (1024 * 1024)}MB) exceeds Azure 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' + locale_str = 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 + + # Default to a broad set of locales when none are configured + if len(locale_str) < 2: + locale_str = ','.join( + [ 'en-US', 'es-ES', 'es-MX', @@ -882,281 +791,244 @@ def transcription_handler(request, file_path, metadata, user=None): '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', - ) + if not api_key or not region: + raise HTTPException(status_code=400, detail='Azure API key and region are required for Azure STT') - r = None + # Build the transcription definition payload + definition = json.dumps( + {'locales': locale_str.split(','), 'diarization': {'maxSpeakers': max_speakers, 'enabled': True}} + if locale_str + else {} + ) + endpoint = ( + base_url or f'https://{region}.api.cognitive.microsoft.com' + ) + '/speechtotext/transcriptions:transcribe?api-version=2024-11-15' + + form_data = aiohttp.FormData() + form_data.add_field('definition', definition) + form_data.add_field('audio', open(file_path, 'rb'), filename=filename) + + r = None + try: + session = await get_session() + r = await session.post( + url=endpoint, + data=form_data, + headers={'Ocp-Apim-Subscription-Key': api_key}, + ssl=AIOHTTP_CLIENT_SESSION_SSL, + ) + r.raise_for_status() + response = await r.json() + + if not response.get('combinedPhrases'): + raise ValueError('No transcription found in response') + + transcript = response['combinedPhrases'][0].get('text', '').strip() + if not transcript: + raise ValueError('Empty transcript in response') + + data = {'text': transcript} + + async with aiofiles.open(os.path.join(file_dir, f'{id}.json'), 'w') as f: + await f.write(json.dumps(data)) + + 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 aiohttp.ClientResponseError as e: + log.exception(e) + detail = None try: - # Prepare the request - data = { - 'definition': json.dumps( - { - 'locales': locales.split(','), - 'diarization': {'maxSpeakers': max_speakers, 'enabled': True}, + if r is not None and r.status != 200: + res = await r.json() + 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 locales - else {} - ) - } + 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=e.status if e.status else 500, + detail=detail if detail else 'Open WebUI: Server Connection Error', + ) - 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, - ) +async 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] - r.raise_for_status() - response = r.json() + metadata = metadata or {} - # Extract transcript from response - if not response.get('combinedPhrases'): - raise ValueError('No transcription found in response') + languages = [ + metadata.get('language', None) if not WHISPER_LANGUAGE else WHISPER_LANGUAGE, + None, # Always fallback to None in case transcription fails + ] - # 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', - ) + if request.app.state.config.STT_ENGINE == '': + return await _transcribe_whisper(request, file_path, languages, file_dir, id) + elif request.app.state.config.STT_ENGINE == 'openai': + return await _transcribe_openai(request, file_path, filename, languages, file_dir, id, user) + elif request.app.state.config.STT_ENGINE == 'deepgram': + return await _transcribe_deepgram(request, file_path, languages, file_dir, id) + elif request.app.state.config.STT_ENGINE == 'azure': + return await _transcribe_azure(request, file_path, filename, file_dir, id) 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') + return await _transcribe_mistral(request, file_path, filename, metadata, file_dir, id) - # 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 +async def _transcribe_mistral(request, file_path, filename, metadata, file_dir, id): + """Transcribe audio via the Mistral STT API.""" + if not os.path.isfile(file_path): + raise HTTPException(status_code=400, detail='Audio file not found') - if not api_key: - raise HTTPException( - status_code=400, - detail='Mistral API key is required for Mistral STT', - ) + 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') - r = None - try: - # Use voxtral-mini-latest as the default model for transcription - model = request.app.state.config.STT_MODEL or 'voxtral-mini-latest' + 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 - log.info( - f'Mistral STT - model: {model}, ' - f'method: {"chat_completions" if use_chat_completions else "transcriptions"}' - ) + if not api_key: + raise HTTPException(status_code=400, detail='Mistral API key is required for Mistral STT') - if use_chat_completions: - # Use chat completions API with audio input - # This method requires mp3 or wav format - audio_file_to_use = file_path + r = None + try: + model = request.app.state.config.STT_MODEL or 'voxtral-mini-latest' + log.info( + f'Mistral STT - model: {model}, method: {"chat_completions" if use_chat_completions else "transcriptions"}' + ) - 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.' + session = await get_session() + if use_chat_completions: + 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 = await asyncio.to_thread(convert_audio_to_mp3, file_path) + if converted_path: + audio_file_to_use = converted_path 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, + log.error('Audio conversion failed') + raise HTTPException( + status_code=500, + detail='Audio conversion failed. Chat completions API requires mp3 or wav format.', ) - r.raise_for_status() - response = r.json() + async with aiofiles.open(audio_file_to_use, 'rb') as audio_file: + raw = await audio_file.read() + audio_base64 = { + 'data': base64.b64encode(raw).decode('utf-8'), + 'format': mimetypes.guess_extension(mimetypes.guess_type(audio_file_to_use)[0]).lstrip('.'), + } - # 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', + language = metadata.get('language', None) if metadata else None + text_instruction = ( + f'Transcribe this audio exactly as spoken in {language}. Do not translate it.' + if language + else '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}, + ], + } + ], + } -def transcribe(request: Request, file_path: str, metadata: Optional[dict] = None, user=None): + r = await session.post( + url=f'{api_base_url}/chat/completions', + json=payload, + headers={'Authorization': f'Bearer {api_key}', 'Content-Type': 'application/json'}, + ssl=AIOHTTP_CLIENT_SESSION_SSL, + ) + r.raise_for_status() + response = await r.json() + + transcript = response.get('choices', [{}])[0].get('message', {}).get('content', '').strip() + if not transcript: + raise ValueError('Empty transcript in response') + data = {'text': transcript} + + else: + mime_type, _ = mimetypes.guess_type(file_path) + if not mime_type: + mime_type = 'audio/webm' + + form_data = aiohttp.FormData() + form_data.add_field('model', model) + + language = metadata.get('language', None) if metadata else None + if language: + form_data.add_field('language', language) + + form_data.add_field('file', open(file_path, 'rb'), filename=filename, content_type=mime_type) + + r = await session.post( + url=f'{api_base_url}/audio/transcriptions', + data=form_data, + headers={'Authorization': f'Bearer {api_key}'}, + ssl=AIOHTTP_CLIENT_SESSION_SSL, + ) + r.raise_for_status() + response = await r.json() + + transcript = response.get('text', '').strip() + if not transcript: + raise ValueError('Empty transcript in response') + data = {'text': transcript} + + async with aiofiles.open(os.path.join(file_dir, f'{id}.json'), 'w') as f: + await f.write(json.dumps(data)) + + 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 aiohttp.ClientResponseError as e: + log.exception(e) + detail = None + try: + if r is not None and r.status != 200: + res = await r.json() + if 'error' in res: + detail = f'External: {res["error"].get("message", "")}' + else: + detail = f'External: {await r.text()}' + except Exception: + detail = f'External: {e}' + raise HTTPException( + status_code=e.status if e.status else 500, + detail=detail if detail else 'Open WebUI: Server Connection Error', + ) + + +async def transcribe(request: Request, file_path: str, metadata: Optional[dict] = None, user=None): log.info(f'transcribe: {file_path} {metadata}') if BYPASS_PYDUB_PREPROCESSING: @@ -1164,7 +1036,7 @@ def transcribe(request: Request, file_path: str, metadata: Optional[dict] = None chunk_paths = [file_path] else: if is_audio_conversion_required(file_path): - file_path = convert_audio_to_mp3(file_path) + file_path = await asyncio.to_thread(convert_audio_to_mp3, file_path) if not file_path: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, @@ -1172,13 +1044,13 @@ def transcribe(request: Request, file_path: str, metadata: Optional[dict] = None ) try: - file_path = compress_audio(file_path) + file_path = await asyncio.to_thread(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) + chunk_paths = await asyncio.to_thread(split_audio, file_path, MAX_FILE_SIZE) print(f'Chunk paths: {chunk_paths}') except Exception as e: log.exception(e) @@ -1189,28 +1061,17 @@ def transcribe(request: Request, file_path: str, metadata: Optional[dict] = None results = [] try: - if getattr(request.app.state.config, 'STT_ENGINE', '') == '': - max_workers = 1 - else: - max_workers = None - - with ThreadPoolExecutor(max_workers=max_workers) 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}', - ) + tasks = [transcription_handler(request, chunk_path, metadata, user) for chunk_path in chunk_paths] + for coro in asyncio.as_completed(tasks): + try: + results.append(await coro) + 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: @@ -1341,7 +1202,7 @@ async def transcription( if language: metadata = {'language': language} - result = await asyncio.to_thread(transcribe, request, file_path, metadata, user) + result = await transcribe(request, file_path, metadata, user) return { **result, @@ -1370,57 +1231,62 @@ async def transcription( async def get_available_models(request: Request) -> list[dict]: + """Return the list of available TTS models for the configured engine.""" 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: + engine = request.app.state.config.TTS_ENGINE + _timeout = aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST) + + if engine == 'openai': + base_url = request.app.state.config.TTS_OPENAI_API_BASE_URL + if not base_url.startswith('https://api.openai.com'): + session = await get_session() + try: + async with session.get( + f'{base_url}/audio/models', + ssl=AIOHTTP_CLIENT_SESSION_SSL, + timeout=_timeout, + ) as resp: + resp.raise_for_status() + data = await resp.json() + available_models = data.get('models', []) + except Exception as e: + log.debug(f'/audio/models not available, trying /models fallback: {e}') try: async with session.get( - f'{request.app.state.config.TTS_OPENAI_API_BASE_URL}/audio/models', + f'{base_url}/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'}] + timeout=_timeout, + ) as resp: + resp.raise_for_status() + data = await resp.json() + available_models = data.get('data', data.get('models', [])) + except Exception as e2: + log.error(f'Error fetching models from custom endpoint: {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': + + elif 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] + session = await get_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, + timeout=_timeout, + ) as resp: + resp.raise_for_status() + models = await resp.json() + available_models = [{'name': m['name'], 'id': m['model_id']} for m in models] except Exception as e: - log.error(f'Error fetching models: {str(e)}') - elif request.app.state.config.TTS_ENGINE == 'mistral': + log.error(f'Error fetching models: {e}') + + elif engine == 'mistral': available_models = [{'id': 'voxtral-mini-tts-2603'}] + return available_models @@ -1429,129 +1295,104 @@ async def get_models(request: Request, user=Depends(get_verified_user)): return {'models': await get_available_models(request)} +_OPENAI_DEFAULT_VOICES = { + 'alloy': 'alloy', + 'echo': 'echo', + 'fable': 'fable', + 'onyx': 'onyx', + 'nova': 'nova', + 'shimmer': 'shimmer', +} + + 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'): + """Return ``{voice_id: voice_name}`` for the configured TTS engine.""" + engine = request.app.state.config.TTS_ENGINE + _timeout = aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST) + + if engine == 'openai': + base_url = request.app.state.config.TTS_OPENAI_API_BASE_URL + if not 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} + session = await get_session() + async with session.get( + f'{base_url}/audio/voices', + ssl=AIOHTTP_CLIENT_SESSION_SSL, + timeout=_timeout, + ) as resp: + resp.raise_for_status() + data = await resp.json() + return {v['id']: v['name'] for v in data.get('voices', [])} 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': + log.error(f'Error fetching voices from custom endpoint: {e}') + return dict(_OPENAI_DEFAULT_VOICES) + return dict(_OPENAI_DEFAULT_VOICES) + + if 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': + session = await get_session() + async with session.get( + f'{ELEVENLABS_API_BASE_URL}/v1/voices', + headers={ + 'xi-api-key': request.app.state.config.TTS_API_KEY, + 'Content-Type': 'application/json', + }, + ssl=AIOHTTP_CLIENT_SESSION_SSL, + timeout=_timeout, + ) as resp: + resp.raise_for_status() + voices_data = await resp.json() + return {v['voice_id']: v['name'] for v in voices_data.get('voices', [])} + except Exception as e: + log.error(f'Error fetching ElevenLabs voices: {e}') + return {} + + if 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"]})' + session = await get_session() + async with session.get( + url, + headers={'Ocp-Apim-Subscription-Key': request.app.state.config.TTS_API_KEY}, + ssl=AIOHTTP_CLIENT_SESSION_SSL, + timeout=_timeout, + ) as resp: + resp.raise_for_status() + voices = await resp.json() + return {v['ShortName']: f'{v["DisplayName"]} ({v["ShortName"]})' for v in voices} except Exception as e: - log.error(f'Error fetching voices: {str(e)}') - elif request.app.state.config.TTS_ENGINE == 'mistral': + log.error(f'Error fetching Azure voices: {e}') + return {} + + if 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 + session = await get_session() + async with session.get( + f'{api_base_url}/audio/voices', + headers={'Authorization': f'Bearer {api_key}'}, + ssl=AIOHTTP_CLIENT_SESSION_SSL, + timeout=_timeout, + ) as resp: + resp.raise_for_status() + voices_data = await resp.json() + items = voices_data.get('items', []) if isinstance(voices_data, dict) else voices_data + result = {} + for v in items: + if isinstance(v, dict): + vid = v.get('voice_id', v.get('id', '')) + if vid: + result[vid] = v.get('name', vid) + return result except Exception as e: - log.error(f'Error fetching Mistral voices: {str(e)}') + log.error(f'Error fetching Mistral voices: {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 + return {} @router.get('/voices') diff --git a/backend/open_webui/routers/auths.py b/backend/open_webui/routers/auths.py index 5c2be8f22d..434a6349de 100644 --- a/backend/open_webui/routers/auths.py +++ b/backend/open_webui/routers/auths.py @@ -1,92 +1,86 @@ +from __future__ import annotations + import asyncio -import re -import uuid -import time import datetime import logging -from aiohttp import ClientSession +import re +import time import urllib +import uuid +from ssl import CERT_NONE, CERT_REQUIRED, PROTOCOL_TLS +from typing import List, Optional - +from aiohttp import ClientSession +from fastapi import APIRouter, Depends, HTTPException, Request, status +from fastapi.responses import JSONResponse, RedirectResponse, Response +from ldap3 import NONE, Connection, Server, Tls +from ldap3.utils.conv import escape_filter_chars +from open_webui.config import ( + ENABLE_LDAP, + ENABLE_OAUTH_SIGNUP, + ENABLE_PASSWORD_AUTH, + OAUTH_MERGE_ACCOUNTS_BY_EMAIL, + OAUTH_PROVIDERS, + OPENID_END_SESSION_ENDPOINT, + OPENID_PROVIDER_URL, +) +from open_webui.constants import ERROR_MESSAGES, WEBHOOK_MESSAGES +from open_webui.env import ( + AIOHTTP_CLIENT_SESSION_SSL, + ENABLE_INITIAL_ADMIN_SIGNUP, + ENABLE_OAUTH_TOKEN_EXCHANGE, + WEBUI_AUTH, + WEBUI_AUTH_COOKIE_SAME_SITE, + WEBUI_AUTH_COOKIE_SECURE, + WEBUI_AUTH_SIGNOUT_REDIRECT_URL, + WEBUI_AUTH_TRUSTED_EMAIL_HEADER, + WEBUI_AUTH_TRUSTED_GROUPS_HEADER, + WEBUI_AUTH_TRUSTED_NAME_HEADER, + WEBUI_AUTH_TRUSTED_ROLE_HEADER, +) +from open_webui.internal.db import get_async_session from open_webui.models.auths import ( AddUserForm, ApiKey, Auths, - Token, LdapForm, SigninForm, SigninResponse, SignupForm, + Token, 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 open_webui.models.users import ( + UpdateProfileForm, + UserModel, + UserProfileImageResponse, + Users, + UserStatus, ) -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.access_control import get_permissions, has_permission from open_webui.utils.auth import ( - validate_password, - verify_password, - decode_token, - invalidate_token, create_api_key, create_token, + decode_token, get_admin_user, - get_verified_user, get_current_user, - get_password_hash, get_http_authorization_cred, + get_password_hash, + get_verified_user, + invalidate_token, + validate_password, + verify_password, ) -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.misc import parse_duration, validate_email_format +from open_webui.utils.oauth import auth_manager_config 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 +from open_webui.utils.redis import get_redis_client +from open_webui.utils.webhook import post_webhook +from pydantic import BaseModel +from sqlalchemy.ext.asyncio import AsyncSession router = APIRouter() @@ -155,14 +149,14 @@ async def create_session_response( class SessionUserResponse(Token, UserProfileImageResponse): - expires_at: Optional[int] = None - permissions: Optional[dict] = None + expires_at: int | None = None + permissions: dict | None = None class SessionUserInfoResponse(SessionUserResponse, UserStatus): - bio: Optional[str] = None - gender: Optional[str] = None - date_of_birth: Optional[datetime.date] = None + bio: str | None = None + gender: str | None = None + date_of_birth: datetime.date | None = None @router.get('/', response_model=SessionUserInfoResponse) @@ -209,7 +203,7 @@ async def get_session_user( user_permissions = await get_permissions(user.id, request.app.state.config.USER_PERMISSIONS, db=db) - return { + response_data = { 'token': token, 'token_type': 'Bearer', 'expires_at': expires_at, @@ -227,6 +221,8 @@ async def get_session_user( 'permissions': user_permissions, } + return response_data + ############################ # Update Profile @@ -290,8 +286,9 @@ async def update_password( session_user=Depends(get_current_user), db: AsyncSession = Depends(get_async_session), ): + # Trusted-header auth mode delegates passwords to the reverse proxy if WEBUI_AUTH_TRUSTED_EMAIL_HEADER: - raise HTTPException(400, detail=ERROR_MESSAGES.ACTION_PROHIBITED) + raise HTTPException(status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.ACTION_PROHIBITED) if session_user: user = await Auths.authenticate_user( session_user.email, @@ -582,7 +579,7 @@ async def signin( 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) + raise HTTPException(status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.INVALID_TRUSTED_HEADER) email = request.headers[WEBUI_AUTH_TRUSTED_EMAIL_HEADER].lower() name = email @@ -750,9 +747,12 @@ async def signup( 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: + if has_users: + if not request.app.state.config.ENABLE_SIGNUP or not request.app.state.config.ENABLE_LOGIN_FORM: raise HTTPException(status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.ACCESS_PROHIBITED) + # Don't gate the first admin on ENABLE_SIGNUP: it auto-disables and can persist stale across a DB reset. + elif not request.app.state.config.ENABLE_LOGIN_FORM and 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) @@ -1031,7 +1031,7 @@ async def get_admin_config(request: Request, user=Depends(get_admin_user)): class AdminConfig(BaseModel): SHOW_ADMIN_DETAILS: bool - ADMIN_EMAIL: Optional[str] = None + ADMIN_EMAIL: str | None = None WEBUI_URL: str ENABLE_SIGNUP: bool ENABLE_API_KEYS: bool @@ -1043,9 +1043,9 @@ class AdminConfig(BaseModel): 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 + FOLDER_MAX_FILE_COUNT: int | str | None = None + AUTOMATION_MAX_COUNT: int | str | None = None + AUTOMATION_MIN_INTERVAL: int | str | None = None ENABLE_AUTOMATIONS: bool ENABLE_CHANNELS: bool ENABLE_CALENDAR: bool @@ -1053,9 +1053,9 @@ class AdminConfig(BaseModel): 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 + PENDING_USER_OVERLAY_TITLE: str | None = None + PENDING_USER_OVERLAY_CONTENT: str | None = None + RESPONSE_WATERMARK: str | None = None @router.post('/admin/config') @@ -1140,7 +1140,7 @@ async def update_admin_config(request: Request, form_data: AdminConfig, user=Dep class LdapServerConfig(BaseModel): label: str host: str - port: Optional[int] = None + port: int | None = None attribute_for_mail: str = 'mail' attribute_for_username: str = 'uid' app_dn: str @@ -1148,9 +1148,9 @@ class LdapServerConfig(BaseModel): search_base: str search_filters: str = '' use_tls: bool = True - certificate_path: Optional[str] = None + certificate_path: str | None = None validate_cert: bool = True - ciphers: Optional[str] = 'ALL' + ciphers: str | None = 'ALL' @router.get('/admin/config/ldap/server', response_model=LdapServerConfig) @@ -1223,7 +1223,7 @@ async def get_ldap_config(request: Request, user=Depends(get_admin_user)): class LdapConfigForm(BaseModel): - enable_ldap: Optional[bool] = None + enable_ldap: bool | None = None @router.post('/admin/config/ldap') diff --git a/backend/open_webui/routers/automations.py b/backend/open_webui/routers/automations.py index 4ff66feb97..fced12978a 100644 --- a/backend/open_webui/routers/automations.py +++ b/backend/open_webui/routers/automations.py @@ -1,30 +1,29 @@ import asyncio import logging - from typing import Optional -from fastapi import APIRouter, Depends, HTTPException, Request, status -from sqlalchemy.ext.asyncio import AsyncSession +from fastapi import APIRouter, Depends, HTTPException, Request, status +from open_webui.constants import ERROR_MESSAGES +from open_webui.internal.db import get_async_session from open_webui.models.automations import ( - Automations, - AutomationRuns, AutomationForm, + AutomationListResponse, AutomationModel, AutomationResponse, AutomationRunModel, - AutomationListResponse, + AutomationRuns, + Automations, ) -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 +from open_webui.utils.auth import get_admin_user, get_verified_user +from open_webui.utils.automations import ( + execute_automation, + next_n_runs_ns, + next_run_ns, + rrule_interval_seconds, + validate_rrule, +) +from sqlalchemy.ext.asyncio import AsyncSession log = logging.getLogger(__name__) diff --git a/backend/open_webui/routers/calendar.py b/backend/open_webui/routers/calendar.py index bdc06e819b..5d397ea868 100644 --- a/backend/open_webui/routers/calendar.py +++ b/backend/open_webui/routers/calendar.py @@ -3,28 +3,27 @@ import time from typing import Optional from fastapi import APIRouter, Depends, HTTPException, Request, status - +from open_webui.constants import ERROR_MESSAGES +from open_webui.models.access_grants import AccessGrants from open_webui.models.calendar import ( - Calendars, - CalendarEvents, CalendarEventAttendees, - CalendarForm, - CalendarUpdateForm, CalendarEventForm, - CalendarEventUpdateForm, - CalendarModel, - CalendarEventModel, - CalendarEventUserResponse, CalendarEventListResponse, + CalendarEventModel, + CalendarEvents, + CalendarEventUpdateForm, + CalendarEventUserResponse, + CalendarForm, + CalendarModel, + Calendars, + CalendarUpdateForm, 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.access_control import filter_allowed_access_grants, has_permission from open_webui.utils.auth import get_verified_user -from open_webui.utils.access_control import has_permission, filter_allowed_access_grants from open_webui.utils.calendar import expand_recurring_event -from open_webui.constants import ERROR_MESSAGES log = logging.getLogger(__name__) @@ -188,7 +187,7 @@ async def get_events( cal_id_list is None or SCHEDULED_TASKS_CALENDAR_ID in cal_id_list ): try: - from open_webui.models.automations import Automations, AutomationRuns + from open_webui.models.automations import AutomationRuns, Automations # Future runs: expand RRULEs for active automations only active_automations = await Automations.get_active_by_user(user.id) @@ -302,6 +301,12 @@ async def update_event( await _check_calendar_access(event.calendar_id, user, 'write') + # A new calendar_id in the form moves the event; require write access on the + # destination too, mirroring create_event. Without this, write on the source + # calendar alone is enough to inject an event into any other calendar. + if form_data.calendar_id is not None and form_data.calendar_id != event.calendar_id: + await _check_calendar_access(form_data.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') diff --git a/backend/open_webui/routers/channels.py b/backend/open_webui/routers/channels.py index 70eb799ea6..11d3a4a871 100644 --- a/backend/open_webui/routers/channels.py +++ b/backend/open_webui/routers/channels.py @@ -1,69 +1,58 @@ -import json -import logging import base64 import io +import json +import logging 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 fastapi import APIRouter, BackgroundTasks, Depends, HTTPException, Request, status +from fastapi.responses import FileResponse, Response, StreamingResponse +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.internal.db import get_async_session +from open_webui.models.access_grants import AccessGrants, has_public_read_access_grant, has_public_write_access_grant +from open_webui.models.channels import ( + ChannelForm, + ChannelModel, + ChannelResponse, + Channels, + ChannelWebhookForm, + ChannelWebhookModel, + CreateChannelForm, +) +from open_webui.models.groups import Groups +from open_webui.models.messages import ( + MessageForm, + MessageModel, + MessageResponse, + Messages, + MessageWithReactionsResponse, ) from open_webui.models.users import ( UserIdNameResponse, UserIdNameStatusResponse, UserListResponse, - UserModelResponse, - Users, UserModel, + UserModelResponse, UserNameResponse, + Users, ) - -from open_webui.models.groups import Groups -from open_webui.models.channels import ( - Channels, - ChannelModel, - ChannelForm, - ChannelResponse, - CreateChannelForm, - ChannelWebhookModel, - ChannelWebhookForm, +from open_webui.socket.main import ( + emit_to_users, + enter_room_for_users, + get_user_ids_from_room, + sio, ) -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.access_control import filter_allowed_access_grants, has_permission +from open_webui.utils.auth import get_admin_user, get_verified_user +from open_webui.utils.channels import extract_mentions, replace_mentions 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.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 pydantic import BaseModel, field_validator from sqlalchemy.ext.asyncio import AsyncSession log = logging.getLogger(__name__) @@ -922,6 +911,7 @@ async def model_response_handler(request, channel, message, user, db=None): thread_history = [] images = [] + files = [] # Batch fetch all users in a single query (fixes N+1 problem) user_ids = list({message.user_id for message in thread_messages}) @@ -945,9 +935,11 @@ async def model_response_handler(request, channel, message, user, db=None): 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', '')) + image = await get_image_base64_from_file_id(file.get('id', ''), user=user) if image: images.append(image) + elif file.get('id'): + files.append(file) thread_history_string = '\n\n'.join(thread_history) system_message = { @@ -980,9 +972,9 @@ async def model_response_handler(request, channel, message, user, db=None): # Resolve model config (same helpers automations use) from open_webui.utils.automations import ( - _resolve_model_tool_ids, _resolve_model_features, _resolve_model_filter_ids, + _resolve_model_tool_ids, ) tool_ids = _resolve_model_tool_ids(request.app, model_id) @@ -1005,6 +997,8 @@ async def model_response_handler(request, channel, message, user, db=None): 'session_id': f'channel:{channel.id}', 'background_tasks': {}, } + if files: + form_data['files'] = files if tool_ids: form_data['tool_ids'] = tool_ids if features: diff --git a/backend/open_webui/routers/chats.py b/backend/open_webui/routers/chats.py index 9c4609477c..2689aa6d2f 100644 --- a/backend/open_webui/routers/chats.py +++ b/backend/open_webui/routers/chats.py @@ -1,43 +1,42 @@ +from __future__ import annotations + +import asyncio import json import logging from typing import Optional from uuid import uuid4 -from sqlalchemy.ext.asyncio import AsyncSession -import asyncio + +from fastapi import APIRouter, Depends, HTTPException, Request, status from fastapi.responses import StreamingResponse - - -from open_webui.utils.misc import get_message_list -from open_webui.utils.middleware import serialize_output -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.internal.db import get_async_session +from open_webui.models.access_grants import AccessGrants +from open_webui.models.chats import ( + AggregateChatStats, + ChatBody, + ChatForm, + ChatHistoryStats, + ChatImportForm, + ChatResponse, + Chats, + ChatsImportForm, + ChatStatsExport, + ChatTitleIdResponse, + ChatUsageStatsListResponse, + MessageStats, +) +from open_webui.models.folders import Folders +from open_webui.models.shared_chats import SharedChatResponse, SharedChats +from open_webui.models.tags import TagModel, Tags +from open_webui.socket.main import get_event_emitter +from open_webui.tasks import stop_item_tasks +from open_webui.utils.access_control import filter_allowed_access_grants, has_permission 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.middleware import serialize_output +from open_webui.utils.misc import get_message_list +from pydantic import BaseModel +from sqlalchemy.ext.asyncio import AsyncSession log = logging.getLogger(__name__) @@ -54,9 +53,9 @@ router = APIRouter() @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, + page: int | None = None, + include_pinned: bool | None = False, + include_folders: bool | None = False, db: AsyncSession = Depends(get_async_session), ): try: @@ -92,8 +91,8 @@ async def get_session_user_chat_list( @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, + items_per_page: int | None = 50, + page: int | None = 1, user=Depends(get_verified_user), db: AsyncSession = Depends(get_async_session), ): @@ -210,7 +209,7 @@ class ChatStatsExportList(BaseModel): page: int -def _process_chat_for_export(chat) -> Optional[ChatStatsExport]: +def _process_chat_for_export(chat) -> ChatStatsExport | None: try: def get_message_content_length(message): @@ -395,8 +394,8 @@ async def generate_chat_stats_jsonl_generator(user_id, filter): @router.get('/stats/export', response_model=ChatStatsExportList) async def export_chat_stats( request: Request, - updated_at: Optional[int] = None, - page: Optional[int] = 1, + updated_at: int | None = None, + page: int | None = 1, stream: bool = False, user=Depends(get_verified_user), ): @@ -438,7 +437,7 @@ async def export_chat_stats( ############################ -@router.get('/stats/export/{chat_id}', response_model=Optional[ChatStatsExport]) +@router.get('/stats/export/{chat_id}', response_model=ChatStatsExport | None) async def export_single_chat_stats( request: Request, chat_id: str, @@ -516,24 +515,20 @@ async def delete_all_user_chats( @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, + page: int | None = None, + query: str | None = None, + order_by: str | None = None, + direction: str | None = None, user=Depends(get_admin_user), db: AsyncSession = Depends(get_async_session), ): + """List chat summaries for a given user (admin-only endpoint).""" 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 + raise HTTPException(status.HTTP_401_UNAUTHORIZED, detail=ERROR_MESSAGES.ACCESS_PROHIBITED) + effective_page = page if page is not None else 1 limit = 60 - skip = (page - 1) * limit + skip = (effective_page - 1) * limit filter = {} if query: @@ -553,12 +548,24 @@ async def get_user_chat_list_by_user_id( ############################ -@router.post('/new', response_model=Optional[ChatResponse]) +@router.post('/new', response_model=ChatResponse | None) async def create_new_chat( form_data: ChatForm, user=Depends(get_verified_user), db: AsyncSession = Depends(get_async_session), ): + # Reject a folder_id that doesn't belong to the caller. Without this the + # row is persisted with a dangling foreign reference — no read path + # surfaces it across users (all chat reads are user_id-filtered), but + # the row state is meaningless and downstream consumers shouldn't have + # to assume the column is clean. Also catches non-UUID / nonexistent IDs. + if form_data.folder_id is not None: + if not await Folders.get_folder_by_id_and_user_id(form_data.folder_id, user.id, db=db): + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=ERROR_MESSAGES.NOT_FOUND, + ) + try: chat = await Chats.insert_new_chat(str(uuid4()), user.id, form_data, db=db) return ChatResponse(**chat.model_dump()) @@ -594,7 +601,7 @@ async def import_chats( @router.get('/search', response_model=list[ChatTitleIdResponse]) async def search_user_chats( text: str, - page: Optional[int] = None, + page: int | None = None, user=Depends(get_verified_user), db: AsyncSession = Depends(get_async_session), ): @@ -644,7 +651,7 @@ async def get_chats_by_folder_id( @router.get('/folder/{folder_id}/list') async def get_chat_list_by_folder_id( folder_id: str, - page: Optional[int] = 1, + page: int | None = 1, user=Depends(get_verified_user), db: AsyncSession = Depends(get_async_session), ): @@ -752,10 +759,7 @@ async def get_all_user_tags(user=Depends(get_verified_user), db: AsyncSession = @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, - ) + raise HTTPException(status.HTTP_401_UNAUTHORIZED, detail=ERROR_MESSAGES.ACCESS_PROHIBITED) return [ChatResponse(**chat.model_dump()) for chat in await Chats.get_chats(db=db)] @@ -766,10 +770,10 @@ async def get_all_user_chats_in_db(user=Depends(get_admin_user), db: AsyncSessio @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, + page: int | None = None, + query: str | None = None, + order_by: str | None = None, + direction: str | None = None, user=Depends(get_verified_user), db: AsyncSession = Depends(get_async_session), ): @@ -823,10 +827,10 @@ async def unarchive_all_chats(user=Depends(get_verified_user), db: AsyncSession @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, + page: int | None = None, + query: str | None = None, + order_by: str | None = None, + direction: str | None = None, user=Depends(get_verified_user), db: AsyncSession = Depends(get_async_session), ): @@ -858,7 +862,7 @@ async def get_shared_session_user_chat_list( ############################ -@router.get('/share/{share_id}', response_model=Optional[ChatResponse]) +@router.get('/share/{share_id}', response_model=ChatResponse | None) async def get_shared_chat_by_id( share_id: str, user=Depends(get_verified_user), db: AsyncSession = Depends(get_async_session) ): @@ -904,8 +908,8 @@ class TagForm(BaseModel): class TagFilterForm(TagForm): - skip: Optional[int] = 0 - limit: Optional[int] = 50 + skip: int | None = 0 + limit: int | None = 50 @router.post('/tags', response_model=list[ChatTitleIdResponse]) @@ -928,7 +932,7 @@ async def get_user_chat_list_by_tag_name( ############################ -@router.get('/{id}', response_model=Optional[ChatResponse]) +@router.get('/{id}', response_model=ChatResponse | None) 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) @@ -958,7 +962,7 @@ async def get_chat_by_id(id: str, user=Depends(get_verified_user), db: AsyncSess ############################ -@router.post('/{id}', response_model=Optional[ChatResponse]) +@router.post('/{id}', response_model=ChatResponse | None) async def update_chat_by_id( id: str, form_data: ChatForm, @@ -969,14 +973,25 @@ async def update_chat_by_id( if chat: updated_chat = {**chat.chat, **form_data.chat} - # Re-derive content from output for assistant messages so that - # frontend edits to output items are always reflected in content. - # serialize_output() is the single source of truth for this conversion. - for msg in updated_chat.get('history', {}).get('messages', {}).values(): + # Re-derive content from output for assistant messages so that frontend + # edits to output items are reflected in content. Only when output + # actually changed — otherwise content set independently of output + # (e.g. a `replace` event or an outlet filter footer) would be reverted. + existing_messages = (chat.chat.get('history') or {}).get('messages') or {} + for msg_id, msg in updated_chat.get('history', {}).get('messages', {}).items(): if msg.get('role') == 'assistant' and msg.get('output'): - msg['content'] = serialize_output(msg['output']) + if msg.get('output') != existing_messages.get(msg_id, {}).get('output'): + msg['content'] = serialize_output(msg['output']) chat = await Chats.update_chat_by_id(id, updated_chat, db=db) + + # Reconcile chat_message rows with the committed blob. + # This is the only caller where the frontend pushes a full + # history with potential edits, deletions, or new branches. + messages = (updated_chat.get('history') or {}).get('messages') or {} + if messages: + await Chats.reconcile_messages_by_chat_id(id, user.id, messages) + return ChatResponse(**chat.model_dump()) else: raise HTTPException( @@ -992,7 +1007,7 @@ class MessageForm(BaseModel): content: str -@router.post('/{id}/messages/{message_id}', response_model=Optional[ChatResponse]) +@router.post('/{id}/messages/{message_id}', response_model=ChatResponse | None) async def update_chat_message_by_id( id: str, message_id: str, @@ -1054,7 +1069,7 @@ class EventForm(BaseModel): data: dict -@router.post('/{id}/messages/{message_id}/event', response_model=Optional[bool]) +@router.post('/{id}/messages/{message_id}/event', response_model=bool | None) async def send_chat_message_event_by_id( id: str, message_id: str, @@ -1106,6 +1121,10 @@ async def delete_chat_by_id( user=Depends(get_verified_user), db: AsyncSession = Depends(get_async_session), ): + # Cancel any in-flight LLM tasks (streaming, title/tags generation) + # before deleting the chat to prevent orphaned requests. + await stop_item_tasks(request.app.state.redis, id) + if user.role == 'admin': chat = await Chats.get_chat_by_id(id, db=db) if not chat: @@ -1142,7 +1161,7 @@ async def delete_chat_by_id( ############################ -@router.get('/{id}/pinned', response_model=Optional[bool]) +@router.get('/{id}/pinned', response_model=bool | None) async def get_pinned_status_by_id( id: str, user=Depends(get_verified_user), db: AsyncSession = Depends(get_async_session) ): @@ -1158,7 +1177,7 @@ async def get_pinned_status_by_id( ############################ -@router.post('/{id}/pin', response_model=Optional[ChatResponse]) +@router.post('/{id}/pin', response_model=ChatResponse | None) 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: @@ -1174,10 +1193,10 @@ async def pin_chat_by_id(id: str, user=Depends(get_verified_user), db: AsyncSess class CloneForm(BaseModel): - title: Optional[str] = None + title: str | None = None -@router.post('/{id}/clone', response_model=Optional[ChatResponse]) +@router.post('/{id}/clone', response_model=ChatResponse | None) async def clone_chat_by_id( form_data: CloneForm, id: str, @@ -1225,7 +1244,7 @@ async def clone_chat_by_id( ############################ -@router.post('/{id}/clone/shared', response_model=Optional[ChatResponse]) +@router.post('/{id}/clone/shared', response_model=ChatResponse | None) async def clone_shared_chat_by_id( id: str, user=Depends(get_verified_user), db: AsyncSession = Depends(get_async_session) ): @@ -1294,14 +1313,21 @@ async def clone_shared_chat_by_id( ############################ -@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)): +@router.post('/{id}/archive', response_model=ChatResponse | None) +async def archive_chat_by_id( + request: Request, + 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: + # Cancel any in-flight LLM tasks before archiving + await stop_item_tasks(request.app.state.redis, id) # Archived chats are excluded from count — clean up orphans await Chats.delete_orphan_tags_for_user(tag_ids, user.id, db=db) else: @@ -1313,86 +1339,63 @@ async def archive_chat_by_id(id: str, user=Depends(get_verified_user), db: Async raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail=ERROR_MESSAGES.DEFAULT()) -############################ -# ShareChatById -############################ +# --- Share Chat --- -@router.post('/{id}/share', response_model=Optional[ChatResponse]) +@router.post('/{id}/share', response_model=ChatResponse | None) 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) + 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, - ) + raise HTTPException(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.HTTP_401_UNAUTHORIZED, detail=ERROR_MESSAGES.ACCESS_PROHIBITED) - 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()) + # If a share already exists, re-snapshot it + if chat.share_id: + shared = await SharedChats.update(chat.share_id, db=db) + if shared: + 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()) + # Create a new share + shared = await SharedChats.create(id, user.id, db=db) + if not shared: + raise HTTPException(status.HTTP_500_INTERNAL_SERVER_ERROR, detail=ERROR_MESSAGES.DEFAULT()) - else: - raise HTTPException( - status_code=status.HTTP_401_UNAUTHORIZED, - detail=ERROR_MESSAGES.ACCESS_PROHIBITED, - ) + chat = await Chats.update_chat_share_id_by_id(id, shared.id, db=db) + if not chat: + raise HTTPException(status.HTTP_500_INTERNAL_SERVER_ERROR, detail=ERROR_MESSAGES.DEFAULT()) + + return ChatResponse(**chat.model_dump()) -############################ -# DeleteSharedChatById -############################ +# --- Delete Shared Chat --- -@router.delete('/{id}/share', response_model=Optional[bool]) +@router.delete('/{id}/share', response_model=bool | None) 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 + if not chat: + raise HTTPException(status.HTTP_401_UNAUTHORIZED, detail=ERROR_MESSAGES.ACCESS_PROHIBITED) - await SharedChats.delete_by_chat_id(id, db=db) - await Chats.update_chat_share_id_by_id(id, None, db=db) + if not chat.share_id: + return False - # Revoke all access grants for this shared chat - await AccessGrants.set_access_grants('shared_chat', id, [], db=db) + await SharedChats.delete_by_chat_id(id, db=db) + await Chats.update_chat_share_id_by_id(id, None, db=db) + 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, - ) + return True ############################ @@ -1404,7 +1407,7 @@ class ChatAccessGrantsForm(BaseModel): access_grants: list[dict] -@router.post('/shared/{id}/access/update', response_model=Optional[ChatResponse]) +@router.post('/shared/{id}/access/update', response_model=ChatResponse | None) async def update_shared_chat_access_by_id( request: Request, id: str, @@ -1474,10 +1477,10 @@ async def get_shared_chat_access_by_id( class ChatFolderIdForm(BaseModel): - folder_id: Optional[str] = None + folder_id: str | None = None -@router.post('/{id}/folder', response_model=Optional[ChatResponse]) +@router.post('/{id}/folder', response_model=ChatResponse | None) async def update_chat_folder_id_by_id( id: str, form_data: ChatFolderIdForm, @@ -1486,6 +1489,15 @@ async def update_chat_folder_id_by_id( ): chat = await Chats.get_chat_by_id_and_user_id(id, user.id, db=db) if chat: + # Same ownership check as the create path — reject foreign / dangling + # folder_id values. None is allowed (moves the chat out of any folder). + if form_data.folder_id is not None: + if not await Folders.get_folder_by_id_and_user_id(form_data.folder_id, user.id, db=db): + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=ERROR_MESSAGES.NOT_FOUND, + ) + 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: @@ -1564,23 +1576,3 @@ async def delete_tag_by_id_and_tag_name( 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 index 1d55dba75e..c40131f41f 100644 --- a/backend/open_webui/routers/configs.py +++ b/backend/open_webui/routers/configs.py @@ -1,37 +1,34 @@ -import logging -import copy -from fastapi import APIRouter, Depends, Request, HTTPException -from pydantic import BaseModel, ConfigDict -import aiohttp +from __future__ import annotations +import copy +import logging from typing import Optional +import aiohttp +from fastapi import APIRouter, Depends, HTTPException, Request +from mcp.shared.auth import OAuthMetadata +from open_webui.config import BannerModel, async_save_config, get_config, save_config from open_webui.env import AIOHTTP_CLIENT_SESSION_SSL, AIOHTTP_CLIENT_TIMEOUT +from open_webui.models.oauth_sessions import OAuthSessions from open_webui.utils.auth import get_admin_user, get_verified_user from open_webui.utils.headers import get_custom_headers -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 ( + OAuthClientInformationFull, + decrypt_data, + encrypt_data, 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 +from open_webui.utils.tools import ( + get_tool_server_data, + get_tool_server_url, + set_terminal_servers, + set_tool_servers, +) +from pydantic import BaseModel, ConfigDict router = APIRouter() @@ -102,16 +99,16 @@ async def set_connections_config( class OAuthClientRegistrationForm(BaseModel): url: str client_id: str - client_name: Optional[str] = None - client_secret: Optional[str] = None - oauth_server_url: Optional[str] = None + client_name: str | None = None + client_secret: str | None = None + oauth_server_url: str | None = None @router.post('/oauth/clients/register') async def register_oauth_client( request: Request, form_data: OAuthClientRegistrationForm, - type: Optional[str] = None, + type: str | None = None, user=Depends(get_admin_user), ): try: @@ -154,12 +151,12 @@ async def register_oauth_client( 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] - info: Optional[dict] = None + type: str | None = 'openapi' # openapi, mcp + auth_type: str | None + headers: dict | str | None = None + key: str | None + config: dict | None + info: dict | None = None model_config = ConfigDict(extra='allow') @@ -225,23 +222,23 @@ async def set_tool_servers_config( class TerminalServerConnection(BaseModel): - id: Optional[str] = '' - name: Optional[str] = '' + id: str | None = '' + name: str | None = '' - enabled: Optional[bool] = True + enabled: bool | None = True url: str - path: Optional[str] = '/openapi.json' + path: str | None = '/openapi.json' - key: Optional[str] = '' - auth_type: Optional[str] = 'bearer' + key: str | None = '' + auth_type: str | None = 'bearer' - config: Optional[dict] = None + config: dict | None = None # Orchestrator policy fields - server_type: Optional[str] = None # "orchestrator", "terminal" - policy_id: Optional[str] = None - policy: Optional[dict] = None # cached policy data + server_type: str | None = None # "orchestrator", "terminal" + policy_id: str | None = None + policy: dict | None = None # cached policy data model_config = ConfigDict(extra='allow') @@ -325,8 +322,8 @@ async def verify_terminal_server_connection( class TerminalServerPolicyForm(BaseModel): url: str - key: Optional[str] = '' - auth_type: Optional[str] = 'bearer' + key: str | None = '' + auth_type: str | None = 'bearer' policy_id: str policy_data: dict @@ -504,19 +501,19 @@ async def verify_tool_servers_config(request: Request, form_data: ToolServerConn 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] + CODE_EXECUTION_JUPYTER_URL: str | None + CODE_EXECUTION_JUPYTER_AUTH: str | None + CODE_EXECUTION_JUPYTER_AUTH_TOKEN: str | None + CODE_EXECUTION_JUPYTER_AUTH_PASSWORD: str | None + CODE_EXECUTION_JUPYTER_TIMEOUT: int | None 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] + CODE_INTERPRETER_PROMPT_TEMPLATE: str | None + CODE_INTERPRETER_JUPYTER_URL: str | None + CODE_INTERPRETER_JUPYTER_AUTH: str | None + CODE_INTERPRETER_JUPYTER_AUTH_TOKEN: str | None + CODE_INTERPRETER_JUPYTER_AUTH_PASSWORD: str | None + CODE_INTERPRETER_JUPYTER_TIMEOUT: int | None @router.get('/code_execution', response_model=CodeInterpreterConfigForm) @@ -588,11 +585,11 @@ async def set_code_execution_config( # 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 + DEFAULT_MODELS: str | None + DEFAULT_PINNED_MODELS: str | None + MODEL_ORDER_LIST: list[str | None] + DEFAULT_MODEL_METADATA: dict | None = None + DEFAULT_MODEL_PARAMS: dict | None = None @router.get('/models/defaults') diff --git a/backend/open_webui/routers/evaluations.py b/backend/open_webui/routers/evaluations.py index 072c7fa732..d1c914f4ee 100644 --- a/backend/open_webui/routers/evaluations.py +++ b/backend/open_webui/routers/evaluations.py @@ -1,26 +1,23 @@ -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 typing import Optional -from open_webui.models.users import Users, UserModel +from fastapi import APIRouter, Depends, HTTPException, Request, status +from fastapi.concurrency import run_in_threadpool +from open_webui.constants import ERROR_MESSAGES +from open_webui.internal.db import get_async_session from open_webui.models.feedbacks import ( - FeedbackIdResponse, - FeedbackModel, - FeedbackResponse, FeedbackForm, - FeedbackUserResponse, + FeedbackIdResponse, FeedbackListResponse, + FeedbackModel, + Feedbacks, LeaderboardFeedbackData, ModelHistoryEntry, ModelHistoryResponse, - Feedbacks, ) - -from open_webui.constants import ERROR_MESSAGES +from open_webui.models.users import UserModel, Users from open_webui.utils.auth import get_admin_user, get_verified_user -from open_webui.internal.db import get_async_session +from pydantic import BaseModel from sqlalchemy.ext.asyncio import AsyncSession log = logging.getLogger(__name__) @@ -296,12 +293,6 @@ async def get_feedback_model_ids(user=Depends(get_admin_user), db: AsyncSession 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) @@ -325,10 +316,19 @@ async def export_all_feedbacks( 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 +PAGE_ITEM_COUNT = 30 + + +@router.get('/feedbacks/user', response_model=FeedbackListResponse) +async def get_user_feedbacks( + 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 + return await Feedbacks.get_feedbacks_by_user_id(user.id, skip=skip, limit=limit, db=db) @router.delete('/feedbacks', response_model=bool) @@ -337,9 +337,6 @@ async def delete_feedbacks(user=Depends(get_verified_user), db: AsyncSession = D return success -PAGE_ITEM_COUNT = 30 - - @router.get('/feedbacks/list', response_model=FeedbackListResponse) async def get_feedbacks( order_by: Optional[str] = None, diff --git a/backend/open_webui/routers/files.py b/backend/open_webui/routers/files.py index 86beeec188..dbf1ccb885 100644 --- a/backend/open_webui/routers/files.py +++ b/backend/open_webui/routers/files.py @@ -1,34 +1,32 @@ +import asyncio +import hashlib +import json 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, + BackgroundTasks, Depends, File, Form, HTTPException, + Query, 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.config import BYPASS_ADMIN_ACCESS_CONTROL, STORAGE_LOCAL_CACHE, STORAGE_PROVIDER, UPLOAD_DIR from open_webui.constants import ERROR_MESSAGES -from open_webui.retrieval.vector.async_client import ASYNC_VECTOR_DB_CLIENT - +from open_webui.internal.db import get_async_db_context, get_async_session +from open_webui.models.access_grants import AccessGrants from open_webui.models.channels import Channels -from open_webui.models.users import Users +from open_webui.models.chats import Chats from open_webui.models.files import ( FileForm, FileListResponse, @@ -36,22 +34,17 @@ from open_webui.models.files import ( 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.models.knowledge import Knowledges +from open_webui.models.users import Users +from open_webui.retrieval.vector.async_client import ASYNC_VECTOR_DB_CLIENT from open_webui.routers.audio import transcribe - +from open_webui.routers.retrieval import ProcessFileForm, process_file 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 +from sqlalchemy.ext.asyncio import AsyncSession log = logging.getLogger(__name__) @@ -68,7 +61,11 @@ from open_webui.utils.access_control.files import has_access_to_file 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. + """Check if a file is likely a text file by reading a chunk and decoding it. + + Tries UTF-8 first, then falls back to Latin-1 (which accepts every byte + in 0x00–0xFF) so that legacy-encoded files from Windows environments are + not misclassified as binary. This catches files whose extensions are mis-mapped by mimetypes/browsers (e.g. TypeScript .ts → video/mp2t) without maintaining an extension whitelist. @@ -82,9 +79,15 @@ def _is_text_file(file_path: str, chunk_size: int = 8192) -> bool: # Null bytes are a strong indicator of binary content if b'\x00' in chunk: return False - chunk.decode('utf-8') + try: + chunk.decode('utf-8') + except UnicodeDecodeError: + # Latin-1 always succeeds (every byte is valid), so this + # effectively just means "the file has no null bytes and is + # therefore likely text, even if not valid UTF-8". + chunk.decode('latin-1') return True - except (UnicodeDecodeError, Exception): + except Exception: return False @@ -120,38 +123,48 @@ async def process_uploaded_file( 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', []) + stt_supported = 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 = await asyncio.to_thread( - transcribe, - request, - file_path_processed, - file_metadata, - user, - ) + if content_type and strict_match_mime_type(stt_supported, content_type): + # Audio / STT-supported files → transcribe then index + file_path_processed = await asyncio.to_thread(Storage.get_file, file_path) + result = await 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, + ) - 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, + elif ( + content_type + and content_type.startswith(('image/', 'video/')) + and request.app.state.config.CONTENT_EXTRACTION_ENGINE != 'external' + ): + # Media files without an external extraction engine + if content_type.startswith('video/'): + # Videos are stored as-is for downstream multimodal + # processing (Tools, vision models). Attempting text + # extraction causes "Timeout reached while detecting + # encoding" errors. + log.info(f'Video file detected ({content_type}), skipping text extraction') + await Files.update_file_data_by_id( + file_item.id, + {'status': 'completed'}, 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') + # Documents, or any file when an external engine is configured + if not content_type: + 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), @@ -159,6 +172,28 @@ async def process_uploaded_file( db=db_session, ) + # Auto-link to Knowledge Collection when uploaded from one (#24807). + # Mirrors POST /knowledge/{id}/file/add so linking doesn't depend + # on the frontend staying connected after upload. + knowledge_id = file_metadata.get('knowledge_id') + if knowledge_id: + try: + await Knowledges.add_file_to_knowledge_by_id( + knowledge_id=knowledge_id, + file_id=file_item.id, + user_id=user.id, + directory_id=file_metadata.get('directory_id'), + ) + await process_file( + request, + ProcessFileForm(file_id=file_item.id, collection_name=knowledge_id), + user=user, + db=db_session, + ) + log.info(f'Linked file {file_item.id} to knowledge {knowledge_id}') + except Exception as e: + log.warning(f'Failed to link file {file_item.id} to knowledge {knowledge_id}: {e}') + except Exception as e: log.error(f'Error processing file: {file_item.id}') await Files.update_file_data_by_id( @@ -260,6 +295,10 @@ async def upload_file_handler( }, ) + # SHA-256 of raw uploaded bytes for incremental sync diffing. + # If the client pre-computed and sent file_hash, use that. + file_hash = file_metadata.get('file_hash') or hashlib.sha256(contents).hexdigest() + file_item = await Files.insert_new_file( user.id, FileForm( @@ -274,6 +313,7 @@ async def upload_file_handler( 'name': name, 'content_type': (file.content_type if isinstance(file.content_type, str) else None), 'size': len(contents), + 'file_hash': file_hash, 'data': file_metadata, }, } @@ -773,6 +813,46 @@ async def get_file_content_by_id( ) +############################ +# Rename File By Id +############################ + + +class FileRenameForm(BaseModel): + filename: str + + +@router.post('/{id}/rename') +async def rename_file_by_id( + id: str, + form_data: FileRenameForm, + 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): + result = await Files.update_file_name_by_id(id, form_data.filename, db=db) + if result: + return result + else: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.DEFAULT('Error renaming file'), + ) + else: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=ERROR_MESSAGES.NOT_FOUND, + ) + + ############################ # Delete File By Id ############################ diff --git a/backend/open_webui/routers/folders.py b/backend/open_webui/routers/folders.py index 7dda918821..8d77de4894 100644 --- a/backend/open_webui/routers/folders.py +++ b/backend/open_webui/routers/folders.py @@ -1,36 +1,29 @@ import logging +import mimetypes 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 fastapi import APIRouter, Depends, File, HTTPException, Request, UploadFile, status +from fastapi.responses import FileResponse, StreamingResponse 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.models.chats import Chats +from open_webui.models.folders import ( + FolderForm, + FolderModel, + FolderNameIdResponse, + Folders, + FolderUpdateForm, +) from open_webui.utils.access_control import has_permission from open_webui.utils.access_control.files import get_accessible_folder_files +from open_webui.utils.auth import get_admin_user, get_verified_user +from pydantic import BaseModel +from sqlalchemy.ext.asyncio import AsyncSession log = logging.getLogger(__name__) diff --git a/backend/open_webui/routers/functions.py b/backend/open_webui/routers/functions.py index f40cd1ab82..58ec93657e 100644 --- a/backend/open_webui/routers/functions.py +++ b/backend/open_webui/routers/functions.py @@ -1,32 +1,33 @@ -import os -import re +from __future__ import annotations import logging -import aiohttp +import os +import re from pathlib import Path from typing import Optional +import aiohttp +from fastapi import APIRouter, Depends, HTTPException, Request, status +from open_webui.config import CACHE_DIR +from open_webui.constants import ERROR_MESSAGES from open_webui.env import AIOHTTP_CLIENT_SESSION_SSL, AIOHTTP_CLIENT_TIMEOUT +from open_webui.internal.db import get_async_session from open_webui.models.functions import ( FunctionForm, FunctionModel, FunctionResponse, + Functions, FunctionUserResponse, FunctionWithValvesModel, - Functions, ) +from open_webui.utils.auth import get_admin_user, get_verified_user from open_webui.utils.plugin import ( + get_function_module_from_cache, 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__) @@ -91,7 +92,7 @@ def github_url_to_raw_url(url: str) -> str: return url -@router.post('/load/url', response_model=Optional[dict]) +@router.post('/load/url', response_model=dict | None) 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, @@ -179,7 +180,7 @@ async def sync_functions( ############################ -@router.post('/create', response_model=Optional[FunctionResponse]) +@router.post('/create', response_model=FunctionResponse | None) async def create_new_function( request: Request, form_data: FunctionForm, @@ -240,7 +241,7 @@ async def create_new_function( ############################ -@router.get('/id/{id}', response_model=Optional[FunctionModel]) +@router.get('/id/{id}', response_model=FunctionModel | None) 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) @@ -258,7 +259,7 @@ async def get_function_by_id(id: str, user=Depends(get_admin_user), db: AsyncSes ############################ -@router.post('/id/{id}/toggle', response_model=Optional[FunctionModel]) +@router.post('/id/{id}/toggle', response_model=FunctionModel | None) 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: @@ -283,7 +284,7 @@ async def toggle_function_by_id(id: str, user=Depends(get_admin_user), db: Async ############################ -@router.post('/id/{id}/toggle/global', response_model=Optional[FunctionModel]) +@router.post('/id/{id}/toggle/global', response_model=FunctionModel | None) 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: @@ -308,7 +309,7 @@ async def toggle_global_by_id(id: str, user=Depends(get_admin_user), db: AsyncSe ############################ -@router.post('/id/{id}/update', response_model=Optional[FunctionModel]) +@router.post('/id/{id}/update', response_model=FunctionModel | None) async def update_function_by_id( request: Request, id: str, @@ -374,7 +375,7 @@ async def delete_function_by_id( ############################ -@router.get('/id/{id}/valves', response_model=Optional[dict]) +@router.get('/id/{id}/valves', response_model=dict | None) async def get_function_valves_by_id( id: str, user=Depends(get_admin_user), db: AsyncSession = Depends(get_async_session) ): @@ -400,7 +401,7 @@ async def get_function_valves_by_id( ############################ -@router.get('/id/{id}/valves/spec', response_model=Optional[dict]) +@router.get('/id/{id}/valves/spec', response_model=dict | None) async def get_function_valves_spec_by_id( request: Request, id: str, @@ -430,7 +431,7 @@ async def get_function_valves_spec_by_id( ############################ -@router.post('/id/{id}/valves/update', response_model=Optional[dict]) +@router.post('/id/{id}/valves/update', response_model=dict | None) async def update_function_valves_by_id( request: Request, id: str, @@ -476,7 +477,7 @@ async def update_function_valves_by_id( ############################ -@router.get('/id/{id}/valves/user', response_model=Optional[dict]) +@router.get('/id/{id}/valves/user', response_model=dict | None) async def get_function_user_valves_by_id( id: str, user=Depends(get_verified_user), db: AsyncSession = Depends(get_async_session) ): @@ -497,7 +498,7 @@ async def get_function_user_valves_by_id( ) -@router.get('/id/{id}/valves/user/spec', response_model=Optional[dict]) +@router.get('/id/{id}/valves/user/spec', response_model=dict | None) async def get_function_user_valves_spec_by_id( request: Request, id: str, @@ -522,7 +523,7 @@ async def get_function_user_valves_spec_by_id( ) -@router.post('/id/{id}/valves/user/update', response_model=Optional[dict]) +@router.post('/id/{id}/valves/user/update', response_model=dict | None) async def update_function_user_valves_by_id( request: Request, id: str, diff --git a/backend/open_webui/routers/groups.py b/backend/open_webui/routers/groups.py index c45690fc3a..6efcd3946e 100755 --- a/backend/open_webui/routers/groups.py +++ b/backend/open_webui/routers/groups.py @@ -1,26 +1,27 @@ +import logging 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 fastapi import APIRouter, Depends, HTTPException, Request, status 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.models.access_grants import AccessGrants +from open_webui.models.groups import ( + GroupForm, + GroupInfoResponse, + GroupResponse, + Groups, + GroupUpdateForm, + UserIdsForm, +) +from open_webui.models.knowledge import Knowledges +from open_webui.models.models import Models +from open_webui.models.tools import Tools +from open_webui.models.users import UserInfoResponse, Users from open_webui.utils.auth import get_admin_user, get_verified_user +from sqlalchemy.ext.asyncio import AsyncSession log = logging.getLogger(__name__) @@ -276,3 +277,75 @@ async def delete_group_by_id(id: str, user=Depends(get_admin_user), db: AsyncSes status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT(e), ) + + +############################ +# PreviewGroupAccess +############################ + + +@router.get('/id/{id}/preview') +async def preview_group_access( + id: str, + user=Depends(get_admin_user), + db: AsyncSession = Depends(get_async_session), +): + """Show what resources a group can access (preview audit).""" + group = await Groups.get_group_by_id(id, db=db) + if not group: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=ERROR_MESSAGES.NOT_FOUND, + ) + + group_ids = {group.id} + + # Batch-check accessible resources using existing AccessGrants + all_models = await Models.get_all_models(db=db) + accessible_model_ids = await AccessGrants.get_accessible_resource_ids( + user_id='', + resource_type='model', + resource_ids=[m.id for m in all_models], + permission='read', + user_group_ids=group_ids, + db=db, + ) + + all_knowledge = await Knowledges.get_knowledge_bases(db=db) + accessible_knowledge_ids = await AccessGrants.get_accessible_resource_ids( + user_id='', + resource_type='knowledge', + resource_ids=[k.id for k in all_knowledge], + permission='read', + user_group_ids=group_ids, + db=db, + ) + + all_tools = await Tools.get_tools(defer_content=True, db=db) + accessible_tool_ids = await AccessGrants.get_accessible_resource_ids( + user_id='', + resource_type='tool', + resource_ids=[t.id for t in all_tools], + permission='read', + user_group_ids=group_ids, + db=db, + ) + + active_models = [m for m in all_models if m.is_active] + + return { + 'group': {'id': group.id, 'name': group.name}, + 'models': { + 'items': [{'id': m.id, 'name': m.name} for m in active_models if m.id in accessible_model_ids], + 'total': len(active_models), + }, + 'knowledge': { + 'items': [{'id': k.id, 'name': k.name} for k in all_knowledge if k.id in accessible_knowledge_ids], + 'total': len(all_knowledge), + }, + 'tools': { + 'items': [{'id': t.id, 'name': t.name} for t in all_tools if t.id in accessible_tool_ids], + 'total': len(all_tools), + }, + 'permissions': group.permissions or {}, + } diff --git a/backend/open_webui/routers/images.py b/backend/open_webui/routers/images.py index e55b7c5798..9d65cebfb8 100644 --- a/backend/open_webui/routers/images.py +++ b/backend/open_webui/routers/images.py @@ -1,46 +1,45 @@ +from __future__ import annotations + import asyncio import base64 -import uuid import io import json import logging import mimetypes import re +import uuid from pathlib import Path from typing import Optional +from urllib.parse import quote, urlparse -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, AIOHTTP_CLIENT_ALLOW_REDIRECTS, 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.env import AIOHTTP_CLIENT_ALLOW_REDIRECTS, AIOHTTP_CLIENT_SESSION_SSL, ENABLE_FORWARD_USER_INFO_HEADERS from open_webui.internal.db import get_async_session -from sqlalchemy.ext.asyncio import AsyncSession +from open_webui.models.chats import Chats +from open_webui.retrieval.web.utils import validate_url +from open_webui.routers.files import get_file_content_by_id, upload_file_handler +from open_webui.utils.access_control import has_permission +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.images.comfyui import ( ComfyUICreateImageForm, ComfyUIEditImageForm, ComfyUIWorkflow, - comfyui_upload_image, comfyui_create_image, comfyui_edit_image, + comfyui_upload_image, ) +from open_webui.utils.session_pool import get_session from pydantic import BaseModel +from sqlalchemy.ext.asyncio import AsyncSession log = logging.getLogger(__name__) @@ -121,17 +120,17 @@ class ImagesConfig(BaseModel): IMAGE_GENERATION_ENGINE: str IMAGE_GENERATION_MODEL: str - IMAGE_SIZE: Optional[str] - IMAGE_STEPS: Optional[int] + IMAGE_SIZE: str | None + IMAGE_STEPS: int | None IMAGES_OPENAI_API_BASE_URL: str IMAGES_OPENAI_API_KEY: str IMAGES_OPENAI_API_VERSION: str - IMAGES_OPENAI_API_PARAMS: Optional[dict | str] + IMAGES_OPENAI_API_PARAMS: dict | str | None AUTOMATIC1111_BASE_URL: str - AUTOMATIC1111_API_AUTH: Optional[dict | str] - AUTOMATIC1111_PARAMS: Optional[dict | str] + AUTOMATIC1111_API_AUTH: dict | str | None + AUTOMATIC1111_PARAMS: dict | str | None COMFYUI_BASE_URL: str COMFYUI_API_KEY: str @@ -145,7 +144,7 @@ class ImagesConfig(BaseModel): ENABLE_IMAGE_EDIT: bool IMAGE_EDIT_ENGINE: str IMAGE_EDIT_MODEL: str - IMAGE_EDIT_SIZE: Optional[str] + IMAGE_EDIT_SIZE: str | None IMAGES_EDIT_OPENAI_API_BASE_URL: str IMAGES_EDIT_OPENAI_API_KEY: str @@ -428,22 +427,53 @@ async def get_models(request: Request, user=Depends(get_verified_user)): class CreateImageForm(BaseModel): - model: Optional[str] = None + model: str | None = None prompt: str - size: Optional[str] = None + size: str | None = None n: int = 1 - steps: Optional[int] = None - negative_prompt: Optional[str] = None + steps: int | None = None + negative_prompt: str | None = None GenerateImageForm = CreateImageForm # Alias for backward compatibility -async def get_image_data(data: str, headers=None): +def _is_same_origin(url: str, base_url: str) -> bool: + """Compare scheme + hostname + port of two URLs. + + Pure string-prefix matching (``startswith``) is vulnerable to + userinfo injection (``http://host:port@evil.com/``) and suffix + confusion (``http://host:portevil.com/``). Parsing both URLs + and comparing the three origin components eliminates those + attack vectors. + """ + + def _default_port(scheme: str) -> int: + return 443 if scheme == 'https' else 80 + + parsed = urlparse(url) + trusted = urlparse(base_url) + return ( + parsed.scheme == trusted.scheme + and parsed.hostname == trusted.hostname + and (parsed.port or _default_port(parsed.scheme)) == (trusted.port or _default_port(trusted.scheme)) + ) + + +async def get_image_data(data: str, headers=None, trusted_base_url: str | None = None): try: if data.startswith('http://') or data.startswith('https://'): # Defense-in-depth: gate before fetch (mirrors load_url_image). - validate_url(data) + # For URLs originating from an admin-configured backend (e.g. + # ComfyUI on a private network), skip SSRF validation only when + # the URL shares the exact same origin (scheme + host + port) + # as the admin-configured base. This avoids both the global + # ENABLE_RAG_LOCAL_WEB_FETCH hammer and a blanket trust flag + # that would follow arbitrary redirects. + if trusted_base_url and _is_same_origin(data, trusted_base_url): + log.debug(f'Skipping URL validation for trusted backend: {data}') + else: + validate_url(data) session = await get_session() async with session.get( data, @@ -472,6 +502,8 @@ async def get_image_data(data: str, headers=None): async def upload_image(request, image_data, content_type, metadata, user, db=None): + if image_data is None or content_type is None: + raise ValueError('Failed to retrieve image data from the generation backend') image_format = mimetypes.guess_extension(content_type) file = UploadFile( file=io.BytesIO(image_data), @@ -528,7 +560,7 @@ async def generate_images(request: Request, form_data: CreateImageForm, user=Dep async def image_generations( request: Request, form_data: CreateImageForm, - metadata: Optional[dict] = None, + metadata: dict | None = None, user=None, ): # if IMAGE_SIZE = 'auto', default WidthxHeight to the 512x512 default @@ -594,7 +626,7 @@ async def image_generations( ssl=AIOHTTP_CLIENT_SESSION_SSL, ) as r: r.raise_for_status() - res = await r.json() + res = await r.json(content_type=None) images = [] @@ -644,7 +676,7 @@ async def image_generations( ssl=AIOHTTP_CLIENT_SESSION_SSL, ) as r: r.raise_for_status() - res = await r.json() + res = await r.json(content_type=None) images = [] @@ -710,7 +742,11 @@ async def image_generations( 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) + image_data, content_type = await get_image_data( + image['url'], + headers, + trusted_base_url=request.app.state.config.COMFYUI_BASE_URL, + ) _, url = await upload_image( request, image_data, @@ -750,7 +786,7 @@ async def image_generations( headers={'authorization': get_automatic1111_api_auth(request)}, ssl=AIOHTTP_CLIENT_SESSION_SSL, ) as r: - res = await r.json() + res = await r.json(content_type=None) log.debug(f'res: {res}') images = [] @@ -776,18 +812,18 @@ async def image_generations( 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 + model: str | None = None + size: str | None = None + n: int | None = None + negative_prompt: str | None = None + background: str | None = None @router.post('/edit') async def image_edits( request: Request, form_data: EditImageForm, - metadata: Optional[dict] = None, + metadata: dict | None = None, user=Depends(get_verified_user), ): size = None @@ -925,7 +961,7 @@ async def image_edits( ssl=AIOHTTP_CLIENT_SESSION_SSL, ) as r: r.raise_for_status() - res = await r.json() + res = await r.json(content_type=None) images = [] for image in res['data']: @@ -980,7 +1016,7 @@ async def image_edits( ssl=AIOHTTP_CLIENT_SESSION_SSL, ) as r: r.raise_for_status() - res = await r.json() + res = await r.json(content_type=None) images = [] for image in res['candidates']: @@ -1066,7 +1102,11 @@ async def image_edits( 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) + image_data, content_type = await get_image_data( + image_url, + headers, + trusted_base_url=request.app.state.config.IMAGES_EDIT_COMFYUI_BASE_URL, + ) _, url = await upload_image( request, image_data, diff --git a/backend/open_webui/routers/knowledge.py b/backend/open_webui/routers/knowledge.py index 8ff987b610..3506986429 100644 --- a/backend/open_webui/routers/knowledge.py +++ b/backend/open_webui/routers/knowledge.py @@ -1,42 +1,43 @@ -from typing import List, Optional -from pydantic import BaseModel -from fastapi import APIRouter, Depends, HTTPException, status, Request, Query -from fastapi.responses import StreamingResponse +from __future__ import annotations -import logging +import asyncio import io +import logging import zipfile +from typing import List, Optional from urllib.parse import quote -from sqlalchemy.ext.asyncio import AsyncSession +from fastapi import APIRouter, Depends, HTTPException, Query, Request, status +from fastapi.responses import StreamingResponse +from open_webui.config import BYPASS_ADMIN_ACCESS_CONTROL +from open_webui.constants import ERROR_MESSAGES from open_webui.internal.db import get_async_session +from open_webui.models.access_grants import AccessGrants +from open_webui.models.files import FileMetadataResponse, FileModel, FileModelResponse, Files from open_webui.models.groups import Groups from open_webui.models.knowledge import ( + KnowledgeDirectoryForm, + KnowledgeDirectoryModel, KnowledgeFileListResponse, - Knowledges, KnowledgeForm, KnowledgeResponse, + Knowledges, KnowledgeUserResponse, ) -from open_webui.models.files import Files, FileModel, FileMetadataResponse +from open_webui.models.models import ModelForm, Models 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, + ProcessFileForm, + process_file, + process_files_batch, ) 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.utils.access_control import filter_allowed_access_grants, has_permission from open_webui.utils.access_control.files import has_access_to_file -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 +from open_webui.utils.auth import get_admin_user, get_verified_user +from pydantic import BaseModel +from sqlalchemy.ext.asyncio import AsyncSession log = logging.getLogger(__name__) @@ -100,7 +101,7 @@ async def remove_knowledge_base_metadata_embedding(knowledge_base_id: str) -> bo class KnowledgeAccessResponse(KnowledgeUserResponse): - write_access: Optional[bool] = False + write_access: bool | None = False class KnowledgeAccessListResponse(BaseModel): @@ -110,7 +111,7 @@ class KnowledgeAccessListResponse(BaseModel): @router.get('/', response_model=KnowledgeAccessListResponse) async def get_knowledge_bases( - page: Optional[int] = 1, + page: int | None = 1, user=Depends(get_verified_user), db: AsyncSession = Depends(get_async_session), ): @@ -159,9 +160,9 @@ async def get_knowledge_bases( @router.get('/search', response_model=KnowledgeAccessListResponse) async def search_knowledge_bases( - query: Optional[str] = None, - view_option: Optional[str] = None, - page: Optional[int] = 1, + query: str | None = None, + view_option: str | None = None, + page: int | None = 1, user=Depends(get_verified_user), db: AsyncSession = Depends(get_async_session), ): @@ -215,8 +216,9 @@ async def search_knowledge_bases( @router.get('/search/files', response_model=KnowledgeFileListResponse) async def search_knowledge_files( - query: Optional[str] = None, - page: Optional[int] = 1, + query: str | None = None, + include_content: bool = Query(False, description='Include file content in search (expensive).'), + page: int | None = 1, user=Depends(get_verified_user), db: AsyncSession = Depends(get_async_session), ): @@ -227,6 +229,8 @@ async def search_knowledge_files( filter = {} if query: filter['query'] = query + if include_content: + filter['include_content'] = True groups = await Groups.get_groups_by_member_id(user.id, db=db) if groups: @@ -242,7 +246,7 @@ async def search_knowledge_files( ############################ -@router.post('/create', response_model=Optional[KnowledgeResponse]) +@router.post('/create', response_model=KnowledgeResponse | None) async def create_new_knowledge( request: Request, form_data: KnowledgeForm, @@ -380,11 +384,11 @@ async def reindex_knowledge_base_metadata_embeddings( class KnowledgeFilesResponse(KnowledgeResponse): - files: Optional[list[FileMetadataResponse]] = None - write_access: Optional[bool] = False + files: list[FileMetadataResponse | None] = None + write_access: bool | None = False -@router.get('/{id}', response_model=Optional[KnowledgeFilesResponse]) +@router.get('/{id}', response_model=KnowledgeFilesResponse | None) 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) @@ -431,7 +435,7 @@ async def get_knowledge_by_id(id: str, user=Depends(get_verified_user), db: Asyn ############################ -@router.post('/{id}/update', response_model=Optional[KnowledgeFilesResponse]) +@router.post('/{id}/update', response_model=KnowledgeFilesResponse | None) async def update_knowledge_by_id( request: Request, id: str, @@ -501,7 +505,7 @@ class KnowledgeAccessGrantsForm(BaseModel): access_grants: list[dict] -@router.post('/{id}/access/update', response_model=Optional[KnowledgeFilesResponse]) +@router.post('/{id}/access/update', response_model=KnowledgeFilesResponse | None) async def update_knowledge_access_by_id( request: Request, id: str, @@ -548,6 +552,71 @@ async def update_knowledge_access_by_id( ) +############################ +# GetPendingKnowledgeFiles +############################ + + +@router.get('/{id}/files/pending') +async def get_pending_knowledge_files( + id: str, + stream: bool = Query(False), + user=Depends(get_verified_user), + db: AsyncSession = Depends(get_async_session), +): + """Return files that are being processed for this knowledge base but not yet linked. + + After a file is uploaded with ``knowledge_id`` in its metadata, the backend + processes it in a background task before linking it to the ``knowledge_file`` + join table. During this window the file is invisible to the normal file + list endpoint. This endpoint exposes those in-flight files so the frontend + can show them with a processing indicator even after a page reload. + + When ``stream=true``, returns an SSE stream that polls every 3 seconds + and emits the current pending file list. Closes when no files remain. + """ + 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 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, + ) + + if not stream: + return await Files.get_pending_files_for_knowledge(id, db=db) + + async def event_stream(knowledge_id: str): + MAX_POLL_DURATION = 3600 # 1 hour max + for _ in range(MAX_POLL_DURATION // 3): + pending = await Files.get_pending_files_for_knowledge(knowledge_id) + data = [f.model_dump() for f in pending] + yield f'data: {json.dumps(data)}\n\n' + if len(pending) == 0: + break + await asyncio.sleep(3) + + return StreamingResponse( + event_stream(id), + media_type='text/event-stream', + ) + + ############################ # GetKnowledgeFilesById ############################ @@ -556,11 +625,14 @@ async def update_knowledge_access_by_id( @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, + query: str | None = None, + include_content: bool = Query(False, description='Include file content in search (expensive).'), + view_option: str | None = None, + order_by: str | None = None, + direction: str | None = None, + directory_id: str | None = Query(None, description='Filter by directory ID. Pass empty string for root.'), + page: int | None = 1, + limit: int | None = Query(None, description='Page size (admin only). Defaults to 30.'), user=Depends(get_verified_user), db: AsyncSession = Depends(get_async_session), ): @@ -589,18 +661,27 @@ async def get_knowledge_files_by_id( page = max(page, 1) - limit = 30 + # Allow admins to configure page size; non-admins always get the default + if user.role == 'admin' and limit is not None: + limit = max(1, limit) + else: + limit = PAGE_ITEM_COUNT skip = (page - 1) * limit filter = {} if query: filter['query'] = query + if include_content: + filter['include_content'] = True if view_option: filter['view_option'] = view_option if order_by: filter['order_by'] = order_by if direction: filter['direction'] = direction + # directory_id filtering: present in filter = scope to that directory (None = root) + if directory_id is not None: + filter['directory_id'] = directory_id if directory_id else None return await Knowledges.search_files_by_id(id, user.id, filter=filter, skip=skip, limit=limit, db=db) @@ -612,9 +693,10 @@ async def get_knowledge_files_by_id( class KnowledgeFileIdForm(BaseModel): file_id: str + directory_id: Optional[str] = None -@router.post('/{id}/file/add', response_model=Optional[KnowledgeFilesResponse]) +@router.post('/{id}/file/add', response_model=KnowledgeFilesResponse | None) async def add_file_to_knowledge_by_id( request: Request, id: str, @@ -675,7 +757,13 @@ async def add_file_to_knowledge_by_id( ) # 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) + await Knowledges.add_file_to_knowledge_by_id( + knowledge_id=id, + file_id=form_data.file_id, + user_id=user.id, + directory_id=form_data.directory_id, + db=db, + ) except Exception as e: log.debug(e) raise HTTPException( @@ -695,7 +783,7 @@ async def add_file_to_knowledge_by_id( ) -@router.post('/{id}/file/update', response_model=Optional[KnowledgeFilesResponse]) +@router.post('/{id}/file/update', response_model=KnowledgeFilesResponse | None) async def update_file_from_knowledge_by_id( request: Request, id: str, @@ -774,7 +862,7 @@ async def update_file_from_knowledge_by_id( ############################ -@router.post('/{id}/file/remove', response_model=Optional[KnowledgeFilesResponse]) +@router.post('/{id}/file/remove', response_model=KnowledgeFilesResponse | None) async def remove_file_from_knowledge_by_id( id: str, form_data: KnowledgeFileIdForm, @@ -835,10 +923,7 @@ async def remove_file_from_knowledge_by_id( 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. + # Anyone with write permission or higher can delete files if delete_file and (file.user_id == user.id or user.role == 'admin'): try: # Remove the file's collection from vector database @@ -936,9 +1021,12 @@ async def delete_knowledge_by_id( ############################ -@router.post('/{id}/reset', response_model=Optional[KnowledgeResponse]) +@router.post('/{id}/reset', response_model=KnowledgeResponse | None) async def reset_knowledge_by_id( - id: str, user=Depends(get_verified_user), db: AsyncSession = Depends(get_async_session) + id: str, + include_directories: 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: @@ -969,16 +1057,195 @@ async def reset_knowledge_by_id( log.debug(e) pass - knowledge = await Knowledges.reset_knowledge_by_id(id=id, db=db) + knowledge = await Knowledges.reset_knowledge_by_id(id=id, include_directories=include_directories, db=db) return knowledge +############################ +# SyncKnowledgeDiff +############################ + + +class FileManifestEntry(BaseModel): + filename: str # basename: "readme.md" + path: str # relative dir: "docs/api" or "" for root + checksum: str # SHA-256 of raw bytes + size: int + + +class SyncDiffForm(BaseModel): + manifest: list[FileManifestEntry] + + +class SyncDiffResponse(BaseModel): + added: list[dict] # [{filename, path}] — new files + modified: list[dict] # [{filename, path, stale_file_id}] — changed files + deleted: list[dict] # [{file_id, filename}] — files to remove + mkdir: list[str] # directory paths to create + rmdir: list[str] # directory IDs to remove + unmodified_count: int + directory_map: dict[str, str] # existing path → directory ID + + +@router.post('/{id}/sync/diff', response_model=SyncDiffResponse) +async def sync_knowledge_diff( + id: str, + form_data: SyncDiffForm, + user=Depends(get_verified_user), + db: AsyncSession = Depends(get_async_session), +): + """ + Compare a local file manifest against the knowledge base to determine + which files need uploading, removing, and which directories to create/remove. + """ + await _verify_knowledge_write_access(id, user, db) + + # ── Index existing state ── + knowledge_files = await Knowledges.get_files_with_directory_ids(id, db=db) + existing_directories = await Knowledges.get_all_directories(id, db=db) + + # Build directory path lookups + directory_path_by_id: dict[str, str] = {} + directory_id_by_path: dict[str, str] = {} + for directory in existing_directories: + segments = [directory.name] + parent_id = directory.parent_id + while parent_id: + parent = next((d for d in existing_directories if d.id == parent_id), None) + if not parent: + break + segments.insert(0, parent.name) + parent_id = parent.parent_id + full_path = '/'.join(segments) + directory_path_by_id[directory.id] = full_path + directory_id_by_path[full_path] = directory.id + + # Index existing files by (path, filename) → {file_id, checksum} + indexed_files: dict[tuple[str, str], dict] = {} + for file_model, directory_id in knowledge_files: + file_path = directory_path_by_id.get(directory_id, '') if directory_id else '' + stored_checksum = (file_model.meta or {}).get('file_hash') + indexed_files[(file_path, file_model.filename)] = { + 'file_id': file_model.id, + 'checksum': stored_checksum, + } + + # ── Diff files ── + added: list[dict] = [] + modified: list[dict] = [] + deleted: list[dict] = [] + unmodified_count = 0 + manifest_keys: set[tuple[str, str]] = set() + + for entry in form_data.manifest: + key = (entry.path, entry.filename) + manifest_keys.add(key) + + if key not in indexed_files: + added.append({'filename': entry.filename, 'path': entry.path}) + elif indexed_files[key]['checksum'] != entry.checksum: + modified.append( + { + 'filename': entry.filename, + 'path': entry.path, + 'stale_file_id': indexed_files[key]['file_id'], + } + ) + else: + unmodified_count += 1 + + for key, file_info in indexed_files.items(): + if key not in manifest_keys: + deleted.append({'file_id': file_info['file_id'], 'filename': key[1]}) + + # ── Diff directories ── + required_directory_paths: set[str] = set() + for entry in form_data.manifest: + if entry.path: + segments = entry.path.split('/') + for depth in range(len(segments)): + required_directory_paths.add('/'.join(segments[: depth + 1])) + + mkdir = sorted([p for p in required_directory_paths if p not in directory_id_by_path], key=lambda p: p.count('/')) + + orphaned_directory_paths = set(directory_id_by_path) - required_directory_paths + rmdir = [directory_id_by_path[p] for p in orphaned_directory_paths] + + return SyncDiffResponse( + added=added, + modified=modified, + deleted=deleted, + mkdir=mkdir, + rmdir=rmdir, + unmodified_count=unmodified_count, + directory_map=directory_id_by_path, + ) + + +############################ +# SyncKnowledgeCleanup +############################ + + +class SyncCleanupForm(BaseModel): + file_ids: list[str] # file IDs to delete + dir_ids: list[str] = [] # directory IDs to rmdir + + +@router.post('/{id}/sync/cleanup') +async def sync_knowledge_cleanup( + id: str, + form_data: SyncCleanupForm, + user=Depends(get_verified_user), + db: AsyncSession = Depends(get_async_session), +): + """ + Remove stale files and orphaned directories from a knowledge base + after an incremental sync. + """ + await _verify_knowledge_write_access(id, user, db) + + # ── Remove deleted files ── + for file_id in form_data.file_ids: + file = await Files.get_file_by_id(file_id, db=db) + if not file: + continue + + await Knowledges.remove_file_from_knowledge_by_id(id, file_id, db=db) + + try: + await ASYNC_VECTOR_DB_CLIENT.delete(collection_name=id, filter={'file_id': file_id}) + await ASYNC_VECTOR_DB_CLIENT.delete(collection_name=id, filter={'hash': file.hash}) + except Exception: + pass + + try: + collection_name = f'file-{file_id}' + if await ASYNC_VECTOR_DB_CLIENT.has_collection(collection_name): + await ASYNC_VECTOR_DB_CLIENT.delete_collection(collection_name) + except Exception: + pass + + if file.user_id == user.id or user.role == 'admin': + await Files.delete_file_by_id(file_id, db=db) + try: + await asyncio.to_thread(Storage.delete_file, file.path) + except Exception: + pass + + # ── Remove orphaned directories (children before parents) ── + for dir_id in reversed(form_data.dir_ids): + await Knowledges.delete_directory(dir_id, move_files_to_parent=False, db=db) + + return {'status': True} + + ############################ # AddFilesToKnowledge ############################ -@router.post('/{id}/files/batch/add', response_model=Optional[KnowledgeFilesResponse]) +@router.post('/{id}/files/batch/add', response_model=KnowledgeFilesResponse | None) async def add_files_to_knowledge_batch( request: Request, id: str, @@ -1035,6 +1302,23 @@ async def add_files_to_knowledge_batch( detail=ERROR_MESSAGES.ACCESS_PROHIBITED, ) + # Filter out files already linked to this knowledge base to prevent + # duplicate embeddings in the vector DB (issue #10679). + new_entries = [] + for form in form_data: + if not await Knowledges.has_file(knowledge_id=id, file_id=form.file_id, db=db): + new_entries.append(form) + + if not new_entries: + return KnowledgeFilesResponse( + **knowledge.model_dump(), + files=await Knowledges.get_file_metadatas_by_id(knowledge.id, db=db), + ) + + # Narrow the file list to only new files for processing + new_file_ids = {form.file_id for form in new_entries} + files = [f for f in files if f.id in new_file_ids] + # Process files try: result = await process_files_batch( @@ -1049,8 +1333,15 @@ async def add_files_to_knowledge_batch( # Only add files that were successfully processed successful_file_ids = [r.file_id for r in result.results if r.status == 'completed'] + dir_map = {form.file_id: form.directory_id for form in new_entries} 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) + await Knowledges.add_file_to_knowledge_by_id( + knowledge_id=id, + file_id=file_id, + user_id=user.id, + directory_id=dir_map.get(file_id), + db=db, + ) # If there were any errors, include them in the response if result.errors: @@ -1119,3 +1410,175 @@ async def export_knowledge_by_id(id: str, user=Depends(get_admin_user), db: Asyn media_type='application/zip', headers={'Content-Disposition': content_disposition}, ) + + +############################ +# Directory endpoints +############################ + + +class KnowledgeDirectoryCreateForm(BaseModel): + name: str + parent_id: Optional[str] = None + + +class KnowledgeDirectoryUpdateForm(BaseModel): + name: Optional[str] = None + parent_id: Optional[str] = '__unset__' + + +class KnowledgeFileMoveForm(BaseModel): + file_id: str + directory_id: Optional[str] = None + + +async def _verify_knowledge_write_access(id: str, user, db: AsyncSession): + """Verify the user has write access to the knowledge base. Returns the knowledge model.""" + 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_403_FORBIDDEN, + detail=ERROR_MESSAGES.ACCESS_PROHIBITED, + ) + return knowledge + + +@router.post('/{id}/dirs/create', response_model=KnowledgeDirectoryModel) +async def create_knowledge_directory( + id: str, + form_data: KnowledgeDirectoryCreateForm, + user=Depends(get_verified_user), + db: AsyncSession = Depends(get_async_session), +): + await _verify_knowledge_write_access(id, user, db) + + directory = await Knowledges.create_directory( + knowledge_id=id, + name=form_data.name, + user_id=user.id, + parent_id=form_data.parent_id, + db=db, + ) + if not directory: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail='Failed to create directory. A directory with this name may already exist at this level.', + ) + return directory + + +@router.post('/{id}/dirs/{dir_id}/update', response_model=KnowledgeDirectoryModel) +async def update_knowledge_directory( + id: str, + dir_id: str, + form_data: KnowledgeDirectoryUpdateForm, + user=Depends(get_verified_user), + db: AsyncSession = Depends(get_async_session), +): + await _verify_knowledge_write_access(id, user, db) + + # Verify directory belongs to this knowledge base + directory = await Knowledges.get_directory_by_id(dir_id, db=db) + if not directory or directory.knowledge_id != id: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=ERROR_MESSAGES.NOT_FOUND, + ) + + result = await Knowledges.update_directory( + directory_id=dir_id, + name=form_data.name, + parent_id=form_data.parent_id, + db=db, + ) + if not result: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail='Failed to update directory. This may be caused by a naming conflict or circular move.', + ) + return result + + +@router.delete('/{id}/dirs/{dir_id}/delete') +async def delete_knowledge_directory( + id: str, + dir_id: str, + move_files: bool = Query(True, description='If true, move contained files to parent. If false, delete them.'), + user=Depends(get_verified_user), + db: AsyncSession = Depends(get_async_session), +): + await _verify_knowledge_write_access(id, user, db) + + # Verify directory belongs to this knowledge base + directory = await Knowledges.get_directory_by_id(dir_id, db=db) + if not directory or directory.knowledge_id != id: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=ERROR_MESSAGES.NOT_FOUND, + ) + + success = await Knowledges.delete_directory( + directory_id=dir_id, + move_files_to_parent=move_files, + db=db, + ) + if not success: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail='Failed to delete directory.', + ) + return {'status': True} + + +@router.post('/{id}/file/move') +async def move_file_in_knowledge( + id: str, + form_data: KnowledgeFileMoveForm, + user=Depends(get_verified_user), + db: AsyncSession = Depends(get_async_session), +): + await _verify_knowledge_write_access(id, user, db) + + # Verify file 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_404_NOT_FOUND, + detail=ERROR_MESSAGES.NOT_FOUND, + ) + + # If target directory is set, verify it belongs to this knowledge base + if form_data.directory_id: + directory = await Knowledges.get_directory_by_id(form_data.directory_id, db=db) + if not directory or directory.knowledge_id != id: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail='Target directory not found.', + ) + + success = await Knowledges.move_file_to_directory( + knowledge_id=id, + file_id=form_data.file_id, + directory_id=form_data.directory_id, + db=db, + ) + if not success: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail='Failed to move file.', + ) + return {'status': True} diff --git a/backend/open_webui/routers/memories.py b/backend/open_webui/routers/memories.py index 6522118258..aad881b6da 100644 --- a/backend/open_webui/routers/memories.py +++ b/backend/open_webui/routers/memories.py @@ -1,17 +1,19 @@ -from fastapi import APIRouter, Depends, HTTPException, Request, status -from pydantic import BaseModel -import logging +from __future__ import annotations + import asyncio +import logging from typing import Optional +from fastapi import APIRouter, Depends, HTTPException, Request, status +from open_webui.constants import ERROR_MESSAGES +from open_webui.internal.db import get_async_session 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.config import RAG_EMBEDDING_QUERY_PREFIX from open_webui.utils.access_control import has_permission -from open_webui.constants import ERROR_MESSAGES +from open_webui.utils.auth import get_verified_user +from pydantic import BaseModel +from sqlalchemy.ext.asyncio import AsyncSession log = logging.getLogger(__name__) @@ -56,19 +58,21 @@ class AddMemoryForm(BaseModel): class MemoryUpdateModel(BaseModel): - content: Optional[str] = None + content: str | None = None -@router.post('/add', response_model=Optional[MemoryModel]) +@router.post('/add', response_model=MemoryModel | None) 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). + """Persist a new memory and embed it into the user's vector collection. + + Does NOT use ``Depends(get_async_session)`` — database operations manage their + own short-lived sessions so a connection is not held during the external + embedding API call (``EMBEDDING_FUNCTION``), which can take 1-5+ seconds. + """ if not request.app.state.config.ENABLE_MEMORIES: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, @@ -107,7 +111,7 @@ async def add_memory( class QueryMemoryForm(BaseModel): content: str - k: Optional[int] = 1 + k: int | None = 1 @router.post('/query') @@ -136,7 +140,7 @@ async def query_memory( 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) + vector = await request.app.state.EMBEDDING_FUNCTION(form_data.content, RAG_EMBEDDING_QUERY_PREFIX, user=user) results = await ASYNC_VECTOR_DB_CLIENT.search( collection_name=f'user-memory-{user.id}', @@ -275,7 +279,7 @@ async def delete_memory_by_user_id( ############################ -@router.post('/{memory_id}/update', response_model=Optional[MemoryModel]) +@router.post('/{memory_id}/update', response_model=MemoryModel | None) async def update_memory_by_id( memory_id: str, request: Request, diff --git a/backend/open_webui/routers/models.py b/backend/open_webui/routers/models.py index 2a78daa94d..75ee4e723b 100644 --- a/backend/open_webui/routers/models.py +++ b/backend/open_webui/routers/models.py @@ -1,28 +1,14 @@ -from typing import Optional -import io -import base64 -import json +from __future__ import annotations + import asyncio +import base64 +import io +import json import logging import posixpath +from typing import Optional 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, @@ -32,13 +18,27 @@ from fastapi import ( status, ) 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.env import ENABLE_PROFILE_IMAGE_URL_FORWARDING +from open_webui.constants import ERROR_MESSAGES +from open_webui.env import ENABLE_PROFILE_IMAGE_URL_FORWARDING, PROFILE_IMAGE_ALLOWED_MIME_TYPES from open_webui.internal.db import get_async_session +from open_webui.models.access_grants import AccessGrants +from open_webui.models.groups import Groups +from open_webui.models.models import ( + ModelAccessListResponse, + ModelAccessResponse, + ModelForm, + ModelListResponse, + ModelMeta, + ModelModel, + ModelParams, + ModelResponse, + Models, +) +from open_webui.utils.access_control import filter_allowed_access_grants, has_permission +from open_webui.utils.access_control.files import has_access_to_file +from open_webui.utils.auth import get_admin_user, get_verified_user +from pydantic import BaseModel from sqlalchemy.ext.asyncio import AsyncSession log = logging.getLogger(__name__) @@ -46,7 +46,7 @@ log = logging.getLogger(__name__) router = APIRouter() -def _safe_static_redirect_path(url: str) -> Optional[str]: +def _safe_static_redirect_path(url: str) -> str | None: """ 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). @@ -78,6 +78,32 @@ def is_valid_model_id(model_id: str) -> bool: return model_id and len(model_id) <= 256 +async def _verify_knowledge_file_access( + knowledge_items: list | None, + user, + db: AsyncSession, +) -> None: + """Raise 403 if any knowledge item references a file the caller cannot read.""" + if not knowledge_items or user.role == 'admin': + return + for item in knowledge_items: + if not isinstance(item, dict) or item.get('type') != 'file': + continue + file_id = item.get('id') + if not file_id: + continue + if not await has_access_to_file(file_id, 'read', user, db=db): + log.warning( + 'knowledge file access denied: user %s cannot read file %s', + user.id, + file_id, + ) + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail=ERROR_MESSAGES.ACCESS_PROHIBITED, + ) + + ########################### # GetModels # Let each model here be judged by what it does and not @@ -90,12 +116,12 @@ 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, + query: str | None = None, + view_option: str | None = None, + tag: str | None = None, + order_by: str | None = None, + direction: str | None = None, + page: int | None = 1, user=Depends(get_verified_user), db: AsyncSession = Depends(get_async_session), ): @@ -192,13 +218,14 @@ async def get_model_tags(user=Depends(get_verified_user), db: AsyncSession = Dep ############################ -@router.post('/create', response_model=Optional[ModelModel]) +@router.post('/create', response_model=ModelModel | None) async def create_new_model( request: Request, form_data: ModelForm, user=Depends(get_verified_user), db: AsyncSession = Depends(get_async_session), ): + """Create a new workspace model entry.""" if user.role != 'admin' and not await has_permission( user.id, 'workspace.models', request.app.state.config.USER_PERMISSIONS, db=db ): @@ -221,6 +248,12 @@ async def create_new_model( ) else: + await _verify_knowledge_file_access( + getattr(form_data.meta, 'knowledge', None) if form_data.meta else None, + user, + db, + ) + form_data.access_grants = await filter_allowed_access_grants( request.app.state.config.USER_PERMISSIONS, user.id, @@ -327,6 +360,21 @@ async def import_models( model_id = model_data.get('id') if model_id and is_valid_model_id(model_id): + # Defense-in-depth: skip models referencing inaccessible files + try: + await _verify_knowledge_file_access( + (model_data.get('meta') or {}).get('knowledge'), + user, + db, + ) + except HTTPException: + log.warning( + 'import_models: user %s skipped model %s (knowledge file access denied)', + user.id, + model_id, + ) + continue + existing_model = existing_models.get(model_id) if existing_model: # Enforce ownership/write-access before allowing overwrite @@ -409,7 +457,7 @@ class ModelIdForm(BaseModel): # 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]) +@router.get('/model', response_model=ModelAccessResponse | None) 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: @@ -505,9 +553,19 @@ async def get_model_profile_image( 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:') + media_type = header.split(';')[0].lstrip('data:').lower() - headers = {'Content-Disposition': 'inline'} + # only serve known-safe raster types inline; reject SVG/unknown (can run script on our origin) + if media_type not in PROFILE_IMAGE_ALLOWED_MIME_TYPES: + return RedirectResponse( + url='/static/favicon.png', + status_code=status.HTTP_302_FOUND, + ) + + headers = { + 'Content-Disposition': 'inline', + 'X-Content-Type-Options': 'nosniff', + } if updated_at: headers['ETag'] = f'"{updated_at}"' @@ -537,7 +595,7 @@ async def get_model_profile_image( ############################ -@router.post('/model/toggle', response_model=Optional[ModelResponse]) +@router.post('/model/toggle', response_model=ModelResponse | None) 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: @@ -578,13 +636,14 @@ async def toggle_model_by_id(id: str, user=Depends(get_verified_user), db: Async ############################ -@router.post('/model/update', response_model=Optional[ModelModel]) +@router.post('/model/update', response_model=ModelModel | None) async def update_model_by_id( request: Request, form_data: ModelForm, user=Depends(get_verified_user), db: AsyncSession = Depends(get_async_session), ): + """Update a workspace model's configuration.""" model = await Models.get_model_by_id(form_data.id, db=db) if not model: raise HTTPException( @@ -608,6 +667,12 @@ async def update_model_by_id( detail=ERROR_MESSAGES.ACCESS_PROHIBITED, ) + await _verify_knowledge_file_access( + getattr(form_data.meta, 'knowledge', None) if form_data.meta else None, + user, + db, + ) + form_data.access_grants = await filter_allowed_access_grants( request.app.state.config.USER_PERMISSIONS, user.id, @@ -627,11 +692,11 @@ async def update_model_by_id( class ModelAccessGrantsForm(BaseModel): id: str - name: Optional[str] = None + name: str | None = None access_grants: list[dict] -@router.post('/model/access/update', response_model=Optional[ModelModel]) +@router.post('/model/access/update', response_model=ModelModel | None) async def update_model_access_by_id( request: Request, form_data: ModelAccessGrantsForm, diff --git a/backend/open_webui/routers/notes.py b/backend/open_webui/routers/notes.py index 5ed46b5d61..6dccc73f6d 100644 --- a/backend/open_webui/routers/notes.py +++ b/backend/open_webui/routers/notes.py @@ -2,39 +2,33 @@ 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 fastapi import APIRouter, BackgroundTasks, Depends, HTTPException, Request, status 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.internal.db import get_async_session +from open_webui.models.access_grants import AccessGrants +from open_webui.models.groups import Groups +from open_webui.models.notes import ( + NoteForm, + NoteListResponse, + NoteModel, + Notes, + NoteUserResponse, +) +from open_webui.models.users import UserResponse, Users +from open_webui.socket.main import sio from open_webui.utils.access_control import ( + filter_allowed_access_grants, 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 open_webui.utils.auth import get_admin_user, get_verified_user +from pydantic import BaseModel from sqlalchemy.ext.asyncio import AsyncSession log = logging.getLogger(__name__) diff --git a/backend/open_webui/routers/ollama.py b/backend/open_webui/routers/ollama.py index 01fbf10f4f..a4e166ba9e 100644 --- a/backend/open_webui/routers/ollama.py +++ b/backend/open_webui/routers/ollama.py @@ -1,6 +1,4 @@ -# 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. +from __future__ import annotations import asyncio import json @@ -10,82 +8,45 @@ 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 import APIRouter, Depends, File, HTTPException, Request, UploadFile from fastapi.responses import StreamingResponse from pydantic import BaseModel, ConfigDict, validator - from sqlalchemy.ext.asyncio import AsyncSession +from open_webui.config import UPLOAD_DIR +from open_webui.constants import ERROR_MESSAGES +from open_webui.env import ( + AIOHTTP_CLIENT_SESSION_SSL, + AIOHTTP_CLIENT_TIMEOUT, + AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST, + BYPASS_MODEL_ACCESS_CONTROL, + ENABLE_FORWARD_USER_INFO_HEADERS, + FORWARD_SESSION_INFO_HEADER_CHAT_ID, + MODELS_CACHE_TTL, +) 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.models.models import Models +from open_webui.models.users import UserModel 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.auth import get_admin_user, get_verified_user +from open_webui.utils.headers import include_user_info_headers +from open_webui.utils.misc import calculate_sha256 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 +from open_webui.utils.session_pool import cleanup_response, get_session, stream_wrapper 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 @@ -98,27 +59,31 @@ def _clean_proxy_headers(raw_headers) -> dict: 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) +async def send_get_request( + url: str, + key: str | None = None, + user: UserModel | None = None, +): + """Issue a GET request to an Ollama backend and return JSON, or *None* on failure.""" try: - async with aiohttp.ClientSession(timeout=timeout, trust_env=True) as session: - headers = { - 'Content-Type': 'application/json', - **({'Authorization': f'Bearer {key}'} if key else {}), - } + session = await get_session() + headers: dict = { + 'Content-Type': 'application/json', + } + if key: + headers['Authorization'] = f'Bearer {key}' + if ENABLE_FORWARD_USER_INFO_HEADERS and user: + headers = include_user_info_headers(headers, user) - 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}') + async with session.get( + url, + headers=headers, + ssl=AIOHTTP_CLIENT_SESSION_SSL, + timeout=aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST), + ) as r: + return await r.json() + except Exception as exc: + log.error(f'Connection error: {exc}') return None @@ -126,12 +91,12 @@ async def send_request( url: str, method: str = 'POST', *, - payload: Optional[Union[str, bytes]] = None, - key: Optional[str] = None, + payload: Union[str, bytes | None] = None, + key: str | None = None, user: UserModel = None, stream: bool = False, - content_type: Optional[str] = None, - metadata: Optional[dict] = None, + content_type: str | None = None, + metadata: dict | None = None, ): r = None streaming = False @@ -219,58 +184,58 @@ router = APIRouter() @router.head('/') @router.get('/') -async def get_status(): +async def get_status() -> dict: + """Health-check endpoint.""" return {'status': True} class ConnectionVerificationForm(BaseModel): url: str - key: Optional[str] = None + key: str | None = 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 def verify_connection( + form_data: ConnectionVerificationForm, + user=Depends(get_admin_user), +): + """Verify that an Ollama backend at *form_data.url* is reachable.""" + try: + session = await get_session() + headers: dict = {} + if form_data.key: + headers['Authorization'] = f'Bearer {form_data.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_MODEL_LIST), - ) as session: - try: - headers = { - **({'Authorization': f'Bearer {key}'} if key else {}), - } + async with session.get( + f'{form_data.url}/api/version', + headers=headers, + ssl=AIOHTTP_CLIENT_SESSION_SSL, + timeout=aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST), + ) 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) - 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) + return await r.json() + except aiohttp.ClientError as exc: + log.exception(f'Client error: {exc}') + raise HTTPException(status_code=500, detail=ERROR_MESSAGES.SERVER_CONNECTION_ERROR) + except Exception as exc: + log.exception(f'Unexpected error: {exc}') + raise HTTPException(status_code=500, detail=f'Unexpected error: {exc}') @router.get('/config') -async def get_config(request: Request, user=Depends(get_admin_user)): +async def get_config( + request: Request, + user=Depends(get_admin_user), +) -> dict: + """Return the current Ollama connection configuration.""" return { 'ENABLE_OLLAMA_API': request.app.state.config.ENABLE_OLLAMA_API, 'OLLAMA_BASE_URLS': request.app.state.config.OLLAMA_BASE_URLS, @@ -279,22 +244,28 @@ async def get_config(request: Request, user=Depends(get_admin_user)): class OllamaConfigForm(BaseModel): - ENABLE_OLLAMA_API: Optional[bool] = None + """Payload for updating the Ollama connection configuration.""" + + ENABLE_OLLAMA_API: bool | None = 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)): +async def update_config( + request: Request, + form_data: OllamaConfigForm, + user=Depends(get_admin_user), +) -> dict: + """Persist updated Ollama connection settings.""" 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)))) + # Prune stale config entries that no longer map to a URL index + valid_keys = {str(i) for i in 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 + k: v for k, v in request.app.state.config.OLLAMA_API_CONFIGS.items() if k in valid_keys } return { @@ -304,120 +275,103 @@ async def update_config(request: Request, form_data: OllamaConfigForm, user=Depe } -def merge_ollama_models_lists(model_lists): - merged_models = {} +def merge_models_lists(model_lists) -> list[dict]: + """De-duplicate model entries across multiple Ollama backends, tracking which URL index hosts each model.""" + merged: dict[str, dict] = {} + for idx, entries in enumerate(model_lists): + if entries is None: + continue + for entry in entries: + model_id = entry.get('model') + if model_id is None: + continue + if model_id not in merged: + entry['urls'] = [idx] + merged[model_id] = entry + else: + merged[model_id]['urls'].append(idx) + return list(merged.values()) - 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()) +def _resolve_api_config(request: Request, idx: int, url: str) -> dict: + """Look up the API config for a backend by numeric index, falling back to URL key (legacy).""" + api_configs = request.app.state.config.OLLAMA_API_CONFIGS + return api_configs.get(str(idx), api_configs.get(url, {})) @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): +async def get_all_models(request: Request, user: UserModel | None = None): + """Aggregate model tags from every enabled Ollama backend.""" 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 not request.app.state.config.ENABLE_OLLAMA_API: + models_dict: dict = {'models': []} + request.app.state.OLLAMA_MODELS = {} + return models_dict - 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))) + # Fan-out tag requests to every backend + tasks = [] + for idx, url in enumerate(request.app.state.config.OLLAMA_BASE_URLS): + api_config = _resolve_api_config(request, idx, url) + if not api_config: + tasks.append(send_get_request(f'{url}/api/tags', user=user)) + elif api_config.get('enable', True): + tasks.append(send_get_request(f'{url}/api/tags', api_config.get('key'), user=user)) + else: + tasks.append(asyncio.ensure_future(asyncio.sleep(0, None))) - responses = await asyncio.gather(*request_tasks) + responses = await asyncio.gather(*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 - ) + # Post-process each response: apply prefix_id, tags, model filtering + for idx, response in enumerate(responses): + if not response: + continue + url = request.app.state.config.OLLAMA_BASE_URLS[idx] + api_config = _resolve_api_config(request, idx, url) - connection_type = api_config.get('connection_type', 'local') + connection_type = api_config.get('connection_type', 'local') + prefix_id = api_config.get('prefix_id') + allowed_tags = api_config.get('tags', []) + allowed_model_ids = api_config.get('model_ids', []) - prefix_id = api_config.get('prefix_id', None) - tags = api_config.get('tags', []) - model_ids = api_config.get('model_ids', []) + if allowed_model_ids and 'models' in response: + response['models'] = [m for m in response['models'] if m['model'] in allowed_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 m in response.get('models', []): + if prefix_id: + m['model'] = f'{prefix_id}.{m["model"]}' + if allowed_tags: + m['tags'] = allowed_tags + if connection_type: + m['connection_type'] = connection_type - for model in response.get('models', []): - if prefix_id: - model['model'] = f'{prefix_id}.{model["model"]}' + models_dict = {'models': merge_models_lists(r.get('models', []) if r else None for r in responses)} - if tags: - model['tags'] = tags + # Annotate with expiry info from loaded-model state + try: + loaded = await get_ollama_loaded_models(request, user=user) + expires_map = {m['model']: m['expires_at'] for m in loaded['models'] if 'expires_at' in m} + for m in models_dict['models']: + if m['model'] in expires_map: + dt = datetime.fromisoformat(expires_map[m['model']]) + m['expires_at'] = int(dt.timestamp()) + except Exception as exc: + log.debug(f'Failed to get loaded models: {exc}') - 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 + request.app.state.OLLAMA_MODELS = {m['model']: m for m in models_dict['models']} + return models_dict 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)} + """Return only the models the given *user* is allowed to access.""" + model_ids = [m['model'] for m in models.get('models', [])] + model_infos = {mi.id: mi for mi in await Models.get_models_by_ids(model_ids, db=db)} + user_group_ids = {g.id for g 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( + accessible_ids = await AccessGrants.get_accessible_resource_ids( user_id=user.id, resource_type='model', resource_ids=list(model_infos.keys()), @@ -425,145 +379,112 @@ async def get_filtered_models(models, user, db=None): 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 + return [ + m + for m in models.get('models', []) + if (mi := model_infos.get(m['model'])) and (user.id == mi.user_id or mi.id in accessible_ids) + ] @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)): +async def get_ollama_tags( + request: Request, + url_idx: int | None = None, + user=Depends(get_verified_user), +): + """List Ollama model tags, optionally from a specific backend.""" 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) + result = 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) + result = 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) + result['models'] = await get_filtered_models(result, user) - return models + return result @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 - ) +async def get_ollama_loaded_models( + request: Request, + user=Depends(get_admin_user), +) -> dict: + """List models currently loaded in Ollama memory across all backends.""" + if not request.app.state.config.ENABLE_OLLAMA_API: + return {'models': []} - enable = api_config.get('enable', True) - key = api_config.get('key', None) + tasks = [] + for idx, url in enumerate(request.app.state.config.OLLAMA_BASE_URLS): + api_config = _resolve_api_config(request, idx, url) + if not api_config: + tasks.append(send_get_request(f'{url}/api/ps', user=user)) + elif api_config.get('enable', True): + tasks.append(send_get_request(f'{url}/api/ps', api_config.get('key'), user=user)) + else: + tasks.append(asyncio.ensure_future(asyncio.sleep(0, 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(*tasks) - responses = await asyncio.gather(*request_tasks) + for idx, response in enumerate(responses): + if not response: + continue + api_config = _resolve_api_config(request.app.state.config, idx, request.app.state.config.OLLAMA_BASE_URLS[idx]) + prefix_id = api_config.get('prefix_id') + if prefix_id: + for m in response.get('models', []): + m['model'] = f'{prefix_id}.{m["model"]}' - 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 + return {'models': merge_models_lists(r.get('models', []) if r else None for r in responses)} @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: +async def get_ollama_versions( + request: Request, + url_idx: int | None = None, +): + """Return the lowest Ollama version across all configured backends.""" + if not request.app.state.config.ENABLE_OLLAMA_API: return {'version': False} + if url_idx is not None: + url = request.app.state.config.OLLAMA_BASE_URLS[url_idx] + return await send_request(f'{url}/api/version', 'GET') + + # Fan-out to every enabled backend + 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, {}), + ) + if api_config.get('enable', True): + tasks.append(send_get_request(f'{url}/api/version', api_config.get('key'))) + + raw = await asyncio.gather(*tasks) + valid = [r for r in raw if r is not None] + + if not valid: + raise HTTPException(status_code=500, detail=ERROR_MESSAGES.OLLAMA_NOT_FOUND) + + lowest = min( + valid, + key=lambda v: tuple(map(int, re.sub(r'^v|-.*', '', v['version']).split('.'))), + ) + return {'version': lowest['version']} + class ModelNameForm(BaseModel): - model: Optional[str] = None - model_config = ConfigDict( - extra='allow', - ) + """Generic form carrying an optional model identifier.""" + + model: str | None = None + model_config = ConfigDict(extra='allow') @router.post('/api/unload') @@ -573,18 +494,18 @@ async def unload_model( user=Depends(get_admin_user), ): form_data = form_data.model_dump(exclude_none=True) - model_name = form_data.get('model', form_data.get('name')) + model = form_data.get('model', form_data.get('name')) - if not model_name: + if not model: 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'] + if model not in models: + raise HTTPException(status_code=400, detail=ERROR_MESSAGES.MODEL_NOT_FOUND(model)) + url_indices = models[model]['urls'] # Send unload to ALL url_indices results = [] @@ -597,10 +518,10 @@ async def unload_model( 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}.') :] + if prefix_id and model.startswith(f'{prefix_id}.'): + model = model[len(f'{prefix_id}.') :] - payload = {'model': model_name, 'keep_alive': 0, 'prompt': ''} + payload = {'model': model, 'keep_alive': 0, 'prompt': ''} try: res = await send_request( @@ -640,12 +561,10 @@ async def pull_model( 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} - + # Admins may pull from any registry return await send_request( f'{url}/api/pull', - payload=json.dumps(payload), + payload=json.dumps({**form_data, 'insecure': True}), key=get_api_key(url_idx, url, request.app.state.config.OLLAMA_API_CONFIGS), user=user, stream=True, @@ -653,9 +572,11 @@ async def pull_model( class PushModelForm(BaseModel): + """Payload for pushing a model to a registry.""" + model: str - insecure: Optional[bool] = None - stream: Optional[bool] = None + insecure: bool | None = None + stream: bool | None = None @router.delete('/api/push') @@ -663,23 +584,19 @@ class PushModelForm(BaseModel): async def push_model( request: Request, form_data: PushModelForm, - url_idx: Optional[int] = None, + url_idx: int | None = None, user=Depends(get_admin_user), ): + """Push a local model to a remote registry.""" 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), - ) + if form_data.model not in models: + raise HTTPException(status_code=400, detail=ERROR_MESSAGES.MODEL_NOT_FOUND(form_data.model)) + url_idx = models[form_data.model]['urls'][0] url = request.app.state.config.OLLAMA_BASE_URLS[url_idx] log.debug(f'url: {url}') @@ -694,10 +611,11 @@ async def push_model( class CreateModelForm(BaseModel): - model: Optional[str] = None - stream: Optional[bool] = None - path: Optional[str] = None + """Payload for creating a new model via Modelfile.""" + model: str | None = None + stream: bool | None = None + path: str | None = None model_config = ConfigDict(extra='allow') @@ -725,6 +643,8 @@ async def create_model( class CopyModelForm(BaseModel): + """Payload for duplicating an existing model under a new name.""" + source: str destination: str @@ -734,23 +654,19 @@ class CopyModelForm(BaseModel): async def copy_model( request: Request, form_data: CopyModelForm, - url_idx: Optional[int] = None, + url_idx: int | None = None, user=Depends(get_admin_user), ): + """Duplicate an existing model under a new name.""" 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), - ) + if form_data.source not in models: + raise HTTPException(status_code=400, detail=ERROR_MESSAGES.MODEL_NOT_FOUND(form_data.source)) + url_idx = models[form_data.source]['urls'][0] url = request.app.state.config.OLLAMA_BASE_URLS[url_idx] key = get_api_key(url_idx, url, request.app.state.config.OLLAMA_API_CONFIGS) @@ -769,28 +685,23 @@ async def copy_model( async def delete_model( request: Request, form_data: ModelNameForm, - url_idx: Optional[int] = None, + url_idx: int | None = None, user=Depends(get_admin_user), ): + """Remove a model from an Ollama backend.""" 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') + payload = form_data.model_dump(exclude_none=True) + payload['model'] = payload.get('model', payload.get('name')) + model = payload.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), - ) + if model not in models: + raise HTTPException(status_code=400, detail=ERROR_MESSAGES.MODEL_NOT_FOUND(model)) + url_idx = models[model]['urls'][0] url = request.app.state.config.OLLAMA_BASE_URLS[url_idx] key = get_api_key(url_idx, url, request.app.state.config.OLLAMA_API_CONFIGS) @@ -798,7 +709,7 @@ async def delete_model( await send_request( f'{url}/api/delete', 'DELETE', - payload=json.dumps(form_data), + payload=json.dumps(payload), key=key, user=user, ) @@ -806,50 +717,48 @@ async def delete_model( @router.post('/api/show') -async def show_model_info(request: Request, form_data: ModelNameForm, user=Depends(get_verified_user)): +async def show_model_info( + request: Request, + form_data: ModelNameForm, + user=Depends(get_verified_user), +): + """Retrieve model metadata from the Ollama backend.""" 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')) + payload = form_data.model_dump(exclude_none=True) + payload['model'] = payload.get('model', payload.get('name')) + model = payload.get('model') - 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), - ) + 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), + payload=json.dumps(payload), key=key, user=user, ) class GenerateEmbedForm(BaseModel): + """Payload for the newer /api/embed endpoint (batch-capable).""" + 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', - ) + truncate: bool | None = None + options: dict | None = None + keep_alive: Union[int, str | None] = None + model_config = ConfigDict(extra='allow') @router.post('/api/embed') @@ -857,42 +766,35 @@ class GenerateEmbedForm(BaseModel): async def embed( request: Request, form_data: GenerateEmbedForm, - url_idx: Optional[int] = None, + url_idx: int | None = None, user=Depends(get_verified_user), ): + """Generate embeddings via the Ollama /api/embed endpoint.""" 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) + await validate_ollama_backend_idx(request, form_data.model, url_idx, user) 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), - ) + if model not in models: + raise HTTPException(status_code=400, detail=ERROR_MESSAGES.MODEL_NOT_FOUND(form_data.model)) + url_idx = random.choice(models[model]['urls']) 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 + request.app.state.config.OLLAMA_API_CONFIGS.get(url, {}), ) key = get_api_key(url_idx, url, request.app.state.config.OLLAMA_API_CONFIGS) - prefix_id = api_config.get('prefix_id', None) + prefix_id = api_config.get('prefix_id') if prefix_id: form_data.model = form_data.model.replace(f'{prefix_id}.', '') @@ -905,10 +807,12 @@ async def embed( class GenerateEmbeddingsForm(BaseModel): + """Payload for the legacy /api/embeddings endpoint (single-prompt).""" + model: str prompt: str - options: Optional[dict] = None - keep_alive: Optional[Union[int, str]] = None + options: dict | None = None + keep_alive: Union[int, str | None] = None @router.post('/api/embeddings') @@ -916,42 +820,35 @@ class GenerateEmbeddingsForm(BaseModel): async def embeddings( request: Request, form_data: GenerateEmbeddingsForm, - url_idx: Optional[int] = None, + url_idx: int | None = None, user=Depends(get_verified_user), ): + """Generate embeddings via the legacy Ollama /api/embeddings endpoint.""" 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) + await validate_ollama_backend_idx(request, form_data.model, url_idx, user) 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), - ) + if model not in models: + raise HTTPException(status_code=400, detail=ERROR_MESSAGES.MODEL_NOT_FOUND(form_data.model)) + url_idx = random.choice(models[model]['urls']) 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 + request.app.state.config.OLLAMA_API_CONFIGS.get(url, {}), ) key = get_api_key(url_idx, url, request.app.state.config.OLLAMA_API_CONFIGS) - prefix_id = api_config.get('prefix_id', None) + prefix_id = api_config.get('prefix_id') if prefix_id: form_data.model = form_data.model.replace(f'{prefix_id}.', '') @@ -964,18 +861,20 @@ async def embeddings( class GenerateCompletionForm(BaseModel): + """Payload for the Ollama /api/generate endpoint.""" + 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 + prompt: str | None = None + suffix: str | None = None + images: list[str | None] = None + format: Union[dict, str | None] = None + options: dict | None = None + system: str | None = None + template: str | None = None + context: list[int | None] = None + stream: bool | None = True + raw: bool | None = None + keep_alive: Union[int, str | None] = None @router.post('/api/generate') @@ -983,36 +882,31 @@ class GenerateCompletionForm(BaseModel): async def generate_completion( request: Request, form_data: GenerateCompletionForm, - url_idx: Optional[int] = None, + url_idx: int | None = None, user=Depends(get_verified_user), ): + """Run text completion via Ollama /api/generate.""" 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) + await validate_ollama_backend_idx(request, form_data.model, url_idx, user) 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), - ) + if model not in models: + raise HTTPException(status_code=400, detail=ERROR_MESSAGES.MODEL_NOT_FOUND(form_data.model)) + url_idx = random.choice(models[model]['urls']) 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 + request.app.state.config.OLLAMA_API_CONFIGS.get(url, {}), ) - prefix_id = api_config.get('prefix_id', None) + prefix_id = api_config.get('prefix_id') if prefix_id: form_data.model = form_data.model.replace(f'{prefix_id}.', '') @@ -1026,38 +920,51 @@ async def generate_completion( class ChatMessage(BaseModel): - role: str - content: Optional[str] = None - tool_calls: Optional[list[dict]] = None - images: Optional[list[str]] = None + """A single message in an Ollama chat conversation.""" + role: str + content: str | None = None + tool_calls: list[dict | None] = None + images: list[str | None] = 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): + """Payload for the Ollama /api/chat endpoint.""" + 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', - ) + format: Union[dict, str | None] = None + options: dict | None = None + template: str | None = None + stream: bool | None = True + keep_alive: Union[int, str | None] = None + tools: list[dict | None] = None + model_config = ConfigDict(extra='allow') -async def get_ollama_url(request: Request, model: str, url_idx: Optional[int] = None): +async def validate_ollama_backend_idx(request: Request, model: str, url_idx: int | None, user) -> None: + # A caller-supplied url_idx must point to a backend the model is actually + # served from; the None path is already constrained to that allow-list. + if url_idx is None or user is None or getattr(user, 'role', None) == 'admin' or BYPASS_MODEL_ACCESS_CONTROL: + return + 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 url_idx not in (models.get(model) or {}).get('urls', []): + raise HTTPException(status_code=403, detail=ERROR_MESSAGES.ACCESS_PROHIBITED) + + +async def get_ollama_url(request: Request, model: str, url_idx: int | None = None, user=None): + await validate_ollama_backend_idx(request, model, url_idx, user) if url_idx is None: models = request.app.state.OLLAMA_MODELS if model not in models: @@ -1075,10 +982,10 @@ async def get_ollama_url(request: Request, model: str, url_idx: Optional[int] = 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, + url_idx: int | None = None, + user=Depends(get_verified_user), # noqa: B008 ): + """Forward a chat completion request to an Ollama backend.""" if not request.app.state.config.ENABLE_OLLAMA_API: raise HTTPException(status_code=503, detail=ERROR_MESSAGES.OLLAMA_API_DISABLED) @@ -1087,44 +994,38 @@ async def generate_chat_completion( # 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 and bypass_system_prompt are read from request.state to prevent + # external clients from setting them via query parameter. Only internal + # server-side callers (e.g. utils/chat.py) should set + # request.state.bypass_filter / request.state.bypass_system_prompt = True. bypass_filter = getattr(request.state, 'bypass_filter', False) if BYPASS_MODEL_ACCESS_CONTROL: bypass_filter = True + bypass_system_prompt = getattr(request.state, 'bypass_system_prompt', False) 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), - ) + except Exception as exc: + log.exception(exc) + raise HTTPException(status_code=400, detail=str(exc)) if isinstance(form_data, BaseModel): payload = {**form_data.model_dump(exclude_none=True)} - if 'metadata' in payload: - del payload['metadata'] + payload.pop('metadata', None) model_id = payload['model'] model_info = await Models.get_model_by_id(model_id) - if model_info: + if model_info is not None: 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 + base_model_id = request.base_model_id if hasattr(request, 'base_model_id') else model_info.base_model_id 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 = await apply_system_prompt_to_body(system, payload, metadata, user) @@ -1133,13 +1034,10 @@ async def generate_chat_completion( 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 - ) + url, url_idx = await get_ollama_url(request, payload['model'], url_idx, user) + api_config = _resolve_api_config(request, url_idx, url) - prefix_id = api_config.get('prefix_id', None) + prefix_id = api_config.get('prefix_id') if prefix_id: payload['model'] = payload['model'].replace(f'{prefix_id}.', '') @@ -1156,28 +1054,33 @@ async def generate_chat_completion( # TODO: we should update this part once Ollama supports other types class OpenAIChatMessageContent(BaseModel): + """Content block within an OpenAI-style chat message.""" + type: str model_config = ConfigDict(extra='allow') class OpenAIChatMessage(BaseModel): - role: str - content: Union[Optional[str], list[OpenAIChatMessageContent]] + """A single message in an OpenAI-compatible chat request.""" + role: str + content: Union[str | None, list[OpenAIChatMessageContent]] model_config = ConfigDict(extra='allow') class OpenAIChatCompletionForm(BaseModel): + """Payload for the OpenAI-compatible /v1/chat/completions proxy.""" + model: str messages: list[OpenAIChatMessage] - model_config = ConfigDict(extra='allow') class OpenAICompletionForm(BaseModel): + """Payload for the OpenAI-compatible /v1/completions proxy.""" + model: str prompt: str - model_config = ConfigDict(extra='allow') @@ -1186,9 +1089,10 @@ class OpenAICompletionForm(BaseModel): async def generate_openai_completion( request: Request, form_data: dict, - url_idx: Optional[int] = None, - user=Depends(get_verified_user), + url_idx: int | None = None, + user=Depends(get_verified_user), # noqa: B008 ): + """Forward a text completion request via the OpenAI-compatible proxy.""" # NOTE: We intentionally do NOT use Depends(get_async_session) here. # Database operations (get_model_by_id, AccessGrants.has_access) manage their own short-lived sessions. # This prevents holding a connection during the entire LLM call (30-60+ seconds), @@ -1197,39 +1101,29 @@ async def generate_openai_completion( try: form_data = OpenAICompletionForm(**form_data) - except Exception as e: - log.exception(e) - raise HTTPException( - status_code=400, - detail=str(e), - ) + except Exception as exc: + log.exception(exc) + raise HTTPException(status_code=400, detail=str(exc)) payload = {**form_data.model_dump(exclude_none=True, exclude=['metadata'])} - if 'metadata' in payload: - del payload['metadata'] + payload.pop('metadata', None) model_id = form_data.model model_info = await Models.get_model_by_id(model_id) - if model_info: + if model_info is not None: 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) + url, url_idx = await get_ollama_url(request, payload['model'], url_idx, user) + api_config = _resolve_api_config(request, url_idx, url) + prefix_id = api_config.get('prefix_id') if prefix_id: payload['model'] = payload['model'].replace(f'{prefix_id}.', '') @@ -1248,9 +1142,10 @@ async def generate_openai_completion( async def generate_openai_chat_completion( request: Request, form_data: dict, - url_idx: Optional[int] = None, - user=Depends(get_verified_user), + url_idx: int | None = None, + user=Depends(get_verified_user), # noqa: B008 ): + """Forward a chat completion request via the OpenAI-compatible proxy.""" # NOTE: We intentionally do NOT use Depends(get_async_session) here. # Database operations (get_model_by_id, AccessGrants.has_access) manage their own short-lived sessions. # This prevents holding a connection during the entire LLM call (30-60+ seconds), @@ -1258,29 +1153,23 @@ async def generate_openai_chat_completion( 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), - ) + form_data = OpenAIChatCompletionForm(**form_data) + except Exception as exc: + log.exception(exc) + raise HTTPException(status_code=400, detail=str(exc)) - payload = {**completion_form.model_dump(exclude_none=True, exclude=['metadata'])} - if 'metadata' in payload: - del payload['metadata'] + payload = {**form_data.model_dump(exclude_none=True, exclude=['metadata'])} + payload.pop('metadata', None) - model_id = completion_form.model + model_id = form_data.model model_info = await Models.get_model_by_id(model_id) - if model_info: + if model_info is not None: 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 = await apply_system_prompt_to_body(system, payload, metadata, user) @@ -1288,13 +1177,10 @@ async def generate_openai_chat_completion( 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 - ) + url, url_idx = await get_ollama_url(request, payload['model'], url_idx, user) + api_config = _resolve_api_config(request, url_idx, url) - prefix_id = api_config.get('prefix_id', None) + prefix_id = api_config.get('prefix_id') if prefix_id: payload['model'] = payload['model'].replace(f'{prefix_id}.', '') @@ -1313,7 +1199,7 @@ async def generate_openai_chat_completion( async def generate_anthropic_messages( request: Request, form_data: dict, - url_idx: Optional[int] = None, + url_idx: int | None = None, user=Depends(get_verified_user), ): """ @@ -1340,7 +1226,7 @@ async def generate_anthropic_messages( else: await check_model_access(user, None) - url, url_idx = await get_ollama_url(request, payload['model'], url_idx) + url, url_idx = await get_ollama_url(request, payload['model'], url_idx, user) api_config = request.app.state.config.OLLAMA_API_CONFIGS.get( str(url_idx), request.app.state.config.OLLAMA_API_CONFIGS.get(url, {}), # Legacy support @@ -1371,7 +1257,7 @@ class ResponsesForm(BaseModel): async def generate_responses( request: Request, form_data: ResponsesForm, - url_idx: Optional[int] = None, + url_idx: int | None = None, user=Depends(get_verified_user), ): """ @@ -1398,7 +1284,7 @@ async def generate_responses( else: await check_model_access(user, None) - url, url_idx = await get_ollama_url(request, payload['model'], url_idx) + url, url_idx = await get_ollama_url(request, payload['model'], url_idx, user) api_config = request.app.state.config.OLLAMA_API_CONFIGS.get( str(url_idx), request.app.state.config.OLLAMA_API_CONFIGS.get(url, {}), # Legacy support @@ -1422,45 +1308,27 @@ async def generate_responses( @router.get('/v1/models/{url_idx}') async def get_openai_models( request: Request, - url_idx: Optional[int] = None, + url_idx: int | None = None, user=Depends(get_verified_user), db: AsyncSession = Depends(get_async_session), -): - models = [] +) -> dict: + """List models in the OpenAI-compatible format.""" 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'] - ] - + raw_models = model_list['models'] else: url = request.app.state.config.OLLAMA_BASE_URLS[url_idx] model_list = await send_request(f'{url}/api/tags', 'GET') + raw_models = model_list.get('models', []) - models = [ - { - 'id': model['model'], - 'object': 'model', - 'created': int(time.time()), - 'owned_by': 'openai', - } - for model in model_list.get('models', []) - ] + now_ts = int(time.time()) + models = [{'id': m['model'], 'object': 'model', 'created': now_ts, 'owned_by': 'openai'} for m in raw_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( + model_ids = [m['id'] for m in models] + model_infos = {mi.id: mi for mi in await Models.get_models_by_ids(model_ids, db=db)} + user_group_ids = {g.id for g in await Groups.get_groups_by_member_id(user.id, db=db)} + accessible_ids = await AccessGrants.get_accessible_resource_ids( user_id=user.id, resource_type='model', resource_ids=list(model_infos.keys()), @@ -1468,153 +1336,131 @@ async def get_openai_models( user_group_ids=user_group_ids, db=db, ) + models = [ + m for m in models if (mi := model_infos.get(m['id'])) and (user.id == mi.user_id or mi.id in accessible_ids) + ] - 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', - } + return {'data': models, 'object': 'list'} class UrlForm(BaseModel): + """Form carrying a single URL string.""" + url: str class UploadBlobForm(BaseModel): + """Form carrying a filename for blob uploads.""" + filename: str -def parse_huggingface_url(hf_url): +def parse_huggingface_url(hf_url: str) -> str | None: + """Extract the filename from a HuggingFace download 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 urlparse(hf_url).path.split('/')[-1] + except (ValueError, IndexError): 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 - +async def download_file_stream( + ollama_url: str, + file_url: str, + file_path: str, + file_name: str, + chunk_size: int = 1024 * 1024, +): + """Stream a model file download from *file_url*, then push the blob to Ollama.""" + current_size = os.path.getsize(file_path) if os.path.exists(file_path) else 0 headers = {'Range': f'bytes={current_size}-'} if current_size > 0 else {} - timeout = aiohttp.ClientTimeout(total=600) # Set the timeout + session = await get_session() + async with session.get( + file_url, + headers=headers, + ssl=AIOHTTP_CLIENT_SESSION_SSL, + timeout=aiohttp.ClientTimeout(total=600), + ) as response: + total_size = int(response.headers.get('content-length', 0)) + current_size - 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 f: + async for data in response.content.iter_chunked(chunk_size): + current_size += len(data) + f.write(data) - 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' - done = current_size == total_size - progress = round((current_size / total_size) * 100, 2) + if done: + f.close() + hashed = calculate_sha256(file_path, chunk_size) - yield f'data: {{"progress": {progress}, "completed": {current_size}, "total": {total_size}}}\n\n' + with open(file_path, 'rb') as blob_f: + blob_data = blob_f.read() - 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.') + blob_url = f'{ollama_url}/api/blobs/sha256:{hashed}' + async with session.post( + blob_url, + data=blob_data, + ssl=AIOHTTP_CLIENT_SESSION_SSL, + timeout=aiohttp.ClientTimeout(total=30), + ) as blob_resp: + if blob_resp.ok: + os.remove(file_path) + yield f'data: {json.dumps({"done": done, "blob": f"sha256:{hashed}", "name": file_name})}\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, + url_idx: int | None = None, user=Depends(get_admin_user), ): + """Download a GGUF model from HuggingFace or GitHub and register it with Ollama.""" 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] - + url = request.app.state.config.OLLAMA_BASE_URLS[url_idx if url_idx is not None else 0] 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: + if not file_name: return None + file_path = os.path.join(UPLOAD_DIR, file_name) + return StreamingResponse( + download_file_stream(url, form_data.url, file_path, file_name), + ) + -# 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, + url_idx: int | None = 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] + """Upload a local model file, push it as a blob, and create the model in Ollama.""" + ollama_url = request.app.state.config.OLLAMA_BASE_URLS[url_idx if url_idx is not None else 0] 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 + # Stage 1: persist the uploaded file to disk + chunk_size = 1024 * 1024 * 2 # 2 MiB 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) @@ -1622,74 +1468,63 @@ async def upload_model( 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 + log.info(f'Total Model Size: {total_size}') - # --- P2: SSE progress + calculate sha256 hash --- + # Stage 2: hash the file and emit SSE progress file_hash = calculate_sha256(file_path, chunk_size) - log.info(f'Model Hash: {str(file_hash)}') # DEBUG + log.info(f'Model Hash: {file_hash}') + 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' + yield f'data: {json.dumps({"progress": progress, "total": total_size, "completed": bytes_read})}\n\n' - # --- P3: Upload to ollama /api/blobs --- + # Stage 3: push blob to Ollama 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.') + session = await get_session() + blob_url = f'{ollama_url}/api/blobs/sha256:{file_hash}' + async with session.post( + blob_url, + data=blob_data, + ssl=AIOHTTP_CLIENT_SESSION_SSL, + timeout=aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT), + ) as resp: + if not resp.ok: + raise Exception('Ollama: Could not create blob, Please try again.') - log.info(f'Uploaded to /api/blobs') # DEBUG - # Remove local file + log.info('Uploaded to /api/blobs') os.remove(file_path) - # Create model in ollama - model_name, ext = os.path.splitext(filename) - log.info(f'Created Model: {model_name}') # DEBUG + # Stage 4: create the model + model, _ext = os.path.splitext(filename) + log.info(f'Created Model: {model}') create_payload = { - 'model': model_name, - # Reference the file by its original name => the uploaded blob's digest + 'model': model, 'files': {filename: f'sha256:{file_hash}'}, } - log.info(f'Model Payload: {create_payload}') # DEBUG + log.info(f'Model Payload: {create_payload}') - # 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}') + async with session.post( + f'{ollama_url}/api/create', + headers={'Content-Type': 'application/json'}, + data=json.dumps(create_payload), + ssl=AIOHTTP_CLIENT_SESSION_SSL, + timeout=aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT), + ) as create_resp: + if create_resp.ok: + log.info('API SUCCESS!') + yield f'data: {json.dumps({"done": True, "blob": f"sha256:{file_hash}", "name": filename, "model_created": model})}\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' + except Exception as exc: + yield f'data: {json.dumps({"error": str(exc)})}\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 index 3cf3cd9f4a..8aa8eff36e 100644 --- a/backend/open_webui/routers/openai.py +++ b/backend/open_webui/routers/openai.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import asyncio import hashlib import json @@ -8,62 +10,52 @@ 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 import APIRouter, Depends, HTTPException, Request, status from fastapi.responses import ( FileResponse, - StreamingResponse, JSONResponse, PlainTextResponse, + StreamingResponse, ) -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.constants import ERROR_MESSAGES 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_FORWARD_USER_INFO_HEADERS, ENABLE_OPENAI_API_PASSTHROUGH, + FORWARD_SESSION_INFO_HEADER_CHAT_ID, + MODELS_CACHE_TTL, ) +from open_webui.internal.db import get_async_session +from open_webui.models.access_grants import AccessGrants +from open_webui.models.groups import Groups +from open_webui.models.models import Models 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.access_control import check_model_access, has_connection_access +from open_webui.utils.anthropic import get_anthropic_models, is_anthropic_url +from open_webui.utils.auth import get_admin_user, get_verified_user +from open_webui.utils.headers import get_custom_headers, include_user_info_headers from open_webui.utils.misc import ( convert_logit_bias_input_to_json, stream_chunks_handler, ) +from open_webui.utils.payload import ( + apply_model_params_to_body_openai, + apply_system_prompt_to_body, +) 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, get_custom_headers -from open_webui.utils.anthropic import is_anthropic_url, get_anthropic_models +from pydantic import BaseModel, ConfigDict +from sqlalchemy.ext.asyncio import AsyncSession log = logging.getLogger(__name__) @@ -160,7 +152,7 @@ async def get_headers_and_cookies( url, key=None, config=None, - metadata: Optional[dict] = None, + metadata: dict | None = None, user: UserModel = None, ): cookies = {} @@ -256,7 +248,7 @@ async def get_config(request: Request, user=Depends(get_admin_user)): class OpenAIConfigForm(BaseModel): - ENABLE_OPENAI_API: Optional[bool] = None + ENABLE_OPENAI_API: bool | None = None OPENAI_API_BASE_URLS: list[str] OPENAI_API_KEYS: list[str] OPENAI_API_CONFIGS: dict @@ -493,39 +485,6 @@ async def get_filtered_models(models, user, db=None): return filtered_models -async def get_openai_loaded_models(request: Request, models: dict, api_base_urls: list): - """ - Fetch loaded-model state from providers that expose it and annotate - each model dict with a ``loaded`` boolean. - - Currently supports: - - **llama.cpp** – queries ``GET /slots`` and matches slot model IDs. - """ - api_configs = request.app.state.config.OPENAI_API_CONFIGS - api_keys = request.app.state.config.OPENAI_API_KEYS - - for idx, url in enumerate(api_base_urls): - api_config = api_configs.get( - str(idx), - api_configs.get(url, {}), - ) - provider = api_config.get('provider', '') - - if provider == 'llama.cpp': - try: - root_url = url.rstrip('/').removesuffix('/v1') - key = api_keys[idx] if idx < len(api_keys) else None - slots = await send_get_request(url=f'{root_url}/slots', key=key) - loaded_model_ids = ( - {s.get('model') for s in slots if s.get('model')} if isinstance(slots, list) else set() - ) - for model_id, model in models.items(): - if model.get('urlIdx') == idx: - model['loaded'] = model_id in loaded_model_ids - except Exception as e: - log.debug(f'Failed to fetch llama.cpp slots for idx {idx}: {e}') - - @cached( ttl=MODELS_CACHE_TTL, key=lambda _, user: f'openai_all_models_{user.id}' if user else 'openai_all_models', @@ -580,7 +539,7 @@ async def get_all_models(request: Request, user: UserModel) -> dict[str, list]: continue if model_id and model_id not in models: - models[model_id] = { + merged = { **model, 'name': model.get('name', model_id), 'owned_by': 'openai', @@ -590,21 +549,26 @@ async def get_all_models(request: Request, user: UserModel) -> dict[str, list]: 'urlIdx': idx, } + # llama.cpp router mode: derive loaded state from + # the status object returned by GET /v1/models. + status = model.get('status') + if isinstance(status, dict) and 'value' in status: + merged['loaded'] = status['value'] in ('loaded', 'sleeping') + + models[model_id] = merged + return models models = get_merged_models(map(extract_data, responses)) log.debug(f'models: {models}') - # Fetch loaded state for providers that support it (e.g. llama.cpp /slots) - await get_openai_loaded_models(request, models, api_base_urls) - 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)): +async def get_models(request: Request, url_idx: int | None = 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') @@ -631,7 +595,7 @@ async def get_models(request: Request, url_idx: Optional[int] = None, user=Depen try: headers, cookies = await get_headers_and_cookies(request, url, key, api_config, user=user) - if api_config.get('azure', False): + if api_config.get('azure') or api_config.get('provider') == 'azure': models = { 'data': api_config.get('model_ids', []) or [], 'object': 'list', @@ -696,7 +660,7 @@ class ConnectionVerificationForm(BaseModel): url: str key: str - config: Optional[dict] = None + config: dict | None = None @router.post('/verify') @@ -717,15 +681,24 @@ async def verify_connection( try: headers, cookies = await get_headers_and_cookies(request, url, key, api_config, user=user) - if api_config.get('azure', False): + if api_config.get('azure') or api_config.get('provider') == 'azure': # 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' + # Azure v1 format: base URL already ends with /openai/v1, + # use standard /models endpoint without api-version. + is_azure_v1 = bool(re.search(r'/openai/v1(?:/|$)', url)) + + if is_azure_v1: + verify_url = f'{url.rstrip("/")}/models' + else: + api_version = api_config.get('api_version', '') or '2023-03-15-preview' + verify_url = f'{url}/openai/models?api-version={api_version}' + async with session.get( - url=f'{url}/openai/models?api-version={api_version}', + url=verify_url, headers=headers, cookies=cookies, ssl=AIOHTTP_CLIENT_SESSION_SSL, @@ -1081,19 +1054,20 @@ 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 and bypass_system_prompt are read from request.state to prevent + # external clients from setting them via query parameter. Only internal + # server-side callers (e.g. utils/chat.py) should set + # request.state.bypass_filter / request.state.bypass_system_prompt = True. bypass_filter = getattr(request.state, 'bypass_filter', False) if BYPASS_MODEL_ACCESS_CONTROL: bypass_filter = True + bypass_system_prompt = getattr(request.state, 'bypass_system_prompt', False) idx = 0 @@ -1187,7 +1161,7 @@ async def generate_chat_completion( is_responses = api_config.get('api_type') == 'responses' - if api_config.get('azure', False): + if api_config.get('azure') or api_config.get('provider') == 'azure': # 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'): @@ -1340,11 +1314,32 @@ async def embeddings(request: Request, form_data: dict, user): streaming = False headers, cookies = await get_headers_and_cookies(request, url, key, api_config, user=user) + + if api_config.get('azure') or api_config.get('provider') == 'azure': + # 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: + embeddings_url = f'{url.rstrip("/")}/embeddings' + else: + api_version = api_config.get('api_version', '2023-03-15-preview') + model = _sanitize_model_for_url(form_data.get('model', '')) + embeddings_url = f'{url}/openai/deployments/{model}/embeddings?api-version={api_version}' + headers['api-version'] = api_version + else: + embeddings_url = f'{url}/embeddings' + try: session = await get_session() r = await session.request( method='POST', - url=f'{url}/embeddings', + url=embeddings_url, data=body, headers=headers, cookies=cookies, @@ -1387,20 +1382,20 @@ 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 + input: list | str | None = None + instructions: str | None = None + stream: bool | None = None + temperature: float | None = None + max_output_tokens: int | None = None + top_p: float | None = None + tools: list | None = None + tool_choice: str | dict | None = None + text: dict | None = None + truncation: str | None = None + metadata: dict | None = None + store: bool | None = None + reasoning: dict | None = None + previous_response_id: str | None = None @router.post('/responses') @@ -1444,7 +1439,7 @@ async def responses( try: headers, cookies = await get_headers_and_cookies(request, url, key, api_config, user=user) - if api_config.get('azure', False): + if api_config.get('azure') or api_config.get('provider') == 'azure': auth_type = api_config.get('auth_type', 'bearer') if auth_type not in ('azure_ad', 'microsoft_entra_id'): headers['api-key'] = key @@ -1555,7 +1550,7 @@ async def proxy(path: str, request: Request, user=Depends(get_verified_user)): try: headers, cookies = await get_headers_and_cookies(request, url, key, api_config, user=user) - if api_config.get('azure', False): + if api_config.get('azure') or api_config.get('provider') == 'azure': # 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'): diff --git a/backend/open_webui/routers/pipelines.py b/backend/open_webui/routers/pipelines.py index 580fb42fb2..5e0d4dc199 100644 --- a/backend/open_webui/routers/pipelines.py +++ b/backend/open_webui/routers/pipelines.py @@ -1,4 +1,11 @@ +import logging +import os +import shutil +from typing import Optional + +import aiohttp from fastapi import ( + APIRouter, Depends, FastAPI, File, @@ -7,24 +14,14 @@ from fastapi import ( 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.env import AIOHTTP_CLIENT_SESSION_SSL from open_webui.routers.openai import get_all_models_responses - from open_webui.utils.auth import get_admin_user +from pydantic import BaseModel +from starlette.responses import FileResponse log = logging.getLogger(__name__) diff --git a/backend/open_webui/routers/prompts.py b/backend/open_webui/routers/prompts.py index 755034f880..1054288da0 100644 --- a/backend/open_webui/routers/prompts.py +++ b/backend/open_webui/routers/prompts.py @@ -1,14 +1,11 @@ -from typing import Optional -from fastapi import APIRouter, Depends, HTTPException, status, Request +from __future__ import annotations -from open_webui.models.prompts import ( - PromptForm, - PromptUserResponse, - PromptAccessResponse, - PromptAccessListResponse, - PromptModel, - Prompts, -) +from typing import Optional + +from fastapi import APIRouter, Depends, HTTPException, Request, status +from open_webui.config import BYPASS_ADMIN_ACCESS_CONTROL +from open_webui.constants import ERROR_MESSAGES +from open_webui.internal.db import get_async_session from open_webui.models.access_grants import AccessGrants from open_webui.models.groups import Groups from open_webui.models.prompt_history import ( @@ -16,13 +13,18 @@ from open_webui.models.prompt_history import ( PromptHistoryModel, PromptHistoryResponse, ) -from open_webui.constants import ERROR_MESSAGES +from open_webui.models.prompts import ( + PromptAccessListResponse, + PromptAccessResponse, + PromptForm, + PromptModel, + Prompts, + PromptUserResponse, +) +from open_webui.utils.access_control import filter_allowed_access_grants, has_permission 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 +from sqlalchemy.ext.asyncio import AsyncSession class PromptVersionUpdateForm(BaseModel): @@ -32,7 +34,7 @@ class PromptVersionUpdateForm(BaseModel): class PromptMetadataForm(BaseModel): name: str command: str - tags: Optional[list[str]] = None + tags: list[str | None] = None router = APIRouter() @@ -66,12 +68,12 @@ async def get_prompt_tags(user=Depends(get_verified_user), db: AsyncSession = De @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, + query: str | None = None, + view_option: str | None = None, + tag: str | None = None, + order_by: str | None = None, + direction: str | None = None, + page: int | None = 1, user=Depends(get_verified_user), db: AsyncSession = Depends(get_async_session), ): @@ -136,7 +138,7 @@ async def get_prompt_list( ############################ -@router.post('/create', response_model=Optional[PromptModel]) +@router.post('/create', response_model=PromptModel | None) async def create_new_prompt( request: Request, form_data: PromptForm, @@ -186,56 +188,12 @@ async def create_new_prompt( ) -############################ -# 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]) +@router.get('/id/{prompt_id}', response_model=PromptAccessResponse | None) async def get_prompt_by_id( prompt_id: str, user=Depends(get_verified_user), db: AsyncSession = Depends(get_async_session) ): @@ -279,7 +237,7 @@ async def get_prompt_by_id( ############################ -@router.post('/id/{prompt_id}/update', response_model=Optional[PromptModel]) +@router.post('/id/{prompt_id}/update', response_model=PromptModel | None) async def update_prompt_by_id( request: Request, prompt_id: str, @@ -287,6 +245,7 @@ async def update_prompt_by_id( user=Depends(get_verified_user), db: AsyncSession = Depends(get_async_session), ): + """Update a prompt's content, creating a new history entry if changed.""" prompt = await Prompts.get_prompt_by_id(prompt_id, db=db) if not prompt: @@ -345,7 +304,7 @@ async def update_prompt_by_id( ############################ -@router.post('/id/{prompt_id}/update/meta', response_model=Optional[PromptModel]) +@router.post('/id/{prompt_id}/update/meta', response_model=PromptModel | None) async def update_prompt_metadata( prompt_id: str, form_data: PromptMetadataForm, @@ -398,7 +357,7 @@ async def update_prompt_metadata( ) -@router.post('/id/{prompt_id}/update/version', response_model=Optional[PromptModel]) +@router.post('/id/{prompt_id}/update/version', response_model=PromptModel | None) async def set_prompt_version( prompt_id: str, form_data: PromptVersionUpdateForm, @@ -447,7 +406,7 @@ class PromptAccessGrantsForm(BaseModel): access_grants: list[dict] -@router.post('/id/{prompt_id}/access/update', response_model=Optional[PromptModel]) +@router.post('/id/{prompt_id}/access/update', response_model=PromptModel | None) async def update_prompt_access_by_id( request: Request, prompt_id: str, @@ -496,7 +455,7 @@ async def update_prompt_access_by_id( ############################ -@router.post('/id/{prompt_id}/toggle', response_model=Optional[PromptModel]) +@router.post('/id/{prompt_id}/toggle', response_model=PromptModel | None) async def toggle_prompt_active( prompt_id: str, user=Depends(get_verified_user), db: AsyncSession = Depends(get_async_session) ): @@ -697,7 +656,7 @@ async def delete_prompt_history_entry( detail='Cannot delete the active production version', ) - success = await PromptHistories.delete_history_entry(history_id, db=db) + success = await PromptHistories.delete_history_entry(history_id, prompt.id, db=db) if not success: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, @@ -741,7 +700,7 @@ async def get_prompt_diff( detail=ERROR_MESSAGES.ACCESS_PROHIBITED, ) - diff = await PromptHistories.compute_diff(from_id, to_id, db=db) + diff = await PromptHistories.compute_diff(from_id, to_id, prompt.id, db=db) if not diff: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, diff --git a/backend/open_webui/routers/retrieval.py b/backend/open_webui/routers/retrieval.py index 201e6a63fb..70f6cf6309 100644 --- a/backend/open_webui/routers/retrieval.py +++ b/backend/open_webui/routers/retrieval.py @@ -1,130 +1,123 @@ +from __future__ import annotations + +import asyncio import json import logging import mimetypes import os -import shutil -import asyncio - import re +import shutil import uuid from datetime import datetime from pathlib import Path -from typing import Iterator, List, Optional, Sequence, Union +from typing import Callable, Iterator, Optional, Sequence, Union +import tiktoken from fastapi import ( + APIRouter, Depends, FastAPI, - Query, File, Form, HTTPException, - UploadFile, + Query, Request, + UploadFile, status, - APIRouter, ) -from fastapi.middleware.cors import CORSMiddleware from fastapi.concurrency import run_in_threadpool -from pydantic import BaseModel -import tiktoken - - +from fastapi.middleware.cors import CORSMiddleware +from langchain_core.documents import Document from langchain_text_splitters import ( + MarkdownHeaderTextSplitter, 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.brave_llm_context import search_brave_llm_context -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 ( + DEFAULT_LOCALE, ENV, + RAG_EMBEDDING_CONTENT_PREFIX, RAG_EMBEDDING_MODEL_AUTO_UPDATE, RAG_EMBEDDING_MODEL_TRUST_REMOTE_CODE, + RAG_EMBEDDING_QUERY_PREFIX, 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.constants import ERROR_MESSAGES 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, + SENTENCE_TRANSFORMERS_MODEL_KWARGS, ) +from open_webui.internal.db import get_async_db, get_async_session +from open_webui.models.files import FileModel, Files, FileUpdateForm +from open_webui.models.knowledge import Knowledges -from open_webui.constants import ERROR_MESSAGES +# Document loaders +from open_webui.retrieval.loaders.youtube import YoutubeLoader +from open_webui.retrieval.utils import ( + build_loader_from_config, + filter_accessible_collections, + get_content_from_url, + get_embedding_function, + get_model_path, + get_reranking_function, + query_collection, + query_collection_with_hybrid_search, + query_doc, + query_doc_with_hybrid_search, +) +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.retrieval.vector.utils import filter_metadata +from open_webui.retrieval.web.azure import search_azure +from open_webui.retrieval.web.bing import search_bing +from open_webui.retrieval.web.bocha import search_bocha +from open_webui.retrieval.web.brave import search_brave +from open_webui.retrieval.web.brave_llm_context import search_brave_llm_context +from open_webui.retrieval.web.duckduckgo import search_duckduckgo +from open_webui.retrieval.web.exa import search_exa +from open_webui.retrieval.web.external import search_external +from open_webui.retrieval.web.firecrawl import search_firecrawl +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.kagi import search_kagi + +# Web search engines +from open_webui.retrieval.web.main import SearchResult +from open_webui.retrieval.web.mojeek import search_mojeek +from open_webui.retrieval.web.ollama import search_ollama_cloud +from open_webui.retrieval.web.perplexity import search_perplexity +from open_webui.retrieval.web.perplexity_search import search_perplexity_search +from open_webui.retrieval.web.searchapi import search_searchapi +from open_webui.retrieval.web.searxng import search_searxng +from open_webui.retrieval.web.serpapi import search_serpapi +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.sougou import search_sougou +from open_webui.retrieval.web.tavily import search_tavily +from open_webui.retrieval.web.utils import get_web_loader +from open_webui.retrieval.web.yacy import search_yacy +from open_webui.retrieval.web.yandex import search_yandex +from open_webui.retrieval.web.ydc import search_youcom +from open_webui.retrieval.web.linkup import search_linkup +from open_webui.storage.provider import Storage +from open_webui.utils.access_control import has_permission +from open_webui.utils.access_control.files import has_access_to_file +from open_webui.utils.auth import get_admin_user, get_verified_user +from open_webui.utils.misc import ( + calculate_sha256_string, + sanitize_text_for_db, +) +from pydantic import BaseModel +from sqlalchemy.ext.asyncio import AsyncSession log = logging.getLogger(__name__) @@ -162,7 +155,7 @@ def get_ef( def get_rf( engine: str = '', - reranking_model: Optional[str] = None, + reranking_model: str | None = None, external_reranker_url: str = '', external_reranker_api_key: str = '', external_reranker_timeout: str = '', @@ -249,7 +242,7 @@ router = APIRouter() class CollectionNameForm(BaseModel): - collection_name: Optional[str] = None + collection_name: str | None = None class ProcessUrlForm(CollectionNameForm): @@ -257,7 +250,7 @@ class ProcessUrlForm(CollectionNameForm): class SearchForm(BaseModel): - queries: List[str] + queries: list[str] @router.get('/embedding') @@ -302,14 +295,14 @@ class AzureOpenAIConfigForm(BaseModel): class EmbeddingModelUpdateForm(BaseModel): - openai_config: Optional[OpenAIConfigForm] = None - ollama_config: Optional[OllamaConfigForm] = None - azure_openai_config: Optional[AzureOpenAIConfigForm] = None + openai_config: OpenAIConfigForm | None = None + ollama_config: OllamaConfigForm | None = None + azure_openai_config: AzureOpenAIConfigForm | None = 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 + RAG_EMBEDDING_BATCH_SIZE: int | None = 1 + ENABLE_ASYNC_EMBEDDING: bool | None = True + RAG_EMBEDDING_CONCURRENT_REQUESTS: int | None = 0 def unload_embedding_model(request: Request): @@ -473,6 +466,7 @@ async def get_rag_config(request: Request, user=Depends(get_admin_user)): '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, + 'MINERU_FILE_EXTENSIONS': request.app.state.config.MINERU_FILE_EXTENSIONS, # Reranking settings 'RAG_RERANKING_MODEL': request.app.state.config.RAG_RERANKING_MODEL, 'RAG_RERANKING_ENGINE': request.app.state.config.RAG_RERANKING_ENGINE, @@ -561,158 +555,163 @@ async def get_rag_config(request: Request, user=Depends(get_admin_user)): '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, + 'LINKUP_API_KEY': request.app.state.config.LINKUP_API_KEY, + 'LINKUP_SEARCH_PARAMS': request.app.state.config.LINKUP_SEARCH_PARAMS, }, } 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 - BRAVE_SEARCH_CONTEXT_TOKENS: Optional[int] = 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 + ENABLE_WEB_SEARCH: bool | None = None + WEB_SEARCH_ENGINE: str | None = None + WEB_SEARCH_TRUST_ENV: bool | None = None + WEB_SEARCH_RESULT_COUNT: int | None = None + WEB_SEARCH_CONCURRENT_REQUESTS: int | None = None + WEB_SEARCH_DOMAIN_FILTER_LIST: list[str | None] = [] + WEB_FETCH_MAX_CONTENT_LENGTH: int | None = None + WEB_LOADER_CONCURRENT_REQUESTS: int | None = None + BYPASS_WEB_SEARCH_EMBEDDING_AND_RETRIEVAL: bool | None = None + BYPASS_WEB_SEARCH_WEB_LOADER: bool | None = None + OLLAMA_CLOUD_WEB_SEARCH_API_KEY: str | None = None + SEARXNG_QUERY_URL: str | None = None + SEARXNG_LANGUAGE: str | None = None + YACY_QUERY_URL: str | None = None + YACY_USERNAME: str | None = None + YACY_PASSWORD: str | None = None + GOOGLE_PSE_API_KEY: str | None = None + GOOGLE_PSE_ENGINE_ID: str | None = None + BRAVE_SEARCH_API_KEY: str | None = None + BRAVE_SEARCH_CONTEXT_TOKENS: int | None = None + KAGI_SEARCH_API_KEY: str | None = None + MOJEEK_SEARCH_API_KEY: str | None = None + BOCHA_SEARCH_API_KEY: str | None = None + SERPSTACK_API_KEY: str | None = None + SERPSTACK_HTTPS: bool | None = None + SERPER_API_KEY: str | None = None + SERPLY_API_KEY: str | None = None + DDGS_BACKEND: str | None = None + TAVILY_API_KEY: str | None = None + SEARCHAPI_API_KEY: str | None = None + SEARCHAPI_ENGINE: str | None = None + SERPAPI_API_KEY: str | None = None + SERPAPI_ENGINE: str | None = None + JINA_API_KEY: str | None = None + JINA_API_BASE_URL: str | None = None + BING_SEARCH_V7_ENDPOINT: str | None = None + BING_SEARCH_V7_SUBSCRIPTION_KEY: str | None = None + EXA_API_KEY: str | None = None + PERPLEXITY_API_KEY: str | None = None + PERPLEXITY_MODEL: str | None = None + PERPLEXITY_SEARCH_CONTEXT_USAGE: str | None = None + PERPLEXITY_SEARCH_API_URL: str | None = None + SOUGOU_API_SID: str | None = None + SOUGOU_API_SK: str | None = None + WEB_LOADER_ENGINE: str | None = None + WEB_LOADER_TIMEOUT: str | None = None + ENABLE_WEB_LOADER_SSL_VERIFICATION: bool | None = None + PLAYWRIGHT_WS_URL: str | None = None + PLAYWRIGHT_TIMEOUT: int | None = None + FIRECRAWL_API_KEY: str | None = None + FIRECRAWL_API_BASE_URL: str | None = None + FIRECRAWL_TIMEOUT: str | None = None + TAVILY_EXTRACT_DEPTH: str | None = None + EXTERNAL_WEB_SEARCH_URL: str | None = None + EXTERNAL_WEB_SEARCH_API_KEY: str | None = None + EXTERNAL_WEB_LOADER_URL: str | None = None + EXTERNAL_WEB_LOADER_API_KEY: str | None = None + YOUTUBE_LOADER_LANGUAGE: list[str | None] = None + YOUTUBE_LOADER_PROXY_URL: str | None = None + YOUTUBE_LOADER_TRANSLATION: str | None = None + YANDEX_WEB_SEARCH_URL: str | None = None + YANDEX_WEB_SEARCH_API_KEY: str | None = None + YANDEX_WEB_SEARCH_CONFIG: str | None = None + YOUCOM_API_KEY: str | None = None + LINKUP_API_KEY: str | None = None + LINKUP_SEARCH_PARAMS: dict | None = 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 + RAG_TEMPLATE: str | None = None + TOP_K: int | None = None + BYPASS_EMBEDDING_AND_RETRIEVAL: bool | None = None + RAG_FULL_CONTEXT: bool | None = 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 + ENABLE_RAG_HYBRID_SEARCH: bool | None = None + ENABLE_RAG_HYBRID_SEARCH_ENRICHED_TEXTS: bool | None = None + TOP_K_RERANKER: int | None = None + RELEVANCE_THRESHOLD: float | None = None + HYBRID_BM25_WEIGHT: float | None = None # Content extraction settings - CONTENT_EXTRACTION_ENGINE: Optional[str] = None - PDF_EXTRACT_IMAGES: Optional[bool] = None - PDF_LOADER_MODE: Optional[str] = None + CONTENT_EXTRACTION_ENGINE: str | None = None + PDF_EXTRACT_IMAGES: bool | None = None + PDF_LOADER_MODE: str | None = 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 + DATALAB_MARKER_API_KEY: str | None = None + DATALAB_MARKER_API_BASE_URL: str | None = None + DATALAB_MARKER_ADDITIONAL_CONFIG: str | None = None + DATALAB_MARKER_SKIP_CACHE: bool | None = None + DATALAB_MARKER_FORCE_OCR: bool | None = None + DATALAB_MARKER_PAGINATE: bool | None = None + DATALAB_MARKER_STRIP_EXISTING_OCR: bool | None = None + DATALAB_MARKER_DISABLE_IMAGE_EXTRACTION: bool | None = None + DATALAB_MARKER_FORMAT_LINES: bool | None = None + DATALAB_MARKER_USE_LLM: bool | None = None + DATALAB_MARKER_OUTPUT_FORMAT: str | None = None - EXTERNAL_DOCUMENT_LOADER_URL: Optional[str] = None - EXTERNAL_DOCUMENT_LOADER_API_KEY: Optional[str] = None + EXTERNAL_DOCUMENT_LOADER_URL: str | None = None + EXTERNAL_DOCUMENT_LOADER_API_KEY: str | None = 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 + TIKA_SERVER_URL: str | None = None + DOCLING_SERVER_URL: str | None = None + DOCLING_API_KEY: str | None = None + DOCLING_PARAMS: dict | None = None + DOCUMENT_INTELLIGENCE_ENDPOINT: str | None = None + DOCUMENT_INTELLIGENCE_KEY: str | None = None + DOCUMENT_INTELLIGENCE_MODEL: str | None = None + MISTRAL_OCR_API_BASE_URL: str | None = None + MISTRAL_OCR_API_KEY: str | None = None + PADDLEOCR_VL_BASE_URL: str | None = None + PADDLEOCR_VL_TOKEN: str | None = 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 + MINERU_API_MODE: str | None = None + MINERU_API_URL: str | None = None + MINERU_API_KEY: str | None = None + MINERU_API_TIMEOUT: str | None = None + MINERU_PARAMS: dict | None = None + MINERU_FILE_EXTENSIONS: list[str] | None = 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 + RAG_RERANKING_MODEL: str | None = None + RAG_RERANKING_ENGINE: str | None = None + RAG_RERANKING_BATCH_SIZE: int | None = None + RAG_EXTERNAL_RERANKER_URL: str | None = None + RAG_EXTERNAL_RERANKER_API_KEY: str | None = None + RAG_EXTERNAL_RERANKER_TIMEOUT: str | None = 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 + TEXT_SPLITTER: str | None = None + ENABLE_MARKDOWN_HEADER_TEXT_SPLITTER: bool | None = None + CHUNK_SIZE: int | None = None + CHUNK_MIN_SIZE_TARGET: int | None = None + CHUNK_OVERLAP: int | None = 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 + FILE_MAX_SIZE: Union[int, str | None] = None + FILE_MAX_COUNT: Union[int, str | None] = None + FILE_IMAGE_COMPRESSION_WIDTH: Union[int, str | None] = None + FILE_IMAGE_COMPRESSION_HEIGHT: Union[int, str | None] = None + ALLOWED_FILE_EXTENSIONS: list[str | None] = None # Integration settings - ENABLE_GOOGLE_DRIVE_INTEGRATION: Optional[bool] = None - ENABLE_ONEDRIVE_INTEGRATION: Optional[bool] = None + ENABLE_GOOGLE_DRIVE_INTEGRATION: bool | None = None + ENABLE_ONEDRIVE_INTEGRATION: bool | None = None # Web search settings - web: Optional[WebConfig] = None + web: WebConfig | None = None @router.post('/config/update') @@ -907,6 +906,11 @@ async def update_rag_config(request: Request, form_data: ConfigForm, user=Depend 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 ) + request.app.state.config.MINERU_FILE_EXTENSIONS = ( + form_data.MINERU_FILE_EXTENSIONS + if form_data.MINERU_FILE_EXTENSIONS is not None + else request.app.state.config.MINERU_FILE_EXTENSIONS + ) # Reranking settings if request.app.state.config.RAG_RERANKING_ENGINE == '': @@ -1117,6 +1121,8 @@ async def update_rag_config(request: Request, form_data: ConfigForm, user=Depend 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 + request.app.state.config.LINKUP_API_KEY = form_data.web.LINKUP_API_KEY + request.app.state.config.LINKUP_SEARCH_PARAMS = form_data.web.LINKUP_SEARCH_PARAMS return { 'status': True, @@ -1249,6 +1255,8 @@ async def update_rag_config(request: Request, form_data: ConfigForm, user=Depend '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, + 'LINKUP_API_KEY': request.app.state.config.LINKUP_API_KEY, + 'LINKUP_SEARCH_PARAMS': request.app.state.config.LINKUP_SEARCH_PARAMS, }, } @@ -1283,20 +1291,43 @@ def merge_docs_to_target_size( 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: + Uses forward merging first (absorb the next chunk), then + backward merging (append into the previous emitted chunk) + for undersized chunks that can't grow forward. + """ + min_size = request.app.state.config.CHUNK_MIN_SIZE_TARGET + max_size = request.app.state.config.CHUNK_SIZE + + if min_size <= 0: return chunks - measure_chunk_size = len + measure: Callable[[str], int] = 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)) + measure = lambda text: len(encoding.encode(text)) - processed_chunks: list[Document] = [] + def _merge_backward(result: list[Document], content: str, chunk: Document) -> bool: + """Try to append content into the last emitted chunk. Returns True on success.""" + if not result: + return False + prev = result[-1] + if not can_merge_chunks(prev, chunk): + return False + merged = f'{prev.page_content}\n\n{content}' + if measure(merged) > max_size: + return False + result[-1] = Document(page_content=merged, metadata={**prev.metadata}) + return True + def _emit(result: list[Document], content: str, chunk: Document) -> None: + """Emit a chunk, trying backward merge first if it's undersized.""" + is_undersized = measure(content) < min_size + if is_undersized and _merge_backward(result, content, chunk): + return + result.append(Document(page_content=content, metadata={**chunk.metadata})) + + result: list[Document] = [] current_chunk: Document | None = None current_content: str = '' @@ -1304,44 +1335,34 @@ def merge_docs_to_target_size( if current_chunk is None: current_chunk = next_chunk current_content = next_chunk.page_content - continue # First chunk initialization + continue - proposed_content = f'{current_content}\n\n{next_chunk.page_content}' - - can_merge = ( + # Forward merge: absorb next chunk into current if undersized and fits + merged_content = f'{current_content}\n\n{next_chunk.page_content}' + can_merge_forward = ( 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 + and measure(current_content) < min_size + and measure(merged_content) <= max_size ) - if can_merge: - current_content = proposed_content + if can_merge_forward: + current_content = merged_content else: - processed_chunks.append( - Document( - page_content=current_content, - metadata={**current_chunk.metadata}, - ) - ) + _emit(result, current_content, current_chunk) 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}, - ) - ) + _emit(result, current_content, current_chunk) - return processed_chunks + return result def save_docs_to_vector_db( request: Request, docs, collection_name, - metadata: Optional[dict] = None, + metadata: dict | None = None, overwrite: bool = False, split: bool = True, add: bool = False, @@ -1539,8 +1560,8 @@ def save_docs_to_vector_db( class ProcessFileForm(BaseModel): file_id: str - content: Optional[str] = None - collection_name: Optional[str] = None + content: str | None = None + collection_name: str | None = None @router.post('/process/file') @@ -1767,7 +1788,7 @@ async def process_file( class ProcessTextForm(BaseModel): name: str content: str - collection_name: Optional[str] = None + collection_name: str | None = None @router.post('/process/text') @@ -1865,32 +1886,18 @@ async def process_web( ) -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 +async def search_web(request: Request, engine: str, query: str, user=None) -> list[SearchResult]: + """Dispatch a web search query to the configured engine and return results. + + Providers that have been migrated to async (aiohttp) are awaited natively. + Legacy sync providers are offloaded via ``asyncio.to_thread`` to avoid + blocking the event loop. """ # TODO: add playwright to search the web if engine == 'ollama_cloud': - return search_ollama_cloud( + return await asyncio.to_thread( + search_ollama_cloud, 'https://ollama.com', request.app.state.config.OLLAMA_CLOUD_WEB_SEARCH_API_KEY, query, @@ -1899,7 +1906,8 @@ def search_web(request: Request, engine: str, query: str, user=None) -> list[Sea ) elif engine == 'perplexity_search': if request.app.state.config.PERPLEXITY_API_KEY: - return search_perplexity_search( + return await asyncio.to_thread( + search_perplexity_search, request.app.state.config.PERPLEXITY_API_KEY, query, request.app.state.config.WEB_SEARCH_RESULT_COUNT, @@ -1912,7 +1920,7 @@ def search_web(request: Request, engine: str, query: str, user=None) -> list[Sea elif engine == 'searxng': if request.app.state.config.SEARXNG_QUERY_URL: searxng_kwargs = {'language': request.app.state.config.SEARXNG_LANGUAGE} - return search_searxng( + return await search_searxng( request.app.state.config.SEARXNG_QUERY_URL, query, request.app.state.config.WEB_SEARCH_RESULT_COUNT, @@ -1923,7 +1931,8 @@ def search_web(request: Request, engine: str, query: str, user=None) -> list[Sea raise Exception('No SEARXNG_QUERY_URL found in environment variables') elif engine == 'yacy': if request.app.state.config.YACY_QUERY_URL: - return search_yacy( + return await asyncio.to_thread( + search_yacy, request.app.state.config.YACY_QUERY_URL, request.app.state.config.YACY_USERNAME, request.app.state.config.YACY_PASSWORD, @@ -1935,7 +1944,7 @@ def search_web(request: Request, engine: str, query: str, user=None) -> list[Sea 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( + return await search_google_pse( request.app.state.config.GOOGLE_PSE_API_KEY, request.app.state.config.GOOGLE_PSE_ENGINE_ID, query, @@ -1947,7 +1956,7 @@ def search_web(request: Request, engine: str, query: str, user=None) -> list[Sea 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( + return await search_brave( request.app.state.config.BRAVE_SEARCH_API_KEY, query, request.app.state.config.WEB_SEARCH_RESULT_COUNT, @@ -1957,7 +1966,8 @@ def search_web(request: Request, engine: str, query: str, user=None) -> list[Sea raise Exception('No BRAVE_SEARCH_API_KEY found in environment variables') elif engine == 'brave_llm_context': if request.app.state.config.BRAVE_SEARCH_API_KEY: - return search_brave_llm_context( + return await asyncio.to_thread( + search_brave_llm_context, request.app.state.config.BRAVE_SEARCH_API_KEY, query, request.app.state.config.WEB_SEARCH_RESULT_COUNT, @@ -1968,7 +1978,8 @@ def search_web(request: Request, engine: str, query: str, user=None) -> list[Sea 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( + return await asyncio.to_thread( + search_kagi, request.app.state.config.KAGI_SEARCH_API_KEY, query, request.app.state.config.WEB_SEARCH_RESULT_COUNT, @@ -1978,7 +1989,8 @@ def search_web(request: Request, engine: str, query: str, user=None) -> list[Sea 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( + return await asyncio.to_thread( + search_mojeek, request.app.state.config.MOJEEK_SEARCH_API_KEY, query, request.app.state.config.WEB_SEARCH_RESULT_COUNT, @@ -1988,7 +2000,8 @@ def search_web(request: Request, engine: str, query: str, user=None) -> list[Sea 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( + return await asyncio.to_thread( + search_bocha, request.app.state.config.BOCHA_SEARCH_API_KEY, query, request.app.state.config.WEB_SEARCH_RESULT_COUNT, @@ -1998,7 +2011,7 @@ def search_web(request: Request, engine: str, query: str, user=None) -> list[Sea 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( + return await search_serpstack( request.app.state.config.SERPSTACK_API_KEY, query, request.app.state.config.WEB_SEARCH_RESULT_COUNT, @@ -2009,7 +2022,7 @@ def search_web(request: Request, engine: str, query: str, user=None) -> list[Sea raise Exception('No SERPSTACK_API_KEY found in environment variables') elif engine == 'serper': if request.app.state.config.SERPER_API_KEY: - return search_serper( + return await search_serper( request.app.state.config.SERPER_API_KEY, query, request.app.state.config.WEB_SEARCH_RESULT_COUNT, @@ -2019,7 +2032,8 @@ def search_web(request: Request, engine: str, query: str, user=None) -> list[Sea raise Exception('No SERPER_API_KEY found in environment variables') elif engine == 'serply': if request.app.state.config.SERPLY_API_KEY: - return search_serply( + return await asyncio.to_thread( + search_serply, request.app.state.config.SERPLY_API_KEY, query, request.app.state.config.WEB_SEARCH_RESULT_COUNT, @@ -2028,7 +2042,8 @@ def search_web(request: Request, engine: str, query: str, user=None) -> list[Sea else: raise Exception('No SERPLY_API_KEY found in environment variables') elif engine == 'duckduckgo': - return search_duckduckgo( + return await asyncio.to_thread( + search_duckduckgo, query, request.app.state.config.WEB_SEARCH_RESULT_COUNT, request.app.state.config.WEB_SEARCH_DOMAIN_FILTER_LIST, @@ -2037,7 +2052,8 @@ def search_web(request: Request, engine: str, query: str, user=None) -> list[Sea ) elif engine == 'tavily': if request.app.state.config.TAVILY_API_KEY: - return search_tavily( + return await asyncio.to_thread( + search_tavily, request.app.state.config.TAVILY_API_KEY, query, request.app.state.config.WEB_SEARCH_RESULT_COUNT, @@ -2047,7 +2063,8 @@ def search_web(request: Request, engine: str, query: str, user=None) -> list[Sea raise Exception('No TAVILY_API_KEY found in environment variables') elif engine == 'exa': if request.app.state.config.EXA_API_KEY: - return search_exa( + return await asyncio.to_thread( + search_exa, request.app.state.config.EXA_API_KEY, query, request.app.state.config.WEB_SEARCH_RESULT_COUNT, @@ -2057,7 +2074,8 @@ def search_web(request: Request, engine: str, query: str, user=None) -> list[Sea raise Exception('No EXA_API_KEY found in environment variables') elif engine == 'searchapi': if request.app.state.config.SEARCHAPI_API_KEY: - return search_searchapi( + return await asyncio.to_thread( + search_searchapi, request.app.state.config.SEARCHAPI_API_KEY, request.app.state.config.SEARCHAPI_ENGINE, query, @@ -2068,7 +2086,8 @@ def search_web(request: Request, engine: str, query: str, user=None) -> list[Sea raise Exception('No SEARCHAPI_API_KEY found in environment variables') elif engine == 'serpapi': if request.app.state.config.SERPAPI_API_KEY: - return search_serpapi( + return await asyncio.to_thread( + search_serpapi, request.app.state.config.SERPAPI_API_KEY, request.app.state.config.SERPAPI_ENGINE, query, @@ -2078,14 +2097,16 @@ def search_web(request: Request, engine: str, query: str, user=None) -> list[Sea else: raise Exception('No SERPAPI_API_KEY found in environment variables') elif engine == 'jina': - return search_jina( + return await asyncio.to_thread( + 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( + return await asyncio.to_thread( + search_bing, request.app.state.config.BING_SEARCH_V7_SUBSCRIPTION_KEY, request.app.state.config.BING_SEARCH_V7_ENDPOINT, str(DEFAULT_LOCALE), @@ -2099,7 +2120,8 @@ def search_web(request: Request, engine: str, query: str, user=None) -> list[Sea and request.app.state.config.AZURE_AI_SEARCH_ENDPOINT and request.app.state.config.AZURE_AI_SEARCH_INDEX_NAME ): - return search_azure( + return await asyncio.to_thread( + 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, @@ -2111,15 +2133,9 @@ def search_web(request: Request, engine: str, query: str, user=None) -> list[Sea 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( + return await asyncio.to_thread( + search_perplexity, request.app.state.config.PERPLEXITY_API_KEY, query, request.app.state.config.WEB_SEARCH_RESULT_COUNT, @@ -2129,7 +2145,8 @@ def search_web(request: Request, engine: str, query: str, user=None) -> list[Sea ) elif engine == 'sougou': if request.app.state.config.SOUGOU_API_SID and request.app.state.config.SOUGOU_API_SK: - return search_sougou( + return await asyncio.to_thread( + search_sougou, request.app.state.config.SOUGOU_API_SID, request.app.state.config.SOUGOU_API_SK, query, @@ -2139,7 +2156,8 @@ def search_web(request: Request, engine: str, query: str, user=None) -> list[Sea else: raise Exception('No SOUGOU_API_SID or SOUGOU_API_SK found in environment variables') elif engine == 'firecrawl': - return search_firecrawl( + return await asyncio.to_thread( + search_firecrawl, request.app.state.config.FIRECRAWL_API_BASE_URL, request.app.state.config.FIRECRAWL_API_KEY, query, @@ -2147,7 +2165,8 @@ def search_web(request: Request, engine: str, query: str, user=None) -> list[Sea request.app.state.config.WEB_SEARCH_DOMAIN_FILTER_LIST, ) elif engine == 'external': - return search_external( + return await asyncio.to_thread( + search_external, request, request.app.state.config.EXTERNAL_WEB_SEARCH_URL, request.app.state.config.EXTERNAL_WEB_SEARCH_API_KEY, @@ -2157,7 +2176,8 @@ def search_web(request: Request, engine: str, query: str, user=None) -> list[Sea user=user, ) elif engine == 'yandex': - return search_yandex( + return await asyncio.to_thread( + search_yandex, request, request.app.state.config.YANDEX_WEB_SEARCH_URL, request.app.state.config.YANDEX_WEB_SEARCH_API_KEY, @@ -2168,12 +2188,25 @@ def search_web(request: Request, engine: str, query: str, user=None) -> list[Sea user=user, ) elif engine == 'youcom': - return search_youcom( + return await asyncio.to_thread( + 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, ) + elif engine == 'linkup': + if request.app.state.config.LINKUP_API_KEY: + return await asyncio.to_thread( + search_linkup, + api_key=request.app.state.config.LINKUP_API_KEY, + query=query, + count=request.app.state.config.WEB_SEARCH_RESULT_COUNT, + filter_list=request.app.state.config.WEB_SEARCH_DOMAIN_FILTER_LIST, + params=request.app.state.config.LINKUP_SEARCH_PARAMS, + ) + else: + raise Exception('No LINKUP_API_KEY found in environment variables') else: raise Exception('No search engine API key found in environment variables') @@ -2211,8 +2244,7 @@ async def process_web_search(request: Request, form_data: SearchForm, user=Depen async def search_query_with_semaphore(query): async with semaphore: - return await run_in_threadpool( - search_web, + return await search_web( request, request.app.state.config.WEB_SEARCH_ENGINE, query, @@ -2221,10 +2253,9 @@ async def process_web_search(request: Request, form_data: SearchForm, user=Depen search_tasks = [search_query_with_semaphore(query) for query in form_data.queries] else: - # Unlimited parallel execution (previous behavior) + # Unlimited parallel execution search_tasks = [ - run_in_threadpool( - search_web, + search_web( request, request.app.state.config.WEB_SEARCH_ENGINE, query, @@ -2246,12 +2277,8 @@ async def process_web_search(request: Request, form_data: SearchForm, user=Depen 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), - ) + log.exception('Web search failed') + raise HTTPException(status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.WEB_SEARCH_ERROR(e)) if len(urls) == 0: raise HTTPException( @@ -2331,11 +2358,8 @@ async def process_web_search(request: Request, form_data: SearchForm, user=Depen '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), - ) + log.exception('Web search content loading failed') + raise HTTPException(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: @@ -2357,10 +2381,10 @@ async def _validate_collection_access(collection_names: list[str], user, access_ 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 + k: int | None = None + k_reranker: int | None = None + r: float | None = None + hybrid: bool | None = None @router.post('/query/doc') @@ -2423,12 +2447,12 @@ async def query_doc_handler( 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 + k: int | None = None + k_reranker: int | None = None + r: float | None = None + hybrid: bool | None = None + hybrid_bm25_weight: float | None = None + enable_enriched_texts: bool | None = None @router.post('/query/collection') @@ -2581,24 +2605,24 @@ async def reset_upload_dir(user=Depends(get_admin_user)) -> bool: if ENV == 'dev': @router.get('/ef/{text}') - async def get_embeddings(request: Request, text: Optional[str] = 'Hello World!'): + async def get_embeddings(request: Request, text: str | None = 'Hello World!'): return {'result': await request.app.state.EMBEDDING_FUNCTION(text, prefix=RAG_EMBEDDING_QUERY_PREFIX)} class BatchProcessFilesForm(BaseModel): - files: List[FileModel] + files: list[FileModel] collection_name: str class BatchProcessFilesResult(BaseModel): file_id: str status: str - error: Optional[str] = None + error: str | None = None class BatchProcessFilesResponse(BaseModel): - results: List[BatchProcessFilesResult] - errors: List[BatchProcessFilesResult] + results: list[BatchProcessFilesResult] + errors: list[BatchProcessFilesResult] @router.post('/process/files/batch') @@ -2622,12 +2646,12 @@ async def process_files_batch( if collection_name: await _validate_collection_access([collection_name], user, access_type='write') - file_results: List[BatchProcessFilesResult] = [] - file_errors: List[BatchProcessFilesResult] = [] - file_updates: List[FileUpdateForm] = [] + file_results: list[BatchProcessFilesResult] = [] + file_errors: list[BatchProcessFilesResult] = [] + file_updates: list[FileUpdateForm] = [] # Prepare all documents first - all_docs: List[Document] = [] + all_docs: list[Document] = [] for file in form_data.files: try: @@ -2653,7 +2677,7 @@ async def process_files_batch( continue text_content = file.data.get('content', '') - docs: List[Document] = [ + docs: list[Document] = [ Document( page_content=text_content.replace('
', '\n'), metadata={ diff --git a/backend/open_webui/routers/scim.py b/backend/open_webui/routers/scim.py index 75f45bcaf9..9292523adc 100644 --- a/backend/open_webui/routers/scim.py +++ b/backend/open_webui/routers/scim.py @@ -7,31 +7,27 @@ NOTE: This is an experimental implementation and may not fully comply with SCIM import hmac import logging -import uuid import time -from typing import Optional, List, Dict, Any +import uuid from datetime import datetime, timezone +from typing import Any, Dict, List, Optional -from fastapi import APIRouter, Depends, HTTPException, Request, Query, Header, status +from fastapi import APIRouter, Depends, Header, HTTPException, Query, Request, 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.config import OAUTH_PROVIDERS +from open_webui.constants import ERROR_MESSAGES +from open_webui.env import SCIM_AUTH_PROVIDER +from open_webui.internal.db import get_async_session +from open_webui.models.groups import GroupModel, Groups +from open_webui.models.users import UserModel, Users from open_webui.utils.auth import ( + decode_token, 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 pydantic import BaseModel, ConfigDict, Field from sqlalchemy.ext.asyncio import AsyncSession -from open_webui.internal.db import get_async_session log = logging.getLogger(__name__) @@ -263,7 +259,7 @@ def get_scim_auth(request: Request, authorization: Optional[str] = Header(None)) 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 + # Handle both ConfigVar and direct value if hasattr(enable_scim, 'value'): enable_scim = enable_scim.value @@ -275,7 +271,7 @@ def get_scim_auth(request: Request, authorization: Optional[str] = Header(None)) # Verify the SCIM token scim_token = getattr(request.app.state, 'SCIM_TOKEN', None) - # Handle both PersistentConfig and direct value + # Handle both ConfigVar and direct value if hasattr(scim_token, 'value'): scim_token = scim_token.value log.debug(f'SCIM token configured: {bool(scim_token)}') diff --git a/backend/open_webui/routers/skills.py b/backend/open_webui/routers/skills.py index ede5afd814..55aa351ff0 100644 --- a/backend/open_webui/routers/skills.py +++ b/backend/open_webui/routers/skills.py @@ -1,28 +1,25 @@ 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.config import BYPASS_ADMIN_ACCESS_CONTROL +from open_webui.constants import ERROR_MESSAGES from open_webui.internal.db import get_async_session +from open_webui.models.access_grants import AccessGrants +from open_webui.models.groups import Groups from open_webui.models.skills import ( + SkillAccessListResponse, + SkillAccessResponse, SkillForm, SkillModel, SkillResponse, - SkillUserResponse, - SkillAccessResponse, - SkillAccessListResponse, Skills, + SkillUserResponse, ) -from open_webui.models.access_grants import AccessGrants +from open_webui.utils.access_control import filter_allowed_access_grants, has_permission 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 +from pydantic import BaseModel +from sqlalchemy.ext.asyncio import AsyncSession log = logging.getLogger(__name__) diff --git a/backend/open_webui/routers/tasks.py b/backend/open_webui/routers/tasks.py index 56c7e1d1b0..0e88f8594a 100644 --- a/backend/open_webui/routers/tasks.py +++ b/backend/open_webui/routers/tasks.py @@ -1,40 +1,36 @@ -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 typing import Optional -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 fastapi import APIRouter, Depends, HTTPException, Request, Response, status +from fastapi.responses import JSONResponse, RedirectResponse 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_FOLLOW_UP_GENERATION_PROMPT_TEMPLATE, + DEFAULT_IMAGE_PROMPT_GENERATION_PROMPT_TEMPLATE, DEFAULT_MOA_GENERATION_PROMPT_TEMPLATE, + DEFAULT_QUERY_GENERATION_PROMPT_TEMPLATE, + DEFAULT_TAGS_GENERATION_PROMPT_TEMPLATE, + DEFAULT_TITLE_GENERATION_PROMPT_TEMPLATE, DEFAULT_VOICE_MODE_PROMPT_TEMPLATE, ) +from open_webui.constants import ERROR_MESSAGES, TASKS +from open_webui.routers.pipelines import process_pipeline_inlet_filter +from open_webui.utils.auth import get_admin_user, get_verified_user +from open_webui.utils.chat import generate_chat_completion +from open_webui.utils.task import ( + autocomplete_generation_template, + emoji_generation_template, + follow_up_generation_template, + get_task_model_id, + image_prompt_generation_template, + moa_response_generation_template, + query_generation_template, + tags_generation_template, + title_generation_template, +) +from pydantic import BaseModel log = logging.getLogger(__name__) @@ -170,6 +166,11 @@ async def generate_title(request: Request, form_data: dict, user=Depends(get_ver models = request.app.state.MODELS model_id = form_data['model'] + if not model_id: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail='No model specified for title generation. Please ensure a model is selected for this chat.', + ) if model_id not in models: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, diff --git a/backend/open_webui/routers/terminals.py b/backend/open_webui/routers/terminals.py index c251b20d48..6a942cf9b2 100644 --- a/backend/open_webui/routers/terminals.py +++ b/backend/open_webui/routers/terminals.py @@ -12,14 +12,13 @@ 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.config import TERMINAL_PROXY_HEADERS +from open_webui.env import AIOHTTP_CLIENT_SESSION_SSL from open_webui.models.groups import Groups from open_webui.models.users import Users +from open_webui.utils.access_control import has_connection_access +from open_webui.utils.auth import get_verified_user +from starlette.background import BackgroundTask log = logging.getLogger(__name__) @@ -36,7 +35,14 @@ def _sanitize_proxy_path(path: str) -> str | None: Trailing slashes are preserved — many upstream frameworks treat ``/path`` and ``/path/`` differently. """ - decoded = unquote(path) + # Decode until stable: a single unquote pass leaves %252e%252e as %2e%2e, + # which the upstream then re-decodes into '..', bypassing the check below. + decoded = path + for _ in range(8): + once = unquote(decoded) + if once == decoded: + break + decoded = once had_trailing_slash = decoded.endswith('/') normalized = posixpath.normpath(decoded) # Remove any leading slashes that would reset the base @@ -199,6 +205,7 @@ async def _resolve_authenticated_connection(ws: WebSocket, server_id: str): """ import asyncio import json + from open_webui.utils.auth import decode_token # First-message authentication @@ -324,11 +331,20 @@ async def ws_terminal( except Exception: pass - await asyncio.gather( - _client_to_upstream(), - _upstream_to_client(), - return_exceptions=True, - ) + # End the proxy as soon as either direction finishes (e.g. a + # graceful upstream CLOSE) and cancel the sibling, which would + # otherwise hang on a blocked ws.receive() until the browser leaves. + tasks = [ + asyncio.create_task(_client_to_upstream()), + asyncio.create_task(_upstream_to_client()), + ] + _done, pending = await asyncio.wait(tasks, return_when=asyncio.FIRST_COMPLETED) + for task in pending: + task.cancel() + try: + await task + except asyncio.CancelledError: + pass except Exception as e: log.exception('Terminal WebSocket proxy error: %s', e) finally: diff --git a/backend/open_webui/routers/tools.py b/backend/open_webui/routers/tools.py index cd11bcde5e..963a727cde 100644 --- a/backend/open_webui/routers/tools.py +++ b/backend/open_webui/routers/tools.py @@ -1,44 +1,43 @@ +from __future__ import annotations + import logging +import re +import time 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.config import BYPASS_ADMIN_ACCESS_CONTROL, CACHE_DIR +from open_webui.constants import ERROR_MESSAGES +from open_webui.env import AIOHTTP_CLIENT_SESSION_SSL, AIOHTTP_CLIENT_TIMEOUT from open_webui.internal.db import get_async_session - - +from open_webui.models.access_grants import AccessGrants +from open_webui.models.groups import Groups from open_webui.models.oauth_sessions import OAuthSessions from open_webui.models.tools import ( + ToolAccessResponse, ToolForm, ToolModel, ToolResponse, - ToolUserResponse, - ToolAccessResponse, Tools, + ToolUserResponse, ) -from open_webui.models.access_grants import AccessGrants +from open_webui.utils.access_control import ( + filter_allowed_access_grants, + has_access, + has_permission, +) +from open_webui.utils.auth import get_admin_user, get_verified_user from open_webui.utils.plugin import ( + get_tool_module_from_cache, 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 +from open_webui.utils.tools import get_tool_servers, get_tool_specs +from pydantic import BaseModel, HttpUrl +from sqlalchemy.ext.asyncio import AsyncSession log = logging.getLogger(__name__) @@ -248,7 +247,7 @@ def github_url_to_raw_url(url: str) -> str: return url -@router.post('/load/url', response_model=Optional[dict]) +@router.post('/load/url', response_model=dict | None) 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, @@ -323,13 +322,14 @@ async def export_tools( ############################ -@router.post('/create', response_model=Optional[ToolResponse]) +@router.post('/create', response_model=ToolResponse | None) async def create_new_tools( request: Request, form_data: ToolForm, user=Depends(get_verified_user), db: AsyncSession = Depends(get_async_session), ): + """Create a new tool from user-supplied Python source code.""" 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( @@ -401,7 +401,7 @@ async def create_new_tools( ############################ -@router.get('/id/{id}', response_model=Optional[ToolAccessResponse]) +@router.get('/id/{id}', response_model=ToolAccessResponse | None) 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) @@ -448,7 +448,7 @@ async def get_tools_by_id(id: str, user=Depends(get_verified_user), db: AsyncSes ############################ -@router.post('/id/{id}/update', response_model=Optional[ToolModel]) +@router.post('/id/{id}/update', response_model=ToolModel | None) async def update_tools_by_id( request: Request, id: str, @@ -456,6 +456,7 @@ async def update_tools_by_id( user=Depends(get_verified_user), db: AsyncSession = Depends(get_async_session), ): + """Update an existing tool's source code and metadata.""" tools = await Tools.get_tool_by_id(id, db=db) if not tools: raise HTTPException( @@ -541,7 +542,7 @@ class ToolAccessGrantsForm(BaseModel): access_grants: list[dict] -@router.post('/id/{id}/access/update', response_model=Optional[ToolModel]) +@router.post('/id/{id}/access/update', response_model=ToolModel | None) async def update_tool_access_by_id( request: Request, id: str, @@ -634,7 +635,7 @@ async def delete_tools_by_id( ############################ -@router.get('/id/{id}/valves', response_model=Optional[dict]) +@router.get('/id/{id}/valves', response_model=dict | None) async def get_tools_valves_by_id( id: str, user=Depends(get_verified_user), db: AsyncSession = Depends(get_async_session) ): @@ -676,7 +677,7 @@ async def get_tools_valves_by_id( ############################ -@router.get('/id/{id}/valves/spec', response_model=Optional[dict]) +@router.get('/id/{id}/valves/spec', response_model=dict | None) async def get_tools_valves_spec_by_id( request: Request, id: str, @@ -726,7 +727,7 @@ async def get_tools_valves_spec_by_id( ############################ -@router.post('/id/{id}/valves/update', response_model=Optional[dict]) +@router.post('/id/{id}/valves/update', response_model=dict | None) async def update_tools_valves_by_id( request: Request, id: str, @@ -789,7 +790,7 @@ async def update_tools_valves_by_id( ############################ -@router.get('/id/{id}/valves/user', response_model=Optional[dict]) +@router.get('/id/{id}/valves/user', response_model=dict | None) async def get_tools_user_valves_by_id( id: str, user=Depends(get_verified_user), db: AsyncSession = Depends(get_async_session) ): @@ -826,7 +827,7 @@ async def get_tools_user_valves_by_id( ) -@router.get('/id/{id}/valves/user/spec', response_model=Optional[dict]) +@router.get('/id/{id}/valves/user/spec', response_model=dict | None) async def get_tools_user_valves_spec_by_id( request: Request, id: str, @@ -871,7 +872,7 @@ async def get_tools_user_valves_spec_by_id( return None -@router.post('/id/{id}/valves/user/update', response_model=Optional[dict]) +@router.post('/id/{id}/valves/user/update', response_model=dict | None) async def update_tools_user_valves_by_id( request: Request, id: str, diff --git a/backend/open_webui/routers/users.py b/backend/open_webui/routers/users.py index 33d1cd425c..0b8da713df 100644 --- a/backend/open_webui/routers/users.py +++ b/backend/open_webui/routers/users.py @@ -1,46 +1,45 @@ -import logging -from typing import Optional -from sqlalchemy.ext.asyncio import AsyncSession +from __future__ import annotations + import base64 import io - +import logging +import time +from typing import Optional 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 fastapi.responses import FileResponse, Response, StreamingResponse from open_webui.constants import ERROR_MESSAGES from open_webui.env import ENABLE_PROFILE_IMAGE_URL_FORWARDING, PROFILE_IMAGE_ALLOWED_MIME_TYPES, STATIC_DIR from open_webui.internal.db import get_async_session - - +from open_webui.models.auths import Auths +from open_webui.models.groups import Groups +from open_webui.models.oauth_sessions import OAuthSessions +from open_webui.models.users import ( + UserGroupIdsListResponse, + UserGroupIdsModel, + UserInfoListResponse, + UserInfoResponse, + UserModel, + UserRoleUpdateForm, + Users, + UserSettings, + UserStatus, + UserUpdateForm, +) +from open_webui.models.access_grants import AccessGrants +from open_webui.models.knowledge import Knowledges +from open_webui.models.models import Models +from open_webui.models.tools import Tools +from open_webui.socket.main import disconnect_user_sessions +from open_webui.utils.access_control import get_permissions, has_permission 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 +from pydantic import BaseModel, ConfigDict +from sqlalchemy.ext.asyncio import AsyncSession log = logging.getLogger(__name__) @@ -59,10 +58,10 @@ 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, + query: str | None = None, + order_by: str | None = None, + direction: str | None = None, + page: int | None = 1, user=Depends(get_admin_user), db: AsyncSession = Depends(get_async_session), ): @@ -114,10 +113,10 @@ async def get_all_users( @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, + query: str | None = None, + order_by: str | None = None, + direction: str | None = None, + page: int | None = 1, user=Depends(get_verified_user), db: AsyncSession = Depends(get_async_session), ): @@ -275,7 +274,7 @@ async def update_default_user_permissions(request: Request, form_data: UserPermi ############################ -@router.get('/user/settings', response_model=Optional[UserSettings]) +@router.get('/user/settings', response_model=UserSettings | None) async def get_user_settings_by_session_user( user=Depends(get_verified_user), db: AsyncSession = Depends(get_async_session) ): @@ -372,7 +371,7 @@ async def update_user_status_by_session_user( ############################ -@router.get('/user/info', response_model=Optional[dict]) +@router.get('/user/info', response_model=dict | None) 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 @@ -383,22 +382,26 @@ async def get_user_info_by_session_user(user=Depends(get_verified_user), db: Asy ############################ -@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) +@router.post('/user/info/update', response_model=dict | None) +async def update_user_info_by_session_user( # PATCH-style merge + 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: + """Merge caller-supplied fields into the current user's info dict. + + Uses the auth-time snapshot of ``user.info`` as the merge base. This does + NOT eliminate lost-update races on concurrent same-user writes; real safety + would need row locking or an optimistic-concurrency version column. + """ + merged_info = {**(user.info or {}), **form_data} + updated = await Users.update_user_by_id(user.id, {'info': merged_info}, db=db) + if not updated: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.USER_NOT_FOUND, ) + return updated.info ############################ @@ -408,8 +411,8 @@ async def update_user_info_by_session_user( class UserActiveResponse(UserStatus): name: str - profile_image_url: Optional[str] = None - groups: Optional[list] = [] + profile_image_url: str | None = None + groups: list | None = [] is_active: bool model_config = ConfigDict(extra='allow') @@ -536,11 +539,11 @@ async def get_user_active_status_by_id( ############################ -@router.post('/{user_id}/update', response_model=Optional[UserModel]) +@router.post('/{user_id}/update', response_model=UserModel | None) async def update_user_by_id( user_id: str, form_data: UserUpdateForm, - session_user=Depends(get_admin_user), + session_user: UserModel = Depends(get_admin_user), db: AsyncSession = Depends(get_async_session), ): # Prevent modification of the primary admin user by other admins @@ -683,3 +686,76 @@ 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) + + +############################ +# GetUserPreview +############################ + + +@router.get('/{user_id}/preview') +async def get_user_preview( + user_id: str, + user=Depends(get_admin_user), + db: AsyncSession = Depends(get_async_session), +): + """Show what resources a specific user can access across all their groups.""" + target_user = await Users.get_user_by_id(user_id, db=db) + if not target_user: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=ERROR_MESSAGES.USER_NOT_FOUND, + ) + + # Get all group IDs this user belongs to + user_groups = await Groups.get_groups_by_member_id(user_id, db=db) + user_group_ids = {g.id for g in user_groups} + + all_models = await Models.get_all_models(db=db) + accessible_model_ids = await AccessGrants.get_accessible_resource_ids( + user_id=user_id, + resource_type='model', + resource_ids=[m.id for m in all_models], + permission='read', + user_group_ids=user_group_ids, + db=db, + ) + + all_knowledge = await Knowledges.get_knowledge_bases(db=db) + accessible_knowledge_ids = await AccessGrants.get_accessible_resource_ids( + user_id=user_id, + resource_type='knowledge', + resource_ids=[k.id for k in all_knowledge], + permission='read', + user_group_ids=user_group_ids, + db=db, + ) + + all_tools = await Tools.get_tools(defer_content=True, db=db) + accessible_tool_ids = await AccessGrants.get_accessible_resource_ids( + user_id=user_id, + resource_type='tool', + resource_ids=[t.id for t in all_tools], + permission='read', + user_group_ids=user_group_ids, + db=db, + ) + + active_models = [m for m in all_models if m.is_active] + + return { + 'user': {'id': target_user.id, 'name': target_user.name}, + 'groups': [{'id': g.id, 'name': g.name} for g in user_groups], + 'models': { + 'items': [{'id': m.id, 'name': m.name} for m in active_models if m.id in accessible_model_ids], + 'total': len(active_models), + }, + 'knowledge': { + 'items': [{'id': k.id, 'name': k.name} for k in all_knowledge if k.id in accessible_knowledge_ids], + 'total': len(all_knowledge), + }, + 'tools': { + 'items': [{'id': t.id, 'name': t.name} for t in all_tools if t.id in accessible_tool_ids], + 'total': len(all_tools), + }, + } diff --git a/backend/open_webui/routers/utils.py b/backend/open_webui/routers/utils.py index 20705c2c44..4d0f679955 100644 --- a/backend/open_webui/routers/utils.py +++ b/backend/open_webui/routers/utils.py @@ -1,19 +1,18 @@ -import black -import logging -import markdown +from __future__ import annotations -from open_webui.models.chats import ChatTitleMessagesForm +import logging + +import black +from fastapi import APIRouter, Depends, HTTPException, Request, Response, status 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.models.chats import ChatTitleMessagesForm from open_webui.utils.auth import get_admin_user, get_verified_user from open_webui.utils.code_interpreter import execute_code_jupyter +from open_webui.utils.misc import get_gravatar_url +from open_webui.utils.pdf_generator import PDFGenerator +from pydantic import BaseModel +from starlette.responses import FileResponse log = logging.getLogger(__name__) @@ -73,15 +72,6 @@ async def execute_code(request: Request, form_data: CodeForm, user=Depends(get_v ) -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] @@ -104,20 +94,18 @@ async def download_chat_as_pdf(form_data: ChatTitleMessagesForm, user=Depends(ge @router.get('/db/download') async def download_db(user=Depends(get_admin_user)): + """Download the raw SQLite database file (admin-only, SQLite deployments only).""" if not ENABLE_ADMIN_EXPORT: - raise HTTPException( - status_code=status.HTTP_401_UNAUTHORIZED, - detail=ERROR_MESSAGES.ACCESS_PROHIBITED, - ) + raise HTTPException(status.HTTP_401_UNAUTHORIZED, detail=ERROR_MESSAGES.ACCESS_PROHIBITED) + + # Lazy import avoids circular dependency at module load time 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, - ) + raise HTTPException(status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DB_NOT_SQLITE) + return FileResponse( - engine.url.database, + str(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 index d59ff53277..2884847a0e 100644 --- a/backend/open_webui/socket/main.py +++ b/backend/open_webui/socket/main.py @@ -1,55 +1,50 @@ -import asyncio -import random +from __future__ import annotations -import socketio +import asyncio import logging +import random import sys import time -from typing import Dict, Set -from redis import asyncio as aioredis +from typing import Dict + 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, -) - +import socketio from open_webui.config import ( CORS_ALLOW_ORIGIN, ) - from open_webui.env import ( - VERSION, ENABLE_WEBSOCKET_SUPPORT, + GLOBAL_LOG_LEVEL, + REDIS_KEY_PREFIX, + VERSION, + WEBSOCKET_EVENT_CALLER_TIMEOUT, 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_REDIS_URL, + WEBSOCKET_SENTINEL_HOSTS, + WEBSOCKET_SENTINEL_PORT, WEBSOCKET_SERVER_ENGINEIO_LOGGING, - WEBSOCKET_EVENT_CALLER_TIMEOUT, + WEBSOCKET_SERVER_LOGGING, + WEBSOCKET_SERVER_PING_INTERVAL, + WEBSOCKET_SERVER_PING_TIMEOUT, ) -from open_webui.utils.auth import decode_token +from open_webui.models.access_grants import AccessGrants +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.models.users import UserNameResponse, Users 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, +from open_webui.utils.auth import decode_token +from open_webui.utils.redis import ( + build_sentinel_url, + get_redis_connection, + get_sentinels_from_env, ) +from redis import asyncio as aioredis logging.basicConfig(stream=sys.stdout, level=GLOBAL_LOG_LEVEL) log = logging.getLogger(__name__) @@ -63,20 +58,20 @@ REDIS = None 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) + sentinel_hosts = WEBSOCKET_SENTINEL_HOSTS or '' + ws_redis_url = ( + build_sentinel_url(WEBSOCKET_REDIS_URL, sentinel_hosts, WEBSOCKET_SENTINEL_PORT) + if sentinel_hosts + else WEBSOCKET_REDIS_URL + ) + redis_manager = socketio.AsyncRedisManager(ws_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, + client_manager=redis_manager, logger=WEBSOCKET_SERVER_LOGGING, ping_interval=WEBSOCKET_SERVER_PING_INTERVAL, ping_timeout=WEBSOCKET_SERVER_PING_TIMEOUT, @@ -104,32 +99,31 @@ SESSION_POOL_TIMEOUT = 120 # seconds without heartbeat before session is reaped if WEBSOCKET_MANAGER == 'redis': log.debug('Using Redis to manage websockets.') + ws_sentinels = get_sentinels_from_env(WEBSOCKET_SENTINEL_HOSTS, WEBSOCKET_SENTINEL_PORT) REDIS = get_redis_connection( redis_url=WEBSOCKET_REDIS_URL, - redis_sentinels=get_sentinels_from_env(WEBSOCKET_SENTINEL_HOSTS, WEBSOCKET_SENTINEL_PORT), + redis_sentinels=ws_sentinels, 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_sentinels=ws_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_sentinels=ws_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_sentinels=ws_sentinels, redis_cluster=WEBSOCKET_REDIS_CLUSTER, ) @@ -137,7 +131,7 @@ if WEBSOCKET_MANAGER == 'redis': 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_sentinels=ws_sentinels, redis_cluster=WEBSOCKET_REDIS_CLUSTER, ) aquire_func = clean_up_lock.aquire_lock @@ -148,7 +142,7 @@ if WEBSOCKET_MANAGER == 'redis': 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_sentinels=ws_sentinels, redis_cluster=WEBSOCKET_REDIS_CLUSTER, ) session_aquire_func = session_cleanup_lock.aquire_lock @@ -371,15 +365,15 @@ async def connect(sid, environ, auth): @sio.on('user-join') async def user_join(sid, data): - auth = data['auth'] if 'auth' in data else None + auth = data.get('auth') if not auth or 'token' not in auth: return - data = decode_token(auth['token']) - if data is None or 'id' not in data: + 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(data['id']) + user = await Users.get_user_by_id(token_data['id']) if not user: return @@ -811,7 +805,7 @@ async def yjs_awareness_update(sid, data): @sio.event -async def disconnect(sid): +async def disconnect(sid, reason=None): if sid in SESSION_POOL: user = SESSION_POOL[sid] del SESSION_POOL[sid] @@ -845,7 +839,7 @@ async def _make_channel_emitter(request_info): THROTTLE_INTERVAL = 0.15 # ~6 updates/sec async def _emit_channel_update(content: str, done: bool = False): - from open_webui.models.messages import Messages, MessageForm + from open_webui.models.messages import MessageForm, Messages update_form = MessageForm(content=content) if done: @@ -899,7 +893,7 @@ async def _make_channel_emitter(request_info): async def get_event_emitter(request_info, update_db=True): # Channel mode: route pipeline output to channel message updates - if request_info.get('chat_id', '').startswith('channel:'): + if (request_info.get('chat_id') or '').startswith('channel:'): return await _make_channel_emitter(request_info) async def __event_emitter__(event_data): @@ -917,7 +911,7 @@ async def get_event_emitter(request_info, update_db=True): room=f'user:{user_id}', ) - if update_db and message_id and not request_info.get('chat_id', '').startswith('local:'): + if update_db and message_id and not (request_info.get('chat_id') or '').startswith('local:'): event_type = event_data.get('type') if event_type == 'status': diff --git a/backend/open_webui/socket/utils.py b/backend/open_webui/socket/utils.py index 16b0cc3855..b337b08f40 100644 --- a/backend/open_webui/socket/utils.py +++ b/backend/open_webui/socket/utils.py @@ -1,12 +1,21 @@ +"""Redis-backed distributed data structures for WebSocket state management.""" + +from __future__ import annotations + +import hashlib import json import uuid + +import pycrdt as Y 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 + +YDOC_KEY_PREFIX = f'{REDIS_KEY_PREFIX}:ydoc:documents' class RedisLock: + """Distributed lock backed by a Redis SET with NX/EX semantics.""" + def __init__( self, redis_url, @@ -44,6 +53,10 @@ class RedisLock: class RedisDict: def __init__(self, name, redis_url, redis_sentinels=[], redis_cluster=False): self.name = name + # Per-process cache of the last payload fingerprint written by set(). + # Used to skip redundant HSET round-trips when the model list hasn't + # changed — the dominant Redis write source on busy multi-pod setups. + self._last_signature: str | None = None self.redis = get_redis_connection( redis_url, redis_sentinels, @@ -84,6 +97,18 @@ class RedisDict: def set(self, mapping: dict): if not mapping: self.redis.delete(self.name) + self._last_signature = None + return + + # Serialize values once — reused for both the fingerprint and the write. + serialized = {k: json.dumps(v) for k, v in mapping.items()} + + # Skip the write when the prepared mapping is identical to the last one + # this process wrote. The check is per-instance (not distributed), but + # still eliminates the majority of redundant writes because each pod + # typically produces the same model list on consecutive refreshes. + signature = hashlib.sha256(json.dumps(serialized, sort_keys=True).encode()).hexdigest() + if signature == self._last_signature: return # Fetch existing keys before writing so we know which ones to remove. @@ -95,10 +120,12 @@ class RedisDict: # 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()}) + self.redis.hset(self.name, mapping=serialized) if keys_to_remove: self.redis.hdel(self.name, *keys_to_remove) + self._last_signature = signature + def get(self, key, default=None): try: return self[key] @@ -107,6 +134,7 @@ class RedisDict: def clear(self): self.redis.delete(self.name) + self._last_signature = None def update(self, other=None, **kwargs): if other is not None: @@ -127,7 +155,7 @@ class YdocManager: def __init__( self, redis=None, - redis_key_prefix: str = f'{REDIS_KEY_PREFIX}:ydoc:documents', + redis_key_prefix: str = YDOC_KEY_PREFIX, ): self._updates = {} self._users = {} @@ -176,7 +204,7 @@ class YdocManager: ydoc.apply_update(bytes(update)) self._updates[document_id] = [ydoc.get_update()] + updates[mid:] - async def get_updates(self, document_id: str) -> List[bytes]: + async def get_updates(self, document_id: str) -> list[bytes]: document_id = document_id.replace(':', '_') if self._redis: @@ -195,7 +223,7 @@ class YdocManager: else: return document_id in self._updates - async def get_users(self, document_id: str) -> List[str]: + async def get_users(self, document_id: str) -> list[str]: document_id = document_id.replace(':', '_') if self._redis: @@ -211,6 +239,11 @@ class YdocManager: if self._redis: redis_key = f'{self._redis_key_prefix}:{document_id}:users' await self._redis.sadd(redis_key, user_id) + # Maintain a per-session reverse index so disconnect cleanup + # can look up only the documents this session joined, instead + # of issuing a cluster-wide SCAN over the entire keyspace. + session_key = f'{self._redis_key_prefix}:session:{user_id}:documents' + await self._redis.sadd(session_key, document_id) else: if document_id not in self._users: self._users[document_id] = set() @@ -222,22 +255,31 @@ class YdocManager: if self._redis: redis_key = f'{self._redis_key_prefix}:{document_id}:users' await self._redis.srem(redis_key, user_id) + # Keep the reverse index in sync. + session_key = f'{self._redis_key_prefix}:session:{user_id}:documents' + await self._redis.srem(session_key, document_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) + # Use the per-session reverse index instead of a cluster-wide + # SCAN. This set contains only the document IDs that this + # session actually joined, so the cost is proportional to + # the session's footprint — not the total number of documents. + session_key = f'{self._redis_key_prefix}:session:{user_id}:documents' + document_ids = await self._redis.smembers(session_key) - document_id = key.split(':')[-2] - if len(await self.get_users(document_id)) == 0: - await self.clear_document(document_id) + for document_id in document_ids: + users_key = f'{self._redis_key_prefix}:{document_id}:users' + await self._redis.srem(users_key, user_id) + + if len(await self.get_users(document_id)) == 0: + await self.clear_document(document_id) + + # Clean up the reverse index itself. + await self._redis.delete(session_key) else: for document_id in list(self._users.keys()): diff --git a/backend/open_webui/static/swagger-ui/swagger-ui-bundle.js b/backend/open_webui/static/swagger-ui/swagger-ui-bundle.js index 8656897872..c18e0dab76 100644 --- a/backend/open_webui/static/swagger-ui/swagger-ui-bundle.js +++ b/backend/open_webui/static/swagger-ui/swagger-ui-bundle.js @@ -9,163 +9,92 @@ : (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) => { + var s = { + 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; + a = o[1]; + return (3 * (i + a)) / 4 - a; }), (o.toByteArray = function toByteArray(s) { var o, i, - w = getLens(s), - x = w[0], - C = w[1], - j = new _( + _ = getLens(s), + w = _[0], + x = _[1], + C = new u( (function _byteLength(s, o, i) { return (3 * (o + i)) / 4 - i; - })(0, x, C) + })(0, w, x) ), - L = 0, - B = C > 0 ? x - 4 : x; - for (i = 0; i < B; i += 4) + j = 0, + L = x > 0 ? w - 4 : w; + for (i = 0; i < L; 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 && + (a[s.charCodeAt(i)] << 18) | + (a[s.charCodeAt(i + 1)] << 12) | + (a[s.charCodeAt(i + 2)] << 6) | + a[s.charCodeAt(i + 3)]), + (C[j++] = (o >> 16) & 255), + (C[j++] = (o >> 8) & 255), + (C[j++] = 255 & o)); + 2 === x && + ((o = (a[s.charCodeAt(i)] << 2) | (a[s.charCodeAt(i + 1)] >> 4)), + (C[j++] = 255 & o)); + 1 === x && ((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; + (a[s.charCodeAt(i)] << 10) | + (a[s.charCodeAt(i + 1)] << 4) | + (a[s.charCodeAt(i + 2)] >> 2)), + (C[j++] = (o >> 8) & 255), + (C[j++] = 255 & o)); + return C; }), (o.fromByteArray = function fromByteArray(s) { for ( - var o, u = s.length, _ = u % 3, w = [], x = 16383, C = 0, j = u - _; - C < j; - C += x + var o, a = s.length, u = a % 3, _ = [], w = 16383, x = 0, C = a - u; + x < C; + x += w ) - 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(''); + _.push(encodeChunk(s, x, x + w > C ? C : x + w)); + 1 === u + ? ((o = s[a - 1]), _.push(i[o >> 2] + i[(o << 4) & 63] + '==')) + : 2 === u && + ((o = (s[a - 2] << 8) + s[a - 1]), + _.push(i[o >> 10] + i[(o >> 4) & 63] + i[(o << 2) & 63] + '=')); + return _.join(''); })); for ( var i = [], - u = [], - _ = 'undefined' != typeof Uint8Array ? Uint8Array : Array, - w = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/', - x = 0; - x < 64; - ++x + a = [], + u = 'undefined' != typeof Uint8Array ? Uint8Array : Array, + _ = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/', + w = 0; + w < 64; + ++w ) - ((i[x] = w[x]), (u[w.charCodeAt(x)] = x)); + ((i[w] = _[w]), (a[_.charCodeAt(w)] = w)); 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(''); + function encodeChunk(s, o, a) { + for (var u, _, w = [], x = o; x < a; x += 3) + ((u = ((s[x] << 16) & 16711680) + ((s[x + 1] << 8) & 65280) + (255 & s[x + 2])), + w.push(i[((_ = u) >> 18) & 63] + i[(_ >> 12) & 63] + i[(_ >> 6) & 63] + i[63 & _])); + return w.join(''); } - ((u['-'.charCodeAt(0)] = 62), (u['_'.charCodeAt(0)] = 63)); + ((a['-'.charCodeAt(0)] = 62), (a['_'.charCodeAt(0)] = 63)); }, - 48287: (s, o, i) => { + 48287(s, o, i) { 'use strict'; - const u = i(67526), - _ = i(251), - w = + const a = i(67526), + u = i(251), + _ = 'function' == typeof Symbol && 'function' == typeof Symbol.for ? Symbol.for('nodejs.util.inspect.custom') : null; @@ -175,9 +104,9 @@ return Buffer.alloc(+s); }), (o.INSPECT_MAX_BYTES = 50)); - const x = 2147483647; + const w = 2147483647; function createBuffer(s) { - if (s > x) throw new RangeError('The value "' + s + '" is invalid for option "size"'); + if (s > w) throw new RangeError('The value "' + s + '" is invalid for option "size"'); const o = new Uint8Array(s); return (Object.setPrototypeOf(o, Buffer.prototype), o); } @@ -197,10 +126,10 @@ ('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; + let a = createBuffer(i); + const u = a.write(s, o); + u !== i && (a = a.slice(0, u)); + return a; })(s, o); if (ArrayBuffer.isView(s)) return (function fromArrayView(s) { @@ -226,9 +155,9 @@ 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) { + const a = s.valueOf && s.valueOf(); + if (null != a && a !== s) return Buffer.from(a, o, i); + const u = (function fromObject(s) { if (Buffer.isBuffer(s)) { const o = 0 | checked(s.length), i = createBuffer(o); @@ -240,7 +169,7 @@ : fromArrayLike(s); if ('Buffer' === s.type && Array.isArray(s.data)) return fromArrayLike(s.data); })(s); - if (_) return _; + if (u) return u; if ( 'undefined' != typeof Symbol && null != Symbol.toPrimitive && @@ -262,7 +191,7 @@ 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]; + for (let a = 0; a < o; a += 1) i[a] = 255 & s[a]; return i; } function fromArrayBuffer(s, o, i) { @@ -270,23 +199,23 @@ 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; + let a; return ( - (u = + (a = 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 + Object.setPrototypeOf(a, Buffer.prototype), + a ); } function checked(s) { - if (s >= x) + if (s >= w) throw new RangeError( 'Attempt to allocate Buffer larger than maximum size: 0x' + - x.toString(16) + + w.toString(16) + ' bytes' ); return 0 | s; @@ -300,9 +229,9 @@ typeof s ); const i = s.length, - u = arguments.length > 2 && !0 === arguments[2]; - if (!u && 0 === i) return 0; - let _ = !1; + a = arguments.length > 2 && !0 === arguments[2]; + if (!a && 0 === i) return 0; + let u = !1; for (;;) switch (o) { case 'ascii': @@ -322,12 +251,12 @@ case 'base64': return base64ToBytes(s).length; default: - if (_) return u ? -1 : utf8ToBytes(s).length; - ((o = ('' + o).toLowerCase()), (_ = !0)); + if (u) return a ? -1 : utf8ToBytes(s).length; + ((o = ('' + o).toLowerCase()), (u = !0)); } } function slowToString(s, o, i) { - let u = !1; + let a = !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 ''; @@ -351,98 +280,98 @@ case 'utf-16le': return utf16leSlice(this, o, i); default: - if (u) throw new TypeError('Unknown encoding: ' + s); - ((s = (s + '').toLowerCase()), (u = !0)); + if (a) throw new TypeError('Unknown encoding: ' + s); + ((s = (s + '').toLowerCase()), (a = !0)); } } function swap(s, o, i) { - const u = s[o]; - ((s[o] = s[i]), (s[i] = u)); + const a = s[o]; + ((s[o] = s[i]), (s[i] = a)); } - function bidirectionalIndexOf(s, o, i, u, _) { + function bidirectionalIndexOf(s, o, i, a, u) { if (0 === s.length) return -1; if ( ('string' == typeof i - ? ((u = i), (i = 0)) + ? ((a = i), (i = 0)) : i > 2147483647 ? (i = 2147483647) : i < -2147483648 && (i = -2147483648), - numberIsNaN((i = +i)) && (i = _ ? 0 : s.length - 1), + numberIsNaN((i = +i)) && (i = u ? 0 : s.length - 1), i < 0 && (i = s.length + i), i >= s.length) ) { - if (_) return -1; + if (u) return -1; i = s.length - 1; } else if (i < 0) { - if (!_) return -1; + if (!u) 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 (('string' == typeof o && (o = Buffer.from(o, a)), Buffer.isBuffer(o))) + return 0 === o.length ? -1 : arrayIndexOf(s, o, i, a, u); if ('number' == typeof o) return ( (o &= 255), 'function' == typeof Uint8Array.prototype.indexOf - ? _ + ? u ? Uint8Array.prototype.indexOf.call(s, o, i) : Uint8Array.prototype.lastIndexOf.call(s, o, i) - : arrayIndexOf(s, [o], i, u, _) + : arrayIndexOf(s, [o], i, a, 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; + function arrayIndexOf(s, o, i, a, u) { + let _, + w = 1, + x = s.length, + C = o.length; if ( - void 0 !== u && - ('ucs2' === (u = String(u).toLowerCase()) || - 'ucs-2' === u || - 'utf16le' === u || - 'utf-16le' === u) + void 0 !== a && + ('ucs2' === (a = String(a).toLowerCase()) || + 'ucs-2' === a || + 'utf16le' === a || + 'utf-16le' === a) ) { if (s.length < 2 || o.length < 2) return -1; - ((x = 2), (C /= 2), (j /= 2), (i /= 2)); + ((w = 2), (x /= 2), (C /= 2), (i /= 2)); } function read(s, o) { - return 1 === x ? s[o] : s.readUInt16BE(o * x); + return 1 === w ? s[o] : s.readUInt16BE(o * w); } - 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)); + if (u) { + let a = -1; + for (_ = i; _ < x; _++) + if (read(s, _) === read(o, -1 === a ? 0 : _ - a)) { + if ((-1 === a && (a = _), _ - a + 1 === C)) return a * w; + } else (-1 !== a && (_ -= _ - a), (a = -1)); } else - for (i + j > C && (i = C - j), w = i; w >= 0; w--) { + for (i + C > x && (i = x - C), _ = i; _ >= 0; _--) { let i = !0; - for (let u = 0; u < j; u++) - if (read(s, w + u) !== read(o, u)) { + for (let a = 0; a < C; a++) + if (read(s, _ + a) !== read(o, a)) { i = !1; break; } - if (i) return w; + if (i) return _; } return -1; } - function hexWrite(s, o, i, u) { + function hexWrite(s, o, i, a) { 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; + const u = s.length - i; + a ? (a = Number(a)) > u && (a = u) : (a = u); + const _ = o.length; + let w; + for (a > _ / 2 && (a = _ / 2), w = 0; w < a; ++w) { + const a = parseInt(o.substr(2 * w, 2), 16); + if (numberIsNaN(a)) return w; + s[i + w] = a; } - return x; + return w; } - function utf8Write(s, o, i, u) { - return blitBuffer(utf8ToBytes(o, s.length - i), s, i, u); + function utf8Write(s, o, i, a) { + return blitBuffer(utf8ToBytes(o, s.length - i), s, i, a); } - function asciiWrite(s, o, i, u) { + function asciiWrite(s, o, i, a) { return blitBuffer( (function asciiToBytes(s) { const o = []; @@ -451,83 +380,83 @@ })(o), s, i, - u + a ); } - function base64Write(s, o, i, u) { - return blitBuffer(base64ToBytes(o), s, i, u); + function base64Write(s, o, i, a) { + return blitBuffer(base64ToBytes(o), s, i, a); } - function ucs2Write(s, o, i, u) { + function ucs2Write(s, o, i, a) { 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; + let i, a, u; + const _ = []; + for (let w = 0; w < s.length && !((o -= 2) < 0); ++w) + ((i = s.charCodeAt(w)), (a = i >> 8), (u = i % 256), _.push(u), _.push(a)); + return _; })(o, s.length - i), s, i, - u + a ); } function base64Slice(s, o, i) { - return 0 === o && i === s.length ? u.fromByteArray(s) : u.fromByteArray(s.slice(o, i)); + return 0 === o && i === s.length ? a.fromByteArray(s) : a.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) { + const a = []; + let u = o; + for (; u < i; ) { + const o = s[u]; + let _ = null, + w = o > 239 ? 4 : o > 223 ? 3 : o > 191 ? 2 : 1; + if (u + w <= i) { + let i, a, x, C; + switch (w) { case 1: - o < 128 && (w = o); + o < 128 && (_ = o); break; case 2: - ((i = s[_ + 1]), - 128 == (192 & i) && ((j = ((31 & o) << 6) | (63 & i)), j > 127 && (w = j))); + ((i = s[u + 1]), + 128 == (192 & i) && ((C = ((31 & o) << 6) | (63 & i)), C > 127 && (_ = C))); break; case 3: - ((i = s[_ + 1]), - (u = s[_ + 2]), + ((i = s[u + 1]), + (a = s[u + 2]), 128 == (192 & i) && - 128 == (192 & u) && - ((j = ((15 & o) << 12) | ((63 & i) << 6) | (63 & u)), - j > 2047 && (j < 55296 || j > 57343) && (w = j))); + 128 == (192 & a) && + ((C = ((15 & o) << 12) | ((63 & i) << 6) | (63 & a)), + C > 2047 && (C < 55296 || C > 57343) && (_ = C))); break; case 4: - ((i = s[_ + 1]), - (u = s[_ + 2]), - (C = s[_ + 3]), + ((i = s[u + 1]), + (a = s[u + 2]), + (x = s[u + 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))); + 128 == (192 & a) && + 128 == (192 & x) && + ((C = ((15 & o) << 18) | ((63 & i) << 12) | ((63 & a) << 6) | (63 & x)), + C > 65535 && C < 1114112 && (_ = C))); } } - (null === w - ? ((w = 65533), (x = 1)) - : w > 65535 && - ((w -= 65536), u.push(((w >>> 10) & 1023) | 55296), (w = 56320 | (1023 & w))), - u.push(w), - (_ += x)); + (null === _ + ? ((_ = 65533), (w = 1)) + : _ > 65535 && + ((_ -= 65536), a.push(((_ >>> 10) & 1023) | 55296), (_ = 56320 | (1023 & _))), + a.push(_), + (u += w)); } return (function decodeCodePointsArray(s) { const o = s.length; - if (o <= C) return String.fromCharCode.apply(String, s); + if (o <= x) return String.fromCharCode.apply(String, s); let i = '', - u = 0; - for (; u < o; ) i += String.fromCharCode.apply(String, s.slice(u, (u += C))); + a = 0; + for (; a < o; ) i += String.fromCharCode.apply(String, s.slice(a, (a += x))); return i; - })(u); + })(a); } - ((o.kMaxLength = x), + ((o.kMaxLength = w), (Buffer.TYPED_ARRAY_SUPPORT = (function typedArraySupport() { try { const s = new Uint8Array(1), @@ -603,13 +532,13 @@ ); 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[_])); + a = o.length; + for (let u = 0, _ = Math.min(i, a); u < _; ++u) + if (s[u] !== o[u]) { + ((i = s[u]), (a = o[u])); break; } - return i < u ? -1 : u < i ? 1 : 0; + return i < a ? -1 : a < i ? 1 : 0; }), (Buffer.isEncoding = function isEncoding(s) { switch (String(s).toLowerCase()) { @@ -635,22 +564,22 @@ 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; + const a = Buffer.allocUnsafe(o); + let u = 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, _); + u + o.length > a.length + ? (Buffer.isBuffer(o) || (o = Buffer.from(o)), o.copy(a, u)) + : Uint8Array.prototype.set.call(a, o, u); else { if (!Buffer.isBuffer(o)) throw new TypeError('"list" argument must be an Array of Buffers'); - o.copy(u, _); + o.copy(a, u); } - _ += o.length; + u += o.length; } - return u; + return a; }), (Buffer.byteLength = byteLength), (Buffer.prototype._isBuffer = !0), @@ -700,8 +629,8 @@ '' ); }), - w && (Buffer.prototype[w] = Buffer.prototype.inspect), - (Buffer.prototype.compare = function compare(s, o, i, u, _) { + _ && (Buffer.prototype[_] = Buffer.prototype.inspect), + (Buffer.prototype.compare = function compare(s, o, i, a, u) { if ( (isInstance(s, Uint8Array) && (s = Buffer.from(s, s.offset, s.byteLength)), !Buffer.isBuffer(s)) @@ -713,26 +642,26 @@ 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) + void 0 === a && (a = 0), + void 0 === u && (u = this.length), + o < 0 || i > s.length || a < 0 || u > this.length) ) throw new RangeError('out of range index'); - if (u >= _ && o >= i) return 0; - if (u >= _) return -1; + if (a >= u && o >= i) return 0; + if (a >= 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])); + let _ = (u >>>= 0) - (a >>>= 0), + w = (i >>>= 0) - (o >>>= 0); + const x = Math.min(_, w), + C = this.slice(a, u), + j = s.slice(o, i); + for (let s = 0; s < x; ++s) + if (C[s] !== j[s]) { + ((_ = C[s]), (w = j[s])); break; } - return w < x ? -1 : x < w ? 1 : 0; + return _ < w ? -1 : w < _ ? 1 : 0; }), (Buffer.prototype.includes = function includes(s, o, i) { return -1 !== this.indexOf(s, o, i); @@ -743,9 +672,9 @@ (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)); + (Buffer.prototype.write = function write(s, o, i, a) { + if (void 0 === o) ((a = 'utf8'), (i = this.length), (o = 0)); + else if (void 0 === i && 'string' == typeof o) ((a = o), (i = this.length), (o = 0)); else { if (!isFinite(o)) throw new Error( @@ -753,19 +682,19 @@ ); ((o >>>= 0), isFinite(i) - ? ((i >>>= 0), void 0 === u && (u = 'utf8')) - : ((u = i), (i = void 0))); + ? ((i >>>= 0), void 0 === a && (a = 'utf8')) + : ((a = i), (i = void 0))); } - const _ = this.length - o; + const u = this.length - o; if ( - ((void 0 === i || i > _) && (i = _), + ((void 0 === i || i > u) && (i = u), (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; + a || (a = 'utf8'); + let _ = !1; for (;;) - switch (u) { + switch (a) { case 'hex': return hexWrite(this, s, o, i); case 'utf8': @@ -783,113 +712,113 @@ case 'utf-16le': return ucs2Write(this, s, o, i); default: - if (w) throw new TypeError('Unknown encoding: ' + u); - ((u = ('' + u).toLowerCase()), (w = !0)); + if (_) throw new TypeError('Unknown encoding: ' + a); + ((a = ('' + a).toLowerCase()), (_ = !0)); } }), (Buffer.prototype.toJSON = function toJSON() { return { type: 'Buffer', data: Array.prototype.slice.call(this._arr || this, 0) }; })); - const C = 4096; + const x = 4096; function asciiSlice(s, o, i) { - let u = ''; + let a = ''; i = Math.min(s.length, i); - for (let _ = o; _ < i; ++_) u += String.fromCharCode(127 & s[_]); - return u; + for (let u = o; u < i; ++u) a += String.fromCharCode(127 & s[u]); + return a; } function latin1Slice(s, o, i) { - let u = ''; + let a = ''; i = Math.min(s.length, i); - for (let _ = o; _ < i; ++_) u += String.fromCharCode(s[_]); - return u; + for (let u = o; u < i; ++u) a += String.fromCharCode(s[u]); + return a; } 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 _; + const a = s.length; + ((!o || o < 0) && (o = 0), (!i || i < 0 || i > a) && (i = a)); + let u = ''; + for (let a = o; a < i; ++a) u += L[s[a]]; + return u; } 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 _; + const a = s.slice(o, i); + let u = ''; + for (let s = 0; s < a.length - 1; s += 2) + u += String.fromCharCode(a[s] + 256 * a[s + 1]); + return u; } 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) { + function checkInt(s, o, i, a, u, _) { 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'); + if (o > u || o < _) throw new RangeError('"value" argument is out of bounds'); + if (i + a > 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)); + function wrtBigUInt64LE(s, o, i, a, u) { + checkIntBI(o, a, u, s, i, 7); + let _ = Number(o & BigInt(4294967295)); + ((s[i++] = _), + (_ >>= 8), + (s[i++] = _), + (_ >>= 8), + (s[i++] = _), + (_ >>= 8), + (s[i++] = _)); + let w = 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), + (s[i++] = w), + (w >>= 8), + (s[i++] = w), + (w >>= 8), + (s[i++] = w), + (w >>= 8), + (s[i++] = w), 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)); + function wrtBigUInt64BE(s, o, i, a, u) { + checkIntBI(o, a, u, s, i, 7); + let _ = Number(o & BigInt(4294967295)); + ((s[i + 7] = _), + (_ >>= 8), + (s[i + 6] = _), + (_ >>= 8), + (s[i + 5] = _), + (_ >>= 8), + (s[i + 4] = _)); + let w = 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), + (s[i + 3] = w), + (w >>= 8), + (s[i + 2] = w), + (w >>= 8), + (s[i + 1] = w), + (w >>= 8), + (s[i] = w), i + 8 ); } - function checkIEEE754(s, o, i, u, _, w) { - if (i + u > s.length) throw new RangeError('Index out of range'); + function checkIEEE754(s, o, i, a, u, _) { + if (i + a > 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) { + function writeFloat(s, o, i, a, _) { return ( (o = +o), (i >>>= 0), - w || checkIEEE754(s, 0, i, 4), - _.write(s, o, i, u, 23, 4), + _ || checkIEEE754(s, 0, i, 4), + u.write(s, o, i, a, 23, 4), i + 4 ); } - function writeDouble(s, o, i, u, w) { + function writeDouble(s, o, i, a, _) { return ( (o = +o), (i >>>= 0), - w || checkIEEE754(s, 0, i, 8), - _.write(s, o, i, u, 52, 8), + _ || checkIEEE754(s, 0, i, 8), + u.write(s, o, i, a, 52, 8), i + 8 ); } @@ -898,25 +827,25 @@ ((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); + const a = this.subarray(s, o); + return (Object.setPrototypeOf(a, Buffer.prototype), a); }), (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; + let a = this[s], + u = 1, + _ = 0; + for (; ++_ < o && (u *= 256); ) a += this[s + _] * u; + return a; }), (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; + let a = this[s + --o], + u = 1; + for (; o > 0 && (u *= 256); ) a += this[s + --o] * u; + return a; }), (Buffer.prototype.readUint8 = Buffer.prototype.readUInt8 = function readUInt8(s, o) { @@ -959,34 +888,34 @@ 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)); + const a = o + 256 * this[++s] + 65536 * this[++s] + this[++s] * 2 ** 24, + u = this[++s] + 256 * this[++s] + 65536 * this[++s] + i * 2 ** 24; + return BigInt(a) + (BigInt(u) << 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(_); + const a = o * 2 ** 24 + 65536 * this[++s] + 256 * this[++s] + this[++s], + u = this[++s] * 2 ** 24 + 65536 * this[++s] + 256 * this[++s] + i; + return (BigInt(a) << BigInt(32)) + BigInt(u); })), (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); + let a = this[s], + u = 1, + _ = 0; + for (; ++_ < o && (u *= 256); ) a += this[s + _] * u; + return ((u *= 128), a >= u && (a -= Math.pow(2, 8 * o)), a); }), (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); + let a = o, + u = 1, + _ = this[s + --a]; + for (; a > 0 && (u *= 256); ) _ += this[s + --a] * u; + return ((u *= 128), _ >= u && (_ -= Math.pow(2, 8 * o)), _); }), (Buffer.prototype.readInt8 = function readInt8(s, o) { return ( @@ -1024,9 +953,9 @@ 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); + const a = this[s + 4] + 256 * this[s + 5] + 65536 * this[s + 6] + (i << 24); return ( - (BigInt(u) << BigInt(32)) + + (BigInt(a) << BigInt(32)) + BigInt(o + 256 * this[++s] + 65536 * this[++s] + this[++s] * 2 ** 24) ); })), @@ -1035,42 +964,42 @@ 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]; + const a = (o << 24) + 65536 * this[++s] + 256 * this[++s] + this[++s]; return ( - (BigInt(u) << BigInt(32)) + + (BigInt(a) << 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)); + return ((s >>>= 0), o || checkOffset(s, 4, this.length), u.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)); + return ((s >>>= 0), o || checkOffset(s, 4, this.length), u.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)); + return ((s >>>= 0), o || checkOffset(s, 8, this.length), u.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)); + return ((s >>>= 0), o || checkOffset(s, 8, this.length), u.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)) { + function writeUIntLE(s, o, i, a) { + if (((s = +s), (o >>>= 0), (i >>>= 0), !a)) { 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; + let u = 1, + _ = 0; + for (this[o] = 255 & s; ++_ < i && (u *= 256); ) this[o + _] = (s / u) & 255; return o + i; }), (Buffer.prototype.writeUintBE = Buffer.prototype.writeUIntBE = - function writeUIntBE(s, o, i, u) { - if (((s = +s), (o >>>= 0), (i >>>= 0), !u)) { + function writeUIntBE(s, o, i, a) { + if (((s = +s), (o >>>= 0), (i >>>= 0), !a)) { 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; + let u = i - 1, + _ = 1; + for (this[o + u] = 255 & s; --u >= 0 && (_ *= 256); ) this[o + u] = (s / _) & 255; return o + i; }), (Buffer.prototype.writeUint8 = Buffer.prototype.writeUInt8 = @@ -1143,30 +1072,30 @@ ) { 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); + (Buffer.prototype.writeIntLE = function writeIntLE(s, o, i, a) { + if (((s = +s), (o >>>= 0), !a)) { + const a = Math.pow(2, 8 * i - 1); + checkInt(this, s, o, i, a - 1, -a); } - 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)); + let u = 0, + _ = 1, + w = 0; + for (this[o] = 255 & s; ++u < i && (_ *= 256); ) + (s < 0 && 0 === w && 0 !== this[o + u - 1] && (w = 1), + (this[o + u] = (((s / _) | 0) - w) & 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); + (Buffer.prototype.writeIntBE = function writeIntBE(s, o, i, a) { + if (((s = +s), (o >>>= 0), !a)) { + const a = Math.pow(2, 8 * i - 1); + checkInt(this, s, o, i, a - 1, -a); } - 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)); + let u = i - 1, + _ = 1, + w = 0; + for (this[o + u] = 255 & s; --u >= 0 && (_ *= 256); ) + (s < 0 && 0 === w && 0 !== this[o + u + 1] && (w = 1), + (this[o + u] = (((s / _) | 0) - w) & 255)); return o + i; }), (Buffer.prototype.writeInt8 = function writeInt8(s, o, i) { @@ -1260,70 +1189,70 @@ (Buffer.prototype.writeDoubleBE = function writeDoubleBE(s, o, i) { return writeDouble(this, s, o, !1, i); }), - (Buffer.prototype.copy = function copy(s, o, i, u) { + (Buffer.prototype.copy = function copy(s, o, i, a) { if (!Buffer.isBuffer(s)) throw new TypeError('argument should be a Buffer'); if ( (i || (i = 0), - u || 0 === u || (u = this.length), + a || 0 === a || (a = this.length), o >= s.length && (o = s.length), o || (o = 0), - u > 0 && u < i && (u = i), - u === i) + a > 0 && a < i && (a = i), + a === 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; + if (a < 0) throw new RangeError('sourceEnd out of bounds'); + (a > this.length && (a = this.length), + s.length - o < a - i && (a = s.length - o + i)); + const u = a - i; return ( this === s && 'function' == typeof Uint8Array.prototype.copyWithin - ? this.copyWithin(o, i, u) - : Uint8Array.prototype.set.call(s, this.subarray(i, u), o), - _ + ? this.copyWithin(o, i, a) + : Uint8Array.prototype.set.call(s, this.subarray(i, a), o), + u ); }), - (Buffer.prototype.fill = function fill(s, o, i, u) { + (Buffer.prototype.fill = function fill(s, o, i, a) { 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) + ? ((a = o), (o = 0), (i = this.length)) + : 'string' == typeof i && ((a = i), (i = this.length)), + void 0 !== a && 'string' != typeof a) ) throw new TypeError('encoding must be a string'); - if ('string' == typeof u && !Buffer.isEncoding(u)) - throw new TypeError('Unknown encoding: ' + u); + if ('string' == typeof a && !Buffer.isEncoding(a)) + throw new TypeError('Unknown encoding: ' + a); if (1 === s.length) { const o = s.charCodeAt(0); - (('utf8' === u && o < 128) || 'latin1' === u) && (s = o); + (('utf8' === a && o < 128) || 'latin1' === a) && (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 _; + let u; if ( ((o >>>= 0), (i = void 0 === i ? this.length : i >>> 0), s || (s = 0), 'number' == typeof s) ) - for (_ = o; _ < i; ++_) this[_] = s; + for (u = o; u < i; ++u) this[u] = s; else { - const w = Buffer.isBuffer(s) ? s : Buffer.from(s, u), - x = w.length; - if (0 === x) + const _ = Buffer.isBuffer(s) ? s : Buffer.from(s, a), + w = _.length; + if (0 === w) throw new TypeError('The value "' + s + '" is invalid for argument "value"'); - for (_ = 0; _ < i - o; ++_) this[_ + o] = w[_ % x]; + for (u = 0; u < i - o; ++u) this[u + o] = _[u % w]; } return this; })); - const j = {}; + const C = {}; function E(s, o, i) { - j[s] = class NodeError extends i { + C[s] = class NodeError extends i { constructor() { (super(), Object.defineProperty(this, 'message', { @@ -1354,37 +1283,37 @@ 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}`; + const a = '-' === s[0] ? 1 : 0; + for (; i >= a + 4; i -= 3) o = `_${s.slice(i - 3, i)}${o}`; return `${s.slice(0, i)}${o}`; } - function checkIntBI(s, o, i, u, _, w) { + function checkIntBI(s, o, i, a, u, _) { if (s > i || s < o) { - const u = 'bigint' == typeof o ? 'n' : ''; - let _; + const a = 'bigint' == typeof o ? 'n' : ''; + let u; throw ( - (_ = - w > 3 + (u = + _ > 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) + ? `>= 0${a} and < 2${a} ** ${8 * (_ + 1)}${a}` + : `>= -(2${a} ** ${8 * (_ + 1) - 1}${a}) and < 2 ** ${8 * (_ + 1) - 1}${a}` + : `>= ${o}${a} and <= ${i}${a}`), + new C.ERR_OUT_OF_RANGE('value', u, 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); + })(a, u, _); } function validateNumber(s, o) { - if ('number' != typeof s) throw new j.ERR_INVALID_ARG_TYPE(o, 'number', s); + if ('number' != typeof s) throw new C.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); + throw (validateNumber(s, i), new C.ERR_OUT_OF_RANGE(i || 'offset', 'an integer', s)); + if (o < 0) throw new C.ERR_BUFFER_OUT_OF_BOUNDS(); + throw new C.ERR_OUT_OF_RANGE(i || 'offset', `>= ${i ? 1 : 0} and <= ${o}`, s); } (E( 'ERR_BUFFER_OUT_OF_BOUNDS', @@ -1405,62 +1334,62 @@ E( 'ERR_OUT_OF_RANGE', function (s, o, i) { - let u = `The value of "${s}" is out of range.`, - _ = i; + let a = `The value of "${s}" is out of range.`, + u = i; return ( Number.isInteger(i) && Math.abs(i) > 2 ** 32 - ? (_ = addNumericalSeparator(String(i))) + ? (u = addNumericalSeparator(String(i))) : 'bigint' == typeof i && - ((_ = String(i)), + ((u = String(i)), (i > BigInt(2) ** BigInt(32) || i < -(BigInt(2) ** BigInt(32))) && - (_ = addNumericalSeparator(_)), - (_ += 'n')), - (u += ` It must be ${o}. Received ${_}`), - u + (u = addNumericalSeparator(u)), + (u += 'n')), + (a += ` It must be ${o}. Received ${u}`), + a ); }, RangeError )); - const L = /[^+/0-9A-Za-z-_]/g; + const j = /[^+/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 (!_) { + const a = s.length; + let u = null; + const _ = []; + for (let w = 0; w < a; ++w) { + if (((i = s.charCodeAt(w)), i > 55295 && i < 57344)) { + if (!u) { if (i > 56319) { - (o -= 3) > -1 && w.push(239, 191, 189); + (o -= 3) > -1 && _.push(239, 191, 189); continue; } - if (x + 1 === u) { - (o -= 3) > -1 && w.push(239, 191, 189); + if (w + 1 === a) { + (o -= 3) > -1 && _.push(239, 191, 189); continue; } - _ = i; + u = i; continue; } if (i < 56320) { - ((o -= 3) > -1 && w.push(239, 191, 189), (_ = i)); + ((o -= 3) > -1 && _.push(239, 191, 189), (u = i)); continue; } - i = 65536 + (((_ - 55296) << 10) | (i - 56320)); - } else _ && (o -= 3) > -1 && w.push(239, 191, 189); - if (((_ = null), i < 128)) { + i = 65536 + (((u - 55296) << 10) | (i - 56320)); + } else u && (o -= 3) > -1 && _.push(239, 191, 189); + if (((u = null), i < 128)) { if ((o -= 1) < 0) break; - w.push(i); + _.push(i); } else if (i < 2048) { if ((o -= 2) < 0) break; - w.push((i >> 6) | 192, (63 & i) | 128); + _.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); + _.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( + _.push( (i >> 18) | 240, ((i >> 12) & 63) | 128, ((i >> 6) & 63) | 128, @@ -1468,21 +1397,21 @@ ); } } - return w; + return _; } function base64ToBytes(s) { - return u.toByteArray( + return a.toByteArray( (function base64clean(s) { - if ((s = (s = s.split('=')[0]).trim().replace(L, '')).length < 2) return ''; + if ((s = (s = s.split('=')[0]).trim().replace(j, '')).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 blitBuffer(s, o, i, a) { + let u; + for (u = 0; u < a && !(u + i >= o.length || u >= s.length); ++u) o[u + i] = s[u]; + return u; } function isInstance(s, o) { return ( @@ -1496,12 +1425,12 @@ function numberIsNaN(s) { return s != s; } - const B = (function () { + const L = (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[_]; + const a = 16 * i; + for (let u = 0; u < 16; ++u) o[a + u] = s[i] + s[u]; } return o; })(); @@ -1512,97 +1441,160 @@ throw new Error('BigInt not supported'); } }, - 17965: (s, o, i) => { + 13144(s, o, i) { 'use strict'; - var u = i(16426), - _ = { 'text/plain': 'Text', 'text/html': 'Url', default: 'Text' }; + var a = i(66743), + u = i(11002), + _ = i(10076), + w = i(47119); + s.exports = w || a.call(_, u); + }, + 12205(s, o, i) { + 'use strict'; + var a = i(66743), + u = i(11002), + _ = i(13144); + s.exports = function applyBind() { + return _(a, u, arguments); + }; + }, + 11002(s) { + 'use strict'; + s.exports = Function.prototype.apply; + }, + 10076(s) { + 'use strict'; + s.exports = Function.prototype.call; + }, + 73126(s, o, i) { + 'use strict'; + var a = i(66743), + u = i(69675), + _ = i(10076), + w = i(13144); + s.exports = function callBindBasic(s) { + if (s.length < 1 || 'function' != typeof s[0]) throw new u('a function is required'); + return w(a, _, s); + }; + }, + 47119(s) { + 'use strict'; + s.exports = 'undefined' != typeof Reflect && Reflect && Reflect.apply; + }, + 10487(s, o, i) { + 'use strict'; + var a = i(96897), + u = i(30655), + _ = i(73126), + w = i(12205); + ((s.exports = function callBind(s) { + var o = _(arguments), + i = s.length - (arguments.length - 1); + return a(o, 1 + (i > 0 ? i : 0), !0); + }), + u ? u(s.exports, 'apply', { value: w }) : (s.exports.apply = w)); + }, + 36556(s, o, i) { + 'use strict'; + var a = i(70453), + u = i(73126), + _ = u([a('%String.prototype.indexOf%')]); + s.exports = function callBoundIntrinsic(s, o) { + var i = a(s, !!o); + return 'function' == typeof i && _(s, '.prototype.') > -1 ? u([i]) : i; + }; + }, + 17965(s, o, i) { + 'use strict'; + var a = i(16426), + u = { 'text/plain': 'Text', 'text/html': 'Url', default: 'Text' }; s.exports = function copy(s, o) { var i, + _, w, x, C, j, - L, - B = !1; + L = !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)) { + ((w = a()), + (x = document.createRange()), + (C = document.getSelection()), + ((j = document.createElement('span')).textContent = s), + (j.ariaHidden = 'true'), + (j.style.all = 'unset'), + (j.style.position = 'fixed'), + (j.style.top = 0), + (j.style.clip = 'rect(0, 0, 0, 0)'), + (j.style.whiteSpace = 'pre'), + (j.style.webkitUserSelect = 'text'), + (j.style.MozUserSelect = 'text'), + (j.style.msUserSelect = 'text'), + (j.style.userSelect = 'text'), + j.addEventListener('copy', function (a) { + if ((a.stopPropagation(), o.format)) + if ((a.preventDefault(), void 0 === a.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)); + var _ = u[o.format] || u.default; + window.clipboardData.setData(_, s); + } else (a.clipboardData.clearData(), a.clipboardData.setData(o.format, s)); + o.onCopy && (a.preventDefault(), o.onCopy(a.clipboardData)); }), - document.body.appendChild(L), - C.selectNodeContents(L), - j.addRange(C), + document.body.appendChild(j), + x.selectNodeContents(j), + C.addRange(x), !document.execCommand('copy')) ) throw new Error('copy command was unsuccessful'); - B = !0; - } catch (u) { - (i && console.error('unable to copy using execCommand: ', u), + L = !0; + } catch (a) { + (i && console.error('unable to copy using execCommand: ', a), 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), + (L = !0)); + } catch (a) { + (i && console.error('unable to copy using clipboardData: ', a), i && console.error('falling back to prompt'), - (w = (function format(s) { + (_ = (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)); + window.prompt(_, s)); } } finally { - (j && ('function' == typeof j.removeRange ? j.removeRange(C) : j.removeAllRanges()), - L && document.body.removeChild(L), - x()); + (C && ('function' == typeof C.removeRange ? C.removeRange(x) : C.removeAllRanges()), + j && document.body.removeChild(j), + w()); } - return B; + return L; }; }, - 2205: function (s, o, i) { - var u; - ((u = void 0 !== i.g ? i.g : this), + 2205(s, o, i) { + var a; + ((a = 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; + var o, i = String(s), a = i.length, u = -1, _ = '', w = i.charCodeAt(0); + ++u < a; ) - 0 != (o = i.charCodeAt(_)) - ? (w += + 0 != (o = i.charCodeAt(u)) + ? (_ += (o >= 1 && o <= 31) || 127 == o || - (0 == _ && o >= 48 && o <= 57) || - (1 == _ && o >= 48 && o <= 57 && 45 == x) + (0 == u && o >= 48 && o <= 57) || + (1 == u && o >= 48 && o <= 57 && 45 == w) ? '\\' + o.toString(16) + ' ' - : (0 == _ && 1 == u && 45 == o) || + : (0 == u && 1 == a && 45 == o) || !( o >= 128 || 45 == o || @@ -1611,23 +1603,23 @@ (o >= 65 && o <= 90) || (o >= 97 && o <= 122) ) - ? '\\' + i.charAt(_) - : i.charAt(_)) - : (w += '�'); - return w; + ? '\\' + i.charAt(u) + : i.charAt(u)) + : (_ += '�'); + return _; }; return (s.CSS || (s.CSS = {}), (s.CSS.escape = cssEscape), cssEscape); - })(u))); + })(a))); }, - 81919: (s, o, i) => { + 81919(s, o, i) { 'use strict'; - var u = i(48287).Buffer; + var a = i(48287).Buffer; function isSpecificValue(s) { - return s instanceof u || s instanceof Date || s instanceof RegExp; + return s instanceof a || 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); + if (s instanceof a) { + var o = a.alloc ? a.alloc(s.length) : new a(s.length); return (s.copy(o), o); } if (s instanceof Date) return new Date(s.getTime()); @@ -1643,7 +1635,7 @@ ? (o[i] = deepCloneArray(s)) : isSpecificValue(s) ? (o[i] = cloneSpecificValue(s)) - : (o[i] = _({}, s)) + : (o[i] = u({}, s)) : (o[i] = s); }), o @@ -1652,31 +1644,31 @@ function safeGetProperty(s, o) { return '__proto__' === o ? void 0 : s[o]; } - var _ = (s.exports = function () { + var u = (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) { + Array.prototype.slice.call(arguments, 1).forEach(function (a) { + 'object' != typeof a || + null === a || + Array.isArray(a) || + Object.keys(a).forEach(function (_) { return ( - (o = safeGetProperty(i, w)), - (s = safeGetProperty(u, w)) === i + (o = safeGetProperty(i, _)), + (s = safeGetProperty(a, _)) === i ? void 0 : 'object' != typeof s || null === s - ? void (i[w] = s) + ? void (i[_] = s) : Array.isArray(s) - ? void (i[w] = deepCloneArray(s)) + ? void (i[_] = deepCloneArray(s)) : isSpecificValue(s) - ? void (i[w] = cloneSpecificValue(s)) + ? void (i[_] = cloneSpecificValue(s)) : 'object' != typeof o || null === o || Array.isArray(o) - ? void (i[w] = _({}, s)) - : void (i[w] = _(o, s)) + ? void (i[_] = u({}, s)) + : void (i[_] = u(o, s)) ); }); }), @@ -1684,7 +1676,7 @@ ); }); }, - 14744: (s) => { + 14744(s) { 'use strict'; var o = function isMergeableObject(s) { return ( @@ -1739,40 +1731,40 @@ } } function mergeObject(s, o, i) { - var u = {}; + var a = {}; return ( i.isMergeableObject(s) && getKeys(s).forEach(function (o) { - u[o] = cloneUnlessOtherwiseSpecified(s[o], i); + a[o] = cloneUnlessOtherwiseSpecified(s[o], i); }), - getKeys(o).forEach(function (_) { + getKeys(o).forEach(function (u) { (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) { + })(s, u) || + (propertyIsOnObject(s, u) && i.isMergeableObject(o[u]) + ? (a[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, i)(s[u], o[u], i)) + : (a[u] = cloneUnlessOtherwiseSpecified(o[u], i))); }), - u + a ); } - 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); + function deepmerge(s, i, a) { + (((a = a || {}).arrayMerge = a.arrayMerge || defaultArrayMerge), + (a.isMergeableObject = a.isMergeableObject || o), + (a.cloneUnlessOtherwiseSpecified = cloneUnlessOtherwiseSpecified)); + var u = Array.isArray(i); + return u === Array.isArray(s) + ? u + ? a.arrayMerge(s, i, a) + : mergeObject(s, i, a) + : cloneUnlessOtherwiseSpecified(i, a); } deepmerge.all = function deepmergeAll(s, o) { if (!Array.isArray(s)) throw new Error('first argument should be an array'); @@ -1780,1395 +1772,50 @@ return deepmerge(s, i, o); }, {}); }; - var u = deepmerge; - s.exports = u; + var a = deepmerge; + s.exports = a; }, - 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); - }; + 30041(s, o, i) { + 'use strict'; + var a = i(30655), + u = i(58068), + _ = i(69675), + w = i(75795); + s.exports = function defineDataProperty(s, o, i) { + if (!s || ('object' != typeof s && 'function' != typeof s)) + throw new _('`obj` must be an object or a function`'); + if ('string' != typeof o && 'symbol' != typeof o) + throw new _('`property` must be a string or a symbol`'); + if (arguments.length > 3 && 'boolean' != typeof arguments[3] && null !== arguments[3]) + throw new _('`nonEnumerable`, if provided, must be a boolean or null'); + if (arguments.length > 4 && 'boolean' != typeof arguments[4] && null !== arguments[4]) + throw new _('`nonWritable`, if provided, must be a boolean or null'); + if (arguments.length > 5 && 'boolean' != typeof arguments[5] && null !== arguments[5]) + throw new _('`nonConfigurable`, if provided, must be a boolean or null'); + if (arguments.length > 6 && 'boolean' != typeof arguments[6]) + throw new _('`loose`, if provided, must be a boolean'); + var x = arguments.length > 3 ? arguments[3] : null, + C = arguments.length > 4 ? arguments[4] : null, + j = arguments.length > 5 ? arguments[5] : null, + L = arguments.length > 6 && arguments[6], + B = !!w && w(s, o); + if (a) + a(s, o, { + configurable: null === j && B ? B.configurable : !j, + enumerable: null === x && B ? B.enumerable : !x, + value: i, + writable: null === C && B ? B.writable : !C + }); + else { + if (!L && (x || C || j)) + throw new u( + 'This environment does not support defining a property as non-configurable, non-writable, or non-enumerable.' + ); + s[o] = i; } - 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) => { + 78004(s) { 'use strict'; class SubRange { constructor(s, o) { @@ -3242,9 +1889,9 @@ _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++); + var a = Math.max(this.ranges[o].low, s.low), + u = Math.min(this.ranges[o].high, s.high); + (i.push(new SubRange(a, u)), o++); } }; return ( @@ -3283,11 +1930,76 @@ } s.exports = DRange; }, - 37007: (s) => { + 7176(s, o, i) { + 'use strict'; + var a, + u = i(73126), + _ = i(75795); + try { + a = [].__proto__ === Array.prototype; + } catch (s) { + if (!s || 'object' != typeof s || !('code' in s) || 'ERR_PROTO_ACCESS' !== s.code) + throw s; + } + var w = !!a && _ && _(Object.prototype, '__proto__'), + x = Object, + C = x.getPrototypeOf; + s.exports = + w && 'function' == typeof w.get + ? u([w.get]) + : 'function' == typeof C && + function getDunder(s) { + return C(null == s ? s : x(s)); + }; + }, + 30655(s) { + 'use strict'; + var o = Object.defineProperty || !1; + if (o) + try { + o({}, 'a', { value: 1 }); + } catch (s) { + o = !1; + } + s.exports = o; + }, + 41237(s) { + 'use strict'; + s.exports = EvalError; + }, + 69383(s) { + 'use strict'; + s.exports = Error; + }, + 79290(s) { + 'use strict'; + s.exports = RangeError; + }, + 79538(s) { + 'use strict'; + s.exports = ReferenceError; + }, + 58068(s) { + 'use strict'; + s.exports = SyntaxError; + }, + 69675(s) { + 'use strict'; + s.exports = TypeError; + }, + 35345(s) { + 'use strict'; + s.exports = URIError; + }, + 79612(s) { + 'use strict'; + s.exports = Object; + }, + 37007(s) { 'use strict'; var o, i = 'object' == typeof Reflect ? Reflect : null, - u = + a = i && 'function' == typeof i.apply ? i.apply : function ReflectApply(s, o, i) { @@ -3303,7 +2015,7 @@ : function ReflectOwnKeys(s) { return Object.getOwnPropertyNames(s); }; - var _ = + var u = Number.isNaN || function NumberIsNaN(s) { return s != s; @@ -3313,9 +2025,9 @@ } ((s.exports = EventEmitter), (s.exports.once = function once(s, o) { - return new Promise(function (i, u) { + return new Promise(function (i, a) { function errorListener(i) { - (s.removeListener(o, resolver), u(i)); + (s.removeListener(o, resolver), a(i)); } function resolver() { ('function' == typeof s.removeListener && @@ -3333,7 +2045,7 @@ (EventEmitter.prototype._events = void 0), (EventEmitter.prototype._eventsCount = 0), (EventEmitter.prototype._maxListeners = void 0)); - var w = 10; + var _ = 10; function checkListener(s) { if ('function' != typeof s) throw new TypeError( @@ -3343,41 +2055,41 @@ function _getMaxListeners(s) { return void 0 === s._maxListeners ? EventEmitter.defaultMaxListeners : s._maxListeners; } - function _addListener(s, o, i, u) { - var _, w, x; + function _addListener(s, o, i, a) { + var u, _, w; 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) + void 0 === (_ = s._events) + ? ((_ = s._events = Object.create(null)), (s._eventsCount = 0)) + : (void 0 !== _.newListener && + (s.emit('newListener', o, i.listener ? i.listener : i), (_ = s._events)), + (w = _[o])), + void 0 === w) ) - ((x = w[o] = i), ++s._eventsCount); + ((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) + ('function' == typeof w + ? (w = _[o] = a ? [i, w] : [w, i]) + : a + ? w.unshift(i) + : w.push(i), + (u = _getMaxListeners(s)) > 0 && w.length > u && !w.warned) ) { - x.warned = !0; - var C = new Error( + w.warned = !0; + var x = new Error( 'Possible EventEmitter memory leak detected. ' + - x.length + + w.length + ' ' + String(o) + ' listeners added. Use emitter.setMaxListeners() to increase limit' ); - ((C.name = 'MaxListenersExceededWarning'), - (C.emitter = s), - (C.type = o), - (C.count = x.length), + ((x.name = 'MaxListenersExceededWarning'), + (x.emitter = s), + (x.type = o), + (x.count = w.length), (function ProcessEmitWarning(s) { console && console.warn && console.warn(s); - })(C)); + })(x)); } return s; } @@ -3392,27 +2104,27 @@ ); } 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 = _), _); + var a = { fired: !1, wrapFn: void 0, target: s, type: o, listener: i }, + u = onceWrapper.bind(a); + return ((u.listener = i), (a.wrapFn = u), u); } function _listeners(s, o, i) { - var u = s._events; - if (void 0 === u) return []; - var _ = u[o]; - return void 0 === _ + var a = s._events; + if (void 0 === a) return []; + var u = a[o]; + return void 0 === u ? [] - : 'function' == typeof _ + : 'function' == typeof u ? i - ? [_.listener || _] - : [_] + ? [u.listener || u] + : [u] : 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); + })(u) + : arrayClone(u, u.length); } function listenerCount(s) { var o = this._events; @@ -3424,34 +2136,34 @@ return 0; } function arrayClone(s, o) { - for (var i = new Array(o), u = 0; u < o; ++u) i[u] = s[u]; + for (var i = new Array(o), a = 0; a < o; ++a) i[a] = s[a]; return i; } - function eventTargetAgnosticAddListener(s, o, i, u) { - if ('function' == typeof s.on) u.once ? s.once(o, i) : s.on(o, i); + function eventTargetAgnosticAddListener(s, o, i, a) { + if ('function' == typeof s.on) a.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(_)); + s.addEventListener(o, function wrapListener(u) { + (a.once && s.removeEventListener(o, wrapListener), i(u)); }); } } (Object.defineProperty(EventEmitter, 'defaultMaxListeners', { enumerable: !0, get: function () { - return w; + return _; }, set: function (s) { - if ('number' != typeof s || s < 0 || _(s)) + if ('number' != typeof s || s < 0 || u(s)) throw new RangeError( 'The value of "defaultMaxListeners" is out of range. It must be a non-negative number. Received ' + s + '.' ); - w = s; + _ = s; } }), (EventEmitter.init = function () { @@ -3460,7 +2172,7 @@ (this._maxListeners = this._maxListeners || void 0)); }), (EventEmitter.prototype.setMaxListeners = function setMaxListeners(s) { - if ('number' != typeof s || s < 0 || _(s)) + if ('number' != typeof s || s < 0 || u(s)) throw new RangeError( 'The value of "n" is out of range. It must be a non-negative number. Received ' + s + @@ -3473,23 +2185,23 @@ }), (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 u = 'error' === s, + _ = this._events; + if (void 0 !== _) u = u && void 0 === _.error; + else if (!u) return !1; + if (u) { + var w; + if ((o.length > 0 && (w = o[0]), w instanceof Error)) throw w; + var x = new Error('Unhandled error.' + (w ? ' (' + w.message + ')' : '')); + throw ((x.context = w), x); } - var j = w[s]; - if (void 0 === j) return !1; - if ('function' == typeof j) u(j, this, o); + var C = _[s]; + if (void 0 === C) return !1; + if ('function' == typeof C) a(C, this, o); else { - var L = j.length, - B = arrayClone(j, L); - for (i = 0; i < L; ++i) u(B[i], this, o); + var j = C.length, + L = arrayClone(C, j); + for (i = 0; i < j; ++i) a(L[i], this, o); } return !0; }), @@ -3507,35 +2219,35 @@ 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; + var i, a, u, _, w; + if ((checkListener(o), void 0 === (a = this._events))) return this; + if (void 0 === (i = a[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)); + : (delete a[s], + a.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)); + for (u = -1, _ = i.length - 1; _ >= 0; _--) + if (i[_] === o || i[_].listener === o) { + ((w = i[_].listener), (u = _)); break; } - if (_ < 0) return this; - (0 === _ + if (u < 0) return this; + (0 === u ? 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)); + })(i, u), + 1 === i.length && (a[s] = i[0]), + void 0 !== a.removeListener && this.emit('removeListener', s, w || o)); } return this; }), (EventEmitter.prototype.off = EventEmitter.prototype.removeListener), (EventEmitter.prototype.removeAllListeners = function removeAllListeners(s) { - var o, i, u; + var o, i, a; if (void 0 === (i = this._events)) return this; if (void 0 === i.removeListener) return ( @@ -3548,10 +2260,10 @@ this ); if (0 === arguments.length) { - var _, - w = Object.keys(i); - for (u = 0; u < w.length; ++u) - 'removeListener' !== (_ = w[u]) && this.removeAllListeners(_); + var u, + _ = Object.keys(i); + for (a = 0; a < _.length; ++a) + 'removeListener' !== (u = _[a]) && this.removeAllListeners(u); return ( this.removeAllListeners('removeListener'), (this._events = Object.create(null)), @@ -3561,7 +2273,7 @@ } 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]); + for (a = o.length - 1; a >= 0; a--) this.removeListener(s, o[a]); return this; }), (EventEmitter.prototype.listeners = function listeners(s) { @@ -3580,98 +2292,125 @@ return this._eventsCount > 0 ? o(this._events) : []; })); }, - 85587: (s, o, i) => { + 85587(s, o, i) { 'use strict'; - var u = i(26311), - _ = create(Error); + var a = i(26311), + u = 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)); + return (o && (o = a.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)); + ((s.exports = u), + (u.eval = create(EvalError)), + (u.range = create(RangeError)), + (u.reference = create(ReferenceError)), + (u.syntax = create(SyntaxError)), + (u.type = create(TypeError)), + (u.uri = create(URIError)), + (u.create = create)); }, - 26311: (s) => { + 82682(s, o, i) { + 'use strict'; + var a = i(69600), + u = Object.prototype.toString, + _ = Object.prototype.hasOwnProperty; + s.exports = function forEach(s, o, i) { + if (!a(o)) throw new TypeError('iterator must be a function'); + var w; + (arguments.length >= 3 && (w = i), + (function isArray(s) { + return '[object Array]' === u.call(s); + })(s) + ? (function forEachArray(s, o, i) { + for (var a = 0, u = s.length; a < u; a++) + _.call(s, a) && (null == i ? o(s[a], a, s) : o.call(i, s[a], a, s)); + })(s, o, w) + : 'string' == typeof s + ? (function forEachString(s, o, i) { + for (var a = 0, u = s.length; a < u; a++) + null == i ? o(s.charAt(a), a, s) : o.call(i, s.charAt(a), a, s); + })(s, o, w) + : (function forEachObject(s, o, i) { + for (var a in s) + _.call(s, a) && (null == i ? o(s[a], a, s) : o.call(i, s[a], a, s)); + })(s, o, w)); + }; + }, + 26311(s) { !(function () { var o; function format(s) { for ( var o, i, + a, u, - _, - w = 1, - x = [].slice.call(arguments), - C = 0, - j = s.length, - L = '', + _ = 1, + w = [].slice.call(arguments), + x = 0, + C = s.length, + j = '', + L = !1, B = !1, - $ = !1, nextArg = function () { - return x[w++]; + return w[_++]; }, slurpNumber = function () { - for (var i = ''; /\d/.test(s[C]); ) ((i += s[C++]), (o = s[C])); + for (var i = ''; /\d/.test(s[x]); ) ((i += s[x++]), (o = s[x])); return i.length > 0 ? parseInt(i) : null; }; - C < j; - ++C + x < C; + ++x ) - if (((o = s[C]), B)) + if (((o = s[x]), L)) switch ( - ((B = !1), + ((L = !1), '.' == o - ? (($ = !1), (o = s[++C])) - : '0' == o && '.' == s[C + 1] - ? (($ = !0), (o = s[(C += 2)])) - : ($ = !0), - (_ = slurpNumber()), + ? ((B = !1), (o = s[++x])) + : '0' == o && '.' == s[x + 1] + ? ((B = !0), (o = s[(x += 2)])) + : (B = !0), + (u = slurpNumber()), o) ) { case 'b': - L += parseInt(nextArg(), 10).toString(2); + j += parseInt(nextArg(), 10).toString(2); break; case 'c': - L += + j += 'string' == typeof (i = nextArg()) || i instanceof String ? i : String.fromCharCode(parseInt(i, 10)); break; case 'd': - L += parseInt(nextArg(), 10); + j += parseInt(nextArg(), 10); break; case 'f': - ((u = String(parseFloat(nextArg()).toFixed(_ || 6))), - (L += $ ? u : u.replace(/^0/, ''))); + ((a = String(parseFloat(nextArg()).toFixed(u || 6))), + (j += B ? a : a.replace(/^0/, ''))); break; case 'j': - L += JSON.stringify(nextArg()); + j += JSON.stringify(nextArg()); break; case 'o': - L += '0' + parseInt(nextArg(), 10).toString(8); + j += '0' + parseInt(nextArg(), 10).toString(8); break; case 's': - L += nextArg(); + j += nextArg(); break; case 'x': - L += '0x' + parseInt(nextArg(), 10).toString(16); + j += '0x' + parseInt(nextArg(), 10).toString(16); break; case 'X': - L += '0x' + parseInt(nextArg(), 10).toString(16).toUpperCase(); + j += '0x' + parseInt(nextArg(), 10).toString(16).toUpperCase(); break; default: - L += o; + j += o; } - else '%' === o ? (B = !0) : (L += o); - return L; + else '%' === o ? (L = !0) : (j += o); + return j; } (((o = s.exports = format).format = format), (o.vsprintf = function vsprintf(s, o) { @@ -3684,7 +2423,479 @@ })); })(); }, - 45981: (s) => { + 89353(s) { + 'use strict'; + var o = Object.prototype.toString, + i = Math.max, + a = function concatty(s, o) { + for (var i = [], a = 0; a < s.length; a += 1) i[a] = s[a]; + for (var u = 0; u < o.length; u += 1) i[u + s.length] = o[u]; + return i; + }; + s.exports = function bind(s) { + var u = this; + if ('function' != typeof u || '[object Function]' !== o.apply(u)) + throw new TypeError('Function.prototype.bind called on incompatible ' + u); + for ( + var _, + w = (function slicy(s, o) { + for (var i = [], a = o || 0, u = 0; a < s.length; a += 1, u += 1) i[u] = s[a]; + return i; + })(arguments, 1), + x = i(0, u.length - w.length), + C = [], + j = 0; + j < x; + j++ + ) + C[j] = '$' + j; + if ( + ((_ = Function( + 'binder', + 'return function (' + + (function (s, o) { + for (var i = '', a = 0; a < s.length; a += 1) + ((i += s[a]), a + 1 < s.length && (i += o)); + return i; + })(C, ',') + + '){ return binder.apply(this,arguments); }' + )(function () { + if (this instanceof _) { + var o = u.apply(this, a(w, arguments)); + return Object(o) === o ? o : this; + } + return u.apply(s, a(w, arguments)); + })), + u.prototype) + ) { + var L = function Empty() {}; + ((L.prototype = u.prototype), (_.prototype = new L()), (L.prototype = null)); + } + return _; + }; + }, + 66743(s, o, i) { + 'use strict'; + var a = i(89353); + s.exports = Function.prototype.bind || a; + }, + 70453(s, o, i) { + 'use strict'; + var a, + u = i(79612), + _ = i(69383), + w = i(41237), + x = i(79290), + C = i(79538), + j = i(58068), + L = i(69675), + B = i(35345), + $ = i(71514), + U = i(58968), + V = i(6188), + z = i(68002), + Y = i(75880), + Z = i(70414), + ee = i(73093), + ie = Function, + getEvalledConstructor = function (s) { + try { + return ie('"use strict"; return (' + s + ').constructor;')(); + } catch (s) {} + }, + ae = i(75795), + ce = i(30655), + throwTypeError = function () { + throw new L(); + }, + le = ae + ? (function () { + try { + return throwTypeError; + } catch (s) { + try { + return ae(arguments, 'callee').get; + } catch (s) { + return throwTypeError; + } + } + })() + : throwTypeError, + pe = i(64039)(), + de = i(93628), + fe = i(71064), + ye = i(48648), + be = i(11002), + Se = i(10076), + _e = {}, + we = 'undefined' != typeof Uint8Array && de ? de(Uint8Array) : a, + xe = { + __proto__: null, + '%AggregateError%': 'undefined' == typeof AggregateError ? a : AggregateError, + '%Array%': Array, + '%ArrayBuffer%': 'undefined' == typeof ArrayBuffer ? a : ArrayBuffer, + '%ArrayIteratorPrototype%': pe && de ? de([][Symbol.iterator]()) : a, + '%AsyncFromSyncIteratorPrototype%': a, + '%AsyncFunction%': _e, + '%AsyncGenerator%': _e, + '%AsyncGeneratorFunction%': _e, + '%AsyncIteratorPrototype%': _e, + '%Atomics%': 'undefined' == typeof Atomics ? a : Atomics, + '%BigInt%': 'undefined' == typeof BigInt ? a : BigInt, + '%BigInt64Array%': 'undefined' == typeof BigInt64Array ? a : BigInt64Array, + '%BigUint64Array%': 'undefined' == typeof BigUint64Array ? a : BigUint64Array, + '%Boolean%': Boolean, + '%DataView%': 'undefined' == typeof DataView ? a : DataView, + '%Date%': Date, + '%decodeURI%': decodeURI, + '%decodeURIComponent%': decodeURIComponent, + '%encodeURI%': encodeURI, + '%encodeURIComponent%': encodeURIComponent, + '%Error%': _, + '%eval%': eval, + '%EvalError%': w, + '%Float32Array%': 'undefined' == typeof Float32Array ? a : Float32Array, + '%Float64Array%': 'undefined' == typeof Float64Array ? a : Float64Array, + '%FinalizationRegistry%': + 'undefined' == typeof FinalizationRegistry ? a : FinalizationRegistry, + '%Function%': ie, + '%GeneratorFunction%': _e, + '%Int8Array%': 'undefined' == typeof Int8Array ? a : Int8Array, + '%Int16Array%': 'undefined' == typeof Int16Array ? a : Int16Array, + '%Int32Array%': 'undefined' == typeof Int32Array ? a : Int32Array, + '%isFinite%': isFinite, + '%isNaN%': isNaN, + '%IteratorPrototype%': pe && de ? de(de([][Symbol.iterator]())) : a, + '%JSON%': 'object' == typeof JSON ? JSON : a, + '%Map%': 'undefined' == typeof Map ? a : Map, + '%MapIteratorPrototype%': + 'undefined' != typeof Map && pe && de ? de(new Map()[Symbol.iterator]()) : a, + '%Math%': Math, + '%Number%': Number, + '%Object%': u, + '%Object.getOwnPropertyDescriptor%': ae, + '%parseFloat%': parseFloat, + '%parseInt%': parseInt, + '%Promise%': 'undefined' == typeof Promise ? a : Promise, + '%Proxy%': 'undefined' == typeof Proxy ? a : Proxy, + '%RangeError%': x, + '%ReferenceError%': C, + '%Reflect%': 'undefined' == typeof Reflect ? a : Reflect, + '%RegExp%': RegExp, + '%Set%': 'undefined' == typeof Set ? a : Set, + '%SetIteratorPrototype%': + 'undefined' != typeof Set && pe && de ? de(new Set()[Symbol.iterator]()) : a, + '%SharedArrayBuffer%': + 'undefined' == typeof SharedArrayBuffer ? a : SharedArrayBuffer, + '%String%': String, + '%StringIteratorPrototype%': pe && de ? de(''[Symbol.iterator]()) : a, + '%Symbol%': pe ? Symbol : a, + '%SyntaxError%': j, + '%ThrowTypeError%': le, + '%TypedArray%': we, + '%TypeError%': L, + '%Uint8Array%': 'undefined' == typeof Uint8Array ? a : Uint8Array, + '%Uint8ClampedArray%': + 'undefined' == typeof Uint8ClampedArray ? a : Uint8ClampedArray, + '%Uint16Array%': 'undefined' == typeof Uint16Array ? a : Uint16Array, + '%Uint32Array%': 'undefined' == typeof Uint32Array ? a : Uint32Array, + '%URIError%': B, + '%WeakMap%': 'undefined' == typeof WeakMap ? a : WeakMap, + '%WeakRef%': 'undefined' == typeof WeakRef ? a : WeakRef, + '%WeakSet%': 'undefined' == typeof WeakSet ? a : WeakSet, + '%Function.prototype.call%': Se, + '%Function.prototype.apply%': be, + '%Object.defineProperty%': ce, + '%Object.getPrototypeOf%': fe, + '%Math.abs%': $, + '%Math.floor%': U, + '%Math.max%': V, + '%Math.min%': z, + '%Math.pow%': Y, + '%Math.round%': Z, + '%Math.sign%': ee, + '%Reflect.getPrototypeOf%': ye + }; + if (de) + try { + null.error; + } catch (s) { + var Pe = de(de(s)); + xe['%Error.prototype%'] = Pe; + } + var Te = function doEval(s) { + var o; + if ('%AsyncFunction%' === s) o = getEvalledConstructor('async function () {}'); + else if ('%GeneratorFunction%' === s) o = getEvalledConstructor('function* () {}'); + else if ('%AsyncGeneratorFunction%' === s) + o = getEvalledConstructor('async function* () {}'); + else if ('%AsyncGenerator%' === s) { + var i = doEval('%AsyncGeneratorFunction%'); + i && (o = i.prototype); + } else if ('%AsyncIteratorPrototype%' === s) { + var a = doEval('%AsyncGenerator%'); + a && de && (o = de(a.prototype)); + } + return ((xe[s] = o), o); + }, + Re = { + __proto__: null, + '%ArrayBufferPrototype%': ['ArrayBuffer', 'prototype'], + '%ArrayPrototype%': ['Array', 'prototype'], + '%ArrayProto_entries%': ['Array', 'prototype', 'entries'], + '%ArrayProto_forEach%': ['Array', 'prototype', 'forEach'], + '%ArrayProto_keys%': ['Array', 'prototype', 'keys'], + '%ArrayProto_values%': ['Array', 'prototype', 'values'], + '%AsyncFunctionPrototype%': ['AsyncFunction', 'prototype'], + '%AsyncGenerator%': ['AsyncGeneratorFunction', 'prototype'], + '%AsyncGeneratorPrototype%': ['AsyncGeneratorFunction', 'prototype', 'prototype'], + '%BooleanPrototype%': ['Boolean', 'prototype'], + '%DataViewPrototype%': ['DataView', 'prototype'], + '%DatePrototype%': ['Date', 'prototype'], + '%ErrorPrototype%': ['Error', 'prototype'], + '%EvalErrorPrototype%': ['EvalError', 'prototype'], + '%Float32ArrayPrototype%': ['Float32Array', 'prototype'], + '%Float64ArrayPrototype%': ['Float64Array', 'prototype'], + '%FunctionPrototype%': ['Function', 'prototype'], + '%Generator%': ['GeneratorFunction', 'prototype'], + '%GeneratorPrototype%': ['GeneratorFunction', 'prototype', 'prototype'], + '%Int8ArrayPrototype%': ['Int8Array', 'prototype'], + '%Int16ArrayPrototype%': ['Int16Array', 'prototype'], + '%Int32ArrayPrototype%': ['Int32Array', 'prototype'], + '%JSONParse%': ['JSON', 'parse'], + '%JSONStringify%': ['JSON', 'stringify'], + '%MapPrototype%': ['Map', 'prototype'], + '%NumberPrototype%': ['Number', 'prototype'], + '%ObjectPrototype%': ['Object', 'prototype'], + '%ObjProto_toString%': ['Object', 'prototype', 'toString'], + '%ObjProto_valueOf%': ['Object', 'prototype', 'valueOf'], + '%PromisePrototype%': ['Promise', 'prototype'], + '%PromiseProto_then%': ['Promise', 'prototype', 'then'], + '%Promise_all%': ['Promise', 'all'], + '%Promise_reject%': ['Promise', 'reject'], + '%Promise_resolve%': ['Promise', 'resolve'], + '%RangeErrorPrototype%': ['RangeError', 'prototype'], + '%ReferenceErrorPrototype%': ['ReferenceError', 'prototype'], + '%RegExpPrototype%': ['RegExp', 'prototype'], + '%SetPrototype%': ['Set', 'prototype'], + '%SharedArrayBufferPrototype%': ['SharedArrayBuffer', 'prototype'], + '%StringPrototype%': ['String', 'prototype'], + '%SymbolPrototype%': ['Symbol', 'prototype'], + '%SyntaxErrorPrototype%': ['SyntaxError', 'prototype'], + '%TypedArrayPrototype%': ['TypedArray', 'prototype'], + '%TypeErrorPrototype%': ['TypeError', 'prototype'], + '%Uint8ArrayPrototype%': ['Uint8Array', 'prototype'], + '%Uint8ClampedArrayPrototype%': ['Uint8ClampedArray', 'prototype'], + '%Uint16ArrayPrototype%': ['Uint16Array', 'prototype'], + '%Uint32ArrayPrototype%': ['Uint32Array', 'prototype'], + '%URIErrorPrototype%': ['URIError', 'prototype'], + '%WeakMapPrototype%': ['WeakMap', 'prototype'], + '%WeakSetPrototype%': ['WeakSet', 'prototype'] + }, + $e = i(66743), + qe = i(9957), + ze = $e.call(Se, Array.prototype.concat), + We = $e.call(be, Array.prototype.splice), + He = $e.call(Se, String.prototype.replace), + Ye = $e.call(Se, String.prototype.slice), + Xe = $e.call(Se, RegExp.prototype.exec), + Qe = + /[^%.[\]]+|\[(?:(-?\d+(?:\.\d+)?)|(["'])((?:(?!\2)[^\\]|\\.)*?)\2)\]|(?=(?:\.|\[\])(?:\.|\[\]|%$))/g, + et = /\\(\\)?/g, + tt = function getBaseIntrinsic(s, o) { + var i, + a = s; + if ((qe(Re, a) && (a = '%' + (i = Re[a])[0] + '%'), qe(xe, a))) { + var u = xe[a]; + if ((u === _e && (u = Te(a)), void 0 === u && !o)) + throw new L( + 'intrinsic ' + s + ' exists, but is not available. Please file an issue!' + ); + return { alias: i, name: a, value: u }; + } + throw new j('intrinsic ' + s + ' does not exist!'); + }; + s.exports = function GetIntrinsic(s, o) { + if ('string' != typeof s || 0 === s.length) + throw new L('intrinsic name must be a non-empty string'); + if (arguments.length > 1 && 'boolean' != typeof o) + throw new L('"allowMissing" argument must be a boolean'); + if (null === Xe(/^%?[^%]*%?$/, s)) + throw new j( + '`%` may not be present anywhere but at the beginning and end of the intrinsic name' + ); + var i = (function stringToPath(s) { + var o = Ye(s, 0, 1), + i = Ye(s, -1); + if ('%' === o && '%' !== i) + throw new j('invalid intrinsic syntax, expected closing `%`'); + if ('%' === i && '%' !== o) + throw new j('invalid intrinsic syntax, expected opening `%`'); + var a = []; + return ( + He(s, Qe, function (s, o, i, u) { + a[a.length] = i ? He(u, et, '$1') : o || s; + }), + a + ); + })(s), + a = i.length > 0 ? i[0] : '', + u = tt('%' + a + '%', o), + _ = u.name, + w = u.value, + x = !1, + C = u.alias; + C && ((a = C[0]), We(i, ze([0, 1], C))); + for (var B = 1, $ = !0; B < i.length; B += 1) { + var U = i[B], + V = Ye(U, 0, 1), + z = Ye(U, -1); + if ( + ('"' === V || "'" === V || '`' === V || '"' === z || "'" === z || '`' === z) && + V !== z + ) + throw new j('property names with quotes must have matching quotes'); + if ( + (('constructor' !== U && $) || (x = !0), qe(xe, (_ = '%' + (a += '.' + U) + '%'))) + ) + w = xe[_]; + else if (null != w) { + if (!(U in w)) { + if (!o) + throw new L( + 'base intrinsic for ' + s + ' exists, but the property is not available.' + ); + return; + } + if (ae && B + 1 >= i.length) { + var Y = ae(w, U); + w = ($ = !!Y) && 'get' in Y && !('originalValue' in Y.get) ? Y.get : w[U]; + } else (($ = qe(w, U)), (w = w[U])); + $ && !x && (xe[_] = w); + } + } + return w; + }; + }, + 71064(s, o, i) { + 'use strict'; + var a = i(79612); + s.exports = a.getPrototypeOf || null; + }, + 48648(s) { + 'use strict'; + s.exports = ('undefined' != typeof Reflect && Reflect.getPrototypeOf) || null; + }, + 93628(s, o, i) { + 'use strict'; + var a = i(48648), + u = i(71064), + _ = i(7176); + s.exports = a + ? function getProto(s) { + return a(s); + } + : u + ? function getProto(s) { + if (!s || ('object' != typeof s && 'function' != typeof s)) + throw new TypeError('getProto: not an object'); + return u(s); + } + : _ + ? function getProto(s) { + return _(s); + } + : null; + }, + 6549(s) { + 'use strict'; + s.exports = Object.getOwnPropertyDescriptor; + }, + 75795(s, o, i) { + 'use strict'; + var a = i(6549); + if (a) + try { + a([], 'length'); + } catch (s) { + a = null; + } + s.exports = a; + }, + 30592(s, o, i) { + 'use strict'; + var a = i(30655), + u = function hasPropertyDescriptors() { + return !!a; + }; + ((u.hasArrayLengthDefineBug = function hasArrayLengthDefineBug() { + if (!a) return null; + try { + return 1 !== a([], 'length', { value: 1 }).length; + } catch (s) { + return !0; + } + }), + (s.exports = u)); + }, + 64039(s, o, i) { + 'use strict'; + var a = 'undefined' != typeof Symbol && Symbol, + u = i(41333); + s.exports = function hasNativeSymbols() { + return ( + 'function' == typeof a && + 'function' == typeof Symbol && + 'symbol' == typeof a('foo') && + 'symbol' == typeof Symbol('bar') && + u() + ); + }; + }, + 41333(s) { + 'use strict'; + s.exports = function hasSymbols() { + if ('function' != typeof Symbol || 'function' != typeof Object.getOwnPropertySymbols) + return !1; + if ('symbol' == typeof Symbol.iterator) return !0; + var s = {}, + o = Symbol('test'), + i = Object(o); + if ('string' == typeof o) return !1; + if ('[object Symbol]' !== Object.prototype.toString.call(o)) return !1; + if ('[object Symbol]' !== Object.prototype.toString.call(i)) return !1; + for (var a in ((s[o] = 42), s)) return !1; + if ('function' == typeof Object.keys && 0 !== Object.keys(s).length) return !1; + if ( + 'function' == typeof Object.getOwnPropertyNames && + 0 !== Object.getOwnPropertyNames(s).length + ) + return !1; + var u = Object.getOwnPropertySymbols(s); + if (1 !== u.length || u[0] !== o) return !1; + if (!Object.prototype.propertyIsEnumerable.call(s, o)) return !1; + if ('function' == typeof Object.getOwnPropertyDescriptor) { + var _ = Object.getOwnPropertyDescriptor(s, o); + if (42 !== _.value || !0 !== _.enumerable) return !1; + } + return !0; + }; + }, + 49092(s, o, i) { + 'use strict'; + var a = i(41333); + s.exports = function hasToStringTagShams() { + return a() && !!Symbol.toStringTag; + }; + }, + 9957(s, o, i) { + 'use strict'; + var a = Function.prototype.call, + u = Object.prototype.hasOwnProperty, + _ = i(66743); + s.exports = _.call(a, u); + }, + 45981(s) { function deepFreeze(s) { return ( s instanceof Map @@ -3835,45 +3046,45 @@ 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 = { + const a = /\[(?:[^\\\]]|\\.)*\]|\(\??|\\([1-9][0-9]*)|\\./; + const u = '[a-zA-Z]\\w*', + _ = '[a-zA-Z_]\\w*', + w = '\\b\\d+(\\.\\d+)?', + x = '(-?)(\\b0[xX][a-fA-F0-9]+|(\\b\\d+(\\.\\d*)?|\\.\\d+)([eE][-+]?\\d+)?)', + C = '\\b(0b[01]+)', + j = { begin: '\\\\[\\s\\S]', relevance: 0 }, + L = { className: 'string', begin: "'", end: "'", illegal: '\\n', contains: [j] }, + B = { className: 'string', begin: '"', end: '"', illegal: '\\n', contains: [j] }, + $ = { 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); + const a = inherit({ className: 'comment', begin: s, end: o, contains: [] }, i); return ( - u.contains.push(V), - u.contains.push({ + a.contains.push($), + a.contains.push({ className: 'doctag', begin: '(?:TODO|FIXME|NOTE|BUG|OPTIMIZE|HACK|XXX):', relevance: 0 }), - u + a ); }, U = COMMENT('//', '$'), - z = COMMENT('/\\*', '\\*/'), - Y = COMMENT('#', '$'), + V = COMMENT('/\\*', '\\*/'), + z = COMMENT('#', '$'), + Y = { className: 'number', begin: w, relevance: 0 }, Z = { className: 'number', begin: x, relevance: 0 }, ee = { className: 'number', begin: C, relevance: 0 }, - ie = { className: 'number', begin: j, relevance: 0 }, - ae = { + ie = { className: 'number', begin: - x + + w + '(%|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 = { + ae = { begin: /(?=\/[^/\n]*\/)/, contains: [ { @@ -3881,21 +3092,21 @@ begin: /\//, end: /\/[gimuy]*/, illegal: /\n/, - contains: [L, { begin: /\[/, end: /\]/, relevance: 0, contains: [L] }] + contains: [j, { begin: /\[/, end: /\]/, relevance: 0, contains: [j] }] } ] }, - ce = { className: 'title', begin: _, relevance: 0 }, - pe = { className: 'title', begin: w, relevance: 0 }, - de = { begin: '\\.\\s*' + w, relevance: 0 }; - var fe = Object.freeze({ + ce = { className: 'title', begin: u, relevance: 0 }, + le = { className: 'title', begin: _, relevance: 0 }, + pe = { begin: '\\.\\s*' + _, relevance: 0 }; + var de = 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, + IDENT_RE: u, + UNDERSCORE_IDENT_RE: _, + NUMBER_RE: w, + C_NUMBER_RE: x, + BINARY_NUMBER_RE: C, RE_STARTERS_RE: '!|!=|!==|%|%=|&|&&|&=|\\*|\\*=|\\+|\\+=|,|-|-=|/=|/|:|;|<<|<<=|<=|<|===|==|=|>>>=|>>=|>=|>>>|>>|>|\\?|\\[|\\{|\\(|\\^|\\^=|\\||\\|=|\\|\\||~', SHEBANG: (s = {}) => { @@ -3919,22 +3130,22 @@ ) ); }, - BACKSLASH_ESCAPE: L, - APOS_STRING_MODE: B, - QUOTE_STRING_MODE: $, - PHRASAL_WORDS_MODE: V, + BACKSLASH_ESCAPE: j, + APOS_STRING_MODE: L, + QUOTE_STRING_MODE: B, + PHRASAL_WORDS_MODE: $, 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, + C_BLOCK_COMMENT_MODE: V, + HASH_COMMENT_MODE: z, + NUMBER_MODE: Y, + C_NUMBER_MODE: Z, + BINARY_NUMBER_MODE: ee, + CSS_NUMBER_MODE: ie, + REGEXP_MODE: ae, TITLE_MODE: ce, - UNDERSCORE_TITLE_MODE: pe, - METHOD_GUARD: de, + UNDERSCORE_TITLE_MODE: le, + METHOD_GUARD: pe, END_SAME_AS_BEGIN: function (s) { return Object.assign(s, { 'on:begin': (s, o) => { @@ -3973,7 +3184,7 @@ function compileRelevance(s, o) { void 0 === s.relevance && (s.relevance = 1); } - const ye = [ + const fe = [ 'of', 'and', 'for', @@ -3987,22 +3198,22 @@ 'value' ]; function compileKeywords(s, o, i = 'keyword') { - const u = {}; + const a = {}; 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)); + Object.assign(a, compileKeywords(s[i], o, i)); }), - u + a ); 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])]; + a[i[0]] = [s, scoreForKeyword(i[0], i[1])]; })); } } @@ -4010,7 +3221,7 @@ return o ? Number(o) : (function commonKeyword(s) { - return ye.includes(s.toLowerCase()); + return fe.includes(s.toLowerCase()); })(s) ? 0 : 1; @@ -4045,21 +3256,21 @@ .map((s) => { i += 1; const o = i; - let _ = source(s), - w = ''; - for (; _.length > 0; ) { - const s = u.exec(_); + let u = source(s), + _ = ''; + for (; u.length > 0; ) { + const s = a.exec(u); if (!s) { - w += _; + _ += u; break; } - ((w += _.substring(0, s.index)), - (_ = _.substring(s.index + s[0].length)), + ((_ += u.substring(0, s.index)), + (u = u.substring(s.index + s[0].length)), '\\' === s[0][0] && s[1] - ? (w += '\\' + String(Number(s[1]) + o)) - : ((w += s[0]), '(' === s[0] && i++)); + ? (_ += '\\' + String(Number(s[1]) + o)) + : ((_ += s[0]), '(' === s[0] && i++)); } - return w; + return _; }) .map((s) => `(${s})`) .join(o); @@ -4073,8 +3284,8 @@ 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)); + a = this.matchIndexes[i]; + return (o.splice(0, i), Object.assign(o, a)); } } class ResumableMultiRegex { @@ -4132,37 +3343,37 @@ return ( (s.classNameAliases = inherit(s.classNameAliases || {})), (function compileMode(o, i) { - const u = o; - if (o.isCompiled) return u; + const a = o; + if (o.isCompiled) return a; ([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; + let u = null; if ( ('object' == typeof o.keywords && - ((_ = o.keywords.$pattern), delete o.keywords.$pattern), + ((u = o.keywords.$pattern), delete o.keywords.$pattern), o.keywords && (o.keywords = compileKeywords(o.keywords, s.case_insensitive)), - o.lexemes && _) + o.lexemes && u) ) 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)), + (u = u || o.lexemes || /\w+/), + (a.keywordPatternRe = langRe(u, !0)), i && (o.begin || (o.begin = /\B|\b/), - (u.beginRe = langRe(o.begin)), + (a.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.end && (a.endRe = langRe(o.end)), + (a.terminatorEnd = source(o.end) || ''), o.endsWithParent && i.terminatorEnd && - (u.terminatorEnd += (o.end ? '|' : '') + i.terminatorEnd)), - o.illegal && (u.illegalRe = langRe(o.illegal)), + (a.terminatorEnd += (o.end ? '|' : '') + i.terminatorEnd)), + o.illegal && (a.illegalRe = langRe(o.illegal)), o.contains || (o.contains = []), (o.contains = [].concat( ...o.contains.map(function (s) { @@ -4181,10 +3392,10 @@ }) )), o.contains.forEach(function (s) { - compileMode(s, u); + compileMode(s, a); }), o.starts && compileMode(o.starts, i), - (u.matcher = (function buildModeRegex(s) { + (a.matcher = (function buildModeRegex(s) { const o = new ResumableMultiRegex(); return ( s.contains.forEach((s) => o.addRule(s.begin, { rule: s, type: 'begin' })), @@ -4192,8 +3403,8 @@ s.illegal && o.addRule(s.illegal, { type: 'illegal' }), o ); - })(u)), - u + })(a)), + a ); })(s) ); @@ -4254,16 +3465,16 @@ } }; } - const be = { + const ye = { 'after:highlightElement': ({ el: s, result: o, text: i }) => { - const u = nodeStream(s); - if (!u.length) return; - const _ = document.createElement('div'); - ((_.innerHTML = o.value), + const a = nodeStream(s); + if (!a.length) return; + const u = document.createElement('div'); + ((u.innerHTML = o.value), (o.value = (function mergeStreams(s, o, i) { - let u = 0, - _ = ''; - const w = []; + let a = 0, + u = ''; + const _ = []; function selectStream() { return s.length && o.length ? s[0].offset !== o[0].offset @@ -4281,10 +3492,10 @@ function attributeString(s) { return ' ' + s.nodeName + '="' + escapeHTML(s.value) + '"'; } - _ += '<' + tag(s) + [].map.call(s.attributes, attributeString).join('') + '>'; + u += '<' + tag(s) + [].map.call(s.attributes, attributeString).join('') + '>'; } function close(s) { - _ += ''; + u += ''; } function render(s) { ('start' === s.event ? open : close)(s.node); @@ -4292,19 +3503,19 @@ for (; s.length || o.length; ) { let o = selectStream(); if ( - ((_ += escapeHTML(i.substring(u, o[0].offset))), (u = o[0].offset), o === s) + ((u += escapeHTML(i.substring(a, o[0].offset))), (a = o[0].offset), o === s) ) { - w.reverse().forEach(close); + _.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); + } while (o === s && o.length && o[0].offset === a); + _.reverse().forEach(open); } else - ('start' === o[0].event ? w.push(o[0].node) : w.pop(), + ('start' === o[0].event ? _.push(o[0].node) : _.pop(), render(o.splice(0, 1)[0])); } - return _ + escapeHTML(i.substr(u)); - })(u, nodeStream(_), i))); + return u + escapeHTML(i.substr(a)); + })(a, nodeStream(u), i))); } }; function tag(s) { @@ -4314,20 +3525,20 @@ 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 })); + for (let a = s.firstChild; a; a = a.nextSibling) + 3 === a.nodeType + ? (i += a.nodeValue.length) + : 1 === a.nodeType && + (o.push({ event: 'start', offset: i, node: a }), + (i = _nodeStream(a, i)), + tag(a).match(/br|hr|img|input/) || + o.push({ event: 'stop', offset: i, node: a })); return i; })(s, 0), o ); } - const _e = {}, + const be = {}, error = (s) => { console.error(s); }, @@ -4335,22 +3546,22 @@ console.log(`WARN: ${s}`, ...o); }, deprecated = (s, o) => { - _e[`${s}/${o}`] || - (console.log(`Deprecated as of ${s}. ${o}`), (_e[`${s}/${o}`] = !0)); + be[`${s}/${o}`] || + (console.log(`Deprecated as of ${s}. ${o}`), (be[`${s}/${o}`] = !0)); }, - we = escapeHTML, - Se = inherit, - xe = Symbol('nomatch'); - var Pe = (function (s) { + Se = escapeHTML, + _e = inherit, + we = Symbol('nomatch'); + var xe = (function (s) { const i = Object.create(null), - u = Object.create(null), - _ = []; - let w = !0; - const x = /(^(<[^>]+>|\t|)+|\n)/gm, - C = + a = Object.create(null), + u = []; + let _ = !0; + const w = /(^(<[^>]+>|\t|)+|\n)/gm, + x = "Could not find the language '{}', did you forget to load/include a language module?", - j = { disableAutodetect: !0, name: 'Plain text', contains: [] }; - let L = { + C = { disableAutodetect: !0, name: 'Plain text', contains: [] }; + let j = { noHighlightRe: /^(no-?highlight)$/i, languageDetectRe: /\blang(?:uage)?-([\w-]+)\b/i, classPrefix: 'hljs-', @@ -4360,84 +3571,84 @@ __emitter: TokenTreeEmitter }; function shouldNotHighlight(s) { - return L.noHighlightRe.test(s); + return j.noHighlightRe.test(s); } - function highlight(s, o, i, u) { - let _ = '', - w = ''; + function highlight(s, o, i, a) { + let u = '', + _ = ''; 'object' == typeof o - ? ((_ = s), (i = o.ignoreIllegals), (w = o.language), (u = void 0)) + ? ((u = s), (i = o.ignoreIllegals), (_ = o.language), (a = 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); + (_ = s), + (u = o)); + const w = { code: u, language: _ }; + fire('before:highlight', w); + const x = w.result ? w.result : _highlight(w.language, w.code, i, a); + return ((x.code = w.code), fire('after:highlight', x), x); } - function _highlight(s, o, u, x) { + function _highlight(s, o, a, w) { function keywordData(s, o) { - const i = B.case_insensitive ? o[0].toLowerCase() : o[0]; + const i = L.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; + if ('' === Y) 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)); + if (!i[U.subLanguage]) return void z.addText(Y); + ((s = _highlight(U.subLanguage, Y, !0, V[U.subLanguage])), + (V[U.subLanguage] = s.top)); + } else s = highlightAuto(Y, U.subLanguage.length ? U.subLanguage : null); + (U.relevance > 0 && (Z += s.relevance), + z.addSublanguage(s.emitter, s.language)); })() : (function processKeywords() { - if (!U.keywords) return void Y.addText(Z); + if (!U.keywords) return void z.addText(Y); let s = 0; U.keywordPatternRe.lastIndex = 0; - let o = U.keywordPatternRe.exec(Z), + let o = U.keywordPatternRe.exec(Y), 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]; + i += Y.substring(s, o.index); + const a = keywordData(U, o); + if (a) { + const [s, u] = a; + if ((z.addText(i), (i = ''), (Z += u), s.startsWith('_'))) i += o[0]; else { - const i = B.classNameAliases[s] || s; - Y.addKeyword(o[0], i); + const i = L.classNameAliases[s] || s; + z.addKeyword(o[0], i); } } else i += o[0]; - ((s = U.keywordPatternRe.lastIndex), (o = U.keywordPatternRe.exec(Z))); + ((s = U.keywordPatternRe.lastIndex), (o = U.keywordPatternRe.exec(Y))); } - ((i += Z.substr(s)), Y.addText(i)); + ((i += Y.substr(s)), z.addText(i)); })(), - (Z = '')); + (Y = '')); } function startNewMode(s) { return ( - s.className && Y.openNode(B.classNameAliases[s.className] || s.className), + s.className && z.openNode(L.classNameAliases[s.className] || s.className), (U = Object.create(s, { parent: { value: U } })), U ); } function endOfMode(s, o, i) { - let u = (function startsWith(s, o) { + let a = (function startsWith(s, o) { const i = s && s.exec(o); return i && 0 === i.index; })(s.endRe, i); - if (u) { + if (a) { if (s['on:end']) { const i = new Response(s); - (s['on:end'](o, i), i.isMatchIgnored && (u = !1)); + (s['on:end'](o, i), i.isMatchIgnored && (a = !1)); } - if (u) { + if (a) { for (; s.endsParent && s.parent; ) s = s.parent; return s; } @@ -4445,14 +3656,14 @@ if (s.endsWithParent) return endOfMode(s.parent, o, i); } function doIgnore(s) { - return 0 === U.matcher.regexIndex ? ((Z += s[0]), 1) : ((le = !0), 0); + return 0 === U.matcher.regexIndex ? ((Y += s[0]), 1) : ((ae = !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); + a = new Response(i), + u = [i.__beforeBegin, i['on:begin']]; + for (const i of u) if (i && (i(s, a), a.isMatchIgnored)) return doIgnore(o); return ( i && i.endSameAsBegin && @@ -4460,100 +3671,100 @@ return new RegExp(s.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&'), 'm'); })(o)), i.skip - ? (Z += o) - : (i.excludeBegin && (Z += o), + ? (Y += o) + : (i.excludeBegin && (Y += o), processBuffer(), - i.returnBegin || i.excludeBegin || (Z = o)), + i.returnBegin || i.excludeBegin || (Y = 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), + a = o.substr(s.index), + u = endOfMode(U, s, a); + if (!u) return we; + const _ = U; + _.skip + ? (Y += i) + : (_.returnEnd || _.excludeEnd || (Y += i), processBuffer(), - w.excludeEnd && (Z = i)); + _.excludeEnd && (Y = i)); do { - (U.className && Y.closeNode(), - U.skip || U.subLanguage || (ee += U.relevance), + (U.className && z.closeNode(), + U.skip || U.subLanguage || (Z += U.relevance), (U = U.parent)); - } while (U !== _.parent); + } while (U !== u.parent); return ( - _.starts && - (_.endSameAsBegin && (_.starts.endRe = _.endRe), startNewMode(_.starts)), - w.returnEnd ? 0 : i.length + u.starts && + (u.endSameAsBegin && (u.starts.endRe = u.endRe), startNewMode(u.starts)), + _.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)) { + let C = {}; + function processLexeme(i, u) { + const w = u && u[0]; + if (((Y += i), null == w)) return (processBuffer(), 0); + if ('begin' === C.type && 'end' === u.type && C.index === u.index && '' === w) { + if (((Y += o.slice(u.index, u.index + 1)), !_)) { const o = new Error('0 width match regex'); - throw ((o.languageName = s), (o.badRule = j.rule), o); + throw ((o.languageName = s), (o.badRule = C.rule), o); } return 1; } - if (((j = _), 'begin' === _.type)) return doBeginMatch(_); - if ('illegal' === _.type && !u) { + if (((C = u), 'begin' === u.type)) return doBeginMatch(u); + if ('illegal' === u.type && !a) { const s = new Error( - 'Illegal lexeme "' + x + '" for mode "' + (U.className || '') + '"' + 'Illegal lexeme "' + w + '" for mode "' + (U.className || '') + '"' ); throw ((s.mode = U), s); } - if ('end' === _.type) { - const s = doEndMatch(_); - if (s !== xe) return s; + if ('end' === u.type) { + const s = doEndMatch(u); + if (s !== we) return s; } - if ('illegal' === _.type && '' === x) return 1; - if (ae > 1e5 && ae > 3 * _.index) { + if ('illegal' === u.type && '' === w) return 1; + if (ie > 1e5 && ie > 3 * u.index) { throw new Error('potential infinite loop, way more iterations than matches'); } - return ((Z += x), x.length); + return ((Y += w), w.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); + const L = getLanguage(s); + if (!L) throw (error(x.replace('{}', s)), new Error('Unknown language: "' + s + '"')); + const B = compileLanguage(L, { plugins: u }); + let $ = '', + U = w || B; + const V = {}, + z = new j.__emitter(j); !(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)); + for (let o = U; o !== L; o = o.parent) o.className && s.unshift(o.className); + s.forEach((s) => z.openNode(s)); })(); - let Z = '', + let Y = '', + Z = 0, ee = 0, ie = 0, - ae = 0, - le = !1; + ae = !1; try { for (U.matcher.considerAll(); ; ) { - (ae++, le ? (le = !1) : U.matcher.considerAll(), (U.matcher.lastIndex = ie)); + (ie++, ae ? (ae = !1) : U.matcher.considerAll(), (U.matcher.lastIndex = ee)); const s = U.matcher.exec(o); if (!s) break; - const i = processLexeme(o.substring(ie, s.index), s); - ie = s.index + i; + const i = processLexeme(o.substring(ee, s.index), s); + ee = s.index + i; } return ( - processLexeme(o.substr(ie)), - Y.closeAllNodes(), - Y.finalize(), - (V = Y.toHTML()), + processLexeme(o.substr(ee)), + z.closeAllNodes(), + z.finalize(), + ($ = z.toHTML()), { - relevance: Math.floor(ee), - value: V, + relevance: Math.floor(Z), + value: $, language: s, illegal: !1, - emitter: Y, + emitter: z, top: U } ); @@ -4563,20 +3774,20 @@ illegal: !0, illegalBy: { msg: i.message, - context: o.slice(ie - 100, ie + 100), + context: o.slice(ee - 100, ee + 100), mode: i.mode }, - sofar: V, + sofar: $, relevance: 0, - value: we(o), - emitter: Y + value: Se(o), + emitter: z }; - if (w) + if (_) return { illegal: !1, relevance: 0, - value: we(o), - emitter: Y, + value: Se(o), + emitter: z, language: s, top: U, errorRaised: i @@ -4585,23 +3796,23 @@ } } function highlightAuto(s, o) { - o = o || L.languages || Object.keys(i); - const u = (function justTextHighlightResult(s) { + o = o || j.languages || Object.keys(i); + const a = (function justTextHighlightResult(s) { const o = { relevance: 0, - emitter: new L.__emitter(L), - value: we(s), + emitter: new j.__emitter(j), + value: Se(s), illegal: !1, - top: j + top: C }; return (o.emitter.addText(s), o); })(s), - _ = o + u = o .filter(getLanguage) .filter(autoDetection) .map((o) => _highlight(o, s, !1)); - _.unshift(u); - const w = _.sort((s, o) => { + u.unshift(a); + const _ = u.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; @@ -4609,24 +3820,24 @@ } return 0; }), - [x, C] = w, - B = x; - return ((B.second_best = C), B); + [w, x] = _, + L = w; + return ((L.second_best = x), L); } - const B = { + const L = { 'before:highlightElement': ({ el: s }) => { - L.useBR && + j.useBR && (s.innerHTML = s.innerHTML.replace(/\n/g, '').replace(//g, '\n')); }, 'after:highlightElement': ({ result: s }) => { - L.useBR && (s.value = s.value.replace(/\n/g, '
')); + j.useBR && (s.value = s.value.replace(/\n/g, '
')); } }, - $ = /^(<[^>]+>|\t)+/gm, - V = { + B = /^(<[^>]+>|\t)+/gm, + $ = { 'after:highlightElement': ({ result: s }) => { - L.tabReplace && - (s.value = s.value.replace($, (s) => s.replace(/\t/g, L.tabReplace))); + j.tabReplace && + (s.value = s.value.replace(B, (s) => s.replace(/\t/g, j.tabReplace))); } }; function highlightElement(s) { @@ -4634,12 +3845,12 @@ const i = (function blockLanguage(s) { let o = s.className + ' '; o += s.parentNode ? s.parentNode.className : ''; - const i = L.languageDetectRe.exec(o); + const i = j.languageDetectRe.exec(o); if (i) { const o = getLanguage(i[1]); return ( o || - (warn(C.replace('{}', i[1])), + (warn(x.replace('{}', i[1])), warn('Falling back to no-highlight mode for this block.', s)), o ? i[1] : 'no-highlight' ); @@ -4648,20 +3859,20 @@ })(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), + const u = o.textContent, + _ = i ? highlight(u, { language: i, ignoreIllegals: !0 }) : highlightAuto(u); + (fire('after:highlightElement', { el: s, result: _, text: u }), + (s.innerHTML = _.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 && + const u = o ? a[o] : i; + (s.classList.add('hljs'), u && s.classList.add(u)); + })(s, i, _.language), + (s.result = { language: _.language, re: _.relevance, relavance: _.relevance }), + _.second_best && (s.second_best = { - language: w.second_best.language, - re: w.second_best.relevance, - relavance: w.second_best.relevance + language: _.second_best.language, + re: _.second_best.relevance, + relavance: _.second_best.relevance })); } const initHighlighting = () => { @@ -4679,12 +3890,12 @@ document.querySelectorAll('pre code').forEach(highlightElement); } function getLanguage(s) { - return ((s = (s || '').toLowerCase()), i[s] || i[u[s]]); + return ((s = (s || '').toLowerCase()), i[s] || i[a[s]]); } function registerAliases(s, { languageName: o }) { ('string' == typeof s && (s = [s]), s.forEach((s) => { - u[s.toLowerCase()] = o; + a[s.toLowerCase()] = o; })); } function autoDetection(s) { @@ -4693,7 +3904,7 @@ } function fire(s, o) { const i = s; - _.forEach(function (s) { + u.forEach(function (s) { s[i] && s[i](o); }); } @@ -4718,14 +3929,14 @@ 'Please see https://github.com/highlightjs/highlight.js/issues/2534' ), (function fixMarkup(s) { - return L.tabReplace || L.useBR - ? s.replace(x, (s) => + return j.tabReplace || j.useBR + ? s.replace(w, (s) => '\n' === s - ? L.useBR + ? j.useBR ? '
' : s - : L.tabReplace - ? s.replace(/\t/g, L.tabReplace) + : j.tabReplace + ? s.replace(/\t/g, j.tabReplace) : s ) : s; @@ -4747,7 +3958,7 @@ '10.3.0', 'Please see https://github.com/highlightjs/highlight.js/issues/2559' )), - (L = Se(L, s))); + (j = _e(j, s))); }, initHighlighting, initHighlightingOnLoad: function initHighlightingOnLoad() { @@ -4757,28 +3968,28 @@ ), (U = !0)); }, - registerLanguage: function registerLanguage(o, u) { - let _ = null; + registerLanguage: function registerLanguage(o, a) { + let u = null; try { - _ = u(s); + u = a(s); } catch (s) { if ( (error( "Language definition for '{}' could not be registered.".replace('{}', o) ), - !w) + !_) ) throw s; - (error(s), (_ = j)); + (error(s), (u = C)); } - (_.name || (_.name = o), - (i[o] = _), - (_.rawDefinition = u.bind(null, s)), - _.aliases && registerAliases(_.aliases, { languageName: o })); + (u.name || (u.name = o), + (i[o] = u), + (u.rawDefinition = a.bind(null, s)), + u.aliases && registerAliases(u.aliases, { languageName: o })); }, unregisterLanguage: function unregisterLanguage(s) { delete i[s]; - for (const o of Object.keys(u)) u[o] === s && delete u[o]; + for (const o of Object.keys(a)) a[o] === s && delete a[o]; }, listLanguages: function listLanguages() { return Object.keys(i); @@ -4798,7 +4009,7 @@ ); }, autoDetection, - inherit: Se, + inherit: _e, addPlugin: function addPlugin(s) { (!(function upgradePluginAPI(s) { (s['before:highlightBlock'] && @@ -4812,23 +4023,23 @@ s['after:highlightBlock'](Object.assign({ block: o.el }, o)); })); })(s), - _.push(s)); + u.push(s)); }, vuePlugin: BuildVuePlugin(s).VuePlugin }), (s.debugMode = function () { - w = !1; + _ = !1; }), (s.safeMode = function () { - w = !0; + _ = !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); + for (const s in de) 'object' == typeof de[s] && o(de[s]); + return (Object.assign(s, de), s.addPlugin(L), s.addPlugin(ye), s.addPlugin($), s); })({}); - s.exports = Pe; + s.exports = xe; }, - 35344: (s) => { + 35344(s) { function concat(...s) { return s .map((s) => @@ -4845,13 +4056,13 @@ className: 'variable', variants: [{ begin: concat(/\$[\w\d#@][\w\d_]*/, '(?![\\w\\d])(?![$])') }, i] }); - const u = { + const a = { className: 'subst', begin: /\$\(/, end: /\)/, contains: [s.BACKSLASH_ESCAPE] }, - _ = { + u = { begin: /<<-?\s*(?=\w+)/, starts: { contains: [ @@ -4859,23 +4070,23 @@ ] } }, - w = { + _ = { className: 'string', begin: /"/, end: /"/, - contains: [s.BACKSLASH_ESCAPE, o, u] + contains: [s.BACKSLASH_ESCAPE, o, a] }; - u.contains.push(w); - const x = { + a.contains.push(_); + const w = { begin: /\$\(\(/, end: /\)\)/, contains: [{ begin: /\d+#[0-9a-f]+/, className: 'number' }, s.NUMBER_MODE, o] }, - C = s.SHEBANG({ + x = s.SHEBANG({ binary: `(${['fish', 'bash', 'zsh', 'sh', 'csh', 'ksh', 'tcsh', 'dash', 'scsh'].join('|')})`, relevance: 10 }), - j = { + C = { className: 'function', begin: /\w[\w\d_]*\s*\(\s*\)\s*\{/, returnBegin: !0, @@ -4893,13 +4104,13 @@ '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, - _, + s.SHEBANG(), + C, w, + s.HASH_COMMENT_MODE, + u, + _, { className: '', begin: /\\"/ }, { className: 'string', begin: /'/, end: /'/ }, o @@ -4907,7 +4118,7 @@ }; }; }, - 73402: (s) => { + 73402(s) { function concat(...s) { return s .map((s) => @@ -4933,7 +4144,7 @@ ] } }, - u = [i, { begin: '\\n\\n', starts: { subLanguage: [], endsWithParent: !0 } }]; + a = [i, { begin: '\\n\\n', starts: { subLanguage: [], endsWithParent: !0 } }]; return { name: 'HTTP', aliases: ['https'], @@ -4946,7 +4157,7 @@ { className: 'meta', begin: o }, { className: 'number', begin: '\\b\\d{3}\\b' } ], - starts: { end: /\b\B/, illegal: /\S/, contains: u } + starts: { end: /\b\B/, illegal: /\S/, contains: a } }, { begin: '(?=^[A-Z]+ (.*?) ' + o + '$)', @@ -4956,14 +4167,14 @@ { className: 'meta', begin: o }, { className: 'keyword', begin: '[A-Z]+' } ], - starts: { end: /\b\B/, illegal: /\S/, contains: u } + starts: { end: /\b\B/, illegal: /\S/, contains: a } }, s.inherit(i, { relevance: 0 }) ] }; }; }, - 95089: (s) => { + 95089(s) { const o = '[A-Za-z$_][0-9A-Za-z$_]*', i = [ 'as', @@ -5005,8 +4216,8 @@ 'export', 'extends' ], - u = ['true', 'false', 'null', 'undefined', 'NaN', 'Infinity'], - _ = [].concat( + a = ['true', 'false', 'null', 'undefined', 'NaN', 'Infinity'], + u = [].concat( [ 'setInterval', 'setTimeout', @@ -5096,17 +4307,17 @@ .join(''); } s.exports = function javascript(s) { - const w = o, - x = '<>', - C = '', - j = { + const _ = o, + w = '<>', + x = '', + C = { 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 && + a = s.input[i]; + '<' !== a + ? '>' === a && (((s, { after: o }) => { const i = ' { + 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 = { + a = [s.QUOTE_STRING_MODE, s.C_NUMBER_MODE], + u = { end: ',', endsWithParent: !0, excludeEnd: !0, contains: a, keywords: o }, + _ = { begin: /\{/, end: /\}/, contains: [ @@ -5335,21 +4546,21 @@ contains: [s.BACKSLASH_ESCAPE], illegal: '\\n' }, - s.inherit(_, { begin: /:/ }) + s.inherit(u, { begin: /:/ }) ].concat(i), illegal: '\\S' }, - x = { begin: '\\[', end: '\\]', contains: [s.inherit(_)], illegal: '\\S' }; + w = { begin: '\\[', end: '\\]', contains: [s.inherit(u)], illegal: '\\S' }; return ( - u.push(w, x), + a.push(_, w), i.forEach(function (s) { - u.push(s); + a.push(s); }), - { name: 'JSON', contains: u, keywords: o, illegal: '\\S' } + { name: 'JSON', contains: a, keywords: o, illegal: '\\S' } ); }; }, - 26571: (s) => { + 26571(s) { s.exports = function powershell(s) { const o = { $pattern: /-?[A-z\.\-]+\b/, @@ -5359,7 +4570,7 @@ '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 = { + a = { className: 'variable', variants: [ { begin: /\$\B/ }, @@ -5367,22 +4578,22 @@ { begin: /\$[\w\d][\w\d_:]*/ } ] }, - _ = { + u = { className: 'string', variants: [ { begin: /"/, end: /"/ }, { begin: /@"/, end: /^"@/ } ], - contains: [i, u, { className: 'variable', begin: /\$[A-z]/, end: /[^A-z]/ }] + contains: [i, a, { className: 'variable', begin: /\$[A-z]/, end: /[^A-z]/ }] }, - w = { + _ = { className: 'string', variants: [ { begin: /'/, end: /'/ }, { begin: /@'/, end: /^'@/ } ] }, - x = s.inherit(s.COMMENT(null, null), { + w = s.inherit(s.COMMENT(null, null), { variants: [ { begin: /#/, end: /$/ }, { begin: /<#/, end: /#>/ } @@ -5403,7 +4614,7 @@ } ] }), - C = { + x = { className: 'built_in', variants: [ { @@ -5414,7 +4625,7 @@ } ] }, - j = { + C = { className: 'class', beginKeywords: 'class enum', end: /\s*[{]/, @@ -5422,7 +4633,7 @@ relevance: 0, contains: [s.TITLE_MODE] }, - L = { + j = { className: 'function', begin: /function\s+/, end: /\s*\{|$/, @@ -5432,20 +4643,20 @@ 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] } + { begin: /\(/, end: /\)/, className: 'params', relevance: 0, contains: [a] } ] }, - B = { + L = { begin: /using\s/, end: /$/, returnBegin: !0, contains: [ + u, _, - w, { className: 'keyword', begin: /(using|assembly|command|module|namespace|type)/ } ] }, - $ = { + B = { variants: [ { className: 'operator', @@ -5457,7 +4668,7 @@ { className: 'literal', begin: /(-)[\w\d]+/, relevance: 0 } ] }, - V = { + $ = { className: 'function', begin: /\[.*\]\s*[\w]+[ ]??\(/, end: /$/, @@ -5474,18 +4685,18 @@ ] }, U = [ - V, - x, + $, + w, i, s.NUMBER_MODE, - _, - w, - C, u, + _, + x, + a, { className: 'literal', begin: /\$(null|true|false)\b/ }, { className: 'selector-tag', begin: /@\B/, relevance: 0 } ], - z = { + V = { begin: /\[/, end: /\]/, excludeBegin: !0, @@ -5521,18 +4732,18 @@ ) }; return ( - V.contains.unshift(z), + $.contains.unshift(V), { name: 'PowerShell', aliases: ['ps', 'ps1'], case_insensitive: !0, keywords: o, - contains: U.concat(j, L, B, $, z) + contains: U.concat(C, j, L, B, V) } ); }; }, - 17285: (s) => { + 17285(s) { function source(s) { return s ? ('string' == typeof s ? s : s.source) : null; } @@ -5554,16 +4765,16 @@ /[A-Z0-9_.-]*/ ), i = { className: 'symbol', begin: /&[a-z]+;|&#[0-9]+;|&#x[a-f0-9]+;/ }, - u = { + a = { 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 = { + u = s.inherit(a, { begin: /\(/, end: /\)/ }), + _ = s.inherit(s.APOS_STRING_MODE, { className: 'meta-string' }), + w = s.inherit(s.QUOTE_STRING_MODE, { className: 'meta-string' }), + x = { endsWithParent: !0, illegal: //, relevance: 10, contains: [ - u, - x, + a, w, _, + u, { begin: /\[/, end: /\]/, contains: [ - { className: 'meta', begin: //, contains: [u, _, x, w] } + { className: 'meta', begin: //, contains: [a, u, w, _] } ] } ] @@ -5619,7 +4830,7 @@ begin: /)/, end: />/, keywords: { name: 'style' }, - contains: [C], + contains: [x], starts: { end: /<\/style>/, returnEnd: !0, subLanguage: ['css', 'xml'] } }, { @@ -5627,7 +4838,7 @@ begin: /)/, end: />/, keywords: { name: 'script' }, - contains: [C], + contains: [x], starts: { end: /<\/script>/, returnEnd: !0, @@ -5639,7 +4850,7 @@ className: 'tag', begin: concat(//, />/, /\s/)))), end: /\/?>/, - contains: [{ className: 'name', begin: o, relevance: 0, starts: C }] + contains: [{ className: 'name', begin: o, relevance: 0, starts: x }] }, { className: 'tag', @@ -5653,11 +4864,11 @@ }; }; }, - 17533: (s) => { + 17533(s) { s.exports = function yaml(s) { var o = 'true false yes no null', i = "[\\w#;/?:@&=+$,.~*'()[\\]]+", - u = { + a = { className: 'string', relevance: 0, variants: [{ begin: /'/, end: /'/ }, { begin: /"/, end: /"/ }, { begin: /\S+/ }], @@ -5672,22 +4883,22 @@ } ] }, - _ = s.inherit(u, { + u = s.inherit(a, { 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 = [ + w = { end: ',', endsWithParent: !0, excludeEnd: !0, keywords: o, relevance: 0 }, + x = { begin: /\{/, end: /\}/, contains: [w], illegal: '\\n', relevance: 0 }, + C = { begin: '\\[', end: '\\]', contains: [w], illegal: '\\n', relevance: 0 }, + j = [ { className: 'attr', variants: [ @@ -5718,81 +4929,81 @@ { 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 }, + x, C, - j, - u + a ], - B = [...L]; + L = [...j]; return ( - B.pop(), - B.push(_), - (x.contains = B), - { name: 'YAML', case_insensitive: !0, aliases: ['yml'], contains: L } + L.pop(), + L.push(u), + (w.contains = L), + { name: 'YAML', case_insensitive: !0, aliases: ['yml'], contains: j } ); }; }, - 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 + $]; + 251(s, o) { + ((o.read = function (s, o, i, a, u) { + var _, + w, + x = 8 * u - a - 1, + C = (1 << x) - 1, + j = C >> 1, + L = -7, + B = i ? u - 1 : 0, + $ = i ? -1 : 1, + U = s[o + B]; for ( - $ += V, w = U & ((1 << -B) - 1), U >>= -B, B += C; - B > 0; - w = 256 * w + s[o + $], $ += V, B -= 8 + B += $, _ = U & ((1 << -L) - 1), U >>= -L, L += x; + L > 0; + _ = 256 * _ + s[o + B], B += $, L -= 8 ); for ( - x = w & ((1 << -B) - 1), w >>= -B, B += u; - B > 0; - x = 256 * x + s[o + $], $ += V, B -= 8 + w = _ & ((1 << -L) - 1), _ >>= -L, L += a; + L > 0; + w = 256 * w + s[o + B], B += $, L -= 8 ); - if (0 === w) w = 1 - L; + if (0 === _) _ = 1 - j; else { - if (w === j) return x ? NaN : (1 / 0) * (U ? -1 : 1); - ((x += Math.pow(2, u)), (w -= L)); + if (_ === C) return w ? NaN : (1 / 0) * (U ? -1 : 1); + ((w += Math.pow(2, a)), (_ -= j)); } - return (U ? -1 : 1) * x * Math.pow(2, w - u); + return (U ? -1 : 1) * w * Math.pow(2, _ - a); }), - (o.write = function (s, o, i, u, _, w) { - var x, + (o.write = function (s, o, i, a, u, _) { + var w, + 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; + j = 8 * _ - u - 1, + L = (1 << j) - 1, + B = L >> 1, + $ = 23 === u ? Math.pow(2, -24) - Math.pow(2, -77) : 0, + U = a ? 0 : _ - 1, + V = a ? 1 : -1, + z = 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 + ? ((x = isNaN(o) ? 1 : 0), (w = L)) + : ((w = Math.floor(Math.log(o) / Math.LN2)), + o * (C = Math.pow(2, -w)) < 1 && (w--, (C *= 2)), + (o += w + B >= 1 ? $ / C : $ * Math.pow(2, 1 - B)) * C >= 2 && + (w++, (C /= 2)), + w + B >= L + ? ((x = 0), (w = L)) + : w + B >= 1 + ? ((x = (o * C - 1) * Math.pow(2, u)), (w += B)) + : ((x = o * Math.pow(2, B - 1) * Math.pow(2, u)), (w = 0))); + u >= 8; + s[i + U] = 255 & x, U += V, x /= 256, u -= 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; + for (w = (w << u) | x, j += u; j > 0; s[i + U] = 255 & w, U += V, w /= 256, j -= 8); + s[i + U - V] |= 128 * z; })); }, - 9404: function (s) { + 9404(s) { s.exports = (function () { 'use strict'; var s = Array.prototype.slice; @@ -5818,13 +5029,13 @@ return !(!s || !s[i]); } function isIndexed(s) { - return !(!s || !s[u]); + return !(!s || !s[a]); } function isAssociative(s) { return isKeyed(s) || isIndexed(s); } function isOrdered(s) { - return !(!s || !s[_]); + return !(!s || !s[u]); } (createClass(KeyedIterable, Iterable), createClass(IndexedIterable, Iterable), @@ -5839,15 +5050,15 @@ (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 }; + a = '@@__IMMUTABLE_INDEXED__@@', + u = '@@__IMMUTABLE_ORDERED__@@', + _ = 'delete', + w = 5, + x = 1 << w, + C = x - 1, + j = {}, + L = { value: !1 }, + B = { value: !1 }; function MakeRef(s) { return ((s.value = !1), s); } @@ -5857,9 +5068,9 @@ 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; + for (var i = Math.max(0, s.length - o), a = new Array(i), u = 0; u < i; u++) + a[u] = s[u + o]; + return a; } function ensureSize(s) { return (void 0 === s.size && (s.size = s.__iterate(returnTrue)), s.size); @@ -5895,18 +5106,18 @@ ? s : Math.min(o, s); } - var V = 0, + var $ = 0, U = 1, - z = 2, - Y = 'function' == typeof Symbol && Symbol.iterator, - Z = '@@iterator', - ee = Y || Z; + V = 2, + z = 'function' == typeof Symbol && Symbol.iterator, + Y = '@@iterator', + Z = z || Y; 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 iteratorValue(s, o, i, a) { + var u = 0 === s ? o : 1 === s ? i : [o, i]; + return (a ? (a.value = u) : (a = { value: u, done: !1 }), a); } function iteratorDone() { return { value: void 0, done: !0 }; @@ -5922,7 +5133,7 @@ return o && o.call(s); } function getIteratorFn(s) { - var o = s && ((Y && s[Y]) || s[Z]); + var o = s && ((z && s[z]) || s[Y]); if ('function' == typeof o) return o; } function isArrayLike(s) { @@ -5963,14 +5174,14 @@ ((Iterator.prototype.toString = function () { return '[Iterator]'; }), - (Iterator.KEYS = V), + (Iterator.KEYS = $), (Iterator.VALUES = U), - (Iterator.ENTRIES = z), + (Iterator.ENTRIES = V), (Iterator.prototype.inspect = Iterator.prototype.toSource = function () { return this.toString(); }), - (Iterator.prototype[ee] = function () { + (Iterator.prototype[Z] = function () { return this; }), createClass(Seq, Iterable), @@ -6028,9 +5239,9 @@ (Seq.Keyed = KeyedSeq), (Seq.Set = SetSeq), (Seq.Indexed = IndexedSeq)); - var ie, + var ee, + ie, ae, - le, ce = '@@__IMMUTABLE_SEQ__@@'; function ArraySeq(s) { ((this._array = s), (this.size = s.length)); @@ -6049,7 +5260,7 @@ return !(!s || !s[ce]); } function emptySequence() { - return ie || (ie = new ArraySeq([])); + return ee || (ee = new ArraySeq([])); } function keyedSeqFromValue(s) { var o = Array.isArray(s) @@ -6089,25 +5300,25 @@ ? 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; + function seqIterate(s, o, i, a) { + var u = s._cache; + if (u) { + for (var _ = u.length - 1, w = 0; w <= _; w++) { + var x = u[i ? _ - w : w]; + if (!1 === o(x[1], a ? x[0] : w, s)) return w + 1; } - return x; + return w; } return s.__iterateUncached(o, i); } - function seqIterator(s, o, i, u) { - var _ = s._cache; - if (_) { - var w = _.length - 1, - x = 0; + function seqIterator(s, o, i, a) { + var u = s._cache; + if (u) { + var _ = u.length - 1, + w = 0; return new Iterator(function () { - var s = _[i ? w - x : x]; - return x++ > w ? iteratorDone() : iteratorValue(o, u ? s[0] : x - 1, s[1]); + var s = u[i ? _ - w : w]; + return w++ > _ ? iteratorDone() : iteratorValue(o, a ? s[0] : w - 1, s[1]); }); } return s.__iteratorUncached(o, i); @@ -6115,21 +5326,21 @@ function fromJS(s, o) { return o ? fromJSWith(o, s, '', { '': s }) : fromJSDefault(s); } - function fromJSWith(s, o, i, u) { + function fromJSWith(s, o, i, a) { return Array.isArray(o) ? s.call( - u, + a, i, - IndexedSeq(o).map(function (i, u) { - return fromJSWith(s, i, u, o); + IndexedSeq(o).map(function (i, a) { + return fromJSWith(s, i, a, o); }) ) : isPlainObj(o) ? s.call( - u, + a, i, - KeyedSeq(o).map(function (i, u) { - return fromJSWith(s, i, u, o); + KeyedSeq(o).map(function (i, a) { + return fromJSWith(s, i, a, o); }) ) : o; @@ -6171,28 +5382,28 @@ if (0 === s.size && 0 === o.size) return !0; var i = !isAssociative(s); if (isOrdered(s)) { - var u = s.entries(); + var a = s.entries(); return ( o.every(function (s, o) { - var _ = u.next().value; - return _ && is(_[1], s) && (i || is(_[0], o)); - }) && u.next().done + var u = a.next().value; + return u && is(u[1], s) && (i || is(u[0], o)); + }) && a.next().done ); } - var _ = !1; + var u = !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)); + u = !0; + var _ = s; + ((s = o), (o = _)); } - 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); + var w = !0, + x = o.__iterate(function (o, a) { + if (i ? !s.has(o) : u ? !is(o, s.get(a, j)) : !is(s.get(a, j), o)) + return ((w = !1), !1); }); - return x && s.size === C; + return w && s.size === x; } function Repeat(s, o) { if (!(this instanceof Repeat)) return new Repeat(s, o); @@ -6201,8 +5412,8 @@ (this.size = void 0 === o ? 1 / 0 : Math.max(0, o)), 0 === this.size) ) { - if (ae) return ae; - ae = this; + if (ie) return ie; + ie = this; } } function invariant(s, o) { @@ -6222,8 +5433,8 @@ (this.size = Math.max(0, Math.ceil((o - s) / i - 1) + 1)), 0 === this.size) ) { - if (le) return le; - le = this; + if (ae) return ae; + ae = this; } } function Collection() { @@ -6238,16 +5449,16 @@ 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 _; + for (var i = this._array, a = i.length - 1, u = 0; u <= a; u++) + if (!1 === s(i[o ? a - u : u], u, this)) return u + 1; + return u; }), (ArraySeq.prototype.__iterator = function (s, o) { var i = this._array, - u = i.length - 1, - _ = 0; + a = i.length - 1, + u = 0; return new Iterator(function () { - return _ > u ? iteratorDone() : iteratorValue(s, _, i[o ? u - _++ : _++]); + return u > a ? iteratorDone() : iteratorValue(s, u, i[o ? a - u++ : u++]); }); }), createClass(ObjectSeq, KeyedSeq), @@ -6258,65 +5469,65 @@ 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; + for (var i = this._object, a = this._keys, u = a.length - 1, _ = 0; _ <= u; _++) { + var w = a[o ? u - _ : _]; + if (!1 === s(i[w], w, this)) return _ + 1; } - return w; + return _; }), (ObjectSeq.prototype.__iterator = function (s, o) { var i = this._object, - u = this._keys, - _ = u.length - 1, - w = 0; + a = this._keys, + u = a.length - 1, + _ = 0; return new Iterator(function () { - var x = u[o ? _ - w : w]; - return w++ > _ ? iteratorDone() : iteratorValue(s, x, i[x]); + var w = a[o ? u - _ : _]; + return _++ > u ? iteratorDone() : iteratorValue(s, w, i[w]); }); }), - (ObjectSeq.prototype[_] = !0), + (ObjectSeq.prototype[u] = !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; + a = 0; if (isIterator(i)) - for (var _; !(_ = i.next()).done && !1 !== s(_.value, u++, this); ); - return u; + for (var u; !(u = i.next()).done && !1 !== s(u.value, a++, this); ); + return a; }), (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; + var a = 0; return new Iterator(function () { var o = i.next(); - return o.done ? o : iteratorValue(s, u++, o.value); + return o.done ? o : iteratorValue(s, a++, 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; + for (var i, a = this._iterator, u = this._iteratorCache, _ = 0; _ < u.length; ) + if (!1 === s(u[_], _++, this)) return _; + for (; !(i = a.next()).done; ) { + var w = i.value; + if (((u[_] = w), !1 === s(w, _++, this))) break; } - return w; + return _; }), (IteratorSeq.prototype.__iteratorUncached = function (s, o) { if (o) return this.cacheResult().__iterator(s, o); var i = this._iterator, - u = this._iteratorCache, - _ = 0; + a = this._iteratorCache, + u = 0; return new Iterator(function () { - if (_ >= u.length) { + if (u >= a.length) { var o = i.next(); if (o.done) return o; - u[_] = o.value; + a[u] = o.value; } - return iteratorValue(s, _, u[_++]); + return iteratorValue(s, u, a[u++]); }); }), createClass(Repeat, IndexedSeq), @@ -6353,9 +5564,9 @@ }), (Repeat.prototype.__iterator = function (s, o) { var i = this, - u = 0; + a = 0; return new Iterator(function () { - return u < i.size ? iteratorValue(s, u++, i._value) : iteratorDone(); + return a < i.size ? iteratorValue(s, a++, i._value) : iteratorDone(); }); }), (Repeat.prototype.equals = function (s) { @@ -6401,25 +5612,25 @@ (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++ + a = this._step, + u = o ? this._start + i * a : this._start, + _ = 0; + _ <= i; + _++ ) { - if (!1 === s(_, w, this)) return w + 1; - _ += o ? -u : u; + if (!1 === s(u, _, this)) return _ + 1; + u += o ? -a : a; } - return w; + return _; }), (Range.prototype.__iterator = function (s, o) { var i = this.size - 1, - u = this._step, - _ = o ? this._start + i * u : this._start, - w = 0; + a = this._step, + u = o ? this._start + i * a : this._start, + _ = 0; return new Iterator(function () { - var x = _; - return ((_ += o ? -u : u), w > i ? iteratorDone() : iteratorValue(s, w++, x)); + var w = u; + return ((u += o ? -a : a), _ > i ? iteratorDone() : iteratorValue(s, _++, w)); }); }), (Range.prototype.equals = function (s) { @@ -6434,13 +5645,13 @@ (Collection.Keyed = KeyedCollection), (Collection.Indexed = IndexedCollection), (Collection.Set = SetCollection)); - var pe = + var le = '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; + a = 65535 & (o |= 0); + return (i * a + ((((s >>> 16) * a + i * (o >>> 16)) << 16) >>> 0)) | 0; }; function smi(s) { return ((s >>> 1) & 1073741824) | (3221225471 & s); @@ -6457,17 +5668,17 @@ 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 ('string' === o) return s.length > _e ? 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]; + var o = Pe[s]; return ( void 0 === o && - ((o = hashString(s)), Pe === xe && ((Pe = 0), (Te = {})), Pe++, (Te[s] = o)), + ((o = hashString(s)), xe === we && ((xe = 0), (Pe = {})), xe++, (Pe[s] = o)), o ); } @@ -6477,18 +5688,18 @@ } 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 (ye && void 0 !== (o = fe.get(s))) return o; + if (void 0 !== (o = s[Se])) return o; + if (!de) { + if (void 0 !== (o = s.propertyIsEnumerable && s.propertyIsEnumerable[Se])) return o; if (void 0 !== (o = getIENodeHash(s))) return o; } - if (((o = ++_e), 1073741824 & _e && (_e = 0), be)) ye.set(s, o); + if (((o = ++be), 1073741824 & be && (be = 0), ye)) fe.set(s, o); else { - if (void 0 !== de && !1 === de(s)) + if (void 0 !== pe && !1 === pe(s)) throw new Error('Non-extensible objects are not allowed as keys.'); - if (fe) - Object.defineProperty(s, we, { + if (de) + Object.defineProperty(s, Se, { enumerable: !1, configurable: !1, writable: !1, @@ -6501,17 +5712,17 @@ ((s.propertyIsEnumerable = function () { return this.constructor.prototype.propertyIsEnumerable.apply(this, arguments); }), - (s.propertyIsEnumerable[we] = o)); + (s.propertyIsEnumerable[Se] = o)); else { if (void 0 === s.nodeType) throw new Error('Unable to set a non-enumerable property on object.'); - s[we] = o; + s[Se] = o; } } return o; } - var de = Object.isExtensible, - fe = (function () { + var pe = Object.isExtensible, + de = (function () { try { return (Object.defineProperty({}, '@', {}), !0); } catch (s) { @@ -6527,16 +5738,16 @@ 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 = {}; + var fe, + ye = 'function' == typeof WeakMap; + ye && (fe = new WeakMap()); + var be = 0, + Se = '__immutablehash__'; + 'function' == typeof Symbol && (Se = Symbol(Se)); + var _e = 16, + we = 255, + xe = 0, + Pe = {}; function assertNotInfinite(s) { invariant(s !== 1 / 0, 'Cannot perform this action with an infinite size.'); } @@ -6554,7 +5765,7 @@ }); } function isMap(s) { - return !(!s || !s[qe]); + return !(!s || !s[Re]); } (createClass(Map, KeyedCollection), (Map.of = function () { @@ -6576,16 +5787,16 @@ return updateMap(this, s, o); }), (Map.prototype.setIn = function (s, o) { - return this.updateIn(s, L, function () { + return this.updateIn(s, j, function () { return o; }); }), (Map.prototype.remove = function (s) { - return updateMap(this, s, L); + return updateMap(this, s, j); }), (Map.prototype.deleteIn = function (s) { return this.updateIn(s, function () { - return L; + return j; }); }), (Map.prototype.update = function (s, o, i) { @@ -6593,8 +5804,8 @@ }), (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; + var a = updateInDeepMap(this, forceIterator(s), o, i); + return a === j ? void 0 : a; }), (Map.prototype.clear = function () { return 0 === this.size @@ -6658,13 +5869,13 @@ }), (Map.prototype.__iterate = function (s, o) { var i = this, - u = 0; + a = 0; return ( this._root && this._root.iterate(function (o) { - return (u++, s(o[1], o[0], i)); + return (a++, s(o[1], o[0], i)); }, o), - u + a ); }), (Map.prototype.__ensureOwner = function (s) { @@ -6675,8 +5886,8 @@ : ((this.__ownerID = s), (this.__altered = !1), this); }), (Map.isMap = isMap)); - var Re, - qe = '@@__IMMUTABLE_MAP__@@', + var Te, + Re = '@@__IMMUTABLE_MAP__@@', $e = Map.prototype; function ArrayMapNode(s, o) { ((this.ownerID = s), (this.entries = o)); @@ -6704,105 +5915,105 @@ function mapIteratorFrame(s, o) { return { node: s, index: 0, __prev: o }; } - function makeMap(s, o, i, u) { - var _ = Object.create($e); + function makeMap(s, o, i, a) { + var u = Object.create($e); return ( - (_.size = s), - (_._root = o), - (_.__ownerID = i), - (_.__hash = u), - (_.__altered = !1), - _ + (u.size = s), + (u._root = o), + (u.__ownerID = i), + (u.__hash = a), + (u.__altered = !1), + u ); } function emptyMap() { - return Re || (Re = makeMap(0)); + return Te || (Te = makeMap(0)); } function updateMap(s, o, i) { - var u, _; + var a, 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)) + var _ = MakeRef(L), + w = MakeRef(B); + if (((a = updateNode(s._root, s.__ownerID, 0, void 0, o, i, _, w)), !w.value)) return s; - _ = s.size + (w.value ? (i === L ? -1 : 1) : 0); + u = s.size + (_.value ? (i === j ? -1 : 1) : 0); } else { - if (i === L) return s; - ((_ = 1), (u = new ArrayMapNode(s.__ownerID, [[o, i]]))); + if (i === j) return s; + ((u = 1), (a = 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) + ? ((s.size = u), (s._root = a), (s.__hash = void 0), (s.__altered = !0), s) + : a + ? makeMap(u, a) : emptyMap(); } - function updateNode(s, o, i, u, _, w, x, C) { + function updateNode(s, o, i, a, u, _, w, x) { return s - ? s.update(o, i, u, _, w, x, C) - : w === L + ? s.update(o, i, a, u, _, w, x) + : _ === j ? s - : (SetRef(C), SetRef(x), new ValueNode(o, u, [_, w])); + : (SetRef(x), SetRef(w), new ValueNode(o, a, [u, _])); } 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; + function mergeIntoNode(s, o, i, a, u) { + if (s.keyHash === a) return new HashCollisionNode(o, a, [s.entry, u]); + var _, + x = (0 === i ? s.keyHash : s.keyHash >>> i) & C, + j = (0 === i ? a : a >>> i) & C; 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]) + (1 << x) | (1 << j), + x === j + ? [mergeIntoNode(s, o, i + w, a, u)] + : ((_ = new ValueNode(o, a, u)), x < j ? [s, _] : [_, s]) ); } - function createNodes(s, o, i, u) { + function createNodes(s, o, i, a) { 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]); + for (var u = new ValueNode(s, hash(i), [i, a]), _ = 0; _ < o.length; _++) { + var w = o[_]; + u = u.update(s, 0, void 0, w[0], w[1]); } - return _; + return u; } - function packNodes(s, o, i, u) { + function packNodes(s, o, i, a) { for ( - var _ = 0, w = 0, x = new Array(i), C = 0, j = 1, L = o.length; - C < L; - C++, j <<= 1 + var u = 0, _ = 0, w = new Array(i), x = 0, C = 1, j = o.length; + x < j; + x++, C <<= 1 ) { - var B = o[C]; - void 0 !== B && C !== u && ((_ |= j), (x[w++] = B)); + var L = o[x]; + void 0 !== L && x !== a && ((u |= C), (w[_++] = L)); } - return new BitmapIndexedNode(s, _, x); + return new BitmapIndexedNode(s, u, w); } - 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 expandNodes(s, o, i, a, u) { + for (var _ = 0, w = new Array(x), C = 0; 0 !== i; C++, i >>>= 1) + w[C] = 1 & i ? o[_++] : void 0; + return ((w[a] = u), new HashArrayMapNode(s, _ + 1, w)); } function mergeIntoMapWith(s, o, i) { - for (var u = [], _ = 0; _ < i.length; _++) { - var w = i[_], - x = KeyedIterable(w); - (isIterable(w) || - (x = x.map(function (s) { + for (var a = [], u = 0; u < i.length; u++) { + var _ = i[u], + w = KeyedIterable(_); + (isIterable(_) || + (w = w.map(function (s) { return fromJS(s); })), - u.push(x)); + a.push(w)); } - return mergeIntoCollectionWith(s, o, u); + return mergeIntoCollectionWith(s, o, a); } 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) { + return function (o, i, a) { if (o && o.mergeDeepWith && isIterable(i)) return o.mergeDeepWith(s, i); - var _ = s(o, i, u); - return is(o, _) ? o : _; + var u = s(o, i, a); + return is(o, u) ? o : u; }; } function mergeIntoCollectionWith(s, o, i) { @@ -6814,36 +6025,36 @@ : 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); + var a = o + ? function (i, a) { + s.update(a, j, function (s) { + return s === j ? i : o(s, i, a); }); } : function (o, i) { s.set(i, o); }, - _ = 0; - _ < i.length; - _++ + u = 0; + u < i.length; + u++ ) - i[_].forEach(u); + i[u].forEach(a); }) : 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; + function updateInDeepMap(s, o, i, a) { + var u = s === j, + _ = o.next(); + if (_.done) { + var w = u ? i : s, + x = a(w); + return x === w ? s : x; } - 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, $); + invariant(u || (s && s.set), 'invalid keyPath'); + var C = _.value, + L = u ? j : s.get(C, j), + B = updateInDeepMap(L, o, i, a); + return B === L ? s : B === j ? s.remove(C) : (u ? emptyMap() : s).set(C, B); } function popCount(s) { return ( @@ -6855,168 +6066,164 @@ 127 & (s += s >> 16) ); } - function setIn(s, o, i, u) { - var _ = u ? s : arrCopy(s); - return ((_[o] = i), _); + function setIn(s, o, i, a) { + var u = a ? s : arrCopy(s); + return ((u[o] = i), u); } - 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); + function spliceIn(s, o, i, a) { + var u = s.length + 1; + if (a && o + 1 === u) return ((s[o] = i), s); for (var _ = new Array(u), w = 0, x = 0; x < u; x++) - (x === o && (w = 1), (_[x] = s[x + w])); + x === o ? ((_[x] = i), (w = -1)) : (_[x] = s[x + w]); return _; } - (($e[qe] = !0), - ($e[w] = $e.remove), + function spliceOut(s, o, i) { + var a = s.length - 1; + if (i && o === a) return (s.pop(), s); + for (var u = new Array(a), _ = 0, w = 0; w < a; w++) + (w === o && (_ = 1), (u[w] = s[w + _])); + return u; + } + (($e[Re] = !0), + ($e[_] = $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.get = function (s, o, i, a) { + for (var u = this.entries, _ = 0, w = u.length; _ < w; _++) + if (is(i, u[_][0])) return u[_][1]; + return a; }), - (ArrayMapNode.prototype.update = function (s, o, i, u, _, w, x) { + (ArrayMapNode.prototype.update = function (s, o, i, a, u, _, w) { for ( - var C = _ === L, j = this.entries, B = 0, $ = j.length; - B < $ && !is(u, j[B][0]); - B++ + var x = u === j, C = this.entries, L = 0, B = C.length; + L < B && !is(a, C[L][0]); + L++ ); - 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 $ = L < B; + if ($ ? C[L][1] === u : x) return this; + if ((SetRef(w), (x || !$) && SetRef(_), !x || 1 !== C.length)) { + if (!$ && !x && C.length >= qe) return createNodes(s, C, a, u); var U = s && s === this.ownerID, - z = U ? j : arrCopy(j); + V = U ? C : arrCopy(C); 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) + $ + ? x + ? L === B - 1 + ? V.pop() + : (V[L] = V.pop()) + : (V[L] = [a, u]) + : V.push([a, u]), + U ? ((this.entries = V), this) : new ArrayMapNode(s, V) ); } }), - (BitmapIndexedNode.prototype.get = function (s, o, i, u) { + (BitmapIndexedNode.prototype.get = function (s, o, i, a) { 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; + var u = 1 << ((0 === s ? o : o >>> s) & C), + _ = this.bitmap; + return _ & u ? this.nodes[popCount(_ & (u - 1))].get(s + w, o, i, a) : a; }), - (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); + (BitmapIndexedNode.prototype.update = function (s, o, i, a, u, _, x) { + void 0 === i && (i = hash(a)); + var L = (0 === o ? i : i >>> o) & C, + B = 1 << L, + $ = this.bitmap, + U = !!($ & B); + if (!U && u === j) return this; + var V = popCount($ & (B - 1)), + z = this.nodes, + Y = U ? z[V] : void 0, + Z = updateNode(Y, s, o + w, i, a, u, _, x); + if (Z === Y) return this; + if (!U && Z && z.length >= ze) return expandNodes(s, z, $, L, Z); + if (U && !Z && 2 === z.length && isLeafNode(z[1 ^ V])) return z[1 ^ V]; + if (U && Z && 1 === z.length && isLeafNode(Z)) return Z; + var ee = s && s === this.ownerID, + ie = U ? (Z ? $ : $ ^ B) : $ | B, + ae = U ? (Z ? setIn(z, V, Z, ee) : spliceOut(z, V, ee)) : spliceIn(z, V, Z, ee); + return ee + ? ((this.bitmap = ie), (this.nodes = ae), this) + : new BitmapIndexedNode(s, ie, ae); }), - (HashArrayMapNode.prototype.get = function (s, o, i, u) { + (HashArrayMapNode.prototype.get = function (s, o, i, a) { 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; + var u = (0 === s ? o : o >>> s) & C, + _ = this.nodes[u]; + return _ ? _.get(s + w, o, i, a) : a; }), - (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; + (HashArrayMapNode.prototype.update = function (s, o, i, a, u, _, x) { + void 0 === i && (i = hash(a)); + var L = (0 === o ? i : i >>> o) & C, + B = u === j, + $ = this.nodes, + U = $[L]; + if (B && !U) return this; + var V = updateNode(U, s, o + w, i, a, u, _, x); + if (V === U) return this; + var z = 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); + if (!V && --z < We) return packNodes(s, $, z, L); + } else z++; + var Y = s && s === this.ownerID, + Z = setIn($, L, V, Y); + return Y + ? ((this.count = z), (this.nodes = Z), this) + : new HashArrayMapNode(s, z, Z); }), - (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.get = function (s, o, i, a) { + for (var u = this.entries, _ = 0, w = u.length; _ < w; _++) + if (is(i, u[_][0])) return u[_][1]; + return a; }), - (HashCollisionNode.prototype.update = function (s, o, i, u, _, w, x) { - void 0 === i && (i = hash(u)); - var C = _ === L; + (HashCollisionNode.prototype.update = function (s, o, i, a, u, _, w) { + void 0 === i && (i = hash(a)); + var x = u === j; 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]); + return x ? this : (SetRef(w), SetRef(_), mergeIntoNode(this, s, o, i, [a, u])); + for (var C = this.entries, L = 0, B = C.length; L < B && !is(a, C[L][0]); L++); + var $ = L < B; + if ($ ? C[L][1] === u : x) return this; + if ((SetRef(w), (x || !$) && SetRef(_), x && 2 === B)) + return new ValueNode(s, this.keyHash, C[1 ^ L]); var U = s && s === this.ownerID, - z = U ? j : arrCopy(j); + V = U ? C : arrCopy(C); 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) + $ + ? x + ? L === B - 1 + ? V.pop() + : (V[L] = V.pop()) + : (V[L] = [a, u]) + : V.push([a, u]), + U ? ((this.entries = V), this) : new HashCollisionNode(s, this.keyHash, V) ); }), - (ValueNode.prototype.get = function (s, o, i, u) { - return is(i, this.entry[0]) ? this.entry[1] : u; + (ValueNode.prototype.get = function (s, o, i, a) { + return is(i, this.entry[0]) ? this.entry[1] : a; }), - (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) + (ValueNode.prototype.update = function (s, o, i, a, u, _, w) { + var x = u === j, + C = is(a, this.entry[0]); + return (C ? u === this.entry[1] : x) ? this - : (SetRef(x), - C - ? void SetRef(w) - : j + : (SetRef(w), + x + ? void SetRef(_) + : C ? s && s === this.ownerID - ? ((this.entry[1] = _), this) - : new ValueNode(s, this.keyHash, [u, _]) - : (SetRef(w), mergeIntoNode(this, s, o, hash(u), [u, _]))); + ? ((this.entry[1] = u), this) + : new ValueNode(s, this.keyHash, [a, u]) + : (SetRef(_), mergeIntoNode(this, s, o, hash(a), [a, 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; + for (var i = this.entries, a = 0, u = i.length - 1; a <= u; a++) + if (!1 === s(i[o ? u - a : a])) 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; + for (var i = this.nodes, a = 0, u = i.length - 1; a <= u; a++) { + var _ = i[o ? u - a : a]; + if (_ && !1 === _.iterate(s, o)) return !1; } }), (ValueNode.prototype.iterate = function (s, o) { @@ -7026,18 +6233,18 @@ (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); + a = o.node, + u = o.index++; + if (a.entry) { + if (0 === u) return mapIteratorValue(s, a.entry); + } else if (a.entries) { + if (u <= (i = a.entries.length - 1)) + return mapIteratorValue(s, a.entries[this._reverse ? i - u : u]); + } else if (u <= (i = a.nodes.length - 1)) { + var _ = a.nodes[this._reverse ? i - u : u]; + if (_) { + if (_.entry) return mapIteratorValue(s, _.entry); + o = this._stack = mapIteratorFrame(_, o); } continue; } @@ -7045,29 +6252,29 @@ } return iteratorDone(); })); - var ze = C / 4, - We = C / 2, - He = C / 4; + var qe = x / 4, + ze = x / 2, + We = x / 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 + a = i.size; + return 0 === a ? o - : (assertNotInfinite(u), - u > 0 && u < C - ? makeList(0, u, x, null, new VNode(i.toArray())) + : (assertNotInfinite(a), + a > 0 && a < x + ? makeList(0, a, w, null, new VNode(i.toArray())) : o.withMutations(function (s) { - (s.setSize(u), + (s.setSize(a), i.forEach(function (o, i) { return s.set(i, o); })); })); } function isList(s) { - return !(!s || !s[Ye]); + return !(!s || !s[He]); } (createClass(List, IndexedCollection), (List.of = function () { @@ -7079,7 +6286,7 @@ (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 i && i.array[s & C]; } return o; }), @@ -7103,7 +6310,7 @@ ? this : this.__ownerID ? ((this.size = this._origin = this._capacity = 0), - (this._level = x), + (this._level = w), (this._root = this._tail = null), (this.__hash = void 0), (this.__altered = !0), @@ -7115,7 +6322,7 @@ 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]); + for (var a = 0; a < s.length; a++) i.set(o + a, s[a]); }); }), (List.prototype.pop = function () { @@ -7155,18 +6362,18 @@ }), (List.prototype.__iterator = function (s, o) { var i = 0, - u = iterateList(this, o); + a = iterateList(this, o); return new Iterator(function () { - var o = u(); - return o === tt ? iteratorDone() : iteratorValue(s, i++, o); + var o = a(); + return o === et ? 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); + var i, a = 0, u = iterateList(this, o); + (i = u()) !== et && !1 !== s(i, a++, this); ); - return u; + return a; }), (List.prototype.__ensureOwner = function (s) { return s === this.__ownerID @@ -7184,115 +6391,115 @@ : ((this.__ownerID = s), this); }), (List.isList = isList)); - var Ye = '@@__IMMUTABLE_LIST__@@', - Xe = List.prototype; + var He = '@@__IMMUTABLE_LIST__@@', + Ye = 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), + ((Ye[He] = !0), + (Ye[_] = Ye.remove), + (Ye.setIn = $e.setIn), + (Ye.deleteIn = Ye.removeIn = $e.removeIn), + (Ye.update = $e.update), + (Ye.updateIn = $e.updateIn), + (Ye.mergeIn = $e.mergeIn), + (Ye.mergeDeepIn = $e.mergeDeepIn), + (Ye.withMutations = $e.withMutations), + (Ye.asMutable = $e.asMutable), + (Ye.asImmutable = $e.asImmutable), + (Ye.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; + var a = (i >>> o) & C; + if (a >= this.array.length) return new VNode([], s); + var u, + _ = 0 === a; if (o > 0) { - var C = this.array[u]; - if ((_ = C && C.removeBefore(s, o - x, i)) === C && w) return this; + var x = this.array[a]; + if ((u = x && x.removeBefore(s, o - w, i)) === x && _) 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); + if (_ && !u) return this; + var j = editableVNode(this, s); + if (!_) for (var L = 0; L < a; L++) j.array[L] = void 0; + return (u && (j.array[a] = u), j); }), (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; + var a, + u = ((i - 1) >>> o) & C; + if (u >= 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) + var _ = this.array[u]; + if ((a = _ && _.removeAfter(s, o - w, i)) === _ && u === this.array.length - 1) return this; } - var C = editableVNode(this, s); - return (C.array.splice(_ + 1), u && (C.array[_] = u), C); + var x = editableVNode(this, s); + return (x.array.splice(u + 1), a && (x.array[u] = a), x); })); - var Qe, - et, - tt = {}; + var Xe, + Qe, + et = {}; function iterateList(s, o) { var i = s._origin, - u = s._capacity, - _ = getTailOffset(u), - w = s._tail; + a = s._capacity, + u = getTailOffset(a), + _ = 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; + function iterateLeaf(s, w) { + var C = w === u ? _ && _.array : s && s.array, + j = w > i ? 0 : i - w, + L = a - w; return ( - B > C && (B = C), + L > x && (L = x), function () { - if (L === B) return tt; - var s = o ? --B : L++; - return j && j[s]; + if (j === L) return et; + var s = o ? --L : j++; + return C && C[s]; } ); } - function iterateNode(s, _, w) { - var j, - L = s && s.array, - B = w > i ? 0 : (i - w) >> _, - $ = 1 + ((u - w) >> _); + function iterateNode(s, u, _) { + var C, + j = s && s.array, + L = _ > i ? 0 : (i - _) >> u, + B = 1 + ((a - _) >> u); return ( - $ > C && ($ = C), + B > x && (B = x), function () { for (;;) { - if (j) { - var s = j(); - if (s !== tt) return s; - j = null; + if (C) { + var s = C(); + if (s !== et) return s; + C = null; } - if (B === $) return tt; - var i = o ? --$ : B++; - j = iterateNodeOrLeaf(L && L[i], _ - x, w + (i << _)); + if (L === B) return et; + var i = o ? --B : L++; + C = iterateNodeOrLeaf(j && j[i], u - w, _ + (i << u)); } } ); } } - function makeList(s, o, i, u, _, w, x) { - var C = Object.create(Xe); + function makeList(s, o, i, a, u, _, w) { + var x = Object.create(Ye); 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 + (x.size = o - s), + (x._origin = s), + (x._capacity = o), + (x._level = i), + (x._root = a), + (x._tail = u), + (x.__ownerID = _), + (x.__hash = w), + (x.__altered = !1), + x ); } function emptyList() { - return Qe || (Qe = makeList(0, 0, x)); + return Xe || (Xe = makeList(0, 0, w)); } function updateList(s, o, i) { if ((o = wrapIndex(s, o)) != o) return s; @@ -7301,110 +6508,110 @@ 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($); + var a = s._tail, + u = s._root, + _ = MakeRef(B); return ( o >= getTailOffset(s._capacity) - ? (u = updateVNode(u, s.__ownerID, 0, o, i, w)) - : (_ = updateVNode(_, s.__ownerID, s._level, o, i, w)), - w.value + ? (a = updateVNode(a, s.__ownerID, 0, o, i, _)) + : (u = updateVNode(u, s.__ownerID, s._level, o, i, _)), + _.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._root = u), (s._tail = a), (s.__hash = void 0), (s.__altered = !0), s) + : makeList(s._origin, s._capacity, s._level, u, a) : 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; + function updateVNode(s, o, i, a, u, _) { + var x, + j = (a >>> i) & C, + L = s && j < s.array.length; + if (!L && void 0 === u) 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); + var B = s && s.array[j], + $ = updateVNode(B, o, i - w, a, u, _); + return $ === B ? s : (((x = editableVNode(s, o)).array[j] = $), x); } - return B && s.array[L] === _ + return L && s.array[j] === u ? s - : (SetRef(w), - (C = editableVNode(s, o)), - void 0 === _ && L === C.array.length - 1 ? C.array.pop() : (C.array[L] = _), - C); + : (SetRef(_), + (x = editableVNode(s, o)), + void 0 === u && j === x.array.length - 1 ? x.array.pop() : (x.array[j] = u), + x); } 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)); + if (o < 1 << (s._level + w)) { + for (var i = s._root, a = s._level; i && a > 0; ) + ((i = i.array[(o >>> a) & C]), (a -= w)); 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); + var a = s.__ownerID || new OwnerID(), + u = s._origin, + _ = s._capacity, + x = u + o, + j = void 0 === i ? _ : i < 0 ? _ + i : u + i; + if (x === u && j === _) return s; + if (x >= j) return s.clear(); + for (var L = s._level, B = s._root, $ = 0; x + $ < 0; ) + ((B = new VNode(B && B.array.length ? [void 0, B] : [], a)), ($ += 1 << (L += w))); + $ && ((x += $), (u += $), (j += $), (_ += $)); + for (var U = getTailOffset(_), V = getTailOffset(j); V >= 1 << (L + w); ) + ((B = new VNode(B && B.array.length ? [B] : [], a)), (L += w)); + var z = s._tail, + Y = V < U ? listNodeFor(s, j - 1) : V > U ? new VNode([], a) : z; + if (z && V > U && x < _ && z.array.length) { + for (var Z = (B = editableVNode(B, a)), ee = L; ee > w; ee -= w) { + var ie = (U >>> ee) & C; + Z = Z.array[ie] = editableVNode(Z.array[ie], a); } - ee.array[(U >>> x) & j] = Y; + Z.array[(U >>> w) & C] = z; } - 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])); + if ((j < _ && (Y = Y && Y.removeAfter(a, 0, j)), x >= V)) + ((x -= V), (j -= V), (L = w), (B = null), (Y = Y && Y.removeBefore(a, 0, x))); + else if (x > u || V < U) { + for ($ = 0; B; ) { + var ae = (x >>> L) & C; + if ((ae !== V >>> L) & C) break; + (ae && ($ += (1 << L) * ae), (L -= w), (B = B.array[ae])); } - ($ && C > _ && ($ = $.removeBefore(u, B, C - V)), - $ && z < U && ($ = $.removeAfter(u, B, z - V)), - V && ((C -= V), (L -= V))); + (B && x > u && (B = B.removeBefore(a, L, x - $)), + B && V < U && (B = B.removeAfter(a, L, V - $)), + $ && ((x -= $), (j -= $))); } return s.__ownerID - ? ((s.size = L - C), - (s._origin = C), - (s._capacity = L), - (s._level = B), - (s._root = $), - (s._tail = Z), + ? ((s.size = j - x), + (s._origin = x), + (s._capacity = j), + (s._level = L), + (s._root = B), + (s._tail = Y), (s.__hash = void 0), (s.__altered = !0), s) - : makeList(C, L, B, $, Z); + : makeList(x, j, L, B, Y); } 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) { + for (var a = [], u = 0, _ = 0; _ < i.length; _++) { + var w = i[_], + x = IndexedIterable(w); + (x.size > u && (u = x.size), + isIterable(w) || + (x = x.map(function (s) { return fromJS(s); })), - u.push(C)); + a.push(x)); } - return (_ > s.size && (s = s.setSize(_)), mergeIntoCollectionWith(s, o, u)); + return (u > s.size && (s = s.setSize(u)), mergeIntoCollectionWith(s, o, a)); } function getTailOffset(s) { - return s < C ? 0 : ((s - 1) >>> x) << x; + return s < x ? 0 : ((s - 1) >>> w) << w; } function OrderedMap(s) { return null == s @@ -7422,32 +6629,32 @@ function isOrderedMap(s) { return isMap(s) && isOrdered(s); } - function makeOrderedMap(s, o, i, u) { - var _ = Object.create(OrderedMap.prototype); + function makeOrderedMap(s, o, i, a) { + var u = Object.create(OrderedMap.prototype); return ( - (_.size = s ? s.size : 0), - (_._map = s), - (_._list = o), - (_.__ownerID = i), - (_.__hash = u), - _ + (u.size = s ? s.size : 0), + (u._map = s), + (u._list = o), + (u.__ownerID = i), + (u.__hash = a), + u ); } function emptyOrderedMap() { - return et || (et = makeOrderedMap(emptyMap(), emptyList())); + return Qe || (Qe = 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; + var a, + u, + _ = s._map, + w = s._list, + C = _.get(o), + L = void 0 !== C; + if (i === j) { + if (!L) return s; + w.size >= x && w.size >= 2 * _.size + ? ((a = (u = w.filter(function (s, o) { + return void 0 !== s && C !== o; })) .toKeyedSeq() .map(function (s) { @@ -7455,15 +6662,15 @@ }) .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]))); + s.__ownerID && (a.__ownerID = u.__ownerID = s.__ownerID)) + : ((a = _.remove(o)), (u = C === w.size - 1 ? w.pop() : w.set(C, void 0))); + } else if (L) { + if (i === w.get(C)[1]) return s; + ((a = _), (u = w.set(C, [o, i]))); + } else ((a = _.set(o, w.size)), (u = w.set(w.size, [o, i]))); return s.__ownerID - ? ((s.size = u.size), (s._map = u), (s._list = _), (s.__hash = void 0), s) - : makeOrderedMap(u, _); + ? ((s.size = a.size), (s._map = a), (s._list = u), (s.__hash = void 0), s) + : makeOrderedMap(a, u); } function ToKeyedSequence(s, o) { ((this._iter = s), (this._useKeys = o), (this.size = s.size)); @@ -7502,16 +6709,16 @@ }), (o.cacheResult = cacheResultThrough), (o.__iterateUncached = function (o, i) { - var u = this; + var a = this; return s.__iterate(function (s, i) { - return !1 !== o(i, s, u); + return !1 !== o(i, s, a); }, i); }), (o.__iteratorUncached = function (o, i) { - if (o === z) { - var u = s.__iterator(o, i); + if (o === V) { + var a = s.__iterator(o, i); return new Iterator(function () { - var s = u.next(); + var s = a.next(); if (!s.done) { var o = s.value[0]; ((s.value[0] = s.value[1]), (s.value[1] = o)); @@ -7519,39 +6726,39 @@ return s; }); } - return s.__iterator(o === U ? V : U, i); + return s.__iterator(o === U ? $ : U, i); }), o ); } function mapFactory(s, o, i) { - var u = makeSequence(s); + var a = makeSequence(s); return ( - (u.size = s.size), - (u.has = function (o) { + (a.size = s.size), + (a.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); + (a.get = function (a, u) { + var _ = s.get(a, j); + return _ === j ? u : o.call(i, _, a, s); }), - (u.__iterateUncached = function (u, _) { - var w = this; - return s.__iterate(function (s, _, x) { - return !1 !== u(o.call(i, s, _, x), _, w); - }, _); + (a.__iterateUncached = function (a, u) { + var _ = this; + return s.__iterate(function (s, u, w) { + return !1 !== a(o.call(i, s, u, w), u, _); + }, u); }), - (u.__iteratorUncached = function (u, _) { - var w = s.__iterator(z, _); + (a.__iteratorUncached = function (a, u) { + var _ = s.__iterator(V, u); 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), _); + var u = _.next(); + if (u.done) return u; + var w = u.value, + x = w[0]; + return iteratorValue(a, x, o.call(i, w[1], x, s), u); }); }), - u + a ); } function reverseFactory(s, o) { @@ -7572,8 +6779,8 @@ o ); }), - (i.get = function (i, u) { - return s.get(o ? i : -1 - i, u); + (i.get = function (i, a) { + return s.get(o ? i : -1 - i, a); }), (i.has = function (i) { return s.has(o ? i : -1 - i); @@ -7583,9 +6790,9 @@ }), (i.cacheResult = cacheResultThrough), (i.__iterate = function (o, i) { - var u = this; + var a = this; return s.__iterate(function (s, i) { - return o(s, i, u); + return o(s, i, a); }, !i); }), (i.__iterator = function (o, i) { @@ -7594,200 +6801,200 @@ 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) { + function filterFactory(s, o, i, a) { var u = makeSequence(s); return ( + a && + ((u.has = function (a) { + var u = s.get(a, j); + return u !== j && !!o.call(i, u, a, s); + }), + (u.get = function (a, u) { + var _ = s.get(a, j); + return _ !== j && o.call(i, _, a, s) ? _ : u; + })), (u.__iterateUncached = function (u, _) { - var w = this; - if (_) return this.cacheResult().__iterate(u, _); - var x = 0; + var w = this, + x = 0; return ( s.__iterate(function (s, _, C) { - return o.call(i, s, _, C) && ++x && u(s, _, w); - }), + if (o.call(i, s, _, C)) return (x++, u(s, a ? _ : x - 1, w)); + }, _), x ); }), (u.__iteratorUncached = function (u, _) { - var w = this; - if (_) return this.cacheResult().__iterator(u, _); - var x = s.__iterator(z, _), - C = !0; + var w = s.__iterator(V, _), + x = 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()); + for (;;) { + var _ = w.next(); + if (_.done) return _; + var C = _.value, + j = C[0], + L = C[1]; + if (o.call(i, L, j, s)) return iteratorValue(u, a ? j : x++, L, _); + } }); }), u ); } - function skipWhileFactory(s, o, i, u) { - var _ = makeSequence(s); + function countByFactory(s, o, i) { + var a = Map().asMutable(); return ( - (_.__iterateUncached = function (_, w) { - var x = this; - if (w) return this.cacheResult().__iterate(_, w); - var C = !0, + s.__iterate(function (u, _) { + a.update(o.call(i, u, _, s), 0, function (s) { + return s + 1; + }); + }), + a.asImmutable() + ); + } + function groupByFactory(s, o, i) { + var a = isKeyed(s), + u = (isOrdered(s) ? OrderedMap() : Map()).asMutable(); + s.__iterate(function (_, w) { + u.update(o.call(i, _, w, s), function (s) { + return ((s = s || []).push(a ? [w, _] : _), s); + }); + }); + var _ = iterableClass(s); + return u.map(function (o) { + return reify(s, _(o)); + }); + } + function sliceFactory(s, o, i, a) { + var u = s.size; + if ( + (void 0 !== o && (o |= 0), + void 0 !== i && (i === 1 / 0 ? (i = u) : (i |= 0)), + wholeSlice(o, i, u)) + ) + return s; + var _ = resolveBegin(o, u), + w = resolveEnd(i, u); + if (_ != _ || w != w) return sliceFactory(s.toSeq().cacheResult(), o, i, a); + var x, + C = w - _; + C == C && (x = C < 0 ? 0 : C); + var j = makeSequence(s); + return ( + (j.size = 0 === x ? x : (s.size && x) || void 0), + !a && + isSeq(s) && + x >= 0 && + (j.get = function (o, i) { + return (o = wrapIndex(this, o)) >= 0 && o < x ? s.get(o + _, i) : i; + }), + (j.__iterateUncached = function (o, i) { + var u = this; + if (0 === x) return 0; + if (i) return this.cacheResult().__iterate(o, i); + var w = 0, + 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)); + s.__iterate(function (s, i) { + if (!C || !(C = w++ < _)) + return (j++, !1 !== o(s, a ? i : j - 1, u) && j !== 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; + (j.__iteratorUncached = function (o, i) { + if (0 !== x && i) return this.cacheResult().__iterator(o, i); + var u = 0 !== x && s.__iterator(o, i), + w = 0, + C = 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); + for (; w++ < _; ) u.next(); + if (++C > x) return iteratorDone(); + var s = u.next(); + return a || o === U + ? s + : iteratorValue(o, C - 1, o === $ ? void 0 : s.value[1], s); }); }), - _ + j + ); + } + function takeWhileFactory(s, o, i) { + var a = makeSequence(s); + return ( + (a.__iterateUncached = function (a, u) { + var _ = this; + if (u) return this.cacheResult().__iterate(a, u); + var w = 0; + return ( + s.__iterate(function (s, u, x) { + return o.call(i, s, u, x) && ++w && a(s, u, _); + }), + w + ); + }), + (a.__iteratorUncached = function (a, u) { + var _ = this; + if (u) return this.cacheResult().__iterator(a, u); + var w = s.__iterator(V, u), + x = !0; + return new Iterator(function () { + if (!x) return iteratorDone(); + var s = w.next(); + if (s.done) return s; + var u = s.value, + C = u[0], + j = u[1]; + return o.call(i, j, C, _) + ? a === V + ? s + : iteratorValue(a, C, j, s) + : ((x = !1), iteratorDone()); + }); + }), + a + ); + } + function skipWhileFactory(s, o, i, a) { + var u = makeSequence(s); + return ( + (u.__iterateUncached = function (u, _) { + var w = this; + if (_) return this.cacheResult().__iterate(u, _); + var x = !0, + C = 0; + return ( + s.__iterate(function (s, _, j) { + if (!x || !(x = o.call(i, s, _, j))) return (C++, u(s, a ? _ : C - 1, w)); + }), + C + ); + }), + (u.__iteratorUncached = function (u, _) { + var w = this; + if (_) return this.cacheResult().__iterator(u, _); + var x = s.__iterator(V, _), + C = !0, + j = 0; + return new Iterator(function () { + var s, _, L; + do { + if ((s = x.next()).done) + return a || u === U + ? s + : iteratorValue(u, j++, u === $ ? void 0 : s.value[1], s); + var B = s.value; + ((_ = B[0]), (L = B[1]), C && (C = o.call(i, L, _, w))); + } while (C); + return u === V ? s : iteratorValue(u, _, L, s); + }); + }), + u ); } function concatFactory(s, o) { var i = isKeyed(s), - u = [s] + a = [s] .concat(o) .map(function (s) { return ( @@ -7802,68 +7009,68 @@ .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 _; + if (0 === a.length) return s; + if (1 === a.length) { + var u = a[0]; + if (u === s || (i && isKeyed(u)) || (isIndexed(s) && isIndexed(u))) return u; } - var w = new ArraySeq(u); + var _ = new ArraySeq(a); return ( - i ? (w = w.toKeyedSeq()) : isIndexed(s) || (w = w.toSetSeq()), - ((w = w.flatten(!0)).size = u.reduce(function (s, o) { + i ? (_ = _.toKeyedSeq()) : isIndexed(s) || (_ = _.toSetSeq()), + ((_ = _.flatten(!0)).size = a.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); + var a = makeSequence(s); return ( - (u.__iterateUncached = function (u, _) { - var w = 0, - x = !1; - function flatDeep(s, C) { - var j = this; - s.__iterate(function (s, _) { + (a.__iterateUncached = function (a, u) { + var _ = 0, + w = !1; + function flatDeep(s, x) { + var C = this; + s.__iterate(function (s, u) { return ( - (!o || C < o) && isIterable(s) - ? flatDeep(s, C + 1) - : !1 === u(s, i ? _ : w++, j) && (x = !0), - !x + (!o || x < o) && isIterable(s) + ? flatDeep(s, x + 1) + : !1 === a(s, i ? u : _++, C) && (w = !0), + !w ); - }, _); + }, u); } - return (flatDeep(s, 0), w); + return (flatDeep(s, 0), _); }), - (u.__iteratorUncached = function (u, _) { - var w = s.__iterator(u, _), - x = [], - C = 0; + (a.__iteratorUncached = function (a, u) { + var _ = s.__iterator(a, u), + w = [], + x = 0; return new Iterator(function () { - for (; w; ) { - var s = w.next(); + for (; _; ) { + var s = _.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(); + var C = s.value; + if ((a === V && (C = C[1]), (o && !(w.length < o)) || !isIterable(C))) + return i ? s : iteratorValue(a, x++, C, s); + (w.push(_), (_ = C.__iterator(a, u))); + } else _ = w.pop(); } return iteratorDone(); }); }), - u + a ); } function flatMapFactory(s, o, i) { - var u = iterableClass(s); + var a = iterableClass(s); return s .toSeq() - .map(function (_, w) { - return u(o.call(i, _, w, s)); + .map(function (u, _) { + return a(o.call(i, u, _, s)); }) .flatten(!0); } @@ -7871,26 +7078,26 @@ var i = makeSequence(s); return ( (i.size = s.size && 2 * s.size - 1), - (i.__iterateUncached = function (i, u) { - var _ = this, - w = 0; + (i.__iterateUncached = function (i, a) { + var u = this, + _ = 0; return ( - s.__iterate(function (s, u) { - return (!w || !1 !== i(o, w++, _)) && !1 !== i(s, w++, _); - }, u), - w + s.__iterate(function (s, a) { + return (!_ || !1 !== i(o, _++, u)) && !1 !== i(s, _++, u); + }, a), + _ ); }), - (i.__iteratorUncached = function (i, u) { - var _, - w = s.__iterator(U, u), - x = 0; + (i.__iteratorUncached = function (i, a) { + var u, + _ = s.__iterator(U, a), + w = 0; return new Iterator(function () { - return (!_ || x % 2) && (_ = w.next()).done - ? _ - : x % 2 - ? iteratorValue(i, x++, o) - : iteratorValue(i, x++, _.value, _); + return (!u || w % 2) && (u = _.next()).done + ? u + : w % 2 + ? iteratorValue(i, w++, o) + : iteratorValue(i, w++, u.value, u); }); }), i @@ -7898,87 +7105,85 @@ } function sortFactory(s, o, i) { o || (o = defaultComparator); - var u = isKeyed(s), - _ = 0, - w = s + var a = isKeyed(s), + u = 0, + _ = s .toSeq() - .map(function (o, u) { - return [u, o, _++, i ? i(o, u, s) : o]; + .map(function (o, a) { + return [a, o, u++, i ? i(o, a, 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) + _.sort(function (s, i) { + return o(s[3], i[3]) || s[2] - i[2]; + }).forEach( + a + ? function (s, o) { + _[o].length = 2; + } + : function (s, o) { + _[o] = s[1]; + } + ), + a ? KeyedSeq(_) : isIndexed(s) ? IndexedSeq(_) : SetSeq(_) ); } function maxFactory(s, o, i) { if ((o || (o = defaultComparator), i)) { - var u = s + var a = s .toSeq() - .map(function (o, u) { - return [o, i(o, u, s)]; + .map(function (o, a) { + return [o, i(o, a, s)]; }) .reduce(function (s, i) { return maxCompare(o, s[1], i[1]) ? i : s; }); - return u && u[0]; + return a && a[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; + var a = s(i, o); + return (0 === a && i !== o && (null == i || i != i)) || a > 0; } function zipWithFactory(s, o, i) { - var u = makeSequence(s); + var a = makeSequence(s); return ( - (u.size = new ArraySeq(i) + (a.size = new ArraySeq(i) .map(function (s) { return s.size; }) .min()), - (u.__iterate = function (s, o) { + (a.__iterate = function (s, o) { for ( - var i, u = this.__iterator(U, o), _ = 0; - !(i = u.next()).done && !1 !== s(i.value, _++, this); + var i, a = this.__iterator(U, o), u = 0; + !(i = a.next()).done && !1 !== s(i.value, u++, this); ); - return _; + return u; }), - (u.__iteratorUncached = function (s, u) { - var _ = i.map(function (s) { - return ((s = Iterable(s)), getIterator(u ? s.reverse() : s)); + (a.__iteratorUncached = function (s, a) { + var u = i.map(function (s) { + return ((s = Iterable(s)), getIterator(a ? s.reverse() : s)); }), - w = 0, - x = !1; + _ = 0, + w = !1; return new Iterator(function () { var i; return ( - x || - ((i = _.map(function (s) { + w || + ((i = u.map(function (s) { return s.next(); })), - (x = i.some(function (s) { + (w = i.some(function (s) { return s.done; }))), - x + w ? iteratorDone() : iteratorValue( s, - w++, + _++, o.apply( null, i.map(function (s) { @@ -7989,7 +7194,7 @@ ); }); }), - u + a ); } function reify(s, o) { @@ -8027,22 +7232,22 @@ } function Record(s, o) { var i, - u = function Record(w) { - if (w instanceof u) return w; - if (!(this instanceof u)) return new u(w); + a = function Record(_) { + if (_ instanceof a) return _; + if (!(this instanceof a)) return new a(_); if (!i) { i = !0; - var x = Object.keys(s); - (setProps(_, x), - (_.size = x.length), - (_._name = o), - (_._keys = x), - (_._defaultValues = s)); + var w = Object.keys(s); + (setProps(u, w), + (u.size = w.length), + (u._name = o), + (u._keys = w), + (u._defaultValues = s)); } - this._map = Map(w); + this._map = Map(_); }, - _ = (u.prototype = Object.create(rt)); - return ((_.constructor = u), u); + u = (a.prototype = Object.create(tt)); + return ((u.constructor = a), a); } (createClass(OrderedMap, Map), (OrderedMap.of = function () { @@ -8066,7 +7271,7 @@ return updateOrderedMap(this, s, o); }), (OrderedMap.prototype.remove = function (s) { - return updateOrderedMap(this, s, L); + return updateOrderedMap(this, s, j); }), (OrderedMap.prototype.wasAltered = function () { return this._map.wasAltered() || this._list.wasAltered(); @@ -8089,8 +7294,8 @@ : ((this.__ownerID = s), (this._map = o), (this._list = i), this); }), (OrderedMap.isOrderedMap = isOrderedMap), - (OrderedMap.prototype[_] = !0), - (OrderedMap.prototype[w] = OrderedMap.prototype.remove), + (OrderedMap.prototype[u] = !0), + (OrderedMap.prototype[_] = OrderedMap.prototype.remove), createClass(ToKeyedSequence, KeyedSeq), (ToKeyedSequence.prototype.get = function (s, o) { return this._iter.get(s, o); @@ -8114,26 +7319,26 @@ }), (ToKeyedSequence.prototype.map = function (s, o) { var i = this, - u = mapFactory(this, s, o); + a = mapFactory(this, s, o); return ( this._useKeys || - (u.valueSeq = function () { + (a.valueSeq = function () { return i._iter.toSeq().map(s, o); }), - u + a ); }), (ToKeyedSequence.prototype.__iterate = function (s, o) { var i, - u = this; + a = this; return this._iter.__iterate( this._useKeys ? function (o, i) { - return s(o, i, u); + return s(o, i, a); } : ((i = o ? resolveSize(this) : 0), - function (_) { - return s(_, o ? --i : i++, u); + function (u) { + return s(u, o ? --i : i++, a); }), o ); @@ -8141,30 +7346,30 @@ (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; + a = o ? resolveSize(this) : 0; return new Iterator(function () { - var _ = i.next(); - return _.done ? _ : iteratorValue(s, o ? --u : u++, _.value, _); + var u = i.next(); + return u.done ? u : iteratorValue(s, o ? --a : a++, u.value, u); }); }), - (ToKeyedSequence.prototype[_] = !0), + (ToKeyedSequence.prototype[u] = !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; + a = 0; return this._iter.__iterate(function (o) { - return s(o, u++, i); + return s(o, a++, i); }, o); }), (ToIndexedSequence.prototype.__iterator = function (s, o) { var i = this._iter.__iterator(U, o), - u = 0; + a = 0; return new Iterator(function () { var o = i.next(); - return o.done ? o : iteratorValue(s, u++, o.value, o); + return o.done ? o : iteratorValue(s, a++, o.value, o); }); }), createClass(ToSetSequence, SetSeq), @@ -8193,8 +7398,8 @@ 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); + var a = isIterable(o); + return s(a ? o.get(1) : o[1], a ? o.get(0) : o[0], i); } }, o); }), @@ -8204,11 +7409,11 @@ 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); + var a = o.value; + if (a) { + validateEntry(a); + var u = isIterable(a); + return iteratorValue(s, u ? a.get(0) : a[0], u ? a.get(1) : a[1], o); } } }); @@ -8271,10 +7476,10 @@ 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; + var tt = Record.prototype; function makeRecord(s, o, i) { - var u = Object.create(Object.getPrototypeOf(s)); - return ((u._map = o), (u.__ownerID = i), u); + var a = Object.create(Object.getPrototypeOf(s)); + return ((a._map = o), (a.__ownerID = i), a); } function recordName(s) { return s._name || s.constructor.name || 'Record'; @@ -8308,22 +7513,22 @@ }); } function isSet(s) { - return !(!s || !s[st]); + return !(!s || !s[nt]); } - ((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), + ((tt[_] = tt.remove), + (tt.deleteIn = tt.removeIn = $e.removeIn), + (tt.merge = $e.merge), + (tt.mergeWith = $e.mergeWith), + (tt.mergeIn = $e.mergeIn), + (tt.mergeDeep = $e.mergeDeep), + (tt.mergeDeepWith = $e.mergeDeepWith), + (tt.mergeDeepIn = $e.mergeDeepIn), + (tt.setIn = $e.setIn), + (tt.update = $e.update), + (tt.updateIn = $e.updateIn), + (tt.withMutations = $e.withMutations), + (tt.asMutable = $e.asMutable), + (tt.asImmutable = $e.asImmutable), createClass(Set, SetCollection), (Set.of = function () { return this(arguments); @@ -8410,8 +7615,8 @@ }), (Set.prototype.__iterate = function (s, o) { var i = this; - return this._map.__iterate(function (o, u) { - return s(u, u, i); + return this._map.__iterate(function (o, a) { + return s(a, a, i); }, o); }), (Set.prototype.__iterator = function (s, o) { @@ -8427,9 +7632,9 @@ return s ? this.__make(o, s) : ((this.__ownerID = s), (this._map = o), this); }), (Set.isSet = isSet)); - var nt, - st = '@@__IMMUTABLE_SET__@@', - ot = Set.prototype; + var rt, + nt = '@@__IMMUTABLE_SET__@@', + st = Set.prototype; function updateSet(s, o) { return s.__ownerID ? ((s.size = o.size), (s._map = o), s) @@ -8440,11 +7645,11 @@ : s.__make(o); } function makeSet(s, o) { - var i = Object.create(ot); + var i = Object.create(st); return ((i.size = s ? s.size : 0), (i._map = s), (i.__ownerID = o), i); } function emptySet() { - return nt || (nt = makeSet(emptyMap())); + return rt || (rt = makeSet(emptyMap())); } function OrderedSet(s) { return null == s @@ -8462,15 +7667,15 @@ 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), + ((st[nt] = !0), + (st[_] = st.remove), + (st.mergeDeep = st.merge), + (st.mergeDeepWith = st.mergeWith), + (st.withMutations = $e.withMutations), + (st.asMutable = $e.asMutable), + (st.asImmutable = $e.asImmutable), + (st.__empty = emptySet), + (st.__make = makeSet), createClass(OrderedSet, Set), (OrderedSet.of = function () { return this(arguments); @@ -8482,14 +7687,14 @@ return this.__toString('OrderedSet {', '}'); }), (OrderedSet.isOrderedSet = isOrderedSet)); - var it, - at = OrderedSet.prototype; + var ot, + it = OrderedSet.prototype; function makeOrderedSet(s, o) { - var i = Object.create(at); + var i = Object.create(it); return ((i.size = s ? s.size : 0), (i._map = s), (i.__ownerID = o), i); } function emptyOrderedSet() { - return it || (it = makeOrderedSet(emptyOrderedMap())); + return ot || (ot = makeOrderedSet(emptyOrderedMap())); } function Stack(s) { return null == s ? emptyStack() : isStack(s) ? s : emptyStack().unshiftAll(s); @@ -8497,9 +7702,9 @@ function isStack(s) { return !(!s || !s[ct]); } - ((at[_] = !0), - (at.__empty = emptyOrderedSet), - (at.__make = makeOrderedSet), + ((it[u] = !0), + (it.__empty = emptyOrderedSet), + (it.__make = makeOrderedSet), createClass(Stack, IndexedCollection), (Stack.of = function () { return this(arguments); @@ -8577,14 +7782,14 @@ 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; + for (var a = this.size - i, u = this._head; i--; ) u = u.next; return this.__ownerID - ? ((this.size = u), - (this._head = _), + ? ((this.size = a), + (this._head = u), (this.__hash = void 0), (this.__altered = !0), this) - : makeStack(u, _); + : makeStack(a, u); }), (Stack.prototype.__ensureOwner = function (s) { return s === this.__ownerID @@ -8595,38 +7800,38 @@ }), (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; + for (var i = 0, a = this._head; a && !1 !== s(a.value, i++, this); ) a = a.next; return i; }), (Stack.prototype.__iterator = function (s, o) { if (o) return this.reverse().__iterator(s); var i = 0, - u = this._head; + a = this._head; return new Iterator(function () { - if (u) { - var o = u.value; - return ((u = u.next), iteratorValue(s, i++, o)); + if (a) { + var o = a.value; + return ((a = a.next), iteratorValue(s, i++, o)); } return iteratorDone(); }); }), (Stack.isStack = isStack)); - var lt, + var at, ct = '@@__IMMUTABLE_STACK__@@', - ut = Stack.prototype; - function makeStack(s, o, i, u) { - var _ = Object.create(ut); + lt = Stack.prototype; + function makeStack(s, o, i, a) { + var u = Object.create(lt); return ( - (_.size = s), - (_._head = o), - (_.__ownerID = i), - (_.__hash = u), - (_.__altered = !1), - _ + (u.size = s), + (u._head = o), + (u.__ownerID = i), + (u.__hash = a), + (u.__altered = !1), + u ); } function emptyStack() { - return lt || (lt = makeStack(0)); + return at || (at = makeStack(0)); } function mixin(s, o) { var keyCopier = function (i) { @@ -8638,11 +7843,14 @@ s ); } - ((ut[ct] = !0), - (ut.withMutations = $e.withMutations), - (ut.asMutable = $e.asMutable), - (ut.asImmutable = $e.asImmutable), - (ut.wasAltered = $e.wasAltered), + function isProtoKey(s) { + return 'string' == typeof s && ('__proto__' === s || 'constructor' === s); + } + ((lt[ct] = !0), + (lt.withMutations = $e.withMutations), + (lt.asMutable = $e.asMutable), + (lt.asImmutable = $e.asImmutable), + (lt.wasAltered = $e.wasAltered), (Iterable.Iterator = Iterator), mixin(Iterable, { toArray: function () { @@ -8683,7 +7891,7 @@ var s = {}; return ( this.__iterate(function (o, i) { - s[i] = o; + isProtoKey(i) || (s[i] = o); }), s ); @@ -8730,14 +7938,14 @@ }); }, entries: function () { - return this.__iterator(z); + return this.__iterator(V); }, 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); + this.__iterate(function (a, u, _) { + if (!s.call(o, a, u, _)) return ((i = !1), !1); }), i ); @@ -8746,8 +7954,8 @@ return reify(this, filterFactory(this, s, o, !0)); }, find: function (s, o, i) { - var u = this.findEntry(s, o); - return u ? u[1] : i; + var a = this.findEntry(s, o); + return a ? a[1] : i; }, forEach: function (s, o) { return (assertNotInfinite(this.size), this.__iterate(o ? s.bind(o) : s)); @@ -8757,32 +7965,32 @@ var o = '', i = !0; return ( - this.__iterate(function (u) { - (i ? (i = !1) : (o += s), (o += null != u ? u.toString() : '')); + this.__iterate(function (a) { + (i ? (i = !1) : (o += s), (o += null != a ? a.toString() : '')); }), o ); }, keys: function () { - return this.__iterator(V); + return this.__iterator($); }, map: function (s, o) { return reify(this, mapFactory(this, s, o)); }, reduce: function (s, o, i) { - var u, _; + var a, 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)); + arguments.length < 2 ? (u = !0) : (a = o), + this.__iterate(function (o, _, w) { + u ? ((u = !1), (a = o)) : (a = s.call(i, a, o, _, w)); }), - u + a ); }, reduceRight: function (s, o, i) { - var u = this.toKeyedSeq().reverse(); - return u.reduce.apply(u, arguments); + var a = this.toKeyedSeq().reverse(); + return a.reduce.apply(a, arguments); }, reverse: function () { return reify(this, reverseFactory(this, !0)); @@ -8833,12 +8041,12 @@ return this.filter(not(s), o); }, findEntry: function (s, o, i) { - var u = i; + var a = i; return ( - this.__iterate(function (i, _, w) { - if (s.call(o, i, _, w)) return ((u = [_, i]), !1); + this.__iterate(function (i, u, _) { + if (s.call(o, i, u, _)) return ((a = [u, i]), !1); }), - u + a ); }, findKey: function (s, o) { @@ -8876,20 +8084,20 @@ ); }, 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; + for (var i, a = this, u = forceIterator(s); !(i = u.next()).done; ) { + var _ = i.value; + if ((a = a && a.get ? a.get(_, j) : j) === j) return o; } - return u; + return a; }, groupBy: function (s, o) { return groupByFactory(this, s, o); }, has: function (s) { - return this.get(s, L) !== L; + return this.get(s, j) !== j; }, hasIn: function (s) { - return this.getIn(s, L) !== L; + return this.getIn(s, j) !== j; }, isSubset: function (s) { return ( @@ -8965,29 +8173,29 @@ 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 = + var ut = Iterable.prototype; + ((ut[o] = !0), + (ut[Z] = ut.values), + (ut.__toJS = ut.toArray), + (ut.__toStringMapper = quoteString), + (ut.inspect = ut.toSource = function () { return this.toString(); }), - (pt.chain = pt.flatMap), - (pt.contains = pt.includes), + (ut.chain = ut.flatMap), + (ut.contains = ut.includes), mixin(KeyedIterable, { flip: function () { return reify(this, flipFactory(this)); }, mapEntries: function (s, o) { var i = this, - u = 0; + a = 0; return reify( this, this.toSeq() - .map(function (_, w) { - return s.call(o, [w, _], u++, i); + .map(function (u, _) { + return s.call(o, [_, u], a++, i); }) .fromEntrySeq() ); @@ -8998,14 +8206,14 @@ this, this.toSeq() .flip() - .map(function (u, _) { - return s.call(o, u, _, i); + .map(function (a, u) { + return s.call(o, a, u, i); }) .flip() ); } })); - var ht = KeyedIterable.prototype; + var pt = KeyedIterable.prototype; function keyMapper(s, o) { return o; } @@ -9035,45 +8243,45 @@ if (s.size === 1 / 0) return 0; var o = isOrdered(s), i = isKeyed(s), - u = o ? 1 : 0; + a = o ? 1 : 0; return murmurHashOfSize( s.__iterate( i ? o ? function (s, o) { - u = (31 * u + hashMerge(hash(s), hash(o))) | 0; + a = (31 * a + hashMerge(hash(s), hash(o))) | 0; } : function (s, o) { - u = (u + hashMerge(hash(s), hash(o))) | 0; + a = (a + hashMerge(hash(s), hash(o))) | 0; } : o ? function (s) { - u = (31 * u + hash(s)) | 0; + a = (31 * a + hash(s)) | 0; } : function (s) { - u = (u + hash(s)) | 0; + a = (a + hash(s)) | 0; } ), - u + a ); } 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))) + (o = le(o, 3432918353)), + (o = le((o << 15) | (o >>> -15), 461845907)), + (o = le((o << 13) | (o >>> -13), 5)), + (o = le((o = (o + 3864292196) ^ s) ^ (o >>> 16), 2246822507)), + (o = smi((o = le(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) { + (pt[i] = !0), + (pt[Z] = ut.entries), + (pt.__toJS = ut.toObject), + (pt.__toStringMapper = function (s, o) { return JSON.stringify(o) + ': ' + quoteString(s); }), mixin(IndexedIterable, { @@ -9105,10 +8313,10 @@ 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); + var a = this.slice(0, s); return reify( this, - 1 === i ? u : u.concat(arrCopy(arguments, 2), this.slice(s + o)) + 1 === i ? a : a.concat(arrCopy(arguments, 2), this.slice(s + o)) ); }, findLastIndex: function (s, o) { @@ -9171,8 +8379,8 @@ return ((o[0] = this), reify(this, zipWithFactory(this, s, o))); } }), + (IndexedIterable.prototype[a] = !0), (IndexedIterable.prototype[u] = !0), - (IndexedIterable.prototype[_] = !0), mixin(SetIterable, { get: function (s, o) { return this.has(s) ? s : o; @@ -9184,7 +8392,7 @@ return this.valueSeq(); } }), - (SetIterable.prototype.has = pt.includes), + (SetIterable.prototype.has = ut.includes), (SetIterable.prototype.contains = SetIterable.prototype.includes), mixin(KeyedSeq, KeyedIterable.prototype), mixin(IndexedSeq, IndexedIterable.prototype), @@ -9211,7 +8419,7 @@ ); })(); }, - 56698: (s) => { + 56698(s) { 'function' == typeof Object.create ? (s.exports = function inherits(s, o) { o && @@ -9230,44 +8438,152 @@ } }); }, - 5419: (s) => { - s.exports = function (s, o, i, u) { - var _ = new Blob(void 0 !== u ? [u, s] : [s], { + 69600(s) { + 'use strict'; + var o, + i, + a = Function.prototype.toString, + u = 'object' == typeof Reflect && null !== Reflect && Reflect.apply; + if ('function' == typeof u && 'function' == typeof Object.defineProperty) + try { + ((o = Object.defineProperty({}, 'length', { + get: function () { + throw i; + } + })), + (i = {}), + u( + function () { + throw 42; + }, + null, + o + )); + } catch (s) { + s !== i && (u = null); + } + else u = null; + var _ = /^\s*class\b/, + w = function isES6ClassFunction(s) { + try { + var o = a.call(s); + return _.test(o); + } catch (s) { + return !1; + } + }, + x = function tryFunctionToStr(s) { + try { + return !w(s) && (a.call(s), !0); + } catch (s) { + return !1; + } + }, + C = Object.prototype.toString, + j = 'function' == typeof Symbol && !!Symbol.toStringTag, + L = !(0 in [,]), + B = function isDocumentDotAll() { + return !1; + }; + if ('object' == typeof document) { + var $ = document.all; + C.call($) === C.call(document.all) && + (B = function isDocumentDotAll(s) { + if ((L || !s) && (void 0 === s || 'object' == typeof s)) + try { + var o = C.call(s); + return ( + ('[object HTMLAllCollection]' === o || + '[object HTML document.all class]' === o || + '[object HTMLCollection]' === o || + '[object Object]' === o) && + null == s('') + ); + } catch (s) {} + return !1; + }); + } + s.exports = u + ? function isCallable(s) { + if (B(s)) return !0; + if (!s) return !1; + if ('function' != typeof s && 'object' != typeof s) return !1; + try { + u(s, null, o); + } catch (s) { + if (s !== i) return !1; + } + return !w(s) && x(s); + } + : function isCallable(s) { + if (B(s)) return !0; + if (!s) return !1; + if ('function' != typeof s && 'object' != typeof s) return !1; + if (j) return x(s); + if (w(s)) return !1; + var o = C.call(s); + return ( + !( + '[object Function]' !== o && + '[object GeneratorFunction]' !== o && + !/^\[object HTML/.test(o) + ) && x(s) + ); + }; + }, + 35680(s, o, i) { + 'use strict'; + var a = i(25767); + s.exports = function isTypedArray(s) { + return !!a(s); + }; + }, + 64634(s) { + var o = {}.toString; + s.exports = + Array.isArray || + function (s) { + return '[object Array]' == o.call(s); + }; + }, + 5419(s) { + s.exports = function (s, o, i, a) { + var u = new Blob(void 0 !== a ? [a, s] : [s], { type: i || 'application/octet-stream' }); - if (void 0 !== window.navigator.msSaveBlob) window.navigator.msSaveBlob(_, o); + if (void 0 !== window.navigator.msSaveBlob) window.navigator.msSaveBlob(u, o); else { - var w = + var _ = 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(), + ? window.URL.createObjectURL(u) + : window.webkitURL.createObjectURL(u), + w = document.createElement('a'); + ((w.style.display = 'none'), + (w.href = _), + w.setAttribute('download', o), + void 0 === w.download && w.setAttribute('target', '_blank'), + document.body.appendChild(w), + w.click(), setTimeout(function () { - (document.body.removeChild(x), window.URL.revokeObjectURL(w)); + (document.body.removeChild(w), window.URL.revokeObjectURL(_)); }, 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, + 20181(s, o, i) { + var a = /^\s+|\s+$/g, + u = /^[-+]0x[0-9a-f]+$/i, + _ = /^0b[01]+$/i, + w = /^0o[0-7]+$/i, + x = parseInt, + C = 'object' == typeof i.g && i.g && i.g.Object === Object && i.g, + j = 'object' == typeof self && self && self.Object === Object && self, + L = C || j || Function('return this')(), + B = Object.prototype.toString, + $ = Math.max, U = Math.min, now = function () { - return B.Date.now(); + return L.Date.now(); }; function isObject(s) { var o = typeof s; @@ -9282,7 +8598,7 @@ ((function isObjectLike(s) { return !!s && 'object' == typeof s; })(s) && - '[object Symbol]' == $.call(s)) + '[object Symbol]' == B.call(s)) ); })(s) ) @@ -9292,101 +8608,101 @@ 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 = s.replace(a, ''); + var i = _.test(s); + return i || w.test(s) ? x(s.slice(2), i ? 2 : 8) : u.test(s) ? NaN : +s; } s.exports = function debounce(s, o, i) { - var u, + var a, + u, _, w, x, C, - j, - L = 0, + j = 0, + L = !1, B = !1, - $ = !1, - z = !0; + V = !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))); + var i = a, + _ = u; + return ((a = u = void 0), (j = o), (w = s.apply(_, i))); } function shouldInvoke(s) { - var i = s - j; - return void 0 === j || i >= o || i < 0 || ($ && s - L >= w); + var i = s - C; + return void 0 === C || i >= o || i < 0 || (B && s - j >= _); } function timerExpired() { var s = now(); if (shouldInvoke(s)) return trailingEdge(s); - C = setTimeout( + x = setTimeout( timerExpired, (function remainingWait(s) { - var i = o - (s - j); - return $ ? U(i, w - (s - L)) : i; + var i = o - (s - C); + return B ? U(i, _ - (s - j)) : i; })(s) ); } function trailingEdge(s) { - return ((C = void 0), z && u ? invokeFunc(s) : ((u = _ = void 0), x)); + return ((x = void 0), V && a ? invokeFunc(s) : ((a = u = void 0), w)); } function debounced() { var s = now(), i = shouldInvoke(s); - if (((u = arguments), (_ = this), (j = s), i)) { - if (void 0 === C) + if (((a = arguments), (u = this), (C = s), i)) { + if (void 0 === x) 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 ((j = s), (x = setTimeout(timerExpired, o)), L ? invokeFunc(s) : w); + })(C); + if (B) return ((x = setTimeout(timerExpired, o)), invokeFunc(C)); } - return (void 0 === C && (C = setTimeout(timerExpired, o)), x); + return (void 0 === x && (x = setTimeout(timerExpired, o)), w); } 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)), + ((L = !!i.leading), + (_ = (B = 'maxWait' in i) ? $(toNumber(i.maxWait) || 0, o) : _), + (V = 'trailing' in i ? !!i.trailing : V)), (debounced.cancel = function cancel() { - (void 0 !== C && clearTimeout(C), (L = 0), (u = j = _ = C = void 0)); + (void 0 !== x && clearTimeout(x), (j = 0), (a = C = u = x = void 0)); }), (debounced.flush = function flush() { - return void 0 === C ? x : trailingEdge(now()); + return void 0 === x ? w : trailingEdge(now()); }), debounced ); }; }, - 55580: (s, o, i) => { - var u = i(56110)(i(9325), 'DataView'); - s.exports = u; + 55580(s, o, i) { + var a = i(56110)(i(9325), 'DataView'); + s.exports = a; }, - 21549: (s, o, i) => { - var u = i(22032), - _ = i(63862), - w = i(66721), - x = i(12749), - C = i(35749); + 21549(s, o, i) { + var a = i(22032), + u = i(63862), + _ = i(66721), + w = i(12749), + x = 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]); + var a = s[o]; + this.set(a[0], a[1]); } } - ((Hash.prototype.clear = u), - (Hash.prototype.delete = _), - (Hash.prototype.get = w), - (Hash.prototype.has = x), - (Hash.prototype.set = C), + ((Hash.prototype.clear = a), + (Hash.prototype.delete = u), + (Hash.prototype.get = _), + (Hash.prototype.has = w), + (Hash.prototype.set = x), (s.exports = Hash)); }, - 30980: (s, o, i) => { - var u = i(39344), - _ = i(94033); + 30980(s, o, i) { + var a = i(39344), + u = i(94033); function LazyWrapper(s) { ((this.__wrapped__ = s), (this.__actions__ = []), @@ -9396,34 +8712,34 @@ (this.__takeCount__ = 4294967295), (this.__views__ = [])); } - ((LazyWrapper.prototype = u(_.prototype)), + ((LazyWrapper.prototype = a(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); + 80079(s, o, i) { + var a = i(63702), + u = i(70080), + _ = i(24739), + w = i(48655), + x = 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]); + var a = s[o]; + this.set(a[0], a[1]); } } - ((ListCache.prototype.clear = u), - (ListCache.prototype.delete = _), - (ListCache.prototype.get = w), - (ListCache.prototype.has = x), - (ListCache.prototype.set = C), + ((ListCache.prototype.clear = a), + (ListCache.prototype.delete = u), + (ListCache.prototype.get = _), + (ListCache.prototype.has = w), + (ListCache.prototype.set = x), (s.exports = ListCache)); }, - 56017: (s, o, i) => { - var u = i(39344), - _ = i(94033); + 56017(s, o, i) { + var a = i(39344), + u = i(94033); function LodashWrapper(s, o) { ((this.__wrapped__ = s), (this.__actions__ = []), @@ -9431,87 +8747,87 @@ (this.__index__ = 0), (this.__values__ = void 0)); } - ((LodashWrapper.prototype = u(_.prototype)), + ((LodashWrapper.prototype = a(u.prototype)), (LodashWrapper.prototype.constructor = LodashWrapper), (s.exports = LodashWrapper)); }, - 68223: (s, o, i) => { - var u = i(56110)(i(9325), 'Map'); - s.exports = u; + 68223(s, o, i) { + var a = i(56110)(i(9325), 'Map'); + s.exports = a; }, - 53661: (s, o, i) => { - var u = i(63040), - _ = i(17670), - w = i(90289), - x = i(4509), - C = i(72949); + 53661(s, o, i) { + var a = i(63040), + u = i(17670), + _ = i(90289), + w = i(4509), + x = 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]); + var a = s[o]; + this.set(a[0], a[1]); } } - ((MapCache.prototype.clear = u), - (MapCache.prototype.delete = _), - (MapCache.prototype.get = w), - (MapCache.prototype.has = x), - (MapCache.prototype.set = C), + ((MapCache.prototype.clear = a), + (MapCache.prototype.delete = u), + (MapCache.prototype.get = _), + (MapCache.prototype.has = w), + (MapCache.prototype.set = x), (s.exports = MapCache)); }, - 32804: (s, o, i) => { - var u = i(56110)(i(9325), 'Promise'); - s.exports = u; + 32804(s, o, i) { + var a = i(56110)(i(9325), 'Promise'); + s.exports = a; }, - 76545: (s, o, i) => { - var u = i(56110)(i(9325), 'Set'); - s.exports = u; + 76545(s, o, i) { + var a = i(56110)(i(9325), 'Set'); + s.exports = a; }, - 38859: (s, o, i) => { - var u = i(53661), - _ = i(31380), - w = i(51459); + 38859(s, o, i) { + var a = i(53661), + u = i(31380), + _ = 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]); + for (this.__data__ = new a(); ++o < i; ) this.add(s[o]); } - ((SetCache.prototype.add = SetCache.prototype.push = _), - (SetCache.prototype.has = w), + ((SetCache.prototype.add = SetCache.prototype.push = u), + (SetCache.prototype.has = _), (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); + 37217(s, o, i) { + var a = i(80079), + u = i(51420), + _ = i(90938), + w = i(63605), + x = i(29817), + C = i(80945); function Stack(s) { - var o = (this.__data__ = new u(s)); + var o = (this.__data__ = new a(s)); this.size = o.size; } - ((Stack.prototype.clear = _), - (Stack.prototype.delete = w), - (Stack.prototype.get = x), - (Stack.prototype.has = C), - (Stack.prototype.set = j), + ((Stack.prototype.clear = u), + (Stack.prototype.delete = _), + (Stack.prototype.get = w), + (Stack.prototype.has = x), + (Stack.prototype.set = C), (s.exports = Stack)); }, - 51873: (s, o, i) => { - var u = i(9325).Symbol; - s.exports = u; + 51873(s, o, i) { + var a = i(9325).Symbol; + s.exports = a; }, - 37828: (s, o, i) => { - var u = i(9325).Uint8Array; - s.exports = u; + 37828(s, o, i) { + var a = i(9325).Uint8Array; + s.exports = a; }, - 28303: (s, o, i) => { - var u = i(56110)(i(9325), 'WeakMap'); - s.exports = u; + 28303(s, o, i) { + var a = i(56110)(i(9325), 'WeakMap'); + s.exports = a; }, - 91033: (s) => { + 91033(s) { s.exports = function apply(s, o, i) { switch (i.length) { case 0: @@ -9526,1056 +8842,1066 @@ return s.apply(o, i); }; }, - 83729: (s) => { + 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); ); + for (var i = -1, a = null == s ? 0 : s.length; ++i < a && !1 !== o(s[i], i, s); ); return s; }; }, - 79770: (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); + for (var i = -1, a = null == s ? 0 : s.length, u = 0, _ = []; ++i < a; ) { + var w = s[i]; + o(w, i, s) && (_[u++] = w); } - 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) => { + 15325(s, o, i) { + var a = i(96131); + s.exports = function arrayIncludes(s, o) { + return !!(null == s ? 0 : s.length) && a(s, o, 0) > -1; + }; + }, + 70695(s, o, i) { + var a = i(78096), + u = i(72428), + _ = i(56449), + w = i(3656), + x = i(30361), + C = i(37167), + j = Object.prototype.hasOwnProperty; + s.exports = function arrayLikeKeys(s, o) { + var i = _(s), + L = !i && u(s), + B = !i && !L && w(s), + $ = !i && !L && !B && C(s), + U = i || L || B || $, + V = U ? a(s.length, String) : [], + z = V.length; + for (var Y in s) + (!o && !j.call(s, Y)) || + (U && + ('length' == Y || + (B && ('offset' == Y || 'parent' == Y)) || + ($ && ('buffer' == Y || 'byteLength' == Y || 'byteOffset' == Y)) || + x(Y, z))) || + V.push(Y); + return V; + }; + }, + 34932(s) { + s.exports = function arrayMap(s, o) { + for (var i = -1, a = null == s ? 0 : s.length, u = Array(a); ++i < a; ) + u[i] = o(s[i], i, s); + return u; + }; + }, + 14528(s) { s.exports = function arrayPush(s, o) { - for (var i = -1, u = o.length, _ = s.length; ++i < u; ) s[_ + i] = o[i]; + for (var i = -1, a = o.length, u = s.length; ++i < a; ) s[u + 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); + 40882(s) { + s.exports = function arrayReduce(s, o, i, a) { + var u = -1, + _ = null == s ? 0 : s.length; + for (a && _ && (i = s[++u]); ++u < _; ) i = o(i, s[u], u, s); return i; }; }, - 14248: (s) => { + 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; + for (var i = -1, a = null == s ? 0 : s.length; ++i < a; ) if (o(s[i], i, s)) return !0; return !1; }; }, - 61074: (s) => { + 61074(s) { s.exports = function asciiToArray(s) { return s.split(''); }; }, - 1733: (s) => { + 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); + 87805(s, o, i) { + var a = i(43360), + u = 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); + ((void 0 !== i && !u(s[o], i)) || (void 0 === i && !(o in s))) && a(s, o, i); }; }, - 16547: (s, o, i) => { - var u = i(43360), - _ = i(75288), - w = Object.prototype.hasOwnProperty; + 16547(s, o, i) { + var a = i(43360), + u = i(75288), + _ = 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); + var w = s[o]; + (_.call(s, o) && u(w, i) && (void 0 !== i || o in s)) || a(s, o, i); }; }, - 26025: (s, o, i) => { - var u = i(75288); + 26025(s, o, i) { + var a = i(75288); s.exports = function assocIndexOf(s, o) { - for (var i = s.length; i--; ) if (u(s[i][0], o)) return i; + for (var i = s.length; i--; ) if (a(s[i][0], o)) return i; return -1; }; }, - 74733: (s, o, i) => { - var u = i(21791), - _ = i(95950); + 74733(s, o, i) { + var a = i(21791), + u = i(95950); s.exports = function baseAssign(s, o) { - return s && u(o, _(o), s); + return s && a(o, u(o), s); }; }, - 43838: (s, o, i) => { - var u = i(21791), - _ = i(37241); + 43838(s, o, i) { + var a = i(21791), + u = i(37241); s.exports = function baseAssignIn(s, o) { - return s && u(o, _(o), s); + return s && a(o, u(o), s); }; }, - 43360: (s, o, i) => { - var u = i(93243); + 43360(s, o, i) { + var a = i(93243); s.exports = function baseAssignValue(s, o, i) { - '__proto__' == o && u - ? u(s, o, { configurable: !0, enumerable: !0, value: i, writable: !0 }) + '__proto__' == o && a + ? a(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), + 9999(s, o, i) { + var a = i(37217), + u = i(83729), + _ = i(16547), + w = i(74733), + x = i(43838), + C = i(93290), + j = i(23007), + L = i(92271), + B = i(48948), + $ = 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), + V = i(5861), + z = i(76189), + Y = i(77199), + Z = i(35529), + ee = i(56449), + ie = i(3656), + ae = 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]'] = + le = i(38440), + pe = i(95950), + de = i(37241), + fe = '[object Arguments]', + ye = '[object Function]', + be = '[object Object]', + Se = {}; + ((Se[fe] = + Se['[object Array]'] = + Se['[object ArrayBuffer]'] = + Se['[object DataView]'] = + Se['[object Boolean]'] = + Se['[object Date]'] = + Se['[object Float32Array]'] = + Se['[object Float64Array]'] = + Se['[object Int8Array]'] = + Se['[object Int16Array]'] = + Se['[object Int32Array]'] = + Se['[object Map]'] = + Se['[object Number]'] = + Se[be] = + Se['[object RegExp]'] = + Se['[object Set]'] = + Se['[object String]'] = + Se['[object Symbol]'] = + Se['[object Uint8Array]'] = + Se['[object Uint8ClampedArray]'] = + Se['[object Uint16Array]'] = + Se['[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, + (Se['[object Error]'] = Se[ye] = Se['[object WeakMap]'] = !1), + (s.exports = function baseClone(s, o, i, _e, we, xe) { + var Pe, + Te = 1 & o, + Re = 2 & o, $e = 4 & o; - if ((i && (Te = xe ? i(s, Se, xe, Pe) : i(s)), void 0 !== Te)) return Te; + if ((i && (Pe = we ? i(s, _e, we, xe) : i(s)), void 0 !== Pe)) return Pe; if (!ce(s)) return s; - var ze = ie(s); - if (ze) { - if (((Te = Y(s)), !Re)) return L(s, Te); + var qe = ee(s); + if (qe) { + if (((Pe = z(s)), !Te)) return j(s, Pe); } 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)); + var ze = V(s), + We = ze == ye || '[object GeneratorFunction]' == ze; + if (ie(s)) return C(s, Te); + if (ze == be || ze == fe || (We && !we)) { + if (((Pe = Re || We ? {} : Z(s)), !Te)) + return Re ? B(s, x(Pe, s)) : L(s, w(Pe, s)); } else { - if (!we[We]) return xe ? s : {}; - Te = Z(s, We, Re); + if (!Se[ze]) return we ? s : {}; + Pe = Y(s, ze, Te); } } - 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)); + xe || (xe = new a()); + var He = xe.get(s); + if (He) return He; + (xe.set(s, Pe), + le(s) + ? s.forEach(function (a) { + Pe.add(baseClone(a, o, i, a, s, xe)); }) - : le(s) && - s.forEach(function (u, _) { - Te.set(_, baseClone(u, o, i, _, s, Pe)); + : ae(s) && + s.forEach(function (a, u) { + Pe.set(u, baseClone(a, o, i, u, s, xe)); })); - var Xe = ze ? void 0 : ($e ? (qe ? U : V) : qe ? fe : de)(s); + var Ye = qe ? void 0 : ($e ? (Re ? U : $) : Re ? de : pe)(s); return ( - _(Xe || s, function (u, _) { - (Xe && (u = s[(_ = u)]), w(Te, _, baseClone(u, o, i, _, s, Pe))); + u(Ye || s, function (a, u) { + (Ye && (a = s[(u = a)]), _(Pe, u, baseClone(a, o, i, u, s, xe))); }), - Te + Pe ); })); }, - 39344: (s, o, i) => { - var u = i(23805), - _ = Object.create, - w = (function () { + 39344(s, o, i) { + var a = i(23805), + u = Object.create, + _ = (function () { function object() {} return function (s) { - if (!u(s)) return {}; - if (_) return _(s); + if (!a(s)) return {}; + if (u) return u(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; + 80909(s, o, i) { + var a = i(30641), + u = i(38329)(a); + s.exports = u; + }, + 2523(s) { + s.exports = function baseFindIndex(s, o, i, a) { + for (var u = s.length, _ = i + (a ? 1 : -1); a ? _-- : ++_ < u; ) + if (o(s[_], _, s)) return _; 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) + 83120(s, o, i) { + var a = i(14528), + u = i(45891); + s.exports = function baseFlatten(s, o, i, _, w) { + var x = -1, + C = s.length; + for (i || (i = u), w || (w = []); ++x < C; ) { + var j = s[x]; + o > 0 && i(j) ? o > 1 - ? baseFlatten(L, o - 1, i, w, x) - : u(x, L) - : w || (x[x.length] = L); + ? baseFlatten(j, o - 1, i, _, w) + : a(w, j) + : _ || (w[w.length] = j); } - return x; + return w; }; }, - 86649: (s, o, i) => { - var u = i(83221)(); - s.exports = u; + 86649(s, o, i) { + var a = i(83221)(); + s.exports = a; }, - 30641: (s, o, i) => { - var u = i(86649), - _ = i(95950); + 30641(s, o, i) { + var a = i(86649), + u = i(95950); s.exports = function baseForOwn(s, o) { - return s && u(s, o, _); + return s && a(s, o, u); }; }, - 47422: (s, o, i) => { - var u = i(31769), - _ = i(77797); + 47422(s, o, i) { + var a = i(31769), + u = 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; + for (var i = 0, _ = (o = a(o, s)).length; null != s && i < _; ) s = s[u(o[i++])]; + return i && i == _ ? s : void 0; }; }, - 82199: (s, o, i) => { - var u = i(14528), - _ = i(56449); + 82199(s, o, i) { + var a = i(14528), + u = i(56449); s.exports = function baseGetAllKeys(s, o, i) { - var w = o(s); - return _(s) ? w : u(w, i(s)); + var _ = o(s); + return u(s) ? _ : a(_, i(s)); }; }, - 72552: (s, o, i) => { - var u = i(51873), - _ = i(659), - w = i(59350), - x = u ? u.toStringTag : void 0; + 72552(s, o, i) { + var a = i(51873), + u = i(659), + _ = i(59350), + w = a ? a.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); + : w && w in Object(s) + ? u(s) + : _(s); }; }, - 20426: (s) => { + 20426(s) { var o = Object.prototype.hasOwnProperty; s.exports = function baseHas(s, i) { return null != s && o.call(s, i); }; }, - 28077: (s) => { + 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); + 96131(s, o, i) { + var a = i(2523), + u = i(85463), + _ = i(76959); s.exports = function baseIndexOf(s, o, i) { - return o == o ? w(s, o, i) : u(s, _, i); + return o == o ? _(s, o, i) : a(s, u, i); }; }, - 27534: (s, o, i) => { - var u = i(72552), - _ = i(40346); + 27534(s, o, i) { + var a = i(72552), + u = i(40346); s.exports = function baseIsArguments(s) { - return _(s) && '[object Arguments]' == u(s); + return u(s) && '[object Arguments]' == a(s); }; }, - 60270: (s, o, i) => { - var u = i(87068), - _ = i(40346); - s.exports = function baseIsEqual(s, o, i, w, x) { + 60270(s, o, i) { + var a = i(87068), + u = i(40346); + s.exports = function baseIsEqual(s, o, i, _, w) { return ( s === o || - (null == s || null == o || (!_(s) && !_(o)) + (null == s || null == o || (!u(s) && !u(o)) ? s != s && o != o - : u(s, o, i, w, baseIsEqual, x)) + : a(s, o, i, _, baseIsEqual, w)) ); }; }, - 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]', + 87068(s, o, i) { + var a = i(37217), + u = i(25911), + _ = i(21986), + w = i(50689), + x = i(5861), + C = i(56449), + j = i(3656), + L = i(37167), + B = '[object Arguments]', + $ = '[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)); + V = Object.prototype.hasOwnProperty; + s.exports = function baseIsEqualDeep(s, o, i, z, Y, Z) { + var ee = C(s), + ie = C(o), + ae = ee ? $ : x(s), + ce = ie ? $ : x(o), + le = (ae = ae == B ? U : ae) == U, + pe = (ce = ce == B ? U : ce) == U, + de = ae == ce; + if (de && j(s)) { + if (!j(o)) return !1; + ((ee = !0), (le = !1)); } - if (fe && !pe) + if (de && !le) return ( - ee || (ee = new u()), - ie || B(s) ? _(s, o, i, Y, Z, ee) : w(s, o, le, i, Y, Z, ee) + Z || (Z = new a()), + ee || L(s) ? u(s, o, i, z, Y, Z) : _(s, o, ae, i, z, Y, Z) ); 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)); + var fe = le && V.call(s, '__wrapped__'), + ye = pe && V.call(o, '__wrapped__'); + if (fe || ye) { + var be = fe ? s.value() : s, + Se = ye ? o.value() : o; + return (Z || (Z = new a()), Y(be, Se, i, z, Z)); } } - return !!fe && (ee || (ee = new u()), x(s, o, i, Y, Z, ee)); + return !!de && (Z || (Z = new a()), w(s, o, i, z, Y, Z)); }; }, - 29172: (s, o, i) => { - var u = i(5861), - _ = i(40346); + 29172(s, o, i) { + var a = i(5861), + u = i(40346); s.exports = function baseIsMap(s) { - return _(s) && '[object Map]' == u(s); + return u(s) && '[object Map]' == a(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; + 41799(s, o, i) { + var a = i(37217), + u = i(60270); + s.exports = function baseIsMatch(s, o, i, _) { + var w = i.length, + x = w, + C = !_; + if (null == s) return !x; + for (s = Object(s); w--; ) { + var j = i[w]; + if (C && j[2] ? j[1] !== s[j[0]] : !(j[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; + for (; ++w < x; ) { + var L = (j = i[w])[0], + B = s[L], + $ = j[1]; + if (C && j[2]) { + if (void 0 === B && !(L 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; + var U = new a(); + if (_) var V = _(B, $, L, s, o, U); + if (!(void 0 === V ? u($, B, 3, _, U) : V)) return !1; } } return !0; }; }, - 85463: (s) => { + 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( + 45083(s, o, i) { + var a = i(1882), + u = i(87296), + _ = i(23805), + w = i(47473), + x = /^\[object .+?Constructor\]$/, + C = Function.prototype, + j = Object.prototype, + L = C.toString, + B = j.hasOwnProperty, + $ = RegExp( '^' + - B.call($) + L.call(B) .replace(/[\\^$.*+?()[\]{}|]/g, '\\$&') .replace(/hasOwnProperty|(function).*?(?=\\\()| for .+?(?=\\\])/g, '$1.*?') + '$' ); s.exports = function baseIsNative(s) { - return !(!w(s) || _(s)) && (u(s) ? V : C).test(x(s)); + return !(!_(s) || u(s)) && (a(s) ? $ : x).test(w(s)); }; }, - 16038: (s, o, i) => { - var u = i(5861), - _ = i(40346); + 16038(s, o, i) { + var a = i(5861), + u = i(40346); s.exports = function baseIsSet(s) { - return _(s) && '[object Set]' == u(s); + return u(s) && '[object Set]' == a(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]'] = + 4901(s, o, i) { + var a = i(72552), + u = i(30294), + _ = i(40346), + w = {}; + ((w['[object Float32Array]'] = + w['[object Float64Array]'] = + w['[object Int8Array]'] = + w['[object Int16Array]'] = + w['[object Int32Array]'] = + w['[object Uint8Array]'] = + w['[object Uint8ClampedArray]'] = + w['[object Uint16Array]'] = + w['[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]'] = + (w['[object Arguments]'] = + w['[object Array]'] = + w['[object ArrayBuffer]'] = + w['[object Boolean]'] = + w['[object DataView]'] = + w['[object Date]'] = + w['[object Error]'] = + w['[object Function]'] = + w['[object Map]'] = + w['[object Number]'] = + w['[object Object]'] = + w['[object RegExp]'] = + w['[object Set]'] = + w['[object String]'] = + w['[object WeakMap]'] = !1), (s.exports = function baseIsTypedArray(s) { - return w(s) && _(s.length) && !!x[u(s)]; + return _(s) && u(s.length) && !!w[a(s)]; })); }, - 15389: (s, o, i) => { - var u = i(93663), - _ = i(87978), - w = i(83488), - x = i(56449), - C = i(50583); + 15389(s, o, i) { + var a = i(93663), + u = i(87978), + _ = i(83488), + w = i(56449), + x = 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); + ? w(s) + ? u(s[0], s[1]) + : a(s) + : x(s); }; }, - 88984: (s, o, i) => { - var u = i(55527), - _ = i(3650), - w = Object.prototype.hasOwnProperty; + 88984(s, o, i) { + var a = i(55527), + u = i(3650), + _ = Object.prototype.hasOwnProperty; s.exports = function baseKeys(s) { - if (!u(s)) return _(s); + if (!a(s)) return u(s); var o = []; - for (var i in Object(s)) w.call(s, i) && 'constructor' != i && o.push(i); + for (var i in Object(s)) _.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; + 72903(s, o, i) { + var a = i(23805), + u = i(55527), + _ = i(90181), + w = Object.prototype.hasOwnProperty; s.exports = function baseKeysIn(s) { - if (!u(s)) return w(s); - var o = _(s), + if (!a(s)) return _(s); + var o = u(s), i = []; - for (var C in s) ('constructor' != C || (!o && x.call(s, C))) && i.push(C); + for (var x in s) ('constructor' != x || (!o && w.call(s, x))) && i.push(x); return i; }; }, - 94033: (s) => { + 94033(s) { s.exports = function baseLodash() {}; }, - 93663: (s, o, i) => { - var u = i(41799), - _ = i(10776), - w = i(67197); + 93663(s, o, i) { + var a = i(41799), + u = i(10776), + _ = i(67197); s.exports = function baseMatches(s) { - var o = _(s); + var o = u(s); return 1 == o.length && o[0][2] - ? w(o[0][0], o[0][1]) + ? _(o[0][0], o[0][1]) : function (i) { - return i === s || u(i, s, o); + return i === s || a(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); + 87978(s, o, i) { + var a = i(60270), + u = i(58156), + _ = i(80631), + w = i(28586), + x = i(30756), + C = i(67197), + j = i(77797); s.exports = function baseMatchesProperty(s, o) { - return x(s) && C(o) - ? j(L(s), o) + return w(s) && x(o) + ? C(j(s), o) : function (i) { - var x = _(i, s); - return void 0 === x && x === o ? w(i, s) : u(o, x, 3); + var w = u(i, s); + return void 0 === w && w === o ? _(i, s) : a(o, w, 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, $) { + 85250(s, o, i) { + var a = i(37217), + u = i(87805), + _ = i(86649), + w = i(42824), + x = i(23805), + C = i(37241), + j = i(14974); + s.exports = function baseMerge(s, o, i, L, B) { s !== o && - w( + _( o, - function (w, j) { - if (($ || ($ = new u()), C(w))) x(s, o, j, i, baseMerge, B, $); + function (_, C) { + if ((B || (B = new a()), x(_))) w(s, o, C, i, baseMerge, L, B); else { - var V = B ? B(L(s, j), w, j + '', s, o, $) : void 0; - (void 0 === V && (V = w), _(s, j, V)); + var $ = L ? L(j(s, C), _, C + '', s, o, B) : void 0; + (void 0 === $ && ($ = _), u(s, C, $)); } }, - j + C ); }; }, - 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), + 42824(s, o, i) { + var a = i(87805), + u = i(93290), + _ = i(71961), + w = i(23007), + x = i(35529), + C = i(72428), + j = i(56449), + L = i(83693), + B = i(3656), + $ = 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); + V = i(11331), + z = i(37167), + Y = i(14974), + Z = i(69884); + s.exports = function baseMergeDeep(s, o, i, ee, ie, ae, ce) { + var le = Y(s, i), + pe = Y(o, i), + de = ce.get(pe); + if (de) a(s, i, de); 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)); + var fe = ae ? ae(le, pe, i + '', s, o, ce) : void 0, + ye = void 0 === fe; + if (ye) { + var be = j(pe), + Se = !be && B(pe), + _e = !be && !Se && z(pe); + ((fe = pe), + be || Se || _e + ? j(le) + ? (fe = le) + : L(le) + ? (fe = w(le)) + : Se + ? ((ye = !1), (fe = u(pe, !0))) + : _e + ? ((ye = !1), (fe = _(pe, !0))) + : (fe = []) + : V(pe) || C(pe) + ? ((fe = le), C(le) ? (fe = Z(le)) : (U(le) && !$(le)) || (fe = x(pe))) + : (ye = !1)); } - (be && (ce.set(de, ye), ae(ye, de, ie, le, ce), ce.delete(de)), u(s, i, ye)); + (ye && (ce.set(pe, fe), ie(fe, pe, ee, ae, ce), ce.delete(pe)), a(s, i, fe)); } }; }, - 47237: (s) => { + 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); + 17255(s, o, i) { + var a = i(47422); s.exports = function basePropertyDeep(s) { return function (o) { - return u(o, s); + return a(o, s); }; }; }, - 54552: (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, _) { + 85558(s) { + s.exports = function baseReduce(s, o, i, a, u) { return ( - _(s, function (s, _, w) { - i = u ? ((u = !1), s) : o(i, s, _, w); + u(s, function (s, u, _) { + i = a ? ((a = !1), s) : o(i, s, u, _); }), i ); }; }, - 69302: (s, o, i) => { - var u = i(83488), - _ = i(56757), - w = i(32865); + 69302(s, o, i) { + var a = i(83488), + u = i(56757), + _ = i(32865); s.exports = function baseRest(s, o) { - return w(_(s, o, u), s + ''); + return _(u(s, o, a), 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; + 73170(s, o, i) { + var a = i(16547), + u = i(31769), + _ = i(30361), + w = i(23805), + x = i(77797); + s.exports = function baseSet(s, o, i, C) { + if (!w(s)) return s; + for (var j = -1, L = (o = u(o, s)).length, B = L - 1, $ = s; null != $ && ++j < L; ) { + var U = x(o[j]), + V = 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]) ? [] : {}); + if (j != B) { + var z = $[U]; + void 0 === (V = C ? C(z, U, $) : void 0) && (V = w(z) ? z : _(o[j + 1]) ? [] : {}); } - (u(V, U, z), (V = V[U])); + (a($, U, V), ($ = $[U])); } return s; }; }, - 68882: (s, o, i) => { - var u = i(83488), - _ = i(48152), - w = _ + 68882(s, o, i) { + var a = i(83488), + u = i(48152), + _ = u ? function (s, o) { - return (_.set(s, o), s); + return (u.set(s, o), s); } - : u; - s.exports = w; + : a; + s.exports = _; }, - 19570: (s, o, i) => { - var u = i(37334), - _ = i(93243), - w = i(83488), - x = _ + 19570(s, o, i) { + var a = i(37334), + u = i(93243), + _ = i(83488), + w = u ? function (s, o) { - return _(s, 'toString', { + return u(s, 'toString', { configurable: !0, enumerable: !1, - value: u(o), + value: a(o), writable: !0 }); } - : w; - s.exports = x; + : _; + s.exports = w; }, - 25160: (s) => { + 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), + var a = -1, + u = s.length; + (o < 0 && (o = -o > u ? 0 : u + o), + (i = i > u ? u : i) < 0 && (i += u), + (u = o > i ? 0 : (i - o) >>> 0), (o >>>= 0)); - for (var w = Array(_); ++u < _; ) w[u] = s[u + o]; - return w; + for (var _ = Array(u); ++a < u; ) _[a] = s[a + o]; + return _; }; }, - 90916: (s, o, i) => { - var u = i(80909); + 90916(s, o, i) { + var a = i(80909); s.exports = function baseSome(s, o) { var i; return ( - u(s, function (s, u, _) { - return !(i = o(s, u, _)); + a(s, function (s, a, u) { + return !(i = o(s, a, u)); }), !!i ); }; }, - 78096: (s) => { + 78096(s) { s.exports = function baseTimes(s, o) { - for (var i = -1, u = Array(s); ++i < s; ) u[i] = o(i); - return u; + for (var i = -1, a = Array(s); ++i < s; ) a[i] = o(i); + return a; }; }, - 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; + 77556(s, o, i) { + var a = i(51873), + u = i(34932), + _ = i(56449), + w = i(44394), + x = a ? a.prototype : void 0, + C = x ? x.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) : ''; + if (_(s)) return u(s, baseToString) + ''; + if (w(s)) return C ? C.call(s) : ''; var o = s + ''; return '0' == o && 1 / s == -1 / 0 ? '-0' : o; }; }, - 54128: (s, o, i) => { - var u = i(31800), - _ = /^\s+/; + 54128(s, o, i) { + var a = i(31800), + u = /^\s+/; s.exports = function baseTrim(s) { - return s ? s.slice(0, u(s) + 1).replace(_, '') : s; + return s ? s.slice(0, a(s) + 1).replace(u, '') : s; }; }, - 27301: (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); + 19931(s, o, i) { + var a = i(31769), + u = i(68090), + _ = i(68969), + w = i(77797), + x = Object.prototype.hasOwnProperty; 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); + var i = -1, + C = (o = a(o, s)).length; + if (!C) return !0; + for (; ++i < C; ) { + var j = w(o[i]); + if ('__proto__' === j && !x.call(s, '__proto__')) return !1; + if (('constructor' === j || 'prototype' === j) && i < C - 1) return !1; } - return x; + var L = _(s, o); + return null == L || delete L[w(u(o))]; }; }, - 19219: (s) => { + 51234(s) { + s.exports = function baseZipObject(s, o, i) { + for (var a = -1, u = s.length, _ = o.length, w = {}; ++a < u; ) { + var x = a < _ ? o[a] : void 0; + i(w, s[a], x); + } + return w; + }; + }, + 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); + 31769(s, o, i) { + var a = i(56449), + u = i(28586), + _ = i(61802), + w = i(13222); s.exports = function castPath(s, o) { - return u(s) ? s : _(s, o) ? [s] : w(x(s)); + return a(s) ? s : u(s, o) ? [s] : _(w(s)); }; }, - 28754: (s, o, i) => { - var u = i(25160); + 28754(s, o, i) { + var a = 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)); + var u = s.length; + return ((i = void 0 === i ? u : i), !o && i >= u ? s : a(s, o, i)); }; }, - 49653: (s, o, i) => { - var u = i(37828); + 49653(s, o, i) { + var a = i(37828); s.exports = function cloneArrayBuffer(s) { var o = new s.constructor(s.byteLength); - return (new u(o).set(new u(s)), o); + return (new a(o).set(new a(s)), o); }; }, - 93290: (s, o, i) => { + 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; + var a = i(9325), + u = o && !o.nodeType && o, + _ = u && s && !s.nodeType && s, + w = _ && _.exports === u ? a.Buffer : void 0, + x = w ? w.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); + a = x ? x(i) : new s.constructor(i); + return (s.copy(a), a); }; }, - 76169: (s, o, i) => { - var u = i(49653); + 76169(s, o, i) { + var a = i(49653); s.exports = function cloneDataView(s, o) { - var i = o ? u(s.buffer) : s.buffer; + var i = o ? a(s.buffer) : s.buffer; return new s.constructor(i, s.byteOffset, s.byteLength); }; }, - 73201: (s) => { + 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; + 93736(s, o, i) { + var a = i(51873), + u = a ? a.prototype : void 0, + _ = u ? u.valueOf : void 0; s.exports = function cloneSymbol(s) { - return w ? Object(w.call(s)) : {}; + return _ ? Object(_.call(s)) : {}; }; }, - 71961: (s, o, i) => { - var u = i(49653); + 71961(s, o, i) { + var a = i(49653); s.exports = function cloneTypedArray(s, o) { - var i = o ? u(s.buffer) : s.buffer; + var i = o ? a(s.buffer) : s.buffer; return new s.constructor(i, s.byteOffset, s.length); }; }, - 91596: (s) => { + 91596(s) { var o = Math.max; - s.exports = function composeArgs(s, i, u, _) { + s.exports = function composeArgs(s, i, a, u) { for ( - var w = -1, - x = s.length, - C = u.length, + var _ = -1, + w = s.length, + x = a.length, + C = -1, + j = i.length, + L = o(w - x, 0), + B = Array(j + L), + $ = !u; + ++C < j; + ) + B[C] = i[C]; + for (; ++_ < x; ) ($ || _ < w) && (B[a[_]] = s[_]); + for (; L--; ) B[C++] = s[_++]; + return B; + }; + }, + 53320(s) { + var o = Math.max; + s.exports = function composeArgsRight(s, i, a, u) { + for ( + var _ = -1, + w = s.length, + x = -1, + C = a.length, j = -1, L = i.length, - B = o(x - C, 0), - $ = Array(L + B), - V = !_; - ++j < L; + B = o(w - C, 0), + $ = Array(B + L), + U = !u; + ++_ < B; ) - $[j] = i[j]; - for (; ++w < C; ) (V || w < x) && ($[u[w]] = s[w]); - for (; B--; ) $[j++] = s[w++]; + $[_] = s[_]; + for (var V = _; ++j < L; ) $[V + j] = i[j]; + for (; ++x < C; ) (U || _ < w) && ($[V + a[x]] = s[_++]); 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) => { + 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]; + a = s.length; + for (o || (o = Array(a)); ++i < a; ) 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; + 21791(s, o, i) { + var a = i(16547), + u = i(43360); + s.exports = function copyObject(s, o, i, _) { + var w = !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)); + for (var x = -1, C = o.length; ++x < C; ) { + var j = o[x], + L = _ ? _(i[j], s[j], j, i, s) : void 0; + (void 0 === L && (L = s[j]), w ? u(i, j, L) : a(i, j, L)); } return i; }; }, - 92271: (s, o, i) => { - var u = i(21791), - _ = i(4664); + 92271(s, o, i) { + var a = i(21791), + u = i(4664); s.exports = function copySymbols(s, o) { - return u(s, _(s), o); + return a(s, u(s), o); }; }, - 48948: (s, o, i) => { - var u = i(21791), - _ = i(86375); + 48948(s, o, i) { + var a = i(21791), + u = i(86375); s.exports = function copySymbolsIn(s, o) { - return u(s, _(s), o); + return a(s, u(s), o); }; }, - 55481: (s, o, i) => { - var u = i(9325)['__core-js_shared__']; - s.exports = u; + 55481(s, o, i) { + var a = i(9325)['__core-js_shared__']; + s.exports = a; }, - 58523: (s) => { + 58523(s) { s.exports = function countHolders(s, o) { - for (var i = s.length, u = 0; i--; ) s[i] === o && ++u; - return u; + for (var i = s.length, a = 0; i--; ) s[i] === o && ++a; + return a; }; }, - 20999: (s, o, i) => { - var u = i(69302), - _ = i(36800); + 20999(s, o, i) { + var a = i(69302), + u = 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; + return a(function (o, i) { + var a = -1, + _ = i.length, + w = _ > 1 ? i[_ - 1] : void 0, + x = _ > 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)), + w = s.length > 3 && 'function' == typeof w ? (_--, w) : void 0, + x && u(i[0], i[1], x) && ((w = _ < 3 ? void 0 : w), (_ = 1)), o = Object(o); - ++u < w; + ++a < _; ) { - var j = i[u]; - j && s(o, j, u, x); + var C = i[a]; + C && s(o, C, a, w); } return o; }); }; }, - 38329: (s, o, i) => { - var u = i(64894); + 38329(s, o, i) { + var a = i(64894); s.exports = function createBaseEach(s, o) { - return function (i, _) { + return function (i, u) { if (null == i) return i; - if (!u(i)) return s(i, _); + if (!a(i)) return s(i, u); for ( - var w = i.length, x = o ? w : -1, C = Object(i); - (o ? x-- : ++x < w) && !1 !== _(C[x], x, C); + var _ = i.length, w = o ? _ : -1, x = Object(i); + (o ? w-- : ++w < _) && !1 !== u(x[w], w, x); ); return i; }; }; }, - 83221: (s) => { + 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 function (o, i, a) { + for (var u = -1, _ = Object(o), w = a(o), x = w.length; x--; ) { + var C = w[s ? x : ++u]; + if (!1 === i(_[C], C, _)) break; } return o; }; }; }, - 11842: (s, o, i) => { - var u = i(82819), - _ = i(9325); + 11842(s, o, i) { + var a = i(82819), + u = i(9325); s.exports = function createBind(s, o, i) { - var w = 1 & o, - x = u(s); + var _ = 1 & o, + w = a(s); return function wrapper() { - return (this && this !== _ && this instanceof wrapper ? x : s).apply( - w ? i : this, + return (this && this !== u && this instanceof wrapper ? w : s).apply( + _ ? i : this, arguments ); }; }; }, - 12507: (s, o, i) => { - var u = i(28754), - _ = i(49698), - w = i(63912), - x = i(13222); + 12507(s, o, i) { + var a = i(28754), + u = i(49698), + _ = i(63912), + w = 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; + o = w(o); + var i = u(o) ? _(o) : void 0, + x = i ? i[0] : o.charAt(0), + C = i ? a(i, 1).join('') : o.slice(1); + return x[s]() + C; }; }; }, - 45539: (s, o, i) => { - var u = i(40882), - _ = i(50828), - w = i(66645), - x = RegExp("['’]", 'g'); + 45539(s, o, i) { + var a = i(40882), + u = i(50828), + _ = i(66645), + w = RegExp("['’]", 'g'); s.exports = function createCompounder(s) { return function (o) { - return u(w(_(o).replace(x, '')), s, ''); + return a(_(u(o).replace(w, '')), s, ''); }; }; }, - 82819: (s, o, i) => { - var u = i(39344), - _ = i(23805); + 82819(s, o, i) { + var a = i(39344), + u = i(23805); s.exports = function createCtor(s) { return function () { var o = arguments; @@ -10597,200 +9923,200 @@ 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; + var i = a(s.prototype), + _ = s.apply(i, o); + return u(_) ? _ : 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); + 77078(s, o, i) { + var a = i(91033), + u = i(82819), + _ = i(37471), + w = i(18073), + x = i(11287), + C = i(36306), + j = i(9325); s.exports = function createCurry(s, o, i) { - var B = _(s); + var L = u(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, $); + for (var u = arguments.length, B = Array(u), $ = u, U = x(wrapper); $--; ) + B[$] = arguments[$]; + var V = u < 3 && B[0] !== U && B[u - 1] !== U ? [] : C(B, U); + return (u -= V.length) < i + ? w(s, o, _, wrapper.placeholder, void 0, B, V, void 0, void 0, i - u) + : a(this && this !== j && this instanceof wrapper ? L : s, this, B); }; }; }, - 62006: (s, o, i) => { - var u = i(15389), - _ = i(64894), - w = i(95950); + 62006(s, o, i) { + var a = i(15389), + u = i(64894), + _ = 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)), + return function (o, i, w) { + var x = Object(o); + if (!u(o)) { + var C = a(i, 3); + ((o = _(o)), (i = function (s) { - return j(C[s], s, C); + return C(x[s], s, x); })); } - var L = s(o, i, x); - return L > -1 ? C[j ? o[L] : L] : void 0; + var j = s(o, i, w); + return j > -1 ? x[C ? o[j] : j] : 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, + 37471(s, o, i) { + var a = i(91596), + u = i(53320), + _ = i(58523), + w = i(82819), + x = i(18073), + C = i(11287), + j = i(68294), + L = i(36306), + B = i(9325); + s.exports = function createHybrid(s, o, i, $, U, V, z, Y, Z, ee) { + var ie = 128 & o, + ae = 1 & o, ce = 2 & o, - pe = 24 & o, - de = 512 & o, - fe = ce ? void 0 : x(s); + le = 24 & o, + pe = 512 & o, + de = ce ? void 0 : w(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); + for (var fe = arguments.length, ye = Array(fe), be = fe; be--; ) + ye[be] = arguments[be]; + if (le) + var Se = C(wrapper), + _e = _(ye, Se); if ( - (V && (be = u(be, V, U, pe)), - z && (be = _(be, z, Y, pe)), - (ye -= Se), - pe && ye < ie) + ($ && (ye = a(ye, $, U, le)), + V && (ye = u(ye, V, z, le)), + (fe -= _e), + le && fe < ee) ) { - var xe = B(be, we); - return C(s, o, createHybrid, wrapper.placeholder, i, be, xe, Z, ee, ie - ye); + var we = L(ye, Se); + return x(s, o, createHybrid, wrapper.placeholder, i, ye, we, Y, Z, ee - fe); } - var Pe = le ? i : this, - Te = ce ? Pe[s] : s; + var xe = ae ? i : this, + Pe = ce ? xe[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) + (fe = ye.length), + Y ? (ye = j(ye, Y)) : pe && fe > 1 && ye.reverse(), + ie && Z < fe && (ye.length = Z), + this && this !== B && this instanceof wrapper && (Pe = de || w(Pe)), + Pe.apply(xe, ye) ); }; }; }, - 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); + 24168(s, o, i) { + var a = i(91033), + u = i(82819), + _ = i(9325); + s.exports = function createPartial(s, o, i, w) { + var x = 1 & o, + C = u(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; + u = arguments.length, + j = -1, + L = w.length, + B = Array(L + u), + $ = this && this !== _ && this instanceof wrapper ? C : s; + ++j < L; ) - $[L] = x[L]; - for (; _--; ) $[L++] = arguments[++o]; - return u(V, C ? i : this, $); + B[j] = w[j]; + for (; u--; ) B[j++] = arguments[++o]; + return a($, x ? i : this, B); }; }; }, - 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) { + 18073(s, o, i) { + var a = i(85087), + u = i(54641), + _ = i(70981); + s.exports = function createRecurry(s, o, i, w, x, C, j, L, B, $) { var U = 8 & o; ((o |= U ? 32 : 64), 4 & (o &= ~(U ? 64 : 32)) || (o &= -4)); - var z = [ + var V = [ s, o, - C, + x, + U ? C : void 0, U ? j : void 0, - U ? L : void 0, + U ? void 0 : C, U ? void 0 : j, - U ? void 0 : L, + L, B, - $, - V + $ ], - Y = i.apply(void 0, z); - return (u(s) && _(Y, z), (Y.placeholder = x), w(Y, s, o)); + z = i.apply(void 0, V); + return (a(s) && u(z, V), (z.placeholder = w), _(z, 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), + 66977(s, o, i) { + var a = i(68882), + u = i(11842), + _ = i(77078), + w = i(37471), + x = i(24168), + C = i(37381), + j = i(3209), + L = i(54641), + B = i(70981), + $ = 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; + s.exports = function createWrap(s, o, i, V, z, Y, Z, ee) { + var ie = 2 & o; + if (!ie && 'function' != typeof s) throw new TypeError('Expected a function'); + var ae = V ? V.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), + (ae || ((o &= -97), (V = z = void 0)), + (Z = void 0 === Z ? Z : U($(Z), 0)), + (ee = void 0 === ee ? ee : $(ee)), + (ae -= z ? z.length : 0), 64 & o) ) { - var ce = z, - pe = Y; - z = Y = void 0; + var ce = V, + le = z; + V = z = void 0; } - var de = ae ? void 0 : j(s), - fe = [s, o, i, z, Y, ce, pe, Z, ee, ie]; + var pe = ie ? void 0 : C(s), + de = [s, o, i, V, z, ce, le, Y, Z, ee]; 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)) && + (pe && j(de, pe), + (s = de[0]), + (o = de[1]), + (i = de[2]), + (V = de[3]), + (z = de[4]), + !(ee = de[9] = void 0 === de[9] ? (ie ? 0 : s.length) : U(de[9] - ae, 0)) && 24 & o && (o &= -25), o && 1 != o) ) - ye = + fe = 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); + ? _(s, o, ee) + : (32 != o && 33 != o) || z.length + ? w.apply(void 0, de) + : x(s, o, i, V); + else var fe = u(s, o, i); + return B((pe ? a : L)(fe, de), s, o); }; }, - 53138: (s, o, i) => { - var u = i(11331); + 53138(s, o, i) { + var a = i(11331); s.exports = function customOmitClone(s) { - return u(s) ? void 0 : s; + return a(s) ? void 0 : s; }; }, - 24647: (s, o, i) => { - var u = i(54552)({ + 24647(s, o, i) { + var a = i(54552)({ À: 'A', Á: 'A', Â: 'A', @@ -10982,352 +10308,352 @@ ʼn: "'n", ſ: 's' }); - s.exports = u; + s.exports = a; }, - 93243: (s, o, i) => { - var u = i(56110), - _ = (function () { + 93243(s, o, i) { + var a = i(56110), + u = (function () { try { - var s = u(Object, 'defineProperty'); + var s = a(Object, 'defineProperty'); return (s({}, '', {}), s); } catch (s) {} })(); - s.exports = _; + s.exports = u; }, - 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; + 25911(s, o, i) { + var a = i(38859), + u = i(14248), + _ = i(19219); + s.exports = function equalArrays(s, o, i, w, x, C) { + var j = 1 & i, + L = s.length, + B = o.length; + if (L != B && !(j && B > L)) return !1; + var $ = C.get(s), + U = C.get(o); + if ($ && U) return $ == o && U == s; + var V = -1, + z = !0, + Y = 2 & i ? new a() : void 0; + for (C.set(s, o), C.set(o, s); ++V < L; ) { + var Z = s[V], + ee = o[V]; + if (w) var ie = j ? w(ee, Z, V, o, s, C) : w(Z, ee, V, s, o, C); + if (void 0 !== ie) { + if (ie) continue; + z = !1; break; } - if (Z) { + if (Y) { if ( - !_(o, function (s, o) { - if (!w(Z, o) && (ee === s || C(ee, s, i, x, j))) return Z.push(o); + !u(o, function (s, o) { + if (!_(Y, o) && (Z === s || x(Z, s, i, w, C))) return Y.push(o); }) ) { - Y = !1; + z = !1; break; } - } else if (ee !== ie && !C(ee, ie, i, x, j)) { - Y = !1; + } else if (Z !== ee && !x(Z, ee, i, w, C)) { + z = !1; break; } } - return (j.delete(s), j.delete(o), Y); + return (C.delete(s), C.delete(o), z); }; }, - 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) { + 21986(s, o, i) { + var a = i(51873), + u = i(37828), + _ = i(75288), + w = i(25911), + x = i(20317), + C = i(84247), + j = a ? a.prototype : void 0, + L = j ? j.valueOf : void 0; + s.exports = function equalByTag(s, o, i, a, j, B, $) { 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))); + return !(s.byteLength != o.byteLength || !B(new u(s), new u(o))); case '[object Boolean]': case '[object Date]': case '[object Number]': - return w(+s, +o); + return _(+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; + var U = x; 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); + var V = 1 & a; + if ((U || (U = C), s.size != o.size && !V)) return !1; + var z = $.get(s); + if (z) return z == o; + ((a |= 2), $.set(s, o)); + var Y = w(U(s), U(o), a, j, B, $); + return ($.delete(s), Y); case '[object Symbol]': - if (B) return B.call(s) == B.call(o); + if (L) return L.call(s) == L.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; + 50689(s, o, i) { + var a = i(50002), + u = Object.prototype.hasOwnProperty; + s.exports = function equalObjects(s, o, i, _, w, x) { + var C = 1 & i, + j = a(s), + L = j.length; + if (L != a(o).length && !C) return !1; + for (var B = L; B--; ) { + var $ = j[B]; + if (!(C ? $ in o : u.call(o, $))) 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; + var U = x.get(s), + V = x.get(o); + if (U && V) return U == o && V == s; + var z = !0; + (x.set(s, o), x.set(o, s)); + for (var Y = C; ++B < L; ) { + var Z = s[($ = j[B])], + ee = o[$]; + if (_) var ie = C ? _(ee, Z, $, o, s, x) : _(Z, ee, $, s, o, x); + if (!(void 0 === ie ? Z === ee || w(Z, ee, i, _, x) : ie)) { + z = !1; break; } - Z || (Z = 'constructor' == V); + Y || (Y = 'constructor' == $); } - if (Y && !Z) { - var le = s.constructor, + if (z && !Y) { + var ae = s.constructor, ce = o.constructor; - le == ce || + ae == ce || !('constructor' in s) || !('constructor' in o) || - ('function' == typeof le && - le instanceof le && + ('function' == typeof ae && + ae instanceof ae && 'function' == typeof ce && ce instanceof ce) || - (Y = !1); + (z = !1); } - return (C.delete(s), C.delete(o), Y); + return (x.delete(s), x.delete(o), z); }; }, - 38816: (s, o, i) => { - var u = i(35970), - _ = i(56757), - w = i(32865); + 38816(s, o, i) { + var a = i(35970), + u = i(56757), + _ = i(32865); s.exports = function flatRest(s) { - return w(_(s, void 0, u), s + ''); + return _(u(s, void 0, a), s + ''); }; }, - 34840: (s, o, i) => { - var u = 'object' == typeof i.g && i.g && i.g.Object === Object && i.g; - s.exports = u; + 34840(s, o, i) { + var a = 'object' == typeof i.g && i.g && i.g.Object === Object && i.g; + s.exports = a; }, - 50002: (s, o, i) => { - var u = i(82199), - _ = i(4664), - w = i(95950); + 50002(s, o, i) { + var a = i(82199), + u = i(4664), + _ = i(95950); s.exports = function getAllKeys(s) { - return u(s, w, _); + return a(s, _, u); }; }, - 83349: (s, o, i) => { - var u = i(82199), - _ = i(86375), - w = i(37241); + 83349(s, o, i) { + var a = i(82199), + u = i(86375), + _ = i(37241); s.exports = function getAllKeysIn(s) { - return u(s, w, _); + return a(s, _, u); }; }, - 37381: (s, o, i) => { - var u = i(48152), - _ = i(63950), - w = u + 37381(s, o, i) { + var a = i(48152), + u = i(63950), + _ = a ? function (s) { - return u.get(s); + return a.get(s); } - : _; - s.exports = w; + : u; + s.exports = _; }, - 62284: (s, o, i) => { - var u = i(84629), - _ = Object.prototype.hasOwnProperty; + 62284(s, o, i) { + var a = i(84629), + u = 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; + for (var o = s.name + '', i = a[o], _ = u.call(a, o) ? i.length : 0; _--; ) { + var w = i[_], + x = w.func; + if (null == x || x == s) return w.name; } return o; }; }, - 11287: (s) => { + 11287(s) { s.exports = function getHolder(s) { return s.placeholder; }; }, - 12651: (s, o, i) => { - var u = i(74218); + 12651(s, o, i) { + var a = i(74218); s.exports = function getMapData(s, o) { var i = s.__data__; - return u(o) ? i['string' == typeof o ? 'string' : 'hash'] : i.map; + return a(o) ? i['string' == typeof o ? 'string' : 'hash'] : i.map; }; }, - 10776: (s, o, i) => { - var u = i(30756), - _ = i(95950); + 10776(s, o, i) { + var a = i(30756), + u = 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)]; + for (var o = u(s), i = o.length; i--; ) { + var _ = o[i], + w = s[_]; + o[i] = [_, w, a(w)]; } return o; }; }, - 56110: (s, o, i) => { - var u = i(45083), - _ = i(10392); + 56110(s, o, i) { + var a = i(45083), + u = i(10392); s.exports = function getNative(s, o) { - var i = _(s, o); - return u(i) ? i : void 0; + var i = u(s, o); + return a(i) ? i : void 0; }; }, - 28879: (s, o, i) => { - var u = i(74335)(Object.getPrototypeOf, Object); - s.exports = u; + 28879(s, o, i) { + var a = i(74335)(Object.getPrototypeOf, Object); + s.exports = a; }, - 659: (s, o, i) => { - var u = i(51873), - _ = Object.prototype, - w = _.hasOwnProperty, - x = _.toString, - C = u ? u.toStringTag : void 0; + 659(s, o, i) { + var a = i(51873), + u = Object.prototype, + _ = u.hasOwnProperty, + w = u.toString, + x = a ? a.toStringTag : void 0; s.exports = function getRawTag(s) { - var o = w.call(s, C), - i = s[C]; + var o = _.call(s, x), + i = s[x]; try { - s[C] = void 0; - var u = !0; + s[x] = void 0; + var a = !0; } catch (s) {} - var _ = x.call(s); - return (u && (o ? (s[C] = i) : delete s[C]), _); + var u = w.call(s); + return (a && (o ? (s[x] = i) : delete s[x]), u); }; }, - 4664: (s, o, i) => { - var u = i(79770), - _ = i(63345), - w = Object.prototype.propertyIsEnumerable, - x = Object.getOwnPropertySymbols, - C = x + 4664(s, o, i) { + var a = i(79770), + u = i(63345), + _ = Object.prototype.propertyIsEnumerable, + w = Object.getOwnPropertySymbols, + x = w ? function (s) { return null == s ? [] : ((s = Object(s)), - u(x(s), function (o) { - return w.call(s, o); + a(w(s), function (o) { + return _.call(s, o); })); } - : _; - s.exports = C; + : u; + s.exports = x; }, - 86375: (s, o, i) => { - var u = i(14528), - _ = i(28879), - w = i(4664), - x = i(63345), - C = Object.getOwnPropertySymbols + 86375(s, o, i) { + var a = i(14528), + u = i(28879), + _ = i(4664), + w = i(63345), + x = Object.getOwnPropertySymbols ? function (s) { - for (var o = []; s; ) (u(o, w(s)), (s = _(s))); + for (var o = []; s; ) (a(o, _(s)), (s = u(s))); return o; } - : x; - s.exports = C; + : w; + s.exports = x; }, - 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]', + 5861(s, o, i) { + var a = i(55580), + u = i(68223), + _ = i(32804), + w = i(76545), + x = i(28303), + C = i(72552), + j = i(47473), + L = '[object Map]', + B = '[object Promise]', + $ = '[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), + V = '[object DataView]', + z = j(a), + Y = j(u), + Z = j(_), + ee = j(w), + ie = j(x), + ae = C; + (((a && ae(new a(new ArrayBuffer(1))) != V) || + (u && ae(new u()) != L) || + (_ && ae(_.resolve()) != B) || + (w && ae(new w()) != $) || + (x && ae(new x()) != U)) && + (ae = function (s) { + var o = C(s), i = '[object Object]' == o ? s.constructor : void 0, - u = i ? L(i) : ''; - if (u) - switch (u) { + a = i ? j(i) : ''; + if (a) + switch (a) { + case z: + return V; case Y: - return z; + return L; case Z: return B; case ee: return $; case ie: - return V; - case ae: return U; } return o; }), - (s.exports = le)); + (s.exports = ae)); }, - 10392: (s) => { + 10392(s) { s.exports = function getValue(s, o) { return null == s ? void 0 : s[o]; }; }, - 75251: (s) => { + 75251(s) { var o = /\{\n\/\* \[wrapped with (.+)\] \*/, i = /,? & /; s.exports = function getWrapDetails(s) { - var u = s.match(o); - return u ? u[1].split(i) : []; + var a = s.match(o); + return a ? a[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); + 49326(s, o, i) { + var a = i(31769), + u = i(72428), + _ = i(56449), + w = i(30361), + x = i(30294), + C = 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]; + for (var j = -1, L = (o = a(o, s)).length, B = !1; ++j < L; ) { + var $ = C(o[j]); + if (!(B = null != s && i(s, $))) break; + s = s[$]; } - return $ || ++L != B - ? $ - : !!(B = null == s ? 0 : s.length) && C(B) && x(V, B) && (w(s) || _(s)); + return B || ++j != L + ? B + : !!(L = null == s ? 0 : s.length) && x(L) && w($, L) && (_(s) || u(s)); }; }, - 49698: (s) => { + 49698(s) { var o = RegExp( '[\\u200d\\ud800-\\udfff\\u0300-\\u036f\\ufe20-\\ufe2f\\u20d0-\\u20ff\\ufe0e\\ufe0f]' ); @@ -11335,85 +10661,85 @@ return o.test(s); }; }, - 45434: (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); + 22032(s, o, i) { + var a = i(81042); s.exports = function hashClear() { - ((this.__data__ = u ? u(null) : {}), (this.size = 0)); + ((this.__data__ = a ? a(null) : {}), (this.size = 0)); }; }, - 63862: (s) => { + 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; + 66721(s, o, i) { + var a = i(81042), + u = Object.prototype.hasOwnProperty; s.exports = function hashGet(s) { var o = this.__data__; - if (u) { + if (a) { var i = o[s]; return '__lodash_hash_undefined__' === i ? void 0 : i; } - return _.call(o, s) ? o[s] : void 0; + return u.call(o, s) ? o[s] : void 0; }; }, - 12749: (s, o, i) => { - var u = i(81042), - _ = Object.prototype.hasOwnProperty; + 12749(s, o, i) { + var a = i(81042), + u = Object.prototype.hasOwnProperty; s.exports = function hashHas(s) { var o = this.__data__; - return u ? void 0 !== o[s] : _.call(o, s); + return a ? void 0 !== o[s] : u.call(o, s); }; }, - 35749: (s, o, i) => { - var u = i(81042); + 35749(s, o, i) { + var a = 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), + (i[s] = a && void 0 === o ? '__lodash_hash_undefined__' : o), this ); }; }, - 76189: (s) => { + 76189(s) { var o = Object.prototype.hasOwnProperty; s.exports = function initCloneArray(s) { var i = s.length, - u = new s.constructor(i); + a = new s.constructor(i); return ( i && 'string' == typeof s[0] && o.call(s, 'index') && - ((u.index = s.index), (u.input = s.input)), - u + ((a.index = s.index), (a.input = s.input)), + a ); }; }, - 77199: (s, o, i) => { - var u = i(49653), - _ = i(76169), - w = i(73201), - x = i(93736), - C = i(71961); + 77199(s, o, i) { + var a = i(49653), + u = i(76169), + _ = i(73201), + w = i(93736), + x = i(71961); s.exports = function initCloneByTag(s, o, i) { - var j = s.constructor; + var C = s.constructor; switch (o) { case '[object ArrayBuffer]': - return u(s); + return a(s); case '[object Boolean]': case '[object Date]': - return new j(+s); + return new C(+s); case '[object DataView]': - return _(s, i); + return u(s, i); case '[object Float32Array]': case '[object Float64Array]': case '[object Int8Array]': @@ -11423,93 +10749,93 @@ case '[object Uint8ClampedArray]': case '[object Uint16Array]': case '[object Uint32Array]': - return C(s, i); + return x(s, i); case '[object Map]': case '[object Set]': - return new j(); + return new C(); case '[object Number]': case '[object String]': - return new j(s); + return new C(s); case '[object RegExp]': - return w(s); + return _(s); case '[object Symbol]': - return x(s); + return w(s); } }; }, - 35529: (s, o, i) => { - var u = i(39344), - _ = i(28879), - w = i(55527); + 35529(s, o, i) { + var a = i(39344), + u = i(28879), + _ = i(55527); s.exports = function initCloneObject(s) { - return 'function' != typeof s.constructor || w(s) ? {} : u(_(s)); + return 'function' != typeof s.constructor || _(s) ? {} : a(u(s)); }; }, - 62060: (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; + var a = i.length; + if (!a) return s; + var u = a - 1; return ( - (i[_] = (u > 1 ? '& ' : '') + i[_]), - (i = i.join(u > 2 ? ', ' : ' ')), + (i[u] = (a > 1 ? '& ' : '') + i[u]), + (i = i.join(a > 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; + 45891(s, o, i) { + var a = i(51873), + u = i(72428), + _ = i(56449), + w = a ? a.isConcatSpreadable : void 0; s.exports = function isFlattenable(s) { - return w(s) || _(s) || !!(x && s && s[x]); + return _(s) || u(s) || !!(w && s && s[w]); }; }, - 30361: (s) => { + 30361(s) { var o = /^(?:0|[1-9]\d*)$/; s.exports = function isIndex(s, i) { - var u = typeof s; + var a = typeof s; return ( !!(i = null == i ? 9007199254740991 : i) && - ('number' == u || ('symbol' != u && o.test(s))) && + ('number' == a || ('symbol' != a && 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); + 36800(s, o, i) { + var a = i(75288), + u = i(64894), + _ = i(30361), + w = i(23805); s.exports = function isIterateeCall(s, o, i) { - if (!x(i)) return !1; - var C = typeof o; + if (!w(i)) return !1; + var x = typeof o; return ( - !!('number' == C ? _(i) && w(o, i.length) : 'string' == C && o in i) && u(i[o], s) + !!('number' == x ? u(i) && _(o, i.length) : 'string' == x && o in i) && a(i[o], s) ); }; }, - 28586: (s, o, i) => { - var u = i(56449), - _ = i(44394), - w = /\.|\[(?:[^[\]]*|(["'])(?:(?!\1)[^\\]|\\.)*?\1)\]/, - x = /^\w*$/; + 28586(s, o, i) { + var a = i(56449), + u = i(44394), + _ = /\.|\[(?:[^[\]]*|(["'])(?:(?!\1)[^\\]|\\.)*?\1)\]/, + w = /^\w*$/; s.exports = function isKey(s, o) { - if (u(s)) return !1; + if (a(s)) return !1; var i = typeof s; return ( - !('number' != i && 'symbol' != i && 'boolean' != i && null != s && !_(s)) || - x.test(s) || - !w.test(s) || + !('number' != i && 'symbol' != i && 'boolean' != i && null != s && !u(s)) || + w.test(s) || + !_.test(s) || (null != o && s in Object(o)) ); }; }, - 74218: (s) => { + 74218(s) { s.exports = function isKeyable(s) { var o = typeof s; return 'string' == o || 'number' == o || 'symbol' == o || 'boolean' == o @@ -11517,301 +10843,301 @@ : null === s; }; }, - 85087: (s, o, i) => { - var u = i(30980), - _ = i(37381), - w = i(62284), - x = i(53758); + 85087(s, o, i) { + var a = i(30980), + u = i(37381), + _ = i(62284), + w = i(53758); s.exports = function isLaziable(s) { - var o = w(s), - i = x[o]; - if ('function' != typeof i || !(o in u.prototype)) return !1; + var o = _(s), + i = w[o]; + if ('function' != typeof i || !(o in a.prototype)) return !1; if (s === i) return !0; - var C = _(i); - return !!C && s === C[0]; + var x = u(i); + return !!x && s === x[0]; }; }, - 87296: (s, o, i) => { - var u, - _ = i(55481), - w = (u = /[^.]+$/.exec((_ && _.keys && _.keys.IE_PROTO) || '')) - ? 'Symbol(src)_1.' + u + 87296(s, o, i) { + var a, + u = i(55481), + _ = (a = /[^.]+$/.exec((u && u.keys && u.keys.IE_PROTO) || '')) + ? 'Symbol(src)_1.' + a : ''; s.exports = function isMasked(s) { - return !!w && w in s; + return !!_ && _ in s; }; }, - 55527: (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); + 30756(s, o, i) { + var a = i(23805); s.exports = function isStrictComparable(s) { - return s == s && !u(s); + return s == s && !a(s); }; }, - 63702: (s) => { + 63702(s) { s.exports = function listCacheClear() { ((this.__data__ = []), (this.size = 0)); }; }, - 70080: (s, o, i) => { - var u = i(26025), - _ = Array.prototype.splice; + 70080(s, o, i) { + var a = i(26025), + u = 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); + i = a(o, s); + return !(i < 0) && (i == o.length - 1 ? o.pop() : u.call(o, i, 1), --this.size, !0); }; }, - 24739: (s, o, i) => { - var u = i(26025); + 24739(s, o, i) { + var a = i(26025); s.exports = function listCacheGet(s) { var o = this.__data__, - i = u(o, s); + i = a(o, s); return i < 0 ? void 0 : o[i][1]; }; }, - 48655: (s, o, i) => { - var u = i(26025); + 48655(s, o, i) { + var a = i(26025); s.exports = function listCacheHas(s) { - return u(this.__data__, s) > -1; + return a(this.__data__, s) > -1; }; }, - 31175: (s, o, i) => { - var u = i(26025); + 31175(s, o, i) { + var a = 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); + u = a(i, s); + return (u < 0 ? (++this.size, i.push([s, o])) : (i[u][1] = o), this); }; }, - 63040: (s, o, i) => { - var u = i(21549), - _ = i(80079), - w = i(68223); + 63040(s, o, i) { + var a = i(21549), + u = i(80079), + _ = i(68223); s.exports = function mapCacheClear() { ((this.size = 0), - (this.__data__ = { hash: new u(), map: new (w || _)(), string: new u() })); + (this.__data__ = { hash: new a(), map: new (_ || u)(), string: new a() })); }; }, - 17670: (s, o, i) => { - var u = i(12651); + 17670(s, o, i) { + var a = i(12651); s.exports = function mapCacheDelete(s) { - var o = u(this, s).delete(s); + var o = a(this, s).delete(s); return ((this.size -= o ? 1 : 0), o); }; }, - 90289: (s, o, i) => { - var u = i(12651); + 90289(s, o, i) { + var a = i(12651); s.exports = function mapCacheGet(s) { - return u(this, s).get(s); + return a(this, s).get(s); }; }, - 4509: (s, o, i) => { - var u = i(12651); + 4509(s, o, i) { + var a = i(12651); s.exports = function mapCacheHas(s) { - return u(this, s).has(s); + return a(this, s).has(s); }; }, - 72949: (s, o, i) => { - var u = i(12651); + 72949(s, o, i) { + var a = 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); + var i = a(this, s), + u = i.size; + return (i.set(s, o), (this.size += i.size == u ? 0 : 1), this); }; }, - 20317: (s) => { + 20317(s) { s.exports = function mapToArray(s) { var o = -1, i = Array(s.size); return ( - s.forEach(function (s, u) { - i[++o] = [u, s]; + s.forEach(function (s, a) { + i[++o] = [a, s]; }), i ); }; }, - 67197: (s) => { + 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); + 62224(s, o, i) { + var a = i(50104); s.exports = function memoizeCapped(s) { - var o = u(s, function (s) { + var o = a(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; + 3209(s, o, i) { + var a = i(91596), + u = i(53320), + _ = i(36306), + w = '__lodash_placeholder__', + x = 128, + C = 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)); + j = o[1], + L = i | j, + B = L < 131, + $ = + (j == x && 8 == i) || + (j == x && 256 == i && s[7].length <= o[8]) || + (384 == j && o[7].length <= o[8] && 8 == i); + if (!B && !$) return s; + 1 & j && ((s[2] = o[2]), (L |= 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])); + var V = s[3]; + ((s[3] = V ? a(V, U, o[4]) : U), (s[4] = V ? _(s[3], w) : 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])), + ((V = s[5]), (s[5] = V ? u(V, U, o[6]) : U), (s[6] = V ? _(s[5], w) : o[6])), (U = o[7]) && (s[7] = U), - L & C && (s[8] = null == s[8] ? o[8] : j(s[8], o[8])), + j & x && (s[8] = null == s[8] ? o[8] : C(s[8], o[8])), null == s[9] && (s[9] = o[9]), (s[0] = o[0]), - (s[1] = B), + (s[1] = L), s ); }; }, - 48152: (s, o, i) => { - var u = i(28303), - _ = u && new u(); - s.exports = _; - }, - 81042: (s, o, i) => { - var u = i(56110)(Object, 'create'); + 48152(s, o, i) { + var a = i(28303), + u = a && new a(); s.exports = u; }, - 3650: (s, o, i) => { - var u = i(74335)(Object.keys, Object); - s.exports = u; + 81042(s, o, i) { + var a = i(56110)(Object, 'create'); + s.exports = a; }, - 90181: (s) => { + 3650(s, o, i) { + var a = i(74335)(Object.keys, Object); + s.exports = a; + }, + 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) => { + 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 () { + var a = i(34840), + u = o && !o.nodeType && o, + _ = u && s && !s.nodeType && s, + w = _ && _.exports === u && a.process, + x = (function () { try { - var s = w && w.require && w.require('util').types; - return s || (x && x.binding && x.binding('util')); + var s = _ && _.require && _.require('util').types; + return s || (w && w.binding && w.binding('util')); } catch (s) {} })(); - s.exports = C; + s.exports = x; }, - 59350: (s) => { + 59350(s) { var o = Object.prototype.toString; s.exports = function objectToString(s) { return o.call(s); }; }, - 74335: (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; + 56757(s, o, i) { + var a = i(91033), + u = Math.max; s.exports = function overRest(s, o, i) { return ( - (o = _(void 0 === o ? s.length - 1 : o, 0)), + (o = u(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)); + for (var _ = arguments, w = -1, x = u(_.length - o, 0), C = Array(x); ++w < x; ) + C[w] = _[o + w]; + w = -1; + for (var j = Array(o + 1); ++w < o; ) j[w] = _[w]; + return ((j[o] = i(C)), a(s, this, j)); } ); }; }, - 68969: (s, o, i) => { - var u = i(47422), - _ = i(25160); + 68969(s, o, i) { + var a = i(47422), + u = i(25160); s.exports = function parent(s, o) { - return o.length < 2 ? s : u(s, _(o, 0, -1)); + return o.length < 2 ? s : a(s, u(o, 0, -1)); }; }, - 84629: (s) => { + 84629(s) { s.exports = {}; }, - 68294: (s, o, i) => { - var u = i(23007), - _ = i(30361), - w = Math.min; + 68294(s, o, i) { + var a = i(23007), + u = i(30361), + _ = 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; + for (var i = s.length, w = _(o.length, i), x = a(s); w--; ) { + var C = o[w]; + s[w] = u(C, i) ? x[C] : void 0; } return s; }; }, - 36306: (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)); + for (var a = -1, u = s.length, _ = 0, w = []; ++a < u; ) { + var x = s[a]; + (x !== i && x !== o) || ((s[a] = o), (w[_++] = a)); } - return x; + return w; }; }, - 9325: (s, o, i) => { - var u = i(34840), - _ = 'object' == typeof self && self && self.Object === Object && self, - w = u || _ || Function('return this')(); - s.exports = w; + 9325(s, o, i) { + var a = i(34840), + u = 'object' == typeof self && self && self.Object === Object && self, + _ = a || u || Function('return this')(); + s.exports = _; }, - 14974: (s) => { + 14974(s) { s.exports = function safeGet(s, o) { if (('constructor' !== o || 'function' != typeof s[o]) && '__proto__' != o) return s[o]; }; }, - 31380: (s) => { + 31380(s) { s.exports = function setCacheAdd(s) { return (this.__data__.set(s, '__lodash_hash_undefined__'), this); }; }, - 51459: (s) => { + 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 = _; + 54641(s, o, i) { + var a = i(68882), + u = i(51811)(a); + s.exports = u; }, - 84247: (s) => { + 84247(s) { s.exports = function setToArray(s) { var o = -1, i = Array(s.size); @@ -11823,113 +11149,113 @@ ); }; }, - 32865: (s, o, i) => { - var u = i(19570), - _ = i(51811)(u); - s.exports = _; + 32865(s, o, i) { + var a = i(19570), + u = i(51811)(a); + s.exports = u; }, - 70981: (s, o, i) => { - var u = i(75251), - _ = i(62060), - w = i(32865), - x = i(75948); + 70981(s, o, i) { + var a = i(75251), + u = i(62060), + _ = i(32865), + w = i(75948); s.exports = function setWrapToString(s, o, i) { - var C = o + ''; - return w(s, _(C, x(u(C), i))); + var x = o + ''; + return _(s, u(x, w(a(x), i))); }; }, - 51811: (s) => { + 51811(s) { var o = Date.now; s.exports = function shortOut(s) { var i = 0, - u = 0; + a = 0; return function () { - var _ = o(), - w = 16 - (_ - u); - if (((u = _), w > 0)) { + var u = o(), + _ = 16 - (u - a); + if (((a = u), _ > 0)) { if (++i >= 800) return arguments[0]; } else i = 0; return s.apply(void 0, arguments); }; }; }, - 51420: (s, o, i) => { - var u = i(80079); + 51420(s, o, i) { + var a = i(80079); s.exports = function stackClear() { - ((this.__data__ = new u()), (this.size = 0)); + ((this.__data__ = new a()), (this.size = 0)); }; }, - 90938: (s) => { + 90938(s) { s.exports = function stackDelete(s) { var o = this.__data__, i = o.delete(s); return ((this.size = o.size), i); }; }, - 63605: (s) => { + 63605(s) { s.exports = function stackGet(s) { return this.__data__.get(s); }; }, - 29817: (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); + 80945(s, o, i) { + var a = i(80079), + u = i(68223), + _ = 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); + if (i instanceof a) { + var w = i.__data__; + if (!u || w.length < 199) return (w.push([s, o]), (this.size = ++i.size), this); + i = this.__data__ = new _(w); } return (i.set(s, o), (this.size = i.size), this); }; }, - 76959: (s) => { + 76959(s) { s.exports = function strictIndexOf(s, o, i) { - for (var u = i - 1, _ = s.length; ++u < _; ) if (s[u] === o) return u; + for (var a = i - 1, u = s.length; ++a < u; ) if (s[a] === o) return a; return -1; }; }, - 63912: (s, o, i) => { - var u = i(61074), - _ = i(49698), - w = i(42054); + 63912(s, o, i) { + var a = i(61074), + u = i(49698), + _ = i(42054); s.exports = function stringToArray(s) { - return _(s) ? w(s) : u(s); + return u(s) ? _(s) : a(s); }; }, - 61802: (s, o, i) => { - var u = i(62224), - _ = + 61802(s, o, i) { + var a = i(62224), + u = /[^.[\]]+|\[(?:(-?\d+(?:\.\d+)?)|(["'])((?:(?!\2)[^\\]|\\.)*?)\2)\]|(?=(?:\.|\[\])(?:\.|\[\]|$))/g, - w = /\\(\\)?/g, - x = u(function (s) { + _ = /\\(\\)?/g, + w = a(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); + s.replace(u, function (s, i, a, u) { + o.push(a ? u.replace(_, '$1') : i || s); }), o ); }); - s.exports = x; + s.exports = w; }, - 77797: (s, o, i) => { - var u = i(44394); + 77797(s, o, i) { + var a = i(44394); s.exports = function toKey(s) { - if ('string' == typeof s || u(s)) return s; + if ('string' == typeof s || a(s)) return s; var o = s + ''; return '0' == o && 1 / s == -1 / 0 ? '-0' : o; }; }, - 47473: (s) => { + 47473(s) { var o = Function.prototype.toString; s.exports = function toSource(s) { if (null != s) { @@ -11943,75 +11269,75 @@ return ''; }; }, - 31800: (s) => { + 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) => { + 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'); + a = '[\\u0300-\\u036f\\ufe20-\\ufe2f\\u20d0-\\u20ff]', + u = '\\ud83c[\\udffb-\\udfff]', + _ = '[^' + o + ']', + w = '(?:\\ud83c[\\udde6-\\uddff]){2}', + x = '[\\ud800-\\udbff][\\udc00-\\udfff]', + C = '(?:' + a + '|' + u + ')' + '?', + j = '[\\ufe0e\\ufe0f]?', + L = j + C + ('(?:\\u200d(?:' + [_, w, x].join('|') + ')' + j + C + ')*'), + B = '(?:' + [_ + a + '?', a, w, x, i].join('|') + ')', + $ = RegExp(u + '(?=' + u + ')|' + B + L, 'g'); s.exports = function unicodeToArray(s) { - return s.match(V) || []; + return s.match($) || []; }; }, - 22225: (s) => { + 22225(s) { var o = '\\ud800-\\udfff', i = '\\u2700-\\u27bf', - u = 'a-z\\xdf-\\xf6\\xf8-\\xff', - _ = 'A-Z\\xc0-\\xd6\\xd8-\\xde', - w = + a = 'a-z\\xdf-\\xf6\\xf8-\\xff', + u = 'A-Z\\xc0-\\xd6\\xd8-\\xde', + _ = '\\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( + w = '[' + _ + ']', + x = '\\d+', + C = '[' + i + ']', + j = '[' + a + ']', + L = '[^' + o + _ + x + i + a + u + ']', + B = '(?:\\ud83c[\\udde6-\\uddff]){2}', + $ = '[\\ud800-\\udbff][\\udc00-\\udfff]', + U = '[' + u + ']', + V = '(?:' + j + '|' + L + ')', + z = '(?:' + U + '|' + L + ')', + Y = "(?:['’](?:d|ll|m|re|s|t|ve))?", + Z = "(?:['’](?:D|LL|M|RE|S|T|VE))?", + ee = '(?:[\\u0300-\\u036f\\ufe20-\\ufe2f\\u20d0-\\u20ff]|\\ud83c[\\udffb-\\udfff])?', + ie = '[\\ufe0e\\ufe0f]?', + ae = + ie + ee + ('(?:\\u200d(?:' + ['[^' + o + ']', B, $].join('|') + ')' + ie + ee + ')*'), + ce = '(?:' + [C, B, $].join('|') + ')' + ae, + le = RegExp( [ - U + '?' + L + '+' + Z + '(?=' + [x, U, '$'].join('|') + ')', - Y + '+' + ee + '(?=' + [x, U + z, '$'].join('|') + ')', - U + '?' + z + '+' + Z, - U + '+' + ee, + U + '?' + j + '+' + Y + '(?=' + [w, U, '$'].join('|') + ')', + z + '+' + Z + '(?=' + [w, U + V, '$'].join('|') + ')', + U + '?' + V + '+' + Y, + U + '+' + Z, '\\d*(?:1ST|2ND|3RD|(?![123])\\dTH)(?=\\b|[a-z_])', '\\d*(?:1st|2nd|3rd|(?![123])\\dth)(?=\\b|[A-Z_])', - C, + x, ce ].join('|'), 'g' ); s.exports = function unicodeWords(s) { - return s.match(pe) || []; + return s.match(le) || []; }; }, - 75948: (s, o, i) => { - var u = i(83729), - _ = i(15325), - w = [ + 75948(s, o, i) { + var a = i(83729), + u = i(15325), + _ = [ ['ary', 128], ['bind', 1], ['bindKey', 2], @@ -12024,191 +11350,191 @@ ]; s.exports = function updateWrapDetails(s, o) { return ( - u(w, function (i) { - var u = '_.' + i[0]; - o & i[1] && !_(s, u) && s.push(u); + a(_, function (i) { + var a = '_.' + i[0]; + o & i[1] && !u(s, a) && s.push(a); }), s.sort() ); }; }, - 80257: (s, o, i) => { - var u = i(30980), - _ = i(56017), - w = i(23007); + 80257(s, o, i) { + var a = i(30980), + u = i(56017), + _ = i(23007); s.exports = function wrapperClone(s) { - if (s instanceof u) return s.clone(); - var o = new _(s.__wrapped__, s.__chain__); + if (s instanceof a) return s.clone(); + var o = new u(s.__wrapped__, s.__chain__); return ( - (o.__actions__ = w(s.__actions__)), + (o.__actions__ = _(s.__actions__)), (o.__index__ = s.__index__), (o.__values__ = s.__values__), o ); }; }, - 64626: (s, o, i) => { - var u = i(66977); + 64626(s, o, i) { + var a = 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) + a(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)); + 84058(s, o, i) { + var a = i(14792), + u = i(45539)(function (s, o, i) { + return ((o = o.toLowerCase()), s + (i ? a(o) : o)); }); - s.exports = _; + s.exports = u; }, - 14792: (s, o, i) => { - var u = i(13222), - _ = i(55808); + 14792(s, o, i) { + var a = i(13222), + u = i(55808); s.exports = function capitalize(s) { - return _(u(s).toLowerCase()); + return u(a(s).toLowerCase()); }; }, - 32629: (s, o, i) => { - var u = i(9999); + 32629(s, o, i) { + var a = i(9999); s.exports = function clone(s) { - return u(s, 4); + return a(s, 4); }; }, - 37334: (s) => { + 37334(s) { s.exports = function constant(s) { return function () { return s; }; }; }, - 49747: (s, o, i) => { - var u = i(66977); + 49747(s, o, i) { + var a = 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), _); + var u = a(s, 8, void 0, void 0, void 0, void 0, void 0, (o = i ? void 0 : o)); + return ((u.placeholder = curry.placeholder), u); } ((curry.placeholder = {}), (s.exports = curry)); }, - 38221: (s, o, i) => { - var u = i(23805), - _ = i(10124), - w = i(99374), - x = Math.max, - C = Math.min; + 38221(s, o, i) { + var a = i(23805), + u = i(10124), + _ = i(99374), + w = Math.max, + x = Math.min; s.exports = function debounce(s, o, i) { - var j, + var C, + j, L, B, $, - V, U, - z = 0, + V = 0, + z = !1, Y = !1, - Z = !1, - ee = !0; + Z = !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))); + var i = C, + a = j; + return ((C = j = void 0), (V = o), (B = s.apply(a, i))); } function shouldInvoke(s) { var i = s - U; - return void 0 === U || i >= o || i < 0 || (Z && s - z >= B); + return void 0 === U || i >= o || i < 0 || (Y && s - V >= L); } function timerExpired() { - var s = _(); + var s = u(); if (shouldInvoke(s)) return trailingEdge(s); - V = setTimeout( + $ = setTimeout( timerExpired, (function remainingWait(s) { var i = o - (s - U); - return Z ? C(i, B - (s - z)) : i; + return Y ? x(i, L - (s - V)) : i; })(s) ); } function trailingEdge(s) { - return ((V = void 0), ee && j ? invokeFunc(s) : ((j = L = void 0), $)); + return (($ = void 0), Z && C ? invokeFunc(s) : ((C = j = void 0), B)); } function debounced() { - var s = _(), + var s = u(), i = shouldInvoke(s); - if (((j = arguments), (L = this), (U = s), i)) { - if (void 0 === V) + if (((C = arguments), (j = this), (U = s), i)) { + if (void 0 === $) return (function leadingEdge(s) { - return ((z = s), (V = setTimeout(timerExpired, o)), Y ? invokeFunc(s) : $); + return ((V = s), ($ = setTimeout(timerExpired, o)), z ? invokeFunc(s) : B); })(U); - if (Z) return (clearTimeout(V), (V = setTimeout(timerExpired, o)), invokeFunc(U)); + if (Y) return (clearTimeout($), ($ = setTimeout(timerExpired, o)), invokeFunc(U)); } - return (void 0 === V && (V = setTimeout(timerExpired, o)), $); + return (void 0 === $ && ($ = setTimeout(timerExpired, o)), B); } 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)), + (o = _(o) || 0), + a(i) && + ((z = !!i.leading), + (L = (Y = 'maxWait' in i) ? w(_(i.maxWait) || 0, o) : L), + (Z = 'trailing' in i ? !!i.trailing : Z)), (debounced.cancel = function cancel() { - (void 0 !== V && clearTimeout(V), (z = 0), (j = U = L = V = void 0)); + (void 0 !== $ && clearTimeout($), (V = 0), (C = U = j = $ = void 0)); }), (debounced.flush = function flush() { - return void 0 === V ? $ : trailingEdge(_()); + return void 0 === $ ? B : trailingEdge(u()); }), 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'); + 50828(s, o, i) { + var a = i(24647), + u = i(13222), + _ = /[\xc0-\xd6\xd8-\xf6\xf8-\xff\u0100-\u017f]/g, + w = RegExp('[\\u0300-\\u036f\\ufe20-\\ufe2f\\u20d0-\\u20ff]', 'g'); s.exports = function deburr(s) { - return (s = _(s)) && s.replace(w, u).replace(x, ''); + return (s = u(s)) && s.replace(_, a).replace(w, ''); }; }, - 75288: (s) => { + 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); + 60680(s, o, i) { + var a = i(13222), + u = /[\\^$.*+?()[\]{}|]/g, + _ = RegExp(u.source); s.exports = function escapeRegExp(s) { - return (s = u(s)) && w.test(s) ? s.replace(_, '\\$&') : s; + return (s = a(s)) && _.test(s) ? s.replace(u, '\\$&') : s; }; }, - 7309: (s, o, i) => { - var u = i(62006)(i(24713)); - s.exports = u; + 7309(s, o, i) { + var a = i(62006)(i(24713)); + s.exports = a; }, - 24713: (s, o, i) => { - var u = i(2523), - _ = i(15389), - w = i(61489), - x = Math.max; + 24713(s, o, i) { + var a = i(2523), + u = i(15389), + _ = i(61489), + w = 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)); + var x = null == s ? 0 : s.length; + if (!x) return -1; + var C = null == i ? 0 : _(i); + return (C < 0 && (C = w(x + C, 0)), a(s, u(o, 3), C)); }; }, - 35970: (s, o, i) => { - var u = i(83120); + 35970(s, o, i) { + var a = i(83120); s.exports = function flatten(s) { - return (null == s ? 0 : s.length) ? u(s, 1) : []; + return (null == s ? 0 : s.length) ? a(s, 1) : []; }; }, - 73424: (s, o, i) => { - var u = i(16962), - _ = i(2874), - w = Array.prototype.push; + 73424(s, o, i) { + var a = i(16962), + u = i(2874), + _ = Array.prototype.push; function baseAry(s, o) { return 2 == o ? function (o, i) { @@ -12226,28 +11552,28 @@ 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), _); + for (var a = Array(i); i--; ) a[i] = arguments[i]; + var u = (a[0] = o.apply(void 0, a)); + return (s.apply(void 0, a), 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 + s.exports = function baseConvert(s, o, i, w) { + var x = 'function' == typeof o, + C = o === Object(o); + if ((C && ((w = i), (i = o), (o = void 0)), null == i)) throw new TypeError(); + w || (w = {}); + var j = !('cap' in w) || w.cap, + L = !('curry' in w) || w.curry, + B = !('fixed' in w) || w.fixed, + $ = !('immutable' in w) || w.immutable, + U = !('rearg' in w) || w.rearg, + V = x ? i : u, + z = 'curry' in w && w.curry, + Y = 'fixed' in w && w.fixed, + Z = 'rearg' in w && w.rearg, + ee = x ? i.runInContext() : void 0, + ie = x ? i : { ary: s.ary, @@ -12265,50 +11591,50 @@ 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 = { + ae = ie.ary, + ce = ie.assign, + le = ie.clone, + pe = ie.curry, + de = ie.forEach, + fe = ie.isArray, + ye = ie.isError, + be = ie.isFunction, + Se = ie.isWeakMap, + _e = ie.keys, + we = ie.rearg, + xe = ie.toInteger, + Pe = ie.toPath, + Te = _e(a.aryMethod), + Re = { castArray: function (s) { return function () { var o = arguments[0]; - return ye(o) ? s(cloneArray(o)) : s.apply(void 0, arguments); + return fe(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)) + a = i.length; + return j && 'number' == typeof o + ? ((o = o > 2 ? o - 2 : 1), a && a <= 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 = []; + if (!be(i)) return s(i, Object(o)); + var a = []; return ( - fe(Se(o), function (s) { - _e(o[s]) && u.push([s, i.prototype[s]]); + de(_e(o), function (s) { + be(o[s]) && a.push([s, i.prototype[s]]); }), s(i, Object(o)), - fe(u, function (s) { + de(a, function (s) { var o = s[1]; - _e(o) ? (i.prototype[s[0]] = o) : delete i.prototype[s[0]]; + be(o) ? (i.prototype[s[0]] = o) : delete i.prototype[s[0]]; }), i ); @@ -12316,25 +11642,25 @@ }, nthArg: function (s) { return function (o) { - var i = o < 0 ? 1 : Pe(o) + 1; - return de(s(o), i); + var i = o < 0 ? 1 : xe(o) + 1; + return pe(s(o), i); }; }, rearg: function (s) { return function (o, i) { - var u = i ? i.length : 0; - return de(s(o, i), u); + var a = i ? i.length : 0; + return pe(s(o, i), a); }; }, runInContext: function (o) { return function (i) { - return baseConvert(s, o(i), x); + return baseConvert(s, o(i), w); }; } }; function castCap(s, o) { - if (L) { - var i = u.iterateeRearg[s]; + if (j) { + var i = a.iterateeRearg[s]; if (i) return (function iterateeRearg(s, o) { return overArg(s, function (s) { @@ -12347,91 +11673,91 @@ : function (o) { return s.apply(void 0, arguments); }; - })(xe(baseAry(s, i), o), i); + })(we(baseAry(s, i), o), i); }); })(o, i); - var _ = !C && u.iterateeAry[s]; - if (_) + var u = !x && a.iterateeAry[s]; + if (u) return (function iterateeAry(s, o) { return overArg(s, function (s) { return 'function' == typeof s ? baseAry(s, o) : s; }); - })(o, _); + })(o, u); } 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) + if (B && (Y || !a.skipFixed[s])) { + var u = a.methodSpread[s], + w = u && u.start; + return void 0 === w + ? ae(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); + for (var i = arguments.length, a = i - 1, u = Array(i); i--; ) + u[i] = arguments[i]; + var w = u[o], + x = u.slice(0, o); return ( - x && w.apply(C, x), - o != u && w.apply(C, _.slice(o + 1)), - s.apply(this, C) + w && _.apply(x, w), + o != a && _.apply(x, u.slice(o + 1)), + s.apply(this, x) ); }; - })(o, x); + })(o, w); } return o; } function castRearg(s, o, i) { - return U && i > 1 && (ee || !u.skipRearg[s]) - ? xe(o, u.methodRearg[s] || u.aryRearg[i]) + return U && i > 1 && (Z || !a.skipRearg[s]) + ? we(o, a.methodRearg[s] || a.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 i = -1, a = (o = Pe(o)).length, u = a - 1, _ = le(Object(s)), w = _; + null != w && ++i < a; ) { - 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])); + var x = o[i], + C = w[x]; + (null == C || be(C) || ye(C) || Se(C) || (w[x] = le(i == u ? C : Object(C))), + (w = w[x])); } - return w; + return _; } function createConverter(s, o) { - var i = u.aliasToReal[s] || s, - _ = u.remap[i] || i, - w = x; + var i = a.aliasToReal[s] || s, + u = a.remap[i] || i, + _ = w; return function (s) { - var u = C ? ie : ae, - x = C ? ie[_] : o, - j = ce(ce({}, w), s); - return baseConvert(u, i, x, j); + var a = x ? ee : ie, + w = x ? ee[u] : o, + C = ce(ce({}, _), s); + return baseConvert(a, i, w, C); }; } 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)); + for (var a = Array(i); i--; ) a[i] = arguments[i]; + var u = U ? 0 : i - 1; + return ((a[u] = o(a[u])), s.apply(void 0, a)); }; } function wrap(s, o, i) { - var _, - w = u.aliasToReal[s] || s, - x = o, - C = qe[w]; + var u, + _ = a.aliasToReal[s] || s, + w = o, + x = Re[_]; return ( - C - ? (x = C(o)) - : V && - (u.mutate.array[w] - ? (x = wrapImmutable(o, cloneArray)) - : u.mutate.object[w] - ? (x = wrapImmutable( + x + ? (w = x(o)) + : $ && + (a.mutate.array[_] + ? (w = wrapImmutable(o, cloneArray)) + : a.mutate.object[_] + ? (w = wrapImmutable( o, (function createCloner(s) { return function (o) { @@ -12439,65 +11765,65 @@ }; })(o) )) - : u.mutate.set[w] && (x = wrapImmutable(o, cloneByPath))), - fe(Re, function (s) { + : a.mutate.set[_] && (w = wrapImmutable(o, cloneByPath))), + de(Te, function (s) { return ( - fe(u.aryMethod[s], function (o) { - if (w == o) { - var i = u.methodSpread[w], - C = i && i.afterRearg; + de(a.aryMethod[s], function (o) { + if (_ == o) { + var i = a.methodSpread[_], + x = 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)), + (u = x + ? castFixed(_, castRearg(_, w, s), s) + : castRearg(_, castFixed(_, w, s), s)), + (u = (function castCurry(s, o, i) { + return z || (L && i > 1) ? pe(o, i) : o; + })(0, (u = castCap(_, u)), s)), !1 ); } }), - !_ + !u ); }), - _ || (_ = x), - _ == o && - (_ = Y - ? de(_, 1) + u || (u = w), + u == o && + (u = z + ? pe(u, 1) : function () { return o.apply(this, arguments); }), - (_.convert = createConverter(w, o)), - (_.placeholder = o.placeholder = i), - _ + (u.convert = createConverter(_, o)), + (u.placeholder = o.placeholder = i), + u ); } - if (!j) return wrap(o, i, z); + if (!C) return wrap(o, i, V); var $e = i, - ze = []; + qe = []; 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)]); + de(Te, function (s) { + de(a.aryMethod[s], function (s) { + var o = $e[a.remap[s] || s]; + o && qe.push([s, wrap(s, o, $e)]); }); }), - fe(Se($e), function (s) { + de(_e($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])); + for (var i = qe.length; i--; ) if (qe[i][0] == s) return; + ((o.convert = createConverter(s, o)), qe.push([s, o])); } }), - fe(ze, function (s) { + de(qe, 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) { + de(_e($e), function (s) { + de(a.realToAlias[s] || [], function (o) { $e[o] = $e[s]; }); }), @@ -12505,7 +11831,7 @@ ); }; }, - 16962: (s, o) => { + 16962(s, o) { ((o.aliasToReal = { each: 'forEach', eachRight: 'forEachRight', @@ -12920,12 +12246,12 @@ (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] = [_]); + a = {}; + for (var u in i) { + var _ = i[u]; + s.call(a, _) ? a[_].push(u) : (a[_] = [u]); } - return u; + return a; })()), (o.remap = { assignAll: 'assign', @@ -13001,7 +12327,7 @@ zipObjectDeep: !0 })); }, - 47934: (s, o, i) => { + 47934(s, o, i) { s.exports = { ary: i(64626), assign: i(74733), @@ -13019,156 +12345,156 @@ toPath: i(42072) }; }, - 56367: (s, o, i) => { + 56367(s, o, i) { s.exports = i(77731); }, - 79920: (s, o, i) => { - var u = i(73424), - _ = i(47934); + 79920(s, o, i) { + var a = i(73424), + u = i(47934); s.exports = function convert(s, o, i) { - return u(_, s, o, i); + return a(u, s, o, i); }; }, - 2874: (s) => { + 2874(s) { s.exports = {}; }, - 77731: (s, o, i) => { - var u = i(79920)('set', i(63560)); - ((u.placeholder = i(2874)), (s.exports = u)); + 77731(s, o, i) { + var a = i(79920)('set', i(63560)); + ((a.placeholder = i(2874)), (s.exports = a)); }, - 58156: (s, o, i) => { - var u = i(47422); + 58156(s, o, i) { + var a = i(47422); s.exports = function get(s, o, i) { - var _ = null == s ? void 0 : u(s, o); - return void 0 === _ ? i : _; + var u = null == s ? void 0 : a(s, o); + return void 0 === u ? i : u; }; }, - 61448: (s, o, i) => { - var u = i(20426), - _ = i(49326); + 61448(s, o, i) { + var a = i(20426), + u = i(49326); s.exports = function has(s, o) { - return null != s && _(s, o, u); + return null != s && u(s, o, a); }; }, - 80631: (s, o, i) => { - var u = i(28077), - _ = i(49326); + 80631(s, o, i) { + var a = i(28077), + u = i(49326); s.exports = function hasIn(s, o) { - return null != s && _(s, o, u); + return null != s && u(s, o, a); }; }, - 83488: (s) => { + 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( + 72428(s, o, i) { + var a = i(27534), + u = i(40346), + _ = Object.prototype, + w = _.hasOwnProperty, + x = _.propertyIsEnumerable, + C = a( (function () { return arguments; })() ) - ? u + ? a : function (s) { - return _(s) && x.call(s, 'callee') && !C.call(s, 'callee'); + return u(s) && w.call(s, 'callee') && !x.call(s, 'callee'); }; - s.exports = j; + s.exports = C; }, - 56449: (s) => { + 56449(s) { var o = Array.isArray; s.exports = o; }, - 64894: (s, o, i) => { - var u = i(1882), - _ = i(30294); + 64894(s, o, i) { + var a = i(1882), + u = i(30294); s.exports = function isArrayLike(s) { - return null != s && _(s.length) && !u(s); + return null != s && u(s.length) && !a(s); }; }, - 83693: (s, o, i) => { - var u = i(64894), - _ = i(40346); + 83693(s, o, i) { + var a = i(64894), + u = i(40346); s.exports = function isArrayLikeObject(s) { - return _(s) && u(s); + return u(s) && a(s); }; }, - 53812: (s, o, i) => { - var u = i(72552), - _ = i(40346); + 53812(s, o, i) { + var a = i(72552), + u = i(40346); s.exports = function isBoolean(s) { - return !0 === s || !1 === s || (_(s) && '[object Boolean]' == u(s)); + return !0 === s || !1 === s || (u(s) && '[object Boolean]' == a(s)); }; }, - 3656: (s, o, i) => { + 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; + var a = i(9325), + u = i(89935), + _ = o && !o.nodeType && o, + w = _ && s && !s.nodeType && s, + x = w && w.exports === _ ? a.Buffer : void 0, + C = (x ? x.isBuffer : void 0) || u; + s.exports = C; }, - 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; + 62193(s, o, i) { + var a = i(88984), + u = i(5861), + _ = i(72428), + w = i(56449), + x = i(64894), + C = i(3656), + j = i(55527), + L = i(37167), + B = Object.prototype.hasOwnProperty; s.exports = function isEmpty(s) { if (null == s) return !0; if ( - C(s) && - (x(s) || + x(s) && + (w(s) || 'string' == typeof s || 'function' == typeof s.splice || - j(s) || - B(s) || - w(s)) + C(s) || + L(s) || + _(s)) ) return !s.length; - var o = _(s); + var o = u(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; + if (j(s)) return !a(s).length; + for (var i in s) if (B.call(s, i)) return !1; return !0; }; }, - 2404: (s, o, i) => { - var u = i(60270); + 2404(s, o, i) { + var a = i(60270); s.exports = function isEqual(s, o) { - return u(s, o); + return a(s, o); }; }, - 23546: (s, o, i) => { - var u = i(72552), - _ = i(40346), - w = i(11331); + 23546(s, o, i) { + var a = i(72552), + u = i(40346), + _ = i(11331); s.exports = function isError(s) { - if (!_(s)) return !1; - var o = u(s); + if (!u(s)) return !1; + var o = a(s); return ( '[object Error]' == o || '[object DOMException]' == o || - ('string' == typeof s.message && 'string' == typeof s.name && !w(s)) + ('string' == typeof s.message && 'string' == typeof s.name && !_(s)) ); }; }, - 1882: (s, o, i) => { - var u = i(72552), - _ = i(23805); + 1882(s, o, i) { + var a = i(72552), + u = i(23805); s.exports = function isFunction(s) { - if (!_(s)) return !1; - var o = u(s); + if (!u(s)) return !1; + var o = a(s); return ( '[object Function]' == o || '[object GeneratorFunction]' == o || @@ -13177,151 +12503,151 @@ ); }; }, - 30294: (s) => { + 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; + 87730(s, o, i) { + var a = i(29172), + u = i(27301), + _ = i(86009), + w = _ && _.isMap, + x = w ? u(w) : a; + s.exports = x; }, - 5187: (s) => { + 5187(s) { s.exports = function isNull(s) { return null === s; }; }, - 98023: (s, o, i) => { - var u = i(72552), - _ = i(40346); + 98023(s, o, i) { + var a = i(72552), + u = i(40346); s.exports = function isNumber(s) { - return 'number' == typeof s || (_(s) && '[object Number]' == u(s)); + return 'number' == typeof s || (u(s) && '[object Number]' == a(s)); }; }, - 23805: (s) => { + 23805(s) { s.exports = function isObject(s) { var o = typeof s; return null != s && ('object' == o || 'function' == o); }; }, - 40346: (s) => { + 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); + 11331(s, o, i) { + var a = i(72552), + u = i(28879), + _ = i(40346), + w = Function.prototype, + x = Object.prototype, + C = w.toString, + j = x.hasOwnProperty, + L = C.call(Object); s.exports = function isPlainObject(s) { - if (!w(s) || '[object Object]' != u(s)) return !1; - var o = _(s); + if (!_(s) || '[object Object]' != a(s)) return !1; + var o = u(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; + var i = j.call(o, 'constructor') && o.constructor; + return 'function' == typeof i && i instanceof i && C.call(i) == L; }; }, - 38440: (s, o, i) => { - var u = i(16038), - _ = i(27301), - w = i(86009), - x = w && w.isSet, - C = x ? _(x) : u; - s.exports = C; + 38440(s, o, i) { + var a = i(16038), + u = i(27301), + _ = i(86009), + w = _ && _.isSet, + x = w ? u(w) : a; + s.exports = x; }, - 85015: (s, o, i) => { - var u = i(72552), - _ = i(56449), - w = i(40346); + 85015(s, o, i) { + var a = i(72552), + u = i(56449), + _ = i(40346); s.exports = function isString(s) { - return 'string' == typeof s || (!_(s) && w(s) && '[object String]' == u(s)); + return 'string' == typeof s || (!u(s) && _(s) && '[object String]' == a(s)); }; }, - 44394: (s, o, i) => { - var u = i(72552), - _ = i(40346); + 44394(s, o, i) { + var a = i(72552), + u = i(40346); s.exports = function isSymbol(s) { - return 'symbol' == typeof s || (_(s) && '[object Symbol]' == u(s)); + return 'symbol' == typeof s || (u(s) && '[object Symbol]' == a(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; + 37167(s, o, i) { + var a = i(4901), + u = i(27301), + _ = i(86009), + w = _ && _.isTypedArray, + x = w ? u(w) : a; + s.exports = x; }, - 47886: (s, o, i) => { - var u = i(5861), - _ = i(40346); + 47886(s, o, i) { + var a = i(5861), + u = i(40346); s.exports = function isWeakMap(s) { - return _(s) && '[object WeakMap]' == u(s); + return u(s) && '[object WeakMap]' == a(s); }; }, - 33855: (s, o, i) => { - var u = i(9999), - _ = i(15389); + 33855(s, o, i) { + var a = i(9999), + u = i(15389); s.exports = function iteratee(s) { - return _('function' == typeof s ? s : u(s, 1)); + return u('function' == typeof s ? s : a(s, 1)); }; }, - 95950: (s, o, i) => { - var u = i(70695), - _ = i(88984), - w = i(64894); + 95950(s, o, i) { + var a = i(70695), + u = i(88984), + _ = i(64894); s.exports = function keys(s) { - return w(s) ? u(s) : _(s); + return _(s) ? a(s) : u(s); }; }, - 37241: (s, o, i) => { - var u = i(70695), - _ = i(72903), - w = i(64894); + 37241(s, o, i) { + var a = i(70695), + u = i(72903), + _ = i(64894); s.exports = function keysIn(s) { - return w(s) ? u(s, !0) : _(s); + return _(s) ? a(s, !0) : u(s); }; }, - 68090: (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); + 50104(s, o, i) { + var a = 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); + a = o ? o.apply(this, i) : i[0], + u = memoized.cache; + if (u.has(a)) return u.get(a); + var _ = s.apply(this, i); + return ((memoized.cache = u.set(a, _) || u), _); }; - return ((memoized.cache = new (memoize.Cache || u)()), memoized); + return ((memoized.cache = new (memoize.Cache || a)()), memoized); } - ((memoize.Cache = u), (s.exports = memoize)); + ((memoize.Cache = a), (s.exports = memoize)); }, - 55364: (s, o, i) => { - var u = i(85250), - _ = i(20999)(function (s, o, i) { - u(s, o, i); + 55364(s, o, i) { + var a = i(85250), + u = i(20999)(function (s, o, i) { + a(s, o, i); }); - s.exports = _; + s.exports = u; }, - 6048: (s) => { + 6048(s) { s.exports = function negate(s) { if ('function' != typeof s) throw new TypeError('Expected a function'); return function () { @@ -13340,100 +12666,100 @@ }; }; }, - 63950: (s) => { + 63950(s) { s.exports = function noop() {}; }, - 10124: (s, o, i) => { - var u = i(9325); + 10124(s, o, i) { + var a = i(9325); s.exports = function () { - return u.Date.now(); + return a.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) { + 90179(s, o, i) { + var a = i(34932), + u = i(9999), + _ = i(19931), + w = i(31769), + x = i(21791), + C = i(53138), + j = i(38816), + L = i(83349), + B = j(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); + var j = !1; + ((o = a(o, function (o) { + return ((o = w(o, s)), j || (j = o.length > 1), o); })), - C(s, B(s), i), - L && (i = _(i, 7, j))); - for (var $ = o.length; $--; ) w(i, o[$]); + x(s, L(s), i), + j && (i = u(i, 7, C))); + for (var B = o.length; B--; ) _(i, o[B]); return i; }); - s.exports = $; + s.exports = B; }, - 50583: (s, o, i) => { - var u = i(47237), - _ = i(17255), - w = i(28586), - x = i(77797); + 50583(s, o, i) { + var a = i(47237), + u = i(17255), + _ = i(28586), + w = i(77797); s.exports = function property(s) { - return w(s) ? u(x(s)) : _(s); + return _(s) ? a(w(s)) : u(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); + 84195(s, o, i) { + var a = i(66977), + u = i(38816), + _ = u(function (s, o) { + return a(s, 256, void 0, void 0, void 0, o); }); - s.exports = w; + s.exports = _; }, - 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), + 40860(s, o, i) { + var a = i(40882), + u = i(80909), _ = 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))); + w = i(85558), + x = i(56449); + s.exports = function reduce(s, o, i) { + var C = x(s) ? a : w, + j = arguments.length < 3; + return C(s, _(o, 4), i, j, u); }; }, - 63345: (s) => { + 63560(s, o, i) { + var a = i(73170); + s.exports = function set(s, o, i) { + return null == s ? s : a(s, o, i); + }; + }, + 42426(s, o, i) { + var a = i(14248), + u = i(15389), + _ = i(90916), + w = i(56449), + x = i(36800); + s.exports = function some(s, o, i) { + var C = w(s) ? a : _; + return (i && x(s, o, i) && (o = void 0), C(s, u(o, 3))); + }; + }, + 63345(s) { s.exports = function stubArray() { return []; }; }, - 89935: (s) => { + 89935(s) { s.exports = function stubFalse() { return !1; }; }, - 17400: (s, o, i) => { - var u = i(99374), - _ = 1 / 0; + 17400(s, o, i) { + var a = i(99374), + u = 1 / 0; s.exports = function toFinite(s) { return s - ? (s = u(s)) === _ || s === -1 / 0 + ? (s = a(s)) === u || s === -1 / 0 ? 17976931348623157e292 * (s < 0 ? -1 : 1) : s == s ? s @@ -13443,154 +12769,154 @@ : 0; }; }, - 61489: (s, o, i) => { - var u = i(17400); + 61489(s, o, i) { + var a = i(17400); s.exports = function toInteger(s) { - var o = u(s), + var o = a(s), i = o % 1; return o == o ? (i ? o - i : o) : 0; }; }, - 80218: (s, o, i) => { - var u = i(13222); + 80218(s, o, i) { + var a = i(13222); s.exports = function toLower(s) { - return u(s).toLowerCase(); + return a(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; + 99374(s, o, i) { + var a = i(54128), + u = i(23805), + _ = i(44394), + w = /^[-+]0x[0-9a-f]+$/i, + x = /^0b[01]+$/i, + C = /^0o[0-7]+$/i, + j = parseInt; s.exports = function toNumber(s) { if ('number' == typeof s) return s; - if (w(s)) return NaN; - if (_(s)) { + if (_(s)) return NaN; + if (u(s)) { var o = 'function' == typeof s.valueOf ? s.valueOf() : s; - s = _(o) ? o + '' : o; + s = u(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; + s = a(s); + var i = x.test(s); + return i || C.test(s) ? j(s.slice(2), i ? 2 : 8) : w.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); + 42072(s, o, i) { + var a = i(34932), + u = i(23007), + _ = i(56449), + w = i(44394), + x = i(61802), + C = i(77797), + j = i(13222); s.exports = function toPath(s) { - return w(s) ? u(s, j) : x(s) ? [s] : _(C(L(s))); + return _(s) ? a(s, C) : w(s) ? [s] : u(x(j(s))); }; }, - 69884: (s, o, i) => { - var u = i(21791), - _ = i(37241); + 69884(s, o, i) { + var a = i(21791), + u = i(37241); s.exports = function toPlainObject(s) { - return u(s, _(s)); + return a(s, u(s)); }; }, - 13222: (s, o, i) => { - var u = i(77556); + 13222(s, o, i) { + var a = i(77556); s.exports = function toString(s) { - return null == s ? '' : u(s); + return null == s ? '' : a(s); }; }, - 55808: (s, o, i) => { - var u = i(12507)('toUpperCase'); - s.exports = u; + 55808(s, o, i) { + var a = i(12507)('toUpperCase'); + s.exports = a; }, - 66645: (s, o, i) => { - var u = i(1733), - _ = i(45434), - w = i(13222), - x = i(22225); + 66645(s, o, i) { + var a = i(1733), + u = i(45434), + _ = i(13222), + w = 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) || [] + (s = _(s)), + void 0 === (o = i ? void 0 : o) ? (u(s) ? w(s) : a(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; + 53758(s, o, i) { + var a = i(30980), + u = i(56017), + _ = i(94033), + w = i(56449), + x = i(40346), + C = i(80257), + j = 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); + if (x(s) && !w(s) && !(s instanceof a)) { + if (s instanceof u) return s; + if (j.call(s, '__wrapped__')) return C(s); } - return new _(s); + return new u(s); } - ((lodash.prototype = w.prototype), + ((lodash.prototype = _.prototype), (lodash.prototype.constructor = lodash), (s.exports = lodash)); }, - 47248: (s, o, i) => { - var u = i(16547), - _ = i(51234); + 47248(s, o, i) { + var a = i(16547), + u = i(51234); s.exports = function zipObject(s, o) { - return _(s || [], o || [], u); + return u(s || [], o || [], a); }; }, - 43768: (s, o, i) => { + 43768(s, o, i) { 'use strict'; - var u = i(45981), - _ = i(85587); + var a = i(45981), + u = i(85587); ((o.highlight = highlight), (o.highlightAuto = function highlightAuto(s, o) { var i, + w, x, C, - j, - L = o || {}, - B = L.subset || u.listLanguages(), - $ = L.prefix, - V = B.length, + j = o || {}, + L = j.subset || a.listLanguages(), + B = j.prefix, + $ = L.length, U = -1; - null == $ && ($ = w); - if ('string' != typeof s) throw _('Expected `string` for value, got `%s`', s); - ((x = { relevance: 0, language: null, value: [] }), + null == B && (B = _); + if ('string' != typeof s) throw u('Expected `string` for value, got `%s`', s); + ((w = { 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); + for (; ++U < $; ) + ((C = L[U]), + a.getLanguage(C) && + (((x = highlight(C, s, o)).language = C), + x.relevance > w.relevance && (w = x), + x.relevance > i.relevance && ((w = i), (i = x)))); + w.language && (i.secondBest = w); return i; }), (o.registerLanguage = function registerLanguage(s, o) { - u.registerLanguage(s, o); + a.registerLanguage(s, o); }), (o.listLanguages = function listLanguages() { - return u.listLanguages(); + return a.listLanguages(); }), (o.registerAlias = function registerAlias(s, o) { var i, - _ = s; - o && ((_ = {})[s] = o); - for (i in _) u.registerAliases(_[i], { languageName: i }); + u = s; + o && ((u = {})[s] = o); + for (i in u) a.registerAliases(u[i], { languageName: i }); }), (Emitter.prototype.addText = function text(s) { var o, i, - u = this.stack; + a = this.stack; if ('' === s) return; - ((o = u[u.length - 1]), + ((o = a[a.length - 1]), (i = o.children[o.children.length - 1]) && 'text' === i.type ? (i.value += s) : o.children.push({ type: 'text', value: s })); @@ -13600,29 +12926,29 @@ }), (Emitter.prototype.addSublanguage = function addSublanguage(s, o) { var i = this.stack, - u = i[i.length - 1], - _ = s.rootNode.children, - w = o + a = i[i.length - 1], + u = s.rootNode.children, + _ = o ? { type: 'element', tagName: 'span', properties: { className: [o] }, - children: _ + children: u } - : _; - u.children = u.children.concat(w); + : u; + a.children = a.children.concat(_); }), (Emitter.prototype.openNode = function open(s) { var o = this.stack, i = this.options.classPrefix + s, - u = o[o.length - 1], - _ = { + a = o[o.length - 1], + u = { type: 'element', tagName: 'span', properties: { className: [i] }, children: [] }; - (u.children.push(_), o.push(_)); + (a.children.push(u), o.push(u)); }), (Emitter.prototype.closeNode = function close() { this.stack.pop(); @@ -13632,26 +12958,26 @@ (Emitter.prototype.toHTML = function toHtmlNoop() { return ''; })); - var w = 'hljs-'; + var _ = '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); + var w, + x = a.configure({}), + C = (i || {}).prefix; + if ('string' != typeof s) throw u('Expected `string` for name, got `%s`', s); + if (!a.getLanguage(s)) throw u('Unknown language: `%s` is not registered', s); + if ('string' != typeof o) throw u('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) + (null == C && (C = _), + a.configure({ __emitter: Emitter, classPrefix: C }), + (w = a.highlight(o, { language: s, ignoreIllegals: !0 })), + a.configure(x || {}), + w.errorRaised) ) - throw x.errorRaised; + throw w.errorRaised; return { - relevance: x.relevance, - language: x.language, - value: x.emitter.rootNode.children + relevance: w.relevance, + language: w.language, + value: w.emitter.rootNode.children }; } function Emitter(s) { @@ -13661,8 +12987,47 @@ } function noop() {} }, - 92340: (s, o, i) => { - const u = i(6048); + 71514(s) { + 'use strict'; + s.exports = Math.abs; + }, + 58968(s) { + 'use strict'; + s.exports = Math.floor; + }, + 94459(s) { + 'use strict'; + s.exports = + Number.isNaN || + function isNaN(s) { + return s != s; + }; + }, + 6188(s) { + 'use strict'; + s.exports = Math.max; + }, + 68002(s) { + 'use strict'; + s.exports = Math.min; + }, + 75880(s) { + 'use strict'; + s.exports = Math.pow; + }, + 70414(s) { + 'use strict'; + s.exports = Math.round; + }, + 73093(s, o, i) { + 'use strict'; + var a = i(94459); + s.exports = function sign(s) { + return a(s) || 0 === s ? s : s < 0 ? -1 : 1; + }; + }, + 92340(s, o, i) { + const a = i(6048); function coerceElementMatchingCallback(s) { return 'string' == typeof s ? (o) => o.element === s @@ -13686,9 +13051,9 @@ compactMap(s, o) { const i = []; return ( - this.forEach((u) => { - const _ = s.bind(o)(u); - _ && i.push(_); + this.forEach((a) => { + const u = s.bind(o)(a); + u && i.push(u); }), i ); @@ -13702,7 +13067,7 @@ reject(s, o) { return ( (s = coerceElementMatchingCallback(s)), - new ArraySlice(this.elements.filter(u(s), o)) + new ArraySlice(this.elements.filter(a(s), o)) ); } find(s, o) { @@ -13752,7 +13117,7 @@ }), (s.exports = ArraySlice)); }, - 55973: (s) => { + 55973(s) { class KeyValuePair { constructor(s, o) { ((this.key = s), (this.value = o)); @@ -13768,20 +13133,20 @@ } 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); + 3110(s, o, i) { + const a = i(5187), + u = i(85015), + _ = i(98023), + w = i(53812), + x = i(23805), + C = i(85105), + j = i(86804); class Namespace { constructor(s) { ((this.elementMap = {}), (this.elementDetection = []), - (this.Element = L.Element), - (this.KeyValuePair = L.KeyValuePair), + (this.Element = j.Element), + (this.KeyValuePair = j.KeyValuePair), (s && s.noDefault) || this.useDefault(), (this._attributeElementKeys = []), (this._attributeElementArrayKeys = [])); @@ -13795,21 +13160,21 @@ } 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('null', j.NullElement) + .register('string', j.StringElement) + .register('number', j.NumberElement) + .register('boolean', j.BooleanElement) + .register('array', j.ArrayElement) + .register('object', j.ObjectElement) + .register('member', j.MemberElement) + .register('ref', j.RefElement) + .register('link', j.LinkElement), + this.detect(a, j.NullElement, !1) + .detect(u, j.StringElement, !1) + .detect(_, j.NumberElement, !1) + .detect(w, j.BooleanElement, !1) + .detect(Array.isArray, j.ArrayElement, !1) + .detect(x, j.ObjectElement, !1), this ); } @@ -13831,10 +13196,10 @@ 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); + const a = this.elementDetection[i][0], + u = this.elementDetection[i][1]; + if (a(s)) { + o = new u(s); break; } } @@ -13862,15 +13227,15 @@ ); } get serialiser() { - return new j(this); + return new C(this); } } - ((j.prototype.Namespace = Namespace), (s.exports = Namespace)); + ((C.prototype.Namespace = Namespace), (s.exports = Namespace)); }, - 10866: (s, o, i) => { - const u = i(6048), - _ = i(92340); - class ObjectSlice extends _ { + 10866(s, o, i) { + const a = i(6048), + u = i(92340); + class ObjectSlice extends u { map(s, o) { return this.elements.map((i) => s.bind(o)(i.value, i.key, i)); } @@ -13878,11 +13243,11 @@ 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))); + return this.filter(a(s.bind(o))); } forEach(s, o) { - return this.elements.forEach((i, u) => { - s.bind(o)(i.value, i.key, i, u); + return this.elements.forEach((i, a) => { + s.bind(o)(i.value, i.key, i, a); }); } keys() { @@ -13894,57 +13259,57 @@ } 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), + 86804(s, o, i) { + const a = i(10316), + u = i(41067), + _ = i(71167), + w = i(40239), + x = i(12242), + C = i(6233), + j = i(87726), + L = i(61045), + B = i(86303), + $ = i(14540), U = i(92340), - z = i(10866), - Y = i(55973); + V = i(10866), + z = 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 (s instanceof a) return s; + if ('string' == typeof s) return new _(s); + if ('number' == typeof s) return new w(s); + if ('boolean' == typeof s) return new x(s); + if (null === s) return new u(); + if (Array.isArray(s)) return new C(s.map(refract)); if ('object' == typeof s) { - return new B(s); + return new L(s); } return s; } - ((u.prototype.ObjectElement = B), - (u.prototype.RefElement = V), - (u.prototype.MemberElement = L), - (u.prototype.refract = refract), + ((a.prototype.ObjectElement = L), + (a.prototype.RefElement = $), + (a.prototype.MemberElement = j), + (a.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, + Element: a, + NullElement: u, + StringElement: _, + NumberElement: w, + BooleanElement: x, + ArrayElement: C, + MemberElement: j, + ObjectElement: L, + LinkElement: B, + RefElement: $, refract, ArraySlice: U, - ObjectSlice: z, - KeyValuePair: Y + ObjectSlice: V, + KeyValuePair: z })); }, - 86303: (s, o, i) => { - const u = i(10316); - s.exports = class LinkElement extends u { + 86303(s, o, i) { + const a = i(10316); + s.exports = class LinkElement extends a { constructor(s, o, i) { (super(s || [], o, i), (this.element = 'link')); } @@ -13962,9 +13327,9 @@ } }; }, - 14540: (s, o, i) => { - const u = i(10316); - s.exports = class RefElement extends u { + 14540(s, o, i) { + const a = i(10316); + s.exports = class RefElement extends a { constructor(s, o, i) { (super(s || [], o, i), (this.element = 'ref'), this.path || (this.path = 'element')); } @@ -13976,32 +13341,32 @@ } }; }, - 34035: (s, o, i) => { - const u = i(3110), - _ = i(86804); - ((o.g$ = u), + 34035(s, o, i) { + const a = i(3110), + u = i(86804); + ((o.g$ = a), (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), + (o.G6 = u.ArraySlice), + (o.ot = u.ObjectSlice), + (o.Hg = u.Element), + (o.Om = u.StringElement), + (o.kT = u.NumberElement), + (o.bd = u.BooleanElement), + (o.Os = u.NullElement), + (o.wE = u.ArrayElement), + (o.Sh = u.ObjectElement), + (o.Pr = u.MemberElement), + (o.sI = u.RefElement), + (o.Ft = u.LinkElement), + (o.e = u.refract), i(85105), i(75147)); }, - 6233: (s, o, i) => { - const u = i(6048), - _ = i(10316), - w = i(92340); - class ArrayElement extends _ { + 6233(s, o, i) { + const a = i(6048), + u = i(10316), + _ = i(92340); + class ArrayElement extends u { constructor(s, o, i) { (super(s || [], o, i), (this.element = 'array')); } @@ -14034,36 +13399,36 @@ compactMap(s, o) { const i = []; return ( - this.forEach((u) => { - const _ = s.bind(o)(u); - _ && i.push(_); + this.forEach((a) => { + const u = s.bind(o)(a); + u && i.push(u); }), i ); } filter(s, o) { - return new w(this.content.filter(s, o)); + return new _(this.content.filter(s, o)); } reject(s, o) { - return this.filter(u(s), o); + return this.filter(a(s), o); } reduce(s, o) { - let i, u; + let i, a; void 0 !== o - ? ((i = 0), (u = this.refract(o))) - : ((i = 1), (u = 'object' === this.primitive() ? this.first.value : this.first)); + ? ((i = 0), (a = this.refract(o))) + : ((i = 1), (a = 'object' === this.primitive() ? this.first.value : this.first)); for (let o = i; o < this.length; o += 1) { const i = this.content[o]; - u = + a = 'object' === this.primitive() - ? this.refract(s(u, i.value, i.key, i, this)) - : this.refract(s(u, i, o, this)); + ? this.refract(s(a, i.value, i.key, i, this)) + : this.refract(s(a, i, o, this)); } - return u; + return a; } forEach(s, o) { - this.content.forEach((i, u) => { - s.bind(o)(i, this.refract(u)); + this.content.forEach((i, a) => { + s.bind(o)(i, this.refract(a)); }); } shift() { @@ -14080,20 +13445,20 @@ } findElements(s, o) { const i = o || {}, - u = !!i.recursive, - _ = void 0 === i.results ? [] : i.results; + a = !!i.recursive, + u = void 0 === i.results ? [] : i.results; return ( - this.forEach((o, i, w) => { - (u && + this.forEach((o, i, _) => { + (a && void 0 !== o.findElements && - o.findElements(s, { results: _, recursive: u }), - s(o, i, w) && _.push(o)); + o.findElements(s, { results: u, recursive: a }), + s(o, i, _) && u.push(o)); }), - _ + u ); } find(s) { - return new w(this.findElements(s, { recursive: !0 })); + return new _(this.findElements(s, { recursive: !0 })); } findByElement(s) { return this.find((o) => o.element === s); @@ -14160,9 +13525,9 @@ }), (s.exports = ArrayElement)); }, - 12242: (s, o, i) => { - const u = i(10316); - s.exports = class BooleanElement extends u { + 12242(s, o, i) { + const a = i(10316); + s.exports = class BooleanElement extends a { constructor(s, o, i) { (super(s, o, i), (this.element = 'boolean')); } @@ -14171,10 +13536,10 @@ } }; }, - 10316: (s, o, i) => { - const u = i(2404), - _ = i(55973), - w = i(92340); + 10316(s, o, i) { + const a = i(2404), + u = i(55973), + _ = i(92340); class Element { constructor(s, o, i) { (o && (this.meta = o), i && (this.attributes = i), (this.content = s)); @@ -14209,7 +13574,7 @@ toValue() { return this.content instanceof Element ? this.content.toValue() - : this.content instanceof _ + : this.content instanceof u ? { key: this.content.key.toValue(), value: this.content.value ? this.content.value.toValue() : void 0 @@ -14230,14 +13595,14 @@ 'Cannot find recursive with multiple element names without first freezing the element. Call `element.freeze()`' ); const o = s.pop(); - let i = new w(); + let i = new _(); const append = (s, o) => (s.push(o), s), checkElement = (s, i) => { i.element === o && s.push(i); - const u = i.findRecursive(o); + const a = i.findRecursive(o); return ( - u && u.reduce(append, s), - i.content instanceof _ && + a && a.reduce(append, s), + i.content instanceof u && (i.content.key && checkElement(s, i.content.key), i.content.value && checkElement(s, i.content.value)), s @@ -14251,10 +13616,10 @@ (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, _); + const a = s[o], + u = i.indexOf(a); + if (-1 === u) return !1; + i = i.splice(0, u); } return !0; })), @@ -14265,7 +13630,7 @@ return ((this.content = s), this); } equals(s) { - return u(this.toValue(), s); + return a(this.toValue(), s); } getMetaProperty(s, o) { if (!this.meta.hasKey(s)) { @@ -14291,7 +13656,7 @@ } set content(s) { if (s instanceof Element) this._content = s; - else if (s instanceof w) this.content = s.elements; + else if (s instanceof _) this.content = s.elements; else if ( 'string' == typeof s || 'number' == typeof s || @@ -14300,7 +13665,7 @@ null == s ) this._content = s; - else if (s instanceof _) this._content = s; + else if (s instanceof u) 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'); @@ -14370,20 +13735,20 @@ } get parents() { let { parent: s } = this; - const o = new w(); + const o = new _(); 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]); + if (Array.isArray(this.content)) return new _(this.content); + if (this.content instanceof u) { + const s = new _([this.content.key]); return (this.content.value && s.push(this.content.value), s); } - return this.content instanceof Element ? new w([this.content]) : new w(); + return this.content instanceof Element ? new _([this.content]) : new _(); } get recursiveChildren() { - const s = new w(); + const s = new _(); return ( this.children.forEach((o) => { (s.push(o), @@ -14397,12 +13762,12 @@ } 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)); + 87726(s, o, i) { + const a = i(55973), + u = i(10316); + s.exports = class MemberElement extends u { + constructor(s, o, i, u) { + (super(new a(), i, u), (this.element = 'member'), (this.key = s), (this.value = o)); } get key() { return this.content.key; @@ -14418,9 +13783,9 @@ } }; }, - 41067: (s, o, i) => { - const u = i(10316); - s.exports = class NullElement extends u { + 41067(s, o, i) { + const a = i(10316); + s.exports = class NullElement extends a { constructor(s, o, i) { (super(s || null, o, i), (this.element = 'null')); } @@ -14432,9 +13797,9 @@ } }; }, - 40239: (s, o, i) => { - const u = i(10316); - s.exports = class NumberElement extends u { + 40239(s, o, i) { + const a = i(10316); + s.exports = class NumberElement extends a { constructor(s, o, i) { (super(s, o, i), (this.element = 'number')); } @@ -14443,13 +13808,13 @@ } }; }, - 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 { + 61045(s, o, i) { + const a = i(6048), + u = i(23805), + _ = i(6233), + w = i(87726), + x = i(10866); + s.exports = class ObjectElement extends _ { constructor(s, o, i) { (super(s || [], o, i), (this.element = 'object')); } @@ -14481,7 +13846,7 @@ if (o) return o.key; } set(s, o) { - if (_(s)) + if (u(s)) return ( Object.keys(s).forEach((o) => { this.set(o, s[o]); @@ -14489,8 +13854,8 @@ this ); const i = s, - u = this.getMember(i); - return (u ? (u.value = o) : this.content.push(new x(i, o)), this); + a = this.getMember(i); + return (a ? (a.value = o) : this.content.push(new w(i, o)), this); } keys() { return this.content.map((s) => s.key.toValue()); @@ -14510,27 +13875,27 @@ compactMap(s, o) { const i = []; return ( - this.forEach((u, _, w) => { - const x = s.bind(o)(u, _, w); - x && i.push(x); + this.forEach((a, u, _) => { + const w = s.bind(o)(a, u, _); + w && i.push(w); }), i ); } filter(s, o) { - return new C(this.content).filter(s, o); + return new x(this.content).filter(s, o); } reject(s, o) { - return this.filter(u(s), o); + return this.filter(a(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 { + 71167(s, o, i) { + const a = i(10316); + s.exports = class StringElement extends a { constructor(s, o, i) { (super(s, o, i), (this.element = 'string')); } @@ -14542,9 +13907,9 @@ } }; }, - 75147: (s, o, i) => { - const u = i(85105); - s.exports = class JSON06Serialiser extends u { + 75147(s, o, i) { + const a = i(85105); + s.exports = class JSON06Serialiser extends a { serialise(s) { if (!(s instanceof this.namespace.elements.Element)) throw new TypeError(`Given element \`${s}\` is not an Element instance`); @@ -14552,28 +13917,28 @@ 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 a = 'enum' === s.element || -1 !== s.attributes.keys().indexOf('enumerations'); + if (a) { 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))); + let { attributes: a } = s; + (a.get('metadata') && + ((a = a.clone()), a.set('meta', a.get('metadata')), a.remove('metadata')), + 'member' === s.element && o && ((a = a.clone()), a.remove('variable')), + a.length > 0 && (i.attributes = this.serialiseObject(a))); } - if (u) i.content = this.enumSerialiseContent(s, i); + if (a) 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; + let a; (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)); + ? ((a = s.content.clone()), + a.key.attributes.set('variable', o), + (a = this.serialiseContent(a))) + : (a = this.serialiseContent(s.content)), + this.shouldSerialiseContent(s, a) && (i.content = a)); } else this.shouldSerialiseContent(s, s.content) && s instanceof this.namespace.elements.Array && @@ -14602,23 +13967,23 @@ 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([]); + a = o.get('default'); + let u = 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) => { + (a && + a.content && + (a.content.attributes && a.content.attributes.remove('typeAttributes'), + o.set('default', new this.namespace.elements.Array([a.content]))), + u.forEach((s) => { s.content && s.content.element && s.content.attributes.remove('typeAttributes'); }), - s.content && 0 !== i.length && _.unshift(s.content), - (_ = _.map((s) => + s.content && 0 !== i.length && u.unshift(s.content), + (u = u.map((s) => s instanceof this.namespace.elements.Array ? [s] : new this.namespace.elements.Array([s.content]) )), - _.length && o.set('samples', _), + u.length && o.set('samples', u), o.length > 0) ) return this.serialiseObject(o); @@ -14650,26 +14015,26 @@ (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)) { + const a = this.deserialiseContent(s.content); + if (((void 0 === a && null !== i.content) || (i.content = a), '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; + const a = s; ((s = new this.namespace.elements.Array()), - u.forEach((u) => { - u.forEach((u) => { - const _ = new o(u); - ((_.element = i.element), s.push(_)); + a.forEach((a) => { + a.forEach((a) => { + const u = new o(a); + ((u.element = i.element), s.push(u)); }); })); - const _ = s.shift(); - ((i.content = _ ? _.content : void 0), i.attributes.set('samples', s)); + const u = s.shift(); + ((i.content = u ? u.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); + let a = i.attributes.get('default'); + if (a && a.length > 0) { + a = a.get(0); + const s = new o(a); ((s.element = i.element), i.attributes.set('default', s)); } } else if ('dataStructure' === i.element && Array.isArray(i.content)) @@ -14739,8 +14104,8 @@ return ( s.forEach((s, i) => { if (s) { - const u = i.toValue(); - o[u] = this.convertKeyToRefract(u, s); + const a = i.toValue(); + o[a] = this.convertKeyToRefract(a, s); } }), o @@ -14753,7 +14118,7 @@ } }; }, - 85105: (s) => { + 85105(s) { s.exports = class JSONSerialiser { constructor(s) { this.namespace = s || new this.Namespace(); @@ -14819,10 +14184,27 @@ } }; }, - 65606: (s) => { + 76578(s) { + 'use strict'; + s.exports = [ + 'Float16Array', + 'Float32Array', + 'Float64Array', + 'Int8Array', + 'Int16Array', + 'Int32Array', + 'Uint8Array', + 'Uint8ClampedArray', + 'Uint16Array', + 'Uint32Array', + 'BigInt64Array', + 'BigUint64Array' + ]; + }, + 65606(s) { var o, i, - u = (s.exports = {}); + a = (s.exports = {}); function defaultSetTimout() { throw new Error('setTimeout has not been defined'); } @@ -14855,23 +14237,23 @@ i = defaultClearTimeout; } })(); - var _, - w = [], - x = !1, - C = -1; + var u, + _ = [], + w = !1, + x = -1; function cleanUpNextTick() { - x && _ && ((x = !1), _.length ? (w = _.concat(w)) : (C = -1), w.length && drainQueue()); + w && u && ((w = !1), u.length ? (_ = u.concat(_)) : (x = -1), _.length && drainQueue()); } function drainQueue() { - if (!x) { + if (!w) { var s = runTimeout(cleanUpNextTick); - x = !0; - for (var o = w.length; o; ) { - for (_ = w, w = []; ++C < o; ) _ && _[C].run(); - ((C = -1), (o = w.length)); + w = !0; + for (var o = _.length; o; ) { + for (u = _, _ = []; ++x < o; ) u && u[x].run(); + ((x = -1), (o = _.length)); } - ((_ = null), - (x = !1), + ((u = null), + (w = !1), (function runClearTimeout(s) { if (i === clearTimeout) return clearTimeout(s); if ((i === defaultClearTimeout || !i) && clearTimeout) @@ -14892,59 +14274,59 @@ ((this.fun = s), (this.array = o)); } function noop() {} - ((u.nextTick = function (s) { + ((a.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)); + (_.push(new Item(s, o)), 1 !== _.length || w || 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) { + (a.title = 'browser'), + (a.browser = !0), + (a.env = {}), + (a.argv = []), + (a.version = ''), + (a.versions = {}), + (a.on = noop), + (a.addListener = noop), + (a.once = noop), + (a.off = noop), + (a.removeListener = noop), + (a.removeAllListeners = noop), + (a.emit = noop), + (a.prependListener = noop), + (a.prependOnceListener = noop), + (a.listeners = function (s) { return []; }), - (u.binding = function (s) { + (a.binding = function (s) { throw new Error('process.binding is not supported'); }), - (u.cwd = function () { + (a.cwd = function () { return '/'; }), - (u.chdir = function (s) { + (a.chdir = function (s) { throw new Error('process.chdir is not supported'); }), - (u.umask = function () { + (a.umask = function () { return 0; })); }, - 2694: (s, o, i) => { + 2694(s, o, i) { 'use strict'; - var u = i(6925); + var a = 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( + function shim(s, o, i, u, _, w) { + if (w !== a) { + var x = 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); + throw ((x.name = 'Invariant Violation'), x); } } function getShim() { @@ -14977,14 +14359,14 @@ return ((s.PropTypes = s), s); })); }, - 5556: (s, o, i) => { + 5556(s, o, i) { s.exports = i(2694)(); }, - 6925: (s) => { + 6925(s) { 'use strict'; s.exports = 'SECRET_DO_NOT_PASS_THIS_OR_YOU_WILL_BE_FIRED'; }, - 73992: (s, o) => { + 73992(s, o) { 'use strict'; var i = Object.prototype.hasOwnProperty; function decode(s) { @@ -15003,35 +14385,35 @@ } ((o.stringify = function querystringify(s, o) { o = o || ''; - var u, - _, - w = []; - for (_ in ('string' != typeof o && (o = '?'), s)) - if (i.call(s, _)) { + var a, + u, + _ = []; + for (u in ('string' != typeof o && (o = '?'), s)) + if (i.call(s, u)) { if ( - ((u = s[_]) || (null != u && !isNaN(u)) || (u = ''), - (_ = encode(_)), + ((a = s[u]) || (null != a && !isNaN(a)) || (a = ''), (u = encode(u)), - null === _ || null === u) + (a = encode(a)), + null === u || null === a) ) continue; - w.push(_ + '=' + u); + _.push(u + '=' + a); } - return w.length ? o + w.join('&') : ''; + return _.length ? o + _.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); + for (var o, i = /([^=?#&]+)=?([^&]*)/g, a = {}; (o = i.exec(s)); ) { + var u = decode(o[1]), + _ = decode(o[2]); + null === u || null === _ || u in a || (a[u] = _); } - return u; + return a; })); }, - 41859: (s, o, i) => { - const u = i(27096), - _ = i(78004), - w = u.types; + 41859(s, o, i) { + const a = i(27096), + u = i(78004), + _ = a.types; s.exports = class RandExp { constructor(s, o) { if ((this._setDefaults(s), s instanceof RegExp)) @@ -15041,7 +14423,7 @@ ((this.ignoreCase = o && -1 !== o.indexOf('i')), (this.multiline = o && -1 !== o.indexOf('m'))); } - this.tokens = u(s); + this.tokens = a(s); } _setDefaults(s) { ((this.max = @@ -15057,42 +14439,42 @@ return this._gen(this.tokens, []); } _gen(s, o) { - var i, u, _, x, C; + var i, a, u, w, x; switch (s.type) { - case w.ROOT: - case w.GROUP: + case _.ROOT: + case _.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++ + a = '', + w = 0, + x = (i = s.options ? this._randSelect(s.options) : s.stack).length; + w < x; + w++ ) - u += this._gen(i[x], o); - return (s.remember && (o[s.groupNumber] = u), u); - case w.POSITION: + a += this._gen(i[w], o); + return (s.remember && (o[s.groupNumber] = a), a); + case _.POSITION: return ''; - case w.SET: - var j = this._expand(s); - return j.length ? String.fromCharCode(this._randSelect(j)) : ''; - case w.REPETITION: + case _.SET: + var C = this._expand(s); + return C.length ? String.fromCharCode(this._randSelect(C)) : ''; + case _.REPETITION: for ( - _ = this.randInt(s.min, s.max === 1 / 0 ? s.min + this.max : s.max), - u = '', - x = 0; - x < _; - x++ + u = this.randInt(s.min, s.max === 1 / 0 ? s.min + this.max : s.max), + a = '', + w = 0; + w < u; + w++ ) - u += this._gen(s.value, o); - return u; - case w.REFERENCE: + a += this._gen(s.value, o); + return a; + case _.REFERENCE: return o[s.value - 1] || ''; - case w.CHAR: - var L = + case _.CHAR: + var j = this.ignoreCase && this._randBool() ? this._toOtherCase(s.value) : s.value; - return String.fromCharCode(L); + return String.fromCharCode(j); } } _toOtherCase(s) { @@ -15102,22 +14484,22 @@ return !this.randInt(0, 1); } _randSelect(s) { - return s instanceof _ + return s instanceof u ? 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); + if (s.type === a.types.CHAR) return new u(s.value); + if (s.type === a.types.RANGE) return new u(s.from, s.to); { - let o = new _(); + let o = new u(); 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(_); + let a = this._expand(s.set[i]); + if ((o.add(a), this.ignoreCase)) + for (let s = 0; s < a.length; s++) { + let i = a.index(s), + u = this._toOtherCase(i); + i !== u && o.add(u); } } return s.not @@ -15129,7 +14511,7 @@ return s + Math.floor(Math.random() * (1 + o - s)); } get defaultRange() { - return (this._range = this._range || new _(32, 126)); + return (this._range = this._range || new u(32, 126)); } set defaultRange(s) { this._range = s; @@ -15151,22 +14533,22 @@ } }; }, - 53209: (s, o, i) => { + 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 + var a = i(65606), + u = 65536, + _ = 4294967295; + var w = i(92861).Buffer, + x = i.g.crypto || i.g.msCrypto; + x && x.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 > _) throw new RangeError('requested too many random bytes'); + var i = w.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 (s > u) for (var C = 0; C < s; C += u) x.getRandomValues(i.slice(C, C + u)); + else x.getRandomValues(i); if ('function' == typeof o) - return u.nextTick(function () { + return a.nextTick(function () { o(null, i); }); return i; @@ -15177,7 +14559,7 @@ ); }); }, - 25264: (s, o, i) => { + 25264(s, o, i) { 'use strict'; function _typeof(s) { return ( @@ -15198,21 +14580,21 @@ ); } (Object.defineProperty(o, '__esModule', { value: !0 }), (o.CopyToClipboard = void 0)); - var u = _interopRequireDefault(i(96540)), - _ = _interopRequireDefault(i(17965)), - w = ['text', 'onCopy', 'options', 'children']; + var a = _interopRequireDefault(i(96540)), + u = _interopRequireDefault(i(17965)), + _ = ['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); + var a = Object.getOwnPropertySymbols(s); (o && - (u = u.filter(function (o) { + (a = a.filter(function (o) { return Object.getOwnPropertyDescriptor(s, o).enumerable; })), - i.push.apply(i, u)); + i.push.apply(i, a)); } return i; } @@ -15234,32 +14616,32 @@ function _objectWithoutProperties(s, o) { if (null == s) return {}; var i, - u, - _ = (function _objectWithoutPropertiesLoose(s, o) { + a, + 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 _; + a, + u = {}, + _ = Object.keys(s); + for (a = 0; a < _.length; a++) ((i = _[a]), o.indexOf(i) >= 0 || (u[i] = s[i])); + return u; })(s, o); if (Object.getOwnPropertySymbols) { - var w = Object.getOwnPropertySymbols(s); - for (u = 0; u < w.length; u++) - ((i = w[u]), + var _ = Object.getOwnPropertySymbols(s); + for (a = 0; a < _.length; a++) + ((i = _[a]), o.indexOf(i) >= 0 || - (Object.prototype.propertyIsEnumerable.call(s, i) && (_[i] = s[i]))); + (Object.prototype.propertyIsEnumerable.call(s, i) && (u[i] = s[i]))); } - return _; + return u; } 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)); + var a = o[i]; + ((a.enumerable = a.enumerable || !1), + (a.configurable = !0), + 'value' in a && (a.writable = !0), + Object.defineProperty(s, a.key, a)); } } function _setPrototypeOf(s, o) { @@ -15288,11 +14670,11 @@ })(); return function _createSuperInternal() { var i, - u = _getPrototypeOf(s); + a = _getPrototypeOf(s); if (o) { - var _ = _getPrototypeOf(this).constructor; - i = Reflect.construct(u, arguments, _); - } else i = u.apply(this, arguments); + var u = _getPrototypeOf(this).constructor; + i = Reflect.construct(a, arguments, u); + } else i = a.apply(this, arguments); return (function _possibleConstructorReturn(s, o) { if (o && ('object' === _typeof(o) || 'function' == typeof o)) return o; if (void 0 !== o) @@ -15329,7 +14711,7 @@ s ); } - var x = (function (s) { + var w = (function (s) { !(function _inherits(s, o) { if ('function' != typeof o && null !== o) throw new TypeError('Super expression must either be null or a function'); @@ -15345,22 +14727,22 @@ !(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]; + for (var i = arguments.length, _ = new Array(i), w = 0; w < i; w++) + _[w] = arguments[w]; return ( _defineProperty( - _assertThisInitialized((s = o.call.apply(o, [this].concat(w)))), + _assertThisInitialized((s = o.call.apply(o, [this].concat(_)))), '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)); + _ = i.text, + w = i.onCopy, + x = i.children, + C = i.options, + j = a.default.Children.only(x), + L = (0, u.default)(_, C); + (w && w(_, L), + j && j.props && 'function' == typeof j.props.onClick && j.props.onClick(o)); } ), s @@ -15380,10 +14762,10 @@ 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( - _, + i = _objectWithoutProperties(s, _), + u = a.default.Children.only(o); + return a.default.cloneElement( + u, _objectSpread(_objectSpread({}, i), {}, { onClick: this.onClick }) ); } @@ -15391,16 +14773,16 @@ ]), CopyToClipboard ); - })(u.default.PureComponent); - ((o.CopyToClipboard = x), - _defineProperty(x, 'defaultProps', { onCopy: void 0, options: void 0 })); + })(a.default.PureComponent); + ((o.CopyToClipboard = w), + _defineProperty(w, 'defaultProps', { onCopy: void 0, options: void 0 })); }, - 59399: (s, o, i) => { + 59399(s, o, i) { 'use strict'; - var u = i(25264).CopyToClipboard; - ((u.CopyToClipboard = u), (s.exports = u)); + var a = i(25264).CopyToClipboard; + ((a.CopyToClipboard = a), (s.exports = a)); }, - 81214: (s, o, i) => { + 81214(s, o, i) { 'use strict'; function _typeof(s) { return ( @@ -15421,9 +14803,9 @@ ); } (Object.defineProperty(o, '__esModule', { value: !0 }), (o.DebounceInput = void 0)); - var u = _interopRequireDefault(i(96540)), - _ = _interopRequireDefault(i(20181)), - w = [ + var a = _interopRequireDefault(i(96540)), + u = _interopRequireDefault(i(20181)), + _ = [ 'element', 'onChange', 'value', @@ -15441,34 +14823,34 @@ function _objectWithoutProperties(s, o) { if (null == s) return {}; var i, - u, - _ = (function _objectWithoutPropertiesLoose(s, o) { + a, + 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 _; + a, + u = {}, + _ = Object.keys(s); + for (a = 0; a < _.length; a++) ((i = _[a]), o.indexOf(i) >= 0 || (u[i] = s[i])); + return u; })(s, o); if (Object.getOwnPropertySymbols) { - var w = Object.getOwnPropertySymbols(s); - for (u = 0; u < w.length; u++) - ((i = w[u]), + var _ = Object.getOwnPropertySymbols(s); + for (a = 0; a < _.length; a++) + ((i = _[a]), o.indexOf(i) >= 0 || - (Object.prototype.propertyIsEnumerable.call(s, i) && (_[i] = s[i]))); + (Object.prototype.propertyIsEnumerable.call(s, i) && (u[i] = s[i]))); } - return _; + return u; } function ownKeys(s, o) { var i = Object.keys(s); if (Object.getOwnPropertySymbols) { - var u = Object.getOwnPropertySymbols(s); + var a = Object.getOwnPropertySymbols(s); (o && - (u = u.filter(function (o) { + (a = a.filter(function (o) { return Object.getOwnPropertyDescriptor(s, o).enumerable; })), - i.push.apply(i, u)); + i.push.apply(i, a)); } return i; } @@ -15489,11 +14871,11 @@ } 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)); + var a = o[i]; + ((a.enumerable = a.enumerable || !1), + (a.configurable = !0), + 'value' in a && (a.writable = !0), + Object.defineProperty(s, a.key, a)); } } function _setPrototypeOf(s, o) { @@ -15522,11 +14904,11 @@ })(); return function _createSuperInternal() { var i, - u = _getPrototypeOf(s); + a = _getPrototypeOf(s); if (o) { - var _ = _getPrototypeOf(this).constructor; - i = Reflect.construct(u, arguments, _); - } else i = u.apply(this, arguments); + var u = _getPrototypeOf(this).constructor; + i = Reflect.construct(a, arguments, u); + } else i = a.apply(this, arguments); return (function _possibleConstructorReturn(s, o) { if (o && ('object' === _typeof(o) || 'function' == typeof o)) return o; if (void 0 !== o) @@ -15563,7 +14945,7 @@ s ); } - var x = (function (s) { + var w = (function (s) { !(function _inherits(s, o) { if ('function' != typeof o && null !== o) throw new TypeError('Super expression must either be null or a function'); @@ -15585,12 +14967,12 @@ function (s) { s.persist(); var o = i.state.value, - u = i.props.minLength; + a = i.props.minLength; i.setState({ value: s.target.value }, function () { - var _ = i.state.value; - _.length >= u + var u = i.state.value; + u.length >= a ? i.notify(s) - : o.length > _.length && + : o.length > u.length && i.notify( _objectSpread( _objectSpread({}, s), @@ -15624,7 +15006,7 @@ }; else if (0 === s) i.notify = i.doNotify; else { - var o = (0, _.default)(function (s) { + var o = (0, u.default)(function (s) { ((i.isDebouncing = !1), i.doNotify(s)); }, s); ((i.notify = function (s) { @@ -15645,23 +15027,23 @@ var o = i.props.debounceTimeout; if (i.isDebouncing || !(o > 0)) { i.cancel && i.cancel(); - var u = i.state.value, - _ = i.props.minLength; - u.length >= _ + var a = i.state.value, + u = i.props.minLength; + a.length >= u ? i.doNotify(s) : i.doNotify( _objectSpread( _objectSpread({}, s), {}, - { target: _objectSpread(_objectSpread({}, s.target), {}, { value: u }) } + { target: _objectSpread(_objectSpread({}, s.target), {}, { value: a }) } ) ); } }), (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); + var a = i.props.debounceTimeout; + return (i.createNotifier(a), i); } return ( (function _createClass(s, o, i) { @@ -15678,12 +15060,12 @@ 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)); + a = o.debounceTimeout, + u = s.debounceTimeout, + _ = s.value, + w = this.state.value; + (void 0 !== i && _ !== i && w !== i && this.setState({ value: i }), + a !== u && this.createNotifier(a)); } } }, @@ -15699,26 +15081,26 @@ var s, o, i = this.props, - _ = i.element, - x = + u = i.element, + w = (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( - _, + x = i.forceNotifyOnBlur, + C = i.onKeyDown, + j = i.onBlur, + L = i.inputRef, + B = _objectWithoutProperties(i, _), + $ = this.state.value; + ((s = w ? { onKeyDown: this.onKeyDown } : C ? { onKeyDown: C } : {}), + (o = x ? { onBlur: this.onBlur } : j ? { onBlur: j } : {})); + var U = L ? { ref: L } : {}; + return a.default.createElement( + u, _objectSpread( _objectSpread( _objectSpread( - _objectSpread({}, $), + _objectSpread({}, B), {}, - { onChange: this.onChange, value: V }, + { onChange: this.onChange, value: $ }, s ), o @@ -15731,9 +15113,9 @@ ]), DebounceInput ); - })(u.default.PureComponent); - ((o.DebounceInput = x), - _defineProperty(x, 'defaultProps', { + })(a.default.PureComponent); + ((o.DebounceInput = w), + _defineProperty(w, 'defaultProps', { element: 'input', type: 'text', onKeyDown: void 0, @@ -15746,15 +15128,15 @@ inputRef: void 0 })); }, - 24677: (s, o, i) => { + 24677(s, o, i) { 'use strict'; - var u = i(81214).DebounceInput; - ((u.DebounceInput = u), (s.exports = u)); + var a = i(81214).DebounceInput; + ((a.DebounceInput = a), (s.exports = a)); }, - 22551: (s, o, i) => { + 22551(s, o, i) { 'use strict'; - var u = i(96540), - _ = i(69982); + var a = i(96540), + u = i(69982); function p(s) { for ( var o = 'https://reactjs.org/docs/error-decoder.html?invariant=' + s, i = 1; @@ -15770,39 +15152,39 @@ ' for the full message or use the non-minified dev environment for full errors and additional helpful warnings.' ); } - var w = new Set(), - x = {}; + var _ = new Set(), + w = {}; 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]); + for (w[s] = o, s = 0; s < o.length; s++) _.add(o[s]); } - var C = !( + var x = !( 'undefined' == typeof window || void 0 === window.document || void 0 === window.document.createElement ), - j = Object.prototype.hasOwnProperty, - L = + C = Object.prototype.hasOwnProperty, + j = /^[: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) { + L = {}, + B = {}; + function v(s, o, i, a, u, _, w) { ((this.acceptsBooleans = 2 === o || 3 === o || 4 === o), - (this.attributeName = u), - (this.attributeNamespace = _), + (this.attributeName = a), + (this.attributeNamespace = u), (this.mustUseProperty = i), (this.propertyName = s), (this.type = o), - (this.sanitizeURL = w), - (this.removeEmptyString = x)); + (this.sanitizeURL = _), + (this.removeEmptyString = w)); } - var V = {}; + var $ = {}; ('children dangerouslySetInnerHTML defaultValue defaultChecked innerHTML suppressContentEditableWarning suppressHydrationWarning style' .split(' ') .forEach(function (s) { - V[s] = new v(s, 0, !1, s, null, !1, !1); + $[s] = new v(s, 0, !1, s, null, !1, !1); }), [ ['acceptCharset', 'accept-charset'], @@ -15811,49 +15193,49 @@ ['httpEquiv', 'http-equiv'] ].forEach(function (s) { var o = s[0]; - V[o] = new v(o, 1, !1, s[1], null, !1, !1); + $[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); + $[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); + $[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); + $[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); + $[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); + $[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); + $[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); + $[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 || + function ta(s, o, i, a) { + var u = $.hasOwnProperty(o) ? $[o] : null; + (null !== u + ? 0 !== u.type + : a || !(2 < o.length) || ('o' !== o[0] && 'O' !== o[0]) || ('n' !== o[1] && 'N' !== o[1])) && - ((function qa(s, o, i, u) { + ((function qa(s, o, i, a) { if ( null == o || - (function pa(s, o, i, u) { + (function pa(s, o, i, a) { if (null !== i && 0 === i.type) return !1; switch (typeof o) { case 'function': @@ -15861,7 +15243,7 @@ return !0; case 'boolean': return ( - !u && + !a && (null !== i ? !i.acceptsBooleans : 'data-' !== (s = s.toLowerCase().slice(0, 5)) && 'aria-' !== s) @@ -15869,10 +15251,10 @@ default: return !1; } - })(s, o, i, u) + })(s, o, i, a) ) return !0; - if (u) return !1; + if (a) return !1; if (null !== i) switch (i.type) { case 3: @@ -15885,43 +15267,43 @@ return isNaN(o) || 1 > o; } return !1; - })(o, i, _, u) && (i = null), - u || null === _ + })(o, i, u, a) && (i = null), + a || null === u ? (function oa(s) { return ( - !!j.call($, s) || - (!j.call(B, s) && (L.test(s) ? ($[s] = !0) : ((B[s] = !0), !1))) + !!C.call(B, s) || + (!C.call(L, s) && (j.test(s) ? (B[s] = !0) : ((L[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), + : u.mustUseProperty + ? (s[u.propertyName] = null === i ? 3 !== u.type && '' : i) + : ((o = u.attributeName), + (a = u.attributeNamespace), null === i ? s.removeAttribute(o) - : ((i = 3 === (_ = _.type) || (4 === _ && !0 === i) ? '' : '' + i), - u ? s.setAttributeNS(u, o, i) : s.setAttribute(o, i)))); + : ((i = 3 === (u = u.type) || (4 === u && !0 === i) ? '' : '' + i), + a ? s.setAttributeNS(a, 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); + $[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); + $[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); + $[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); + $[s] = new v(s, 1, !1, s.toLowerCase(), null, !1, !1); }), - (V.xlinkHref = new v( + ($.xlinkHref = new v( 'xlinkHref', 1, !1, @@ -15931,50 +15313,50 @@ !1 )), ['src', 'href', 'action', 'formAction'].forEach(function (s) { - V[s] = new v(s, 1, !1, s.toLowerCase(), null, !0, !0); + $[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'), + var V = a.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED, + z = Symbol.for('react.element'), + Y = Symbol.for('react.portal'), + Z = Symbol.for('react.fragment'), + ee = Symbol.for('react.strict_mode'), + ie = Symbol.for('react.profiler'), + ae = 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'); + le = Symbol.for('react.forward_ref'), + pe = Symbol.for('react.suspense'), + de = Symbol.for('react.suspense_list'), + fe = Symbol.for('react.memo'), + ye = Symbol.for('react.lazy'); (Symbol.for('react.scope'), Symbol.for('react.debug_trace_mode')); - var _e = Symbol.for('react.offscreen'); + var be = Symbol.for('react.offscreen'); (Symbol.for('react.legacy_hidden'), Symbol.for('react.cache'), Symbol.for('react.tracing_marker')); - var we = Symbol.iterator; + var Se = Symbol.iterator; function Ka(s) { return null === s || 'object' != typeof s ? null - : 'function' == typeof (s = (we && s[we]) || s['@@iterator']) + : 'function' == typeof (s = (Se && s[Se]) || s['@@iterator']) ? s : null; } - var Se, - xe = Object.assign; + var _e, + we = Object.assign; function Ma(s) { - if (void 0 === Se) + if (void 0 === _e) try { throw Error(); } catch (s) { var o = s.stack.trim().match(/\n( *(at )?)/); - Se = (o && o[1]) || ''; + _e = (o && o[1]) || ''; } - return '\n' + Se + s; + return '\n' + _e + s; } - var Pe = !1; + var xe = !1; function Oa(s, o) { - if (!s || Pe) return ''; - Pe = !0; + if (!s || xe) return ''; + xe = !0; var i = Error.prepareStackTrace; Error.prepareStackTrace = void 0; try { @@ -15993,14 +15375,14 @@ try { Reflect.construct(o, []); } catch (s) { - var u = s; + var a = s; } Reflect.construct(s, [], o); } else { try { o.call(); } catch (s) { - u = s; + a = s; } s.call(o.prototype); } @@ -16008,39 +15390,39 @@ try { throw Error(); } catch (s) { - u = s; + a = s; } s(); } } catch (o) { - if (o && u && 'string' == typeof o.stack) { + if (o && a && '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]; + var u = o.stack.split('\n'), + _ = a.stack.split('\n'), + w = u.length - 1, + x = _.length - 1; + 1 <= w && 0 <= x && u[w] !== _[x]; ) - C--; - for (; 1 <= x && 0 <= C; x--, C--) - if (_[x] !== w[C]) { - if (1 !== x || 1 !== C) + x--; + for (; 1 <= w && 0 <= x; w--, x--) + if (u[w] !== _[x]) { + if (1 !== w || 1 !== x) do { - if ((x--, 0 > --C || _[x] !== w[C])) { - var j = '\n' + _[x].replace(' at new ', ' at '); + if ((w--, 0 > --x || u[w] !== _[x])) { + var C = '\n' + u[w].replace(' at new ', ' at '); return ( s.displayName && - j.includes('') && - (j = j.replace('', s.displayName)), - j + C.includes('') && + (C = C.replace('', s.displayName)), + C ); } - } while (1 <= x && 0 <= C); + } while (1 <= w && 0 <= x); break; } } } finally { - ((Pe = !1), (Error.prepareStackTrace = i)); + ((xe = !1), (Error.prepareStackTrace = i)); } return (s = s ? s.displayName || s.name : '') ? Ma(s) : ''; } @@ -16071,26 +15453,26 @@ 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 'Fragment'; + case Y: return 'Portal'; - case ae: - return 'Profiler'; case ie: + return 'Profiler'; + case ee: return 'StrictMode'; - case de: + case pe: return 'Suspense'; - case fe: + case de: return 'SuspenseList'; } if ('object' == typeof s) switch (s.$$typeof) { case ce: return (s.displayName || 'Context') + '.Consumer'; - case le: + case ae: return (s._context.displayName || 'Context') + '.Provider'; - case pe: + case le: var o = s.render; return ( (s = s.displayName) || @@ -16100,9 +15482,9 @@ : 'ForwardRef'), s ); - case ye: + case fe: return null !== (o = s.displayName || null) ? o : Qa(s.type) || 'Memo'; - case be: + case ye: ((o = s._payload), (s = s._init)); try { return Qa(s(o)); @@ -16139,7 +15521,7 @@ case 16: return Qa(o); case 8: - return o === ie ? 'StrictMode' : 'Mode'; + return o === ee ? 'StrictMode' : 'Mode'; case 22: return 'Offscreen'; case 12: @@ -16186,32 +15568,32 @@ (s._valueTracker = (function Ua(s) { var o = Ta(s) ? 'checked' : 'value', i = Object.getOwnPropertyDescriptor(s.constructor.prototype, o), - u = '' + s[o]; + a = '' + s[o]; if ( !s.hasOwnProperty(o) && void 0 !== i && 'function' == typeof i.get && 'function' == typeof i.set ) { - var _ = i.get, - w = i.set; + var u = i.get, + _ = i.set; return ( Object.defineProperty(s, o, { configurable: !0, get: function () { - return _.call(this); + return u.call(this); }, set: function (s) { - ((u = '' + s), w.call(this, s)); + ((a = '' + s), _.call(this, s)); } }), Object.defineProperty(s, o, { enumerable: i.enumerable }), { getValue: function () { - return u; + return a; }, setValue: function (s) { - u = '' + s; + a = '' + s; }, stopTracking: function () { ((s._valueTracker = null), delete s[o]); @@ -16226,10 +15608,10 @@ var o = s._valueTracker; if (!o) return !0; var i = o.getValue(), - u = ''; + a = ''; return ( - s && (u = Ta(s) ? (s.checked ? 'true' : 'false') : s.value), - (s = u) !== i && (o.setValue(s), !0) + s && (a = Ta(s) ? (s.checked ? 'true' : 'false') : s.value), + (s = a) !== i && (o.setValue(s), !0) ); } function Xa(s) { @@ -16243,7 +15625,7 @@ } function Ya(s, o) { var i = o.checked; - return xe({}, o, { + return we({}, o, { defaultChecked: void 0, defaultValue: void 0, value: void 0, @@ -16252,10 +15634,10 @@ } function Za(s, o) { var i = null == o.defaultValue ? '' : o.defaultValue, - u = null != o.checked ? o.checked : o.defaultChecked; + a = null != o.checked ? o.checked : o.defaultChecked; ((i = Sa(null != o.value ? o.value : i)), (s._wrapperState = { - initialChecked: u, + initialChecked: a, initialValue: i, controlled: 'checkbox' === o.type || 'radio' === o.type ? null != o.checked : null != o.value @@ -16267,12 +15649,12 @@ function bb(s, o) { ab(s, o); var i = Sa(o.value), - u = o.type; + a = o.type; if (null != i) - 'number' === u + 'number' === a ? ((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'); + else if ('submit' === a || 'reset' === a) return void s.removeAttribute('value'); (o.hasOwnProperty('value') ? cb(s, o.type, i) : o.hasOwnProperty('defaultValue') && cb(s, o.type, Sa(o.defaultValue)), @@ -16282,8 +15664,8 @@ } 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))) + var a = o.type; + if (!(('submit' !== a && 'reset' !== a) || (void 0 !== o.value && null !== o.value))) return; ((o = '' + s._wrapperState.initialValue), i || o === s.value || (s.value = o), @@ -16299,27 +15681,27 @@ ? (s.defaultValue = '' + s._wrapperState.initialValue) : s.defaultValue !== '' + i && (s.defaultValue = '' + i)); } - var Te = Array.isArray; - function fb(s, o, i, u) { + var Pe = Array.isArray; + function fb(s, o, i, a) { if (((s = s.options), o)) { o = {}; - for (var _ = 0; _ < i.length; _++) o['$' + i[_]] = !0; + for (var u = 0; u < i.length; u++) o['$' + i[u]] = !0; for (i = 0; i < s.length; i++) - ((_ = o.hasOwnProperty('$' + s[i].value)), - s[i].selected !== _ && (s[i].selected = _), - _ && u && (s[i].defaultSelected = !0)); + ((u = o.hasOwnProperty('$' + s[i].value)), + s[i].selected !== u && (s[i].selected = u), + u && a && (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[_]); + for (i = '' + Sa(i), o = null, u = 0; u < s.length; u++) { + if (s[u].value === i) + return ((s[u].selected = !0), void (a && (s[u].defaultSelected = !0))); + null !== o || s[u].disabled || (o = s[u]); } null !== o && (o.selected = !0); } } function gb(s, o) { if (null != o.dangerouslySetInnerHTML) throw Error(p(91)); - return xe({}, o, { + return we({}, o, { value: void 0, defaultValue: void 0, children: '' + s._wrapperState.initialValue @@ -16330,7 +15712,7 @@ if (null == i) { if (((i = o.children), (o = o.defaultValue), null != i)) { if (null != o) throw Error(p(92)); - if (Te(i)) { + if (Pe(i)) { if (1 < i.length) throw Error(p(93)); i = i[0]; } @@ -16342,11 +15724,11 @@ } function ib(s, o) { var i = Sa(o.value), - u = Sa(o.defaultValue); + a = 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)); + null != a && (s.defaultValue = '' + a)); } function jb(s) { var o = s.textContent; @@ -16369,17 +15751,17 @@ ? 'http://www.w3.org/1999/xhtml' : s; } - var Re, - qe, + var Te, + Re, $e = - ((qe = function (s, o) { + ((Re = 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 = + (Te = Te || document.createElement('div')).innerHTML = '' + o.valueOf().toString() + '', - o = Re.firstChild; + o = Te.firstChild; s.firstChild; ) s.removeChild(s.firstChild); @@ -16387,12 +15769,12 @@ } }), 'undefined' != typeof MSApp && MSApp.execUnsafeLocalFunction - ? function (s, o, i, u) { + ? function (s, o, i, a) { MSApp.execUnsafeLocalFunction(function () { - return qe(s, o); + return Re(s, o); }); } - : qe); + : Re); function ob(s, o) { if (o) { var i = s.firstChild; @@ -16400,7 +15782,7 @@ } s.textContent = o; } - var ze = { + var qe = { animationIterationCount: !0, aspectRatio: !0, borderImageOutset: !0, @@ -16445,28 +15827,28 @@ strokeOpacity: !0, strokeWidth: !0 }, - We = ['Webkit', 'ms', 'Moz', 'O']; + ze = ['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]) + : i || 'number' != typeof o || 0 === o || (qe.hasOwnProperty(s) && qe[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] = _)); + var a = 0 === i.indexOf('--'), + u = rb(i, o[i], a); + ('float' === i && (i = 'cssFloat'), a ? s.setProperty(i, u) : (s[i] = u)); } } - Object.keys(ze).forEach(function (s) { - We.forEach(function (o) { - ((o = o + s.charAt(0).toUpperCase() + s.substring(1)), (ze[o] = ze[s])); + Object.keys(qe).forEach(function (s) { + ze.forEach(function (o) { + ((o = o + s.charAt(0).toUpperCase() + s.substring(1)), (qe[o] = qe[s])); }); }); - var He = xe( + var We = we( { menuitem: !0 }, { area: !0, @@ -16488,7 +15870,7 @@ ); function ub(s, o) { if (o) { - if (He[s] && (null != o.children || null != o.dangerouslySetInnerHTML)) + if (We[s] && (null != o.children || null != o.dangerouslySetInnerHTML)) throw Error(p(137, s)); if (null != o.dangerouslySetInnerHTML) { if (null != o.children) throw Error(p(60)); @@ -16517,7 +15899,7 @@ return !0; } } - var Ye = null; + var He = null; function xb(s) { return ( (s = s.target || s.srcElement || window).correspondingUseElement && @@ -16525,46 +15907,46 @@ 3 === s.nodeType ? s.parentNode : s ); } - var Xe = null, - Qe = null, - et = null; + var Ye = null, + Xe = null, + Qe = null; function Bb(s) { if ((s = Cb(s))) { - if ('function' != typeof Xe) throw Error(p(280)); + if ('function' != typeof Ye) throw Error(p(280)); var o = s.stateNode; - o && ((o = Db(o)), Xe(s.stateNode, s.type, o)); + o && ((o = Db(o)), Ye(s.stateNode, s.type, o)); } } function Eb(s) { - Qe ? (et ? et.push(s) : (et = [s])) : (Qe = s); + Xe ? (Qe ? Qe.push(s) : (Qe = [s])) : (Xe = 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]); + if (Xe) { + var s = Xe, + o = Qe; + if (((Qe = Xe = 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; + var et = !1; function Jb(s, o, i) { - if (tt) return s(o, i); - tt = !0; + if (et) return s(o, i); + et = !0; try { return Gb(s, o, i); } finally { - ((tt = !1), (null !== Qe || null !== et) && (Hb(), Fb())); + ((et = !1), (null !== Xe || null !== Qe) && (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]; + var a = Db(i); + if (null === a) return null; + i = a[o]; e: switch (o) { case 'onClick': case 'onClickCapture': @@ -16577,14 +15959,14 @@ case 'onMouseUp': case 'onMouseUpCapture': case 'onMouseEnter': - ((u = !u.disabled) || - (u = !( + ((a = !a.disabled) || + (a = !( 'button' === (s = s.type) || 'input' === s || 'select' === s || 'textarea' === s )), - (s = !u)); + (s = !a)); break e; default: s = !1; @@ -16593,39 +15975,39 @@ if (i && 'function' != typeof i) throw Error(p(231, o, typeof i)); return i; } - var rt = !1; - if (C) + var tt = !1; + if (x) try { - var nt = {}; - (Object.defineProperty(nt, 'passive', { + var rt = {}; + (Object.defineProperty(rt, 'passive', { get: function () { - rt = !0; + tt = !0; } }), - window.addEventListener('test', nt, nt), - window.removeEventListener('test', nt, nt)); - } catch (qe) { - rt = !1; + window.addEventListener('test', rt, rt), + window.removeEventListener('test', rt, rt)); + } catch (Re) { + tt = !1; } - function Nb(s, o, i, u, _, w, x, C, j) { - var L = Array.prototype.slice.call(arguments, 3); + function Nb(s, o, i, a, u, _, w, x, C) { + var j = Array.prototype.slice.call(arguments, 3); try { - o.apply(i, L); + o.apply(i, j); } catch (s) { this.onError(s); } } - var st = !1, - ot = null, - it = !1, - at = null, - lt = { + var nt = !1, + st = null, + ot = !1, + it = null, + at = { onError: function (s) { - ((st = !0), (ot = s)); + ((nt = !0), (st = s)); } }; - function Tb(s, o, i, u, _, w, x, C, j) { - ((st = !1), (ot = null), Nb.apply(lt, arguments)); + function Tb(s, o, i, a, u, _, w, x, C) { + ((nt = !1), (st = null), Nb.apply(at, arguments)); } function Vb(s) { var o = s, @@ -16658,54 +16040,54 @@ 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; + for (var i = s, a = o; ; ) { + var u = i.return; + if (null === u) break; + var _ = u.alternate; + if (null === _) { + if (null !== (a = u.return)) { + i = a; 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; + if (u.child === _.child) { + for (_ = u.child; _; ) { + if (_ === i) return (Xb(u), s); + if (_ === a) return (Xb(u), o); + _ = _.sibling; } throw Error(p(188)); } - if (i.return !== u.return) ((i = _), (u = w)); + if (i.return !== a.return) ((i = u), (a = _)); else { - for (var x = !1, C = _.child; C; ) { - if (C === i) { - ((x = !0), (i = _), (u = w)); + for (var w = !1, x = u.child; x; ) { + if (x === i) { + ((w = !0), (i = u), (a = _)); break; } - if (C === u) { - ((x = !0), (u = _), (i = w)); + if (x === a) { + ((w = !0), (a = u), (i = _)); break; } - C = C.sibling; + x = x.sibling; } - if (!x) { - for (C = w.child; C; ) { - if (C === i) { - ((x = !0), (i = w), (u = _)); + if (!w) { + for (x = _.child; x; ) { + if (x === i) { + ((w = !0), (i = _), (a = u)); break; } - if (C === u) { - ((x = !0), (u = w), (i = _)); + if (x === a) { + ((w = !0), (a = _), (i = u)); break; } - C = C.sibling; + x = x.sibling; } - if (!x) throw Error(p(189)); + if (!w) throw Error(p(189)); } } - if (i.alternate !== u) throw Error(p(190)); + if (i.alternate !== a) throw Error(p(190)); } if (3 !== i.tag) throw Error(p(188)); return i.stateNode.current === i ? s : o; @@ -16722,27 +16104,27 @@ } 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 + var ct = u.unstable_scheduleCallback, + lt = u.unstable_cancelCallback, + ut = u.unstable_shouldYield, + pt = u.unstable_requestPaint, + ht = u.unstable_now, + dt = u.unstable_getCurrentPriorityLevel, + mt = u.unstable_ImmediatePriority, + gt = u.unstable_UserBlockingPriority, + yt = u.unstable_NormalPriority, + vt = u.unstable_LowPriority, + bt = u.unstable_IdlePriority, + St = null, + _t = null; + var Et = Math.clz32 ? Math.clz32 : function nc(s) { - return ((s >>>= 0), 0 === s ? 32 : (31 - ((xt(s) / kt) | 0)) | 0); + return ((s >>>= 0), 0 === s ? 32 : (31 - ((wt(s) / xt) | 0)) | 0); }, - xt = Math.log, - kt = Math.LN2; - var Ct = 64, + wt = Math.log, + xt = Math.LN2; + var kt = 64, Ot = 4194304; function tc(s) { switch (s & -s) { @@ -16796,26 +16178,26 @@ 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; + var a = 0, + u = s.suspendedLanes, + _ = s.pingedLanes, + w = 268435455 & i; + if (0 !== w) { + var x = w & ~u; + 0 !== x ? (a = tc(x)) : 0 !== (_ &= w) && (a = tc(_)); + } else 0 !== (w = i & ~u) ? (a = tc(w)) : 0 !== _ && (a = tc(_)); + if (0 === a) return 0; if ( 0 !== o && - o !== u && - !(o & _) && - ((_ = u & -u) >= (w = o & -o) || (16 === _ && 4194240 & w)) + o !== a && + !(o & u) && + ((u = a & -a) >= (_ = o & -o) || (16 === u && 4194240 & _)) ) 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; + if ((4 & a && (a |= 16 & i), 0 !== (o = s.entangledLanes))) + for (s = s.entanglements, o &= a; 0 < o; ) + ((u = 1 << (i = 31 - Et(o))), (a |= s[i]), (o &= ~u)); + return a; } function vc(s, o) { switch (s) { @@ -16851,8 +16233,8 @@ return 0 !== (s = -1073741825 & s.pendingLanes) ? s : 1073741824 & s ? 1073741824 : 0; } function yc() { - var s = Ct; - return (!(4194240 & (Ct <<= 1)) && (Ct = 64), s); + var s = kt; + return (!(4194240 & (kt <<= 1)) && (kt = 64), s); } function zc(s) { for (var o = [], i = 0; 31 > i; i++) o.push(s); @@ -16861,34 +16243,34 @@ function Ac(s, o, i) { ((s.pendingLanes |= o), 536870912 !== o && ((s.suspendedLanes = 0), (s.pingedLanes = 0)), - ((s = s.eventTimes)[(o = 31 - St(o))] = i)); + ((s = s.eventTimes)[(o = 31 - Et(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 a = 31 - Et(i), + u = 1 << a; + ((u & o) | (s[a] & o) && (s[a] |= o), (i &= ~u)); } } var At = 0; function Dc(s) { return 1 < (s &= -s) ? (4 < s ? (268435455 & s ? 16 : 536870912) : 4) : 1; } - var jt, - It, + var Ct, + jt, Pt, - Mt, + It, Tt, Nt = !1, - Rt = [], + Mt = [], + Rt = null, Dt = null, Lt = null, - Bt = null, Ft = new Map(), - qt = new Map(), + Bt = new Map(), $t = [], - Vt = + qt = '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( ' ' ); @@ -16896,15 +16278,15 @@ switch (s) { case 'focusin': case 'focusout': - Dt = null; + Rt = null; break; case 'dragenter': case 'dragleave': - Lt = null; + Dt = null; break; case 'mouseover': case 'mouseout': - Bt = null; + Lt = null; break; case 'pointerover': case 'pointerout': @@ -16912,23 +16294,23 @@ break; case 'gotpointercapture': case 'lostpointercapture': - qt.delete(o.pointerId); + Bt.delete(o.pointerId); } } - function Tc(s, o, i, u, _, w) { - return null === s || s.nativeEvent !== w + function Tc(s, o, i, a, u, _) { + return null === s || s.nativeEvent !== _ ? ((s = { blockedOn: o, domEventName: i, - eventSystemFlags: u, - nativeEvent: w, - targetContainers: [_] + eventSystemFlags: a, + nativeEvent: _, + targetContainers: [u] }), - null !== o && null !== (o = Cb(o)) && It(o), + null !== o && null !== (o = Cb(o)) && jt(o), s) - : ((s.eventSystemFlags |= u), + : ((s.eventSystemFlags |= a), (o = s.targetContainers), - null !== _ && -1 === o.indexOf(_) && o.push(_), + null !== u && -1 === o.indexOf(u) && o.push(u), s); } function Vc(s) { @@ -16953,9 +16335,9 @@ 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()); + if (null !== i) return (null !== (o = Cb(i)) && jt(o), (s.blockedOn = i), !1); + var a = new (i = s.nativeEvent).constructor(i.type, i); + ((He = a), i.target.dispatchEvent(a), (He = null), o.shift()); } return !0; } @@ -16964,34 +16346,34 @@ } function $c() { ((Nt = !1), + null !== Rt && Xc(Rt) && (Rt = null), null !== Dt && Xc(Dt) && (Dt = null), null !== Lt && Xc(Lt) && (Lt = null), - null !== Bt && Xc(Bt) && (Bt = null), Ft.forEach(Zc), - qt.forEach(Zc)); + Bt.forEach(Zc)); } function ad(s, o) { s.blockedOn === o && ((s.blockedOn = null), - Nt || ((Nt = !0), _.unstable_scheduleCallback(_.unstable_NormalPriority, $c))); + Nt || ((Nt = !0), u.unstable_scheduleCallback(u.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]; + if (0 < Mt.length) { + ad(Mt[0], s); + for (var o = 1; o < Mt.length; o++) { + var i = Mt[o]; i.blockedOn === s && (i.blockedOn = null); } } for ( - null !== Dt && ad(Dt, s), + null !== Rt && ad(Rt, s), + null !== Dt && ad(Dt, s), null !== Lt && ad(Lt, s), - null !== Bt && ad(Bt, s), Ft.forEach(b), - qt.forEach(b), + Bt.forEach(b), o = 0; o < $t.length; o++ @@ -17000,73 +16382,73 @@ 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; + var Ut = V.ReactCurrentBatchConfig, + Vt = !0; + function ed(s, o, i, a) { + var u = At, + _ = Ut.transition; Ut.transition = null; try { - ((At = 1), fd(s, o, i, u)); + ((At = 1), fd(s, o, i, a)); } finally { - ((At = _), (Ut.transition = w)); + ((At = u), (Ut.transition = _)); } } - function gd(s, o, i, u) { - var _ = At, - w = Ut.transition; + function gd(s, o, i, a) { + var u = At, + _ = Ut.transition; Ut.transition = null; try { - ((At = 4), fd(s, o, i, u)); + ((At = 4), fd(s, o, i, a)); } finally { - ((At = _), (Ut.transition = w)); + ((At = u), (Ut.transition = _)); } } - 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)); + function fd(s, o, i, a) { + if (Vt) { + var u = Yc(s, o, i, a); + if (null === u) (hd(s, o, a, zt, i), Sc(s, a)); else if ( - (function Uc(s, o, i, u, _) { + (function Uc(s, o, i, a, u) { switch (o) { case 'focusin': - return ((Dt = Tc(Dt, s, o, i, u, _)), !0); + return ((Rt = Tc(Rt, s, o, i, a, u)), !0); case 'dragenter': - return ((Lt = Tc(Lt, s, o, i, u, _)), !0); + return ((Dt = Tc(Dt, s, o, i, a, u)), !0); case 'mouseover': - return ((Bt = Tc(Bt, s, o, i, u, _)), !0); + return ((Lt = Tc(Lt, s, o, i, a, u)), !0); case 'pointerover': - var w = _.pointerId; - return (Ft.set(w, Tc(Ft.get(w) || null, s, o, i, u, _)), !0); + var _ = u.pointerId; + return (Ft.set(_, Tc(Ft.get(_) || null, s, o, i, a, u)), !0); case 'gotpointercapture': return ( - (w = _.pointerId), - qt.set(w, Tc(qt.get(w) || null, s, o, i, u, _)), + (_ = u.pointerId), + Bt.set(_, Tc(Bt.get(_) || null, s, o, i, a, u)), !0 ); } return !1; - })(_, s, o, i, u) + })(u, s, o, i, a) ) - u.stopPropagation(); - else if ((Sc(s, u), 4 & o && -1 < Vt.indexOf(s))) { - for (; null !== _; ) { - var w = Cb(_); + a.stopPropagation(); + else if ((Sc(s, a), 4 & o && -1 < qt.indexOf(s))) { + for (; null !== u; ) { + var _ = Cb(u); if ( - (null !== w && jt(w), - null === (w = Yc(s, o, i, u)) && hd(s, o, u, Wt, i), - w === _) + (null !== _ && Ct(_), + null === (_ = Yc(s, o, i, a)) && hd(s, o, a, zt, i), + _ === u) ) break; - _ = w; + u = _; } - null !== _ && u.stopPropagation(); - } else hd(s, o, u, null, i); + null !== u && a.stopPropagation(); + } else hd(s, o, a, null, i); } } - var Wt = null; - function Yc(s, o, i, u) { - if (((Wt = null), null !== (s = Wc((s = xb(u)))))) + var zt = null; + function Yc(s, o, i, a) { + if (((zt = null), null !== (s = Wc((s = xb(a)))))) if (null === (o = Vb(s))) s = null; else if (13 === (i = o.tag)) { if (null !== (s = Wb(o))) return s; @@ -17076,7 +16458,7 @@ return 3 === o.tag ? o.stateNode.containerInfo : null; s = null; } else o !== s && (s = null); - return ((Wt = s), null); + return ((zt = s), null); } function jd(s) { switch (s) { @@ -17153,15 +16535,15 @@ case 'pointerleave': return 4; case 'message': - switch (mt()) { - case gt: + switch (dt()) { + case mt: return 1; - case yt: + case gt: return 4; + case yt: case vt: - case bt: return 16; - case _t: + case bt: return 536870912; default: return 16; @@ -17170,21 +16552,21 @@ return 16; } } - var Kt = null, - Ht = null, - Jt = null; + var Wt = null, + Jt = null, + Ht = null; function nd() { - if (Jt) return Jt; + if (Ht) return Ht; 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)); + i = Jt, + a = i.length, + u = 'value' in Wt ? Wt.value : Wt.textContent, + _ = u.length; + for (s = 0; s < a && i[s] === u[s]; s++); + var w = a - s; + for (o = 1; o <= w && i[a - o] === u[_ - o]; o++); + return (Ht = u.slice(s, 1 < o ? 1 - o : void 0)); } function od(s) { var o = s.keyCode; @@ -17201,18 +16583,18 @@ return !1; } function rd(s) { - function b(o, i, u, _, w) { - for (var x in ((this._reactName = o), - (this._targetInst = u), + function b(o, i, a, u, _) { + for (var w in ((this._reactName = o), + (this._targetInst = a), (this.type = i), - (this.nativeEvent = _), - (this.target = w), + (this.nativeEvent = u), + (this.target = _), (this.currentTarget = null), s)) - s.hasOwnProperty(x) && ((o = s[x]), (this[x] = o ? o(_) : _[x])); + s.hasOwnProperty(w) && ((o = s[w]), (this[w] = o ? o(u) : u[w])); return ( (this.isDefaultPrevented = ( - null != _.defaultPrevented ? _.defaultPrevented : !1 === _.returnValue + null != u.defaultPrevented ? u.defaultPrevented : !1 === u.returnValue ) ? pd : qd), @@ -17221,7 +16603,7 @@ ); } return ( - xe(b.prototype, { + we(b.prototype, { preventDefault: function () { this.defaultPrevented = !0; var s = this.nativeEvent; @@ -17245,10 +16627,10 @@ b ); } - var Gt, + var Kt, + Gt, Yt, - Xt, - Zt = { + Xt = { eventPhase: 0, bubbles: 0, cancelable: 0, @@ -17258,10 +16640,10 @@ defaultPrevented: 0, isTrusted: 0 }, - Qt = rd(Zt), - er = xe({}, Zt, { view: 0, detail: 0 }), - tr = rd(er), - rr = xe({}, er, { + Qt = rd(Xt), + Zt = we({}, Xt, { view: 0, detail: 0 }), + er = rd(Zt), + tr = we({}, Zt, { screenX: 0, screenY: 0, clientX: 0, @@ -17285,29 +16667,29 @@ 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); + : (s !== Yt && + (Yt && 'mousemove' === s.type + ? ((Kt = s.screenX - Yt.screenX), (Gt = s.screenY - Yt.screenY)) + : (Gt = Kt = 0), + (Yt = s)), + Kt); }, movementY: function (s) { - return 'movementY' in s ? s.movementY : Yt; + return 'movementY' in s ? s.movementY : Gt; } }), - 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, { + rr = rd(tr), + nr = rd(we({}, tr, { dataTransfer: 0 })), + sr = rd(we({}, Zt, { relatedTarget: 0 })), + ir = rd(we({}, Xt, { animationName: 0, elapsedTime: 0, pseudoElement: 0 })), + ar = we({}, Xt, { clipboardData: function (s) { return 'clipboardData' in s ? s.clipboardData : window.clipboardData; } }), - cr = rd(lr), - ur = rd(xe({}, Zt, { data: 0 })), - pr = { + cr = rd(ar), + lr = rd(we({}, Xt, { data: 0 })), + ur = { Esc: 'Escape', Spacebar: ' ', Left: 'ArrowLeft', @@ -17321,7 +16703,7 @@ Scroll: 'ScrollLock', MozPrintableKey: 'Unidentified' }, - dr = { + pr = { 8: 'Backspace', 9: 'Tab', 12: 'Clear', @@ -17359,18 +16741,18 @@ 145: 'ScrollLock', 224: 'Meta' }, - fr = { Alt: 'altKey', Control: 'ctrlKey', Meta: 'metaKey', Shift: 'shiftKey' }; + dr = { 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]; + return o.getModifierState ? o.getModifierState(s) : !!(s = dr[s]) && !!o[s]; } function zd() { return Pd; } - var mr = xe({}, er, { + var fr = we({}, Zt, { key: function (s) { if (s.key) { - var o = pr[s.key] || s.key; + var o = ur[s.key] || s.key; if ('Unidentified' !== o) return o; } return 'keypress' === s.type @@ -17378,7 +16760,7 @@ ? 'Enter' : String.fromCharCode(s) : 'keydown' === s.type || 'keyup' === s.type - ? dr[s.keyCode] || 'Unidentified' + ? pr[s.keyCode] || 'Unidentified' : ''; }, code: 0, @@ -17404,9 +16786,9 @@ : 0; } }), - gr = rd(mr), - yr = rd( - xe({}, rr, { + mr = rd(fr), + gr = rd( + we({}, tr, { pointerId: 0, width: 0, height: 0, @@ -17419,8 +16801,8 @@ isPrimary: 0 }) ), - vr = rd( - xe({}, er, { + yr = rd( + we({}, Zt, { touches: 0, targetTouches: 0, changedTouches: 0, @@ -17431,8 +16813,8 @@ getModifierState: zd }) ), - br = rd(xe({}, Zt, { propertyName: 0, elapsedTime: 0, pseudoElement: 0 })), - _r = xe({}, rr, { + vr = rd(we({}, Xt, { propertyName: 0, elapsedTime: 0, pseudoElement: 0 })), + br = we({}, tr, { deltaX: function (s) { return 'deltaX' in s ? s.deltaX : 'wheelDeltaX' in s ? -s.wheelDeltaX : 0; }, @@ -17448,19 +16830,19 @@ 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)), + Sr = rd(br), + _r = [9, 13, 27, 32], + Er = x && 'CompositionEvent' in window, + wr = null; + x && 'documentMode' in document && (wr = document.documentMode); + var xr = x && 'TextEvent' in window && !wr, + kr = x && (!Er || (wr && 8 < wr && 11 >= wr)), Or = String.fromCharCode(32), Ar = !1; function ge(s, o) { switch (s) { case 'keyup': - return -1 !== wr.indexOf(o.keyCode); + return -1 !== _r.indexOf(o.keyCode); case 'keydown': return 229 !== o.keyCode; case 'keypress': @@ -17474,8 +16856,8 @@ function he(s) { return 'object' == typeof (s = s.detail) && 'data' in s ? s.data : null; } - var jr = !1; - var Ir = { + var Cr = !1; + var jr = { color: !0, date: !0, datetime: !0, @@ -17494,16 +16876,16 @@ }; function me(s) { var o = s && s.nodeName && s.nodeName.toLowerCase(); - return 'input' === o ? !!Ir[s.type] : 'textarea' === o; + return 'input' === o ? !!jr[s.type] : 'textarea' === o; } - function ne(s, o, i, u) { - (Eb(u), + function ne(s, o, i, a) { + (Eb(a), 0 < (o = oe(o, 'onChange')).length && - ((i = new Qt('onChange', 'change', null, i, u)), + ((i = new Qt('onChange', 'change', null, i, a)), s.push({ event: i, listeners: o }))); } var Pr = null, - Mr = null; + Ir = null; function re(s) { se(s, 0); } @@ -17514,34 +16896,34 @@ if ('change' === s) return o; } var Tr = !1; - if (C) { + if (x) { 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)); + if (x) { + var Mr = 'oninput' in document; + if (!Mr) { + var Rr = document.createElement('div'); + (Rr.setAttribute('oninput', 'return;'), (Mr = 'function' == typeof Rr.oninput)); } - Nr = Rr; + Nr = Mr; } else Nr = !1; Tr = Nr && (!document.documentMode || 9 < document.documentMode); } function Ae() { - Pr && (Pr.detachEvent('onpropertychange', Be), (Mr = Pr = null)); + Pr && (Pr.detachEvent('onpropertychange', Be), (Ir = Pr = null)); } function Be(s) { - if ('value' === s.propertyName && te(Mr)) { + if ('value' === s.propertyName && te(Ir)) { var o = []; - (ne(o, Mr, s, xb(s)), Jb(re, o)); + (ne(o, Ir, s, xb(s)), Jb(re, o)); } } function Ce(s, o, i) { 'focusin' === s - ? (Ae(), (Mr = i), (Pr = o).attachEvent('onpropertychange', Be)) + ? (Ae(), (Ir = i), (Pr = o).attachEvent('onpropertychange', Be)) : 'focusout' === s && Ae(); } function De(s) { - if ('selectionchange' === s || 'keyup' === s || 'keydown' === s) return te(Mr); + if ('selectionchange' === s || 'keyup' === s || 'keydown' === s) return te(Ir); } function Ee(s, o) { if ('click' === s) return te(o); @@ -17549,21 +16931,21 @@ function Fe(s, o) { if ('input' === s || 'change' === s) return te(o); } - var Lr = + var Dr = '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 (Dr(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; + a = Object.keys(o); + if (i.length !== a.length) return !1; + for (a = 0; a < i.length; a++) { + var u = i[a]; + if (!C.call(o, u) || !Dr(s[u], o[u])) return !1; } return !0; } @@ -17573,24 +16955,24 @@ } 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 }; + a = Je(s); + for (s = 0; a; ) { + if (3 === a.nodeType) { + if (((i = s + a.textContent.length), s <= o && i >= o)) + return { node: a, offset: o - s }; s = i; } e: { - for (; u; ) { - if (u.nextSibling) { - u = u.nextSibling; + for (; a; ) { + if (a.nextSibling) { + a = a.nextSibling; break e; } - u = u.parentNode; + a = a.parentNode; } - u = void 0; + a = void 0; } - u = Je(u); + a = Je(a); } } function Le(s, o) { @@ -17634,33 +17016,33 @@ function Oe(s) { var o = Me(), i = s.focusedElem, - u = s.selectionRange; + a = 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)) + if (null !== a && Ne(i)) + if (((o = a.start), void 0 === (s = a.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 && + var u = i.textContent.length, + _ = Math.min(a.start, u); + ((a = void 0 === a.end ? _ : Math.min(a.end, u)), + !s.extend && _ > a && ((u = a), (a = _), (_ = u)), + (u = Ke(i, _))); + var w = Ke(i, a); + u && + w && (1 !== s.rangeCount || - s.anchorNode !== _.node || - s.anchorOffset !== _.offset || - s.focusNode !== x.node || - s.focusOffset !== x.offset) && - ((o = o.createRange()).setStart(_.node, _.offset), + s.anchorNode !== u.node || + s.anchorOffset !== u.offset || + s.focusNode !== w.node || + s.focusOffset !== w.offset) && + ((o = o.createRange()).setStart(u.node, u.offset), s.removeAllRanges(), - w > u - ? (s.addRange(o), s.extend(x.node, x.offset)) - : (o.setEnd(x.node, x.offset), s.addRange(o))); + _ > a + ? (s.addRange(o), s.extend(w.node, w.offset)) + : (o.setEnd(w.node, w.offset), s.addRange(o))); } for (o = [], s = i; (s = s.parentNode); ) 1 === s.nodeType && o.push({ element: s, left: s.scrollLeft, top: s.scrollTop }); @@ -17668,32 +17050,32 @@ (((s = o[i]).element.scrollLeft = s.left), (s.element.scrollTop = s.top)); } } - var Br = C && 'documentMode' in document && 11 >= document.documentMode, + var Lr = x && 'documentMode' in document && 11 >= document.documentMode, Fr = null, - qr = null, + Br = null, $r = null, - Vr = !1; + qr = !1; function Ue(s, o, i) { - var u = i.window === i ? i.document : 9 === i.nodeType ? i : i.ownerDocument; - Vr || + var a = i.window === i ? i.document : 9 === i.nodeType ? i : i.ownerDocument; + qr || 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) || + Fr !== Xa(a) || + ('selectionStart' in (a = Fr) && Ne(a) + ? (a = { start: a.selectionStart, end: a.selectionEnd }) + : (a = { + anchorNode: (a = ( + (a.ownerDocument && a.ownerDocument.defaultView) || window ).getSelection()).anchorNode, - anchorOffset: u.anchorOffset, - focusNode: u.focusNode, - focusOffset: u.focusOffset + anchorOffset: a.anchorOffset, + focusNode: a.focusNode, + focusOffset: a.focusOffset }), - ($r && Ie($r, u)) || - (($r = u), - 0 < (u = oe(qr, 'onSelect')).length && + ($r && Ie($r, a)) || + (($r = a), + 0 < (a = oe(Br, 'onSelect')).length && ((o = new Qt('onSelect', 'select', null, o, i)), - s.push({ event: o, listeners: u }), + s.push({ event: o, listeners: a }), (o.target = Fr)))); } function Ve(s, o) { @@ -17711,46 +17093,46 @@ animationstart: Ve('Animation', 'AnimationStart'), transitionend: Ve('Transition', 'TransitionEnd') }, - zr = {}, - Wr = {}; + Vr = {}, + zr = {}; function Ze(s) { - if (zr[s]) return zr[s]; + if (Vr[s]) return Vr[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]); + for (o in i) if (i.hasOwnProperty(o) && o in zr) return (Vr[s] = i[o]); return s; } - C && - ((Wr = document.createElement('div').style), + x && + ((zr = 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 = + var Wr = Ze('animationend'), + Jr = Ze('animationiteration'), + Hr = Ze('animationstart'), + Kr = Ze('transitionend'), + Gr = new Map(), + Yr = '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])); + (Gr.set(s, o), fa(o, [s])); } - for (var Zr = 0; Zr < Xr.length; Zr++) { - var Qr = Xr[Zr]; + for (var Xr = 0; Xr < Yr.length; Xr++) { + var Qr = Yr[Xr]; ff(Qr.toLowerCase(), 'on' + (Qr[0].toUpperCase() + Qr.slice(1))); } - (ff(Kr, 'onAnimationEnd'), - ff(Hr, 'onAnimationIteration'), - ff(Jr, 'onAnimationStart'), + (ff(Wr, 'onAnimationEnd'), + ff(Jr, 'onAnimationIteration'), + ff(Hr, 'onAnimationStart'), ff('dblclick', 'onDoubleClick'), ff('focusin', 'onFocus'), ff('focusout', 'onBlur'), - ff(Gr, 'onTransitionEnd'), + ff(Kr, 'onTransitionEnd'), ha('onMouseEnter', ['mouseout', 'mouseover']), ha('onMouseLeave', ['mouseout', 'mouseover']), ha('onPointerEnter', ['pointerout', 'pointerover']), @@ -17778,153 +17160,153 @@ 'onCompositionUpdate', 'compositionupdate focusout keydown keypress keyup mousedown'.split(' ') )); - var en = + var Zr = '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)); + en = new Set('cancel close invalid load scroll toggle'.split(' ').concat(Zr)); function nf(s, o, i) { - var u = s.type || 'unknown-event'; + var a = 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))); + (function Ub(s, o, i, a, u, _, w, x, C) { + if ((Tb.apply(this, arguments), nt)) { + if (!nt) throw Error(p(198)); + var j = st; + ((nt = !1), (st = null), ot || ((ot = !0), (it = j))); } - })(u, o, void 0, s), + })(a, 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; + var a = s[i], + u = a.event; + a = a.listeners; e: { - var w = void 0; + var _ = 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)); + for (var w = a.length - 1; 0 <= w; w--) { + var x = a[w], + C = x.instance, + j = x.currentTarget; + if (((x = x.listener), C !== _ && u.isPropagationStopped())) break e; + (nf(u, x, j), (_ = C)); } else - for (x = 0; x < u.length; x++) { + for (w = 0; w < a.length; w++) { if ( - ((j = (C = u[x]).instance), - (L = C.currentTarget), - (C = C.listener), - j !== w && _.isPropagationStopped()) + ((C = (x = a[w]).instance), + (j = x.currentTarget), + (x = x.listener), + C !== _ && u.isPropagationStopped()) ) break e; - (nf(_, C, L), (w = j)); + (nf(u, x, j), (_ = C)); } } } - if (it) throw ((s = at), (it = !1), (at = null), s); + if (ot) throw ((s = it), (ot = !1), (it = 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)); + var i = o[mn]; + void 0 === i && (i = o[mn] = new Set()); + var a = s + '__bubble'; + i.has(a) || (pf(o, s, 2, !1), i.add(a)); } function qf(s, o, i) { - var u = 0; - (o && (u |= 4), pf(i, s, u, o)); + var a = 0; + (o && (a |= 4), pf(i, s, a, o)); } - var rn = '_reactListening' + Math.random().toString(36).slice(2); + var tn = '_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)); + if (!s[tn]) { + ((s[tn] = !0), + _.forEach(function (o) { + 'selectionchange' !== o && (en.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)); + null === o || o[tn] || ((o[tn] = !0), qf('selectionchange', !1, o)); } } - function pf(s, o, i, u) { + function pf(s, o, i, a) { switch (jd(o)) { case 1: - var _ = ed; + var u = ed; break; case 4: - _ = gd; + u = gd; break; default: - _ = fd; + u = 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: _ }) + ((i = u.bind(null, o, i, s)), + (u = void 0), + !tt || ('touchstart' !== o && 'touchmove' !== o && 'wheel' !== o) || (u = !0), + a + ? void 0 !== u + ? s.addEventListener(o, i, { capture: !0, passive: u }) : s.addEventListener(o, i, !0) - : void 0 !== _ - ? s.addEventListener(o, i, { passive: _ }) + : void 0 !== u + ? s.addEventListener(o, i, { passive: u }) : s.addEventListener(o, i, !1)); } - function hd(s, o, i, u, _) { - var w = u; - if (!(1 & o || 2 & o || null === u)) + function hd(s, o, i, a, u) { + var _ = a; + if (!(1 & o || 2 & o || null === a)) 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 (null === a) return; + var w = a.tag; + if (3 === w || 4 === w) { + var x = a.stateNode.containerInfo; + if (x === u || (8 === x.nodeType && x.parentNode === u)) break; + if (4 === w) + for (w = a.return; null !== w; ) { + var C = w.tag; if ( - (3 === j || 4 === j) && - ((j = x.stateNode.containerInfo) === _ || - (8 === j.nodeType && j.parentNode === _)) + (3 === C || 4 === C) && + ((C = w.stateNode.containerInfo) === u || + (8 === C.nodeType && C.parentNode === u)) ) return; - x = x.return; + w = w.return; } - for (; null !== C; ) { - if (null === (x = Wc(C))) return; - if (5 === (j = x.tag) || 6 === j) { - u = w = x; + for (; null !== x; ) { + if (null === (w = Wc(x))) return; + if (5 === (C = w.tag) || 6 === C) { + a = _ = w; continue e; } - C = C.parentNode; + x = x.parentNode; } } - u = u.return; + a = a.return; } Jb(function () { - var u = w, - _ = xb(i), - x = []; + var a = _, + u = xb(i), + w = []; e: { - var C = Yr.get(s); - if (void 0 !== C) { - var j = Qt, - L = s; + var x = Gr.get(s); + if (void 0 !== x) { + var C = Qt, + j = s; switch (s) { case 'keypress': if (0 === od(i)) break e; case 'keydown': case 'keyup': - j = gr; + C = mr; break; case 'focusin': - ((L = 'focus'), (j = ir)); + ((j = 'focus'), (C = sr)); break; case 'focusout': - ((L = 'blur'), (j = ir)); + ((j = 'blur'), (C = sr)); break; case 'beforeblur': case 'afterblur': - j = ir; + C = sr; break; case 'click': if (2 === i.button) break e; @@ -17936,7 +17318,7 @@ case 'mouseout': case 'mouseover': case 'contextmenu': - j = nr; + C = rr; break; case 'drag': case 'dragend': @@ -17946,32 +17328,32 @@ case 'dragover': case 'dragstart': case 'drop': - j = sr; + C = nr; break; case 'touchcancel': case 'touchend': case 'touchmove': case 'touchstart': - j = vr; + C = yr; + break; + case Wr: + case Jr: + case Hr: + C = ir; break; case Kr: - case Hr: - case Jr: - j = ar; - break; - case Gr: - j = br; + C = vr; break; case 'scroll': - j = tr; + C = er; break; case 'wheel': - j = Er; + C = Sr; break; case 'copy': case 'cut': case 'paste': - j = cr; + C = cr; break; case 'gotpointercapture': case 'lostpointercapture': @@ -17981,163 +17363,163 @@ case 'pointerout': case 'pointerover': case 'pointerup': - j = yr; + C = gr; } - 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; + var L = !!(4 & o), + B = !L && 'scroll' === s, + $ = L ? (null !== x ? x + 'Capture' : null) : x; + L = []; + for (var U, V = a; null !== V; ) { + var z = (U = V).stateNode; if ( (5 === U.tag && - null !== Y && - ((U = Y), null !== V && null != (Y = Kb(z, V)) && B.push(tf(z, Y, U))), - $) + null !== z && + ((U = z), null !== $ && null != (z = Kb(V, $)) && L.push(tf(V, z, U))), + B) ) break; - z = z.return; + V = V.return; } - 0 < B.length && - ((C = new j(C, L, null, i, _)), x.push({ event: C, listeners: B })); + 0 < L.length && + ((x = new C(x, j, null, i, u)), w.push({ event: x, listeners: L })); } } 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 + ((C = 'mouseout' === s || 'pointerout' === s), + (!(x = 'mouseover' === s || 'pointerover' === s) || + i === He || + !(j = i.relatedTarget || i.fromElement) || + (!Wc(j) && !j[fn])) && + (C || x) && + ((x = + u.window === u + ? u + : (x = u.ownerDocument) + ? x.defaultView || x.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)) + C + ? ((C = a), + null !== (j = (j = i.relatedTarget || i.toElement) ? Wc(j) : null) && + (j !== (B = Vb(j)) || (5 !== j.tag && 6 !== j.tag)) && + (j = null)) + : ((C = null), (j = a)), + C !== j)) ) { if ( - ((B = nr), - (Y = 'onMouseLeave'), - (V = 'onMouseEnter'), - (z = 'mouse'), + ((L = rr), + (z = 'onMouseLeave'), + ($ = 'onMouseEnter'), + (V = '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) + ((L = gr), (z = 'onPointerLeave'), ($ = 'onPointerEnter'), (V = 'pointer')), + (B = null == C ? x : ue(C)), + (U = null == j ? x : ue(j)), + ((x = new L(z, V + 'leave', C, i, u)).target = B), + (x.relatedTarget = U), + (z = null), + Wc(u) === a && + (((L = new L($, V + 'enter', j, i, u)).target = U), + (L.relatedTarget = B), + (z = L)), + (B = z), + C && j) ) 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))); + for ($ = j, V = 0, U = L = C; U; U = vf(U)) V++; + for (U = 0, z = $; z; z = vf(z)) U++; + for (; 0 < V - U; ) ((L = vf(L)), V--); + for (; 0 < U - V; ) (($ = vf($)), U--); + for (; V--; ) { + if (L === $ || (null !== $ && L === $.alternate)) break e; + ((L = vf(L)), ($ = vf($))); } - B = null; + L = null; } - else B = null; - (null !== j && wf(x, C, j, B, !1), - null !== L && null !== $ && wf(x, $, L, B, !0)); + else L = null; + (null !== C && wf(w, x, C, L, !1), + null !== j && null !== B && wf(w, B, j, L, !0)); } if ( 'select' === - (j = (C = u ? ue(u) : window).nodeName && C.nodeName.toLowerCase()) || - ('input' === j && 'file' === C.type) + (C = (x = a ? ue(a) : window).nodeName && x.nodeName.toLowerCase()) || + ('input' === C && 'file' === x.type) ) - var Z = ve; - else if (me(C)) - if (Tr) Z = Fe; + var Y = ve; + else if (me(x)) + if (Tr) Y = Fe; else { - Z = De; - var ee = Ce; + Y = De; + var Z = Ce; } else - (j = C.nodeName) && - 'input' === j.toLowerCase() && - ('checkbox' === C.type || 'radio' === C.type) && - (Z = Ee); + (C = x.nodeName) && + 'input' === C.toLowerCase() && + ('checkbox' === x.type || 'radio' === x.type) && + (Y = Ee); switch ( - (Z && (Z = Z(s, u)) - ? ne(x, Z, i, _) - : (ee && ee(s, C, u), + (Y && (Y = Y(s, a)) + ? ne(w, Y, i, u) + : (Z && Z(s, x, a), 'focusout' === s && - (ee = C._wrapperState) && - ee.controlled && - 'number' === C.type && - cb(C, 'number', C.value)), - (ee = u ? ue(u) : window), + (Z = x._wrapperState) && + Z.controlled && + 'number' === x.type && + cb(x, 'number', x.value)), + (Z = a ? ue(a) : window), s) ) { case 'focusin': - (me(ee) || 'true' === ee.contentEditable) && ((Fr = ee), (qr = u), ($r = null)); + (me(Z) || 'true' === Z.contentEditable) && ((Fr = Z), (Br = a), ($r = null)); break; case 'focusout': - $r = qr = Fr = null; + $r = Br = Fr = null; break; case 'mousedown': - Vr = !0; + qr = !0; break; case 'contextmenu': case 'mouseup': case 'dragend': - ((Vr = !1), Ue(x, i, _)); + ((qr = !1), Ue(w, i, u)); break; case 'selectionchange': - if (Br) break; + if (Lr) break; case 'keydown': case 'keyup': - Ue(x, i, _); + Ue(w, i, u); } - var ie; - if (Sr) + var ee; + if (Er) e: { switch (s) { case 'compositionstart': - var ae = 'onCompositionStart'; + var ie = 'onCompositionStart'; break e; case 'compositionend': - ae = 'onCompositionEnd'; + ie = 'onCompositionEnd'; break e; case 'compositionupdate': - ae = 'onCompositionUpdate'; + ie = 'onCompositionUpdate'; break e; } - ae = void 0; + ie = void 0; } else - jr - ? ge(s, i) && (ae = 'onCompositionEnd') - : 'keydown' === s && 229 === i.keyCode && (ae = 'onCompositionStart'); - (ae && - (Cr && + Cr + ? ge(s, i) && (ie = 'onCompositionEnd') + : 'keydown' === s && 229 === i.keyCode && (ie = 'onCompositionStart'); + (ie && + (kr && '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 + (Cr || 'onCompositionStart' !== ie + ? 'onCompositionEnd' === ie && Cr && (ee = nd()) + : ((Jt = 'value' in (Wt = u) ? Wt.value : Wt.textContent), (Cr = !0))), + 0 < (Z = oe(a, ie)).length && + ((ie = new lr(ie, s, null, i, u)), + w.push({ event: ie, listeners: Z }), + ee ? (ie.data = ee) : null !== (ee = he(i)) && (ie.data = ee))), + (ee = xr ? (function je(s, o) { switch (s) { case 'compositionend': @@ -18151,9 +17533,9 @@ } })(s, i) : (function ke(s, o) { - if (jr) - return 'compositionend' === s || (!Sr && ge(s, o)) - ? ((s = nd()), (Jt = Ht = Kt = null), (jr = !1), s) + if (Cr) + return 'compositionend' === s || (!Er && ge(s, o)) + ? ((s = nd()), (Ht = Jt = Wt = null), (Cr = !1), s) : null; switch (s) { case 'paste': @@ -18166,32 +17548,32 @@ } return null; case 'compositionend': - return Cr && 'ko' !== o.locale ? null : o.data; + return kr && '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))); + 0 < (a = oe(a, 'onBeforeInput')).length && + ((u = new lr('onBeforeInput', 'beforeinput', null, i, u)), + w.push({ event: u, listeners: a }), + (u.data = ee))); } - se(x, o); + se(w, 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, _))), + for (var i = o + 'Capture', a = []; null !== s; ) { + var u = s, + _ = u.stateNode; + (5 === u.tag && + null !== _ && + ((u = _), + null != (_ = Kb(s, i)) && a.unshift(tf(s, _, u)), + null != (_ = Kb(s, o)) && a.push(tf(s, _, u))), (s = s.return)); } - return u; + return a; } function vf(s) { if (null === s) return null; @@ -18200,33 +17582,33 @@ } 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)))), + function wf(s, o, i, a, u) { + for (var _ = o._reactName, w = []; null !== i && i !== a; ) { + var x = i, + C = x.alternate, + j = x.stateNode; + if (null !== C && C === a) break; + (5 === x.tag && + null !== j && + ((x = j), + u + ? null != (C = Kb(i, _)) && w.unshift(tf(i, C, x)) + : u || (null != (C = Kb(i, _)) && w.push(tf(i, C, x)))), (i = i.return)); } - 0 !== x.length && s.push({ event: o, listeners: x }); + 0 !== w.length && s.push({ event: o, listeners: w }); } - var nn = /\r\n?/g, - sn = /\u0000|\uFFFD/g; + var rn = /\r\n?/g, + nn = /\u0000|\uFFFD/g; function zf(s) { - return ('string' == typeof s ? s : '' + s).replace(nn, '\n').replace(sn, ''); + return ('string' == typeof s ? s : '' + s).replace(rn, '\n').replace(nn, ''); } function Af(s, o, i) { if (((o = zf(o)), zf(s) !== o && i)) throw Error(p(425)); } function Bf() {} - var on = null, - an = null; + var sn = null, + on = null; function Ef(s, o) { return ( 'textarea' === s || @@ -18238,17 +17620,17 @@ null != o.dangerouslySetInnerHTML.__html) ); } - var ln = 'function' == typeof setTimeout ? setTimeout : void 0, + var an = 'function' == typeof setTimeout ? setTimeout : void 0, cn = 'function' == typeof clearTimeout ? clearTimeout : void 0, - un = 'function' == typeof Promise ? Promise : void 0, - pn = + ln = 'function' == typeof Promise ? Promise : void 0, + un = 'function' == typeof queueMicrotask ? queueMicrotask - : void 0 !== un + : void 0 !== ln ? function (s) { - return un.resolve(null).then(s).catch(If); + return ln.resolve(null).then(s).catch(If); } - : ln; + : an; function If(s) { setTimeout(function () { throw s; @@ -18256,15 +17638,15 @@ } function Kf(s, o) { var i = o, - u = 0; + a = 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 = _; + var u = i.nextSibling; + if ((s.removeChild(i), u && 8 === u.nodeType)) + if ('/$' === (i = u.data)) { + if (0 === a) return (s.removeChild(u), void bd(o)); + a--; + } else ('$' !== i && '$?' !== i && '$!' !== i) || a++; + i = u; } while (i); bd(o); } @@ -18293,21 +17675,21 @@ } 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; + var pn = Math.random().toString(36).slice(2), + hn = '__reactFiber$' + pn, + dn = '__reactProps$' + pn, + fn = '__reactContainer$' + pn, + mn = '__reactEvents$' + pn, + gn = '__reactListeners$' + pn, + yn = '__reactHandles$' + pn; function Wc(s) { - var o = s[dn]; + var o = s[hn]; if (o) return o; for (var i = s.parentNode; i; ) { - if ((o = i[mn] || i[dn])) { + if ((o = i[fn] || i[hn])) { if (((i = o.alternate), null !== o.child || (null !== i && null !== i.child))) for (s = Mf(s); null !== s; ) { - if ((i = s[dn])) return i; + if ((i = s[hn])) return i; s = Mf(s); } return o; @@ -18317,7 +17699,7 @@ return null; } function Cb(s) { - return !(s = s[dn] || s[mn]) || + return !(s = s[hn] || s[fn]) || (5 !== s.tag && 6 !== s.tag && 13 !== s.tag && 3 !== s.tag) ? null : s; @@ -18327,99 +17709,99 @@ throw Error(p(33)); } function Db(s) { - return s[fn] || null; + return s[dn] || null; } - var bn = [], - _n = -1; + var vn = [], + bn = -1; function Uf(s) { return { current: s }; } function E(s) { - 0 > _n || ((s.current = bn[_n]), (bn[_n] = null), _n--); + 0 > bn || ((s.current = vn[bn]), (vn[bn] = null), bn--); } function G(s, o) { - (_n++, (bn[_n] = s.current), (s.current = o)); + (bn++, (vn[bn] = s.current), (s.current = o)); } - var En = {}, - wn = Uf(En), - Sn = Uf(!1), - xn = En; + var Sn = {}, + _n = Uf(Sn), + En = Uf(!1), + wn = Sn; 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[_]; + if (!i) return Sn; + var a = s.stateNode; + if (a && a.__reactInternalMemoizedUnmaskedChildContext === o) + return a.__reactInternalMemoizedMaskedChildContext; + var u, + _ = {}; + for (u in i) _[u] = o[u]; return ( - u && + a && (((s = s.stateNode).__reactInternalMemoizedUnmaskedChildContext = o), - (s.__reactInternalMemoizedMaskedChildContext = w)), - w + (s.__reactInternalMemoizedMaskedChildContext = _)), + _ ); } function Zf(s) { return null != (s = s.childContextTypes); } function $f() { - (E(Sn), E(wn)); + (E(En), E(_n)); } function ag(s, o, i) { - if (wn.current !== En) throw Error(p(168)); - (G(wn, o), G(Sn, i)); + if (_n.current !== Sn) throw Error(p(168)); + (G(_n, o), G(En, 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); + var a = s.stateNode; + if (((o = o.childContextTypes), 'function' != typeof a.getChildContext)) return i; + for (var u in (a = a.getChildContext())) + if (!(u in o)) throw Error(p(108, Ra(s) || 'Unknown', u)); + return we({}, i, a); } function cg(s) { return ( - (s = ((s = s.stateNode) && s.__reactInternalMemoizedMergedChildContext) || En), - (xn = wn.current), - G(wn, s), - G(Sn, Sn.current), + (s = ((s = s.stateNode) && s.__reactInternalMemoizedMergedChildContext) || Sn), + (wn = _n.current), + G(_n, s), + G(En, En.current), !0 ); } function dg(s, o, i) { - var u = s.stateNode; - if (!u) throw Error(p(169)); + var a = s.stateNode; + if (!a) 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)); + ? ((s = bg(s, o, wn)), + (a.__reactInternalMemoizedMergedChildContext = s), + E(En), + E(_n), + G(_n, s)) + : E(En), + G(En, i)); } - var kn = null, - Cn = !1, + var xn = null, + kn = !1, On = !1; function hg(s) { - null === kn ? (kn = [s]) : kn.push(s); + null === xn ? (xn = [s]) : xn.push(s); } function jg() { - if (!On && null !== kn) { + if (!On && null !== xn) { On = !0; var s = 0, o = At; try { - var i = kn; + var i = xn; for (At = 1; s < i.length; s++) { - var u = i[s]; + var a = i[s]; do { - u = u(!0); - } while (null !== u); + a = a(!0); + } while (null !== a); } - ((kn = null), (Cn = !1)); + ((xn = null), (kn = !1)); } catch (o) { - throw (null !== kn && (kn = kn.slice(s + 1)), ct(gt, jg), o); + throw (null !== xn && (xn = xn.slice(s + 1)), ct(mt, jg), o); } finally { ((At = o), (On = !1)); } @@ -18427,50 +17809,50 @@ return null; } var An = [], - jn = 0, - In = null, + Cn = 0, + jn = null, Pn = 0, - Mn = [], + In = [], Tn = 0, Nn = null, - Rn = 1, - Dn = ''; + Mn = 1, + Rn = ''; function tg(s, o) { - ((An[jn++] = Pn), (An[jn++] = In), (In = s), (Pn = o)); + ((An[Cn++] = Pn), (An[Cn++] = jn), (jn = 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)); + ((In[Tn++] = Mn), (In[Tn++] = Rn), (In[Tn++] = Nn), (Nn = s)); + var a = Mn; + s = Rn; + var u = 32 - Et(a) - 1; + ((a &= ~(1 << u)), (i += 1)); + var _ = 32 - Et(o) + u; + if (30 < _) { + var w = u - (u % 5); + ((_ = (a & ((1 << w) - 1)).toString(32)), + (a >>= w), + (u -= w), + (Mn = (1 << (32 - Et(o) + u)) | (i << u) | a), + (Rn = _ + s)); + } else ((Mn = (1 << _) | (i << u) | a), (Rn = 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 === jn; ) ((jn = An[--Cn]), (An[Cn] = null), (Pn = An[--Cn]), (An[Cn] = null)); for (; s === Nn; ) - ((Nn = Mn[--Tn]), - (Mn[Tn] = null), - (Dn = Mn[--Tn]), - (Mn[Tn] = null), - (Rn = Mn[--Tn]), - (Mn[Tn] = null)); + ((Nn = In[--Tn]), + (In[Tn] = null), + (Rn = In[--Tn]), + (In[Tn] = null), + (Mn = In[--Tn]), + (In[Tn] = null)); } - var Ln = null, - Bn = null, + var Dn = null, + Ln = null, Fn = !1, - qn = null; + Bn = null; function Ag(s, o) { var i = Bg(5, null, null, 0); ((i.elementType = 'DELETED'), @@ -18487,23 +17869,23 @@ (o = 1 !== o.nodeType || i.toLowerCase() !== o.nodeName.toLowerCase() ? null - : o) && ((s.stateNode = o), (Ln = s), (Bn = Lf(o.firstChild)), !0) + : o) && ((s.stateNode = o), (Dn = s), (Ln = Lf(o.firstChild)), !0) ); case 6: return ( null !== (o = '' === s.pendingProps || 3 !== o.nodeType ? null : o) && - ((s.stateNode = o), (Ln = s), (Bn = null), !0) + ((s.stateNode = o), (Dn = s), (Ln = null), !0) ); case 13: return ( null !== (o = 8 !== o.nodeType ? null : o) && - ((i = null !== Nn ? { id: Rn, overflow: Dn } : null), + ((i = null !== Nn ? { id: Mn, overflow: Rn } : 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), + (Dn = s), + (Ln = null), !0) ); default: @@ -18515,37 +17897,37 @@ } function Eg(s) { if (Fn) { - var o = Bn; + var o = Ln; if (o) { var i = o; if (!Cg(s, o)) { if (Dg(s)) throw Error(p(418)); o = Lf(i.nextSibling); - var u = Ln; + var a = Dn; o && Cg(s, o) - ? Ag(u, i) - : ((s.flags = (-4097 & s.flags) | 2), (Fn = !1), (Ln = s)); + ? Ag(a, i) + : ((s.flags = (-4097 & s.flags) | 2), (Fn = !1), (Dn = s)); } } else { if (Dg(s)) throw Error(p(418)); - ((s.flags = (-4097 & s.flags) | 2), (Fn = !1), (Ln = s)); + ((s.flags = (-4097 & s.flags) | 2), (Fn = !1), (Dn = s)); } } } function Fg(s) { for (s = s.return; null !== s && 5 !== s.tag && 3 !== s.tag && 13 !== s.tag; ) s = s.return; - Ln = s; + Dn = s; } function Gg(s) { - if (s !== Ln) return !1; + if (s !== Dn) 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)) + o && (o = Ln)) ) { if (Dg(s)) throw (Hg(), Error(p(418))); for (; o; ) (Ag(s, o), (o = Lf(o.nextSibling))); @@ -18558,7 +17940,7 @@ var i = s.data; if ('/$' === i) { if (0 === o) { - Bn = Lf(s.nextSibling); + Ln = Lf(s.nextSibling); break e; } o--; @@ -18566,41 +17948,41 @@ } s = s.nextSibling; } - Bn = null; + Ln = null; } - } else Bn = Ln ? Lf(s.stateNode.nextSibling) : null; + } else Ln = Dn ? Lf(s.stateNode.nextSibling) : null; return !0; } function Hg() { - for (var s = Bn; s; ) s = Lf(s.nextSibling); + for (var s = Ln; s; ) s = Lf(s.nextSibling); } function Ig() { - ((Bn = Ln = null), (Fn = !1)); + ((Ln = Dn = null), (Fn = !1)); } function Jg(s) { - null === qn ? (qn = [s]) : qn.push(s); + null === Bn ? (Bn = [s]) : Bn.push(s); } - var $n = z.ReactCurrentBatchConfig; + var $n = V.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; + var a = i.stateNode; } - if (!u) throw Error(p(147, s)); - var _ = u, - w = '' + s; + if (!a) throw Error(p(147, s)); + var u = a, + _ = '' + s; return null !== o && null !== o.ref && 'function' == typeof o.ref && - o.ref._stringRef === w + o.ref._stringRef === _ ? o.ref : ((o = function (s) { - var o = _.refs; - null === s ? delete o[w] : (o[w] = s); + var o = u.refs; + null === s ? delete o[_] : (o[_] = s); }), - (o._stringRef = w), + (o._stringRef = _), o); } if ('string' != typeof s) throw Error(p(284)); @@ -18627,8 +18009,8 @@ function Og(s) { function b(o, i) { if (s) { - var u = o.deletions; - null === u ? ((o.deletions = [i]), (o.flags |= 16)) : u.push(i); + var a = o.deletions; + null === a ? ((o.deletions = [i]), (o.flags |= 16)) : a.push(i); } } function c(o, i) { @@ -18644,14 +18026,14 @@ function e(s, o) { return (((s = Pg(s, o)).index = 0), (s.sibling = null), s); } - function f(o, i, u) { + function f(o, i, a) { return ( - (o.index = u), + (o.index = a), s - ? null !== (u = o.alternate) - ? (u = u.index) < i + ? null !== (a = o.alternate) + ? (a = a.index) < i ? ((o.flags |= 2), i) - : u + : a : ((o.flags |= 2), i) : ((o.flags |= 1048576), i) ); @@ -18659,34 +18041,34 @@ function g(o) { return (s && null === o.alternate && (o.flags |= 2), o); } - function h(s, o, i, u) { + function h(s, o, i, a) { return null === o || 6 !== o.tag - ? (((o = Qg(i, s.mode, u)).return = s), o) + ? (((o = Qg(i, s.mode, a)).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) + function k(s, o, i, a) { + var u = i.type; + return u === Z + ? m(s, o, i.props.children, a, 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); + (o.elementType === u || + ('object' == typeof u && null !== u && u.$$typeof === ye && Ng(u) === o.type)) + ? (((a = e(o, i.props)).ref = Lg(s, o, i)), (a.return = s), a) + : (((a = Rg(i.type, i.key, i.props, null, s.mode, a)).ref = Lg(s, o, i)), + (a.return = s), + a); } - function l(s, o, i, u) { + function l(s, o, i, a) { 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 = Sg(i, s.mode, a)).return = s), o) : (((o = e(o, i.children || [])).return = s), o); } - function m(s, o, i, u, _) { + function m(s, o, i, a, u) { return null === o || 7 !== o.tag - ? (((o = Tg(i, s.mode, u, _)).return = s), o) + ? (((o = Tg(i, s.mode, a, u)).return = s), o) : (((o = e(o, i)).return = s), o); } function q(s, o, i) { @@ -18694,188 +18076,188 @@ return (((o = Qg('' + o, s.mode, i)).return = s), o); if ('object' == typeof o && null !== o) { switch (o.$$typeof) { - case Y: + case z: return ( ((i = Rg(o.type, o.key, o.props, null, s.mode, i)).ref = Lg(s, null, o)), (i.return = s), i ); - case Z: + case Y: return (((o = Sg(o, s.mode, i)).return = s), o); - case be: + case ye: 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); + if (Pe(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; + function r(s, o, i, a) { + var u = null !== o ? o.key : null; if (('string' == typeof i && '' !== i) || 'number' == typeof i) - return null !== _ ? null : h(s, o, '' + i, u); + return null !== u ? null : h(s, o, '' + i, a); if ('object' == typeof i && null !== i) { switch (i.$$typeof) { + case z: + return i.key === u ? k(s, o, i, a) : null; 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); + return i.key === u ? l(s, o, i, a) : null; + case ye: + return r(s, o, (u = i._init)(i._payload), a); } - if (Te(i) || Ka(i)) return null !== _ ? null : m(s, o, i, u, null); + if (Pe(i) || Ka(i)) return null !== u ? null : m(s, o, i, a, 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) { + function y(s, o, i, a, u) { + if (('string' == typeof a && '' !== a) || 'number' == typeof a) + return h(o, (s = s.get(i) || null), '' + a, u); + if ('object' == typeof a && null !== a) { + switch (a.$$typeof) { + case z: + return k(o, (s = s.get(null === a.key ? i : a.key) || null), a, u); 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), _); + return l(o, (s = s.get(null === a.key ? i : a.key) || null), a, u); + case ye: + return y(s, o, i, (0, a._init)(a._payload), u); } - if (Te(u) || Ka(u)) return m(o, (s = s.get(i) || null), u, _, null); - Mg(o, u); + if (Pe(a) || Ka(a)) return m(o, (s = s.get(i) || null), a, u, null); + Mg(o, a); } return null; } - function n(o, i, u, _) { + function n(o, i, a, u) { for ( - var w = null, x = null, C = i, j = (i = 0), L = null; - null !== C && j < u.length; - j++ + var _ = null, w = null, x = i, C = (i = 0), j = null; + null !== x && C < a.length; + C++ ) { - C.index > j ? ((L = C), (C = null)) : (L = C.sibling); - var B = r(o, C, u[j], _); + x.index > C ? ((j = x), (x = null)) : (j = x.sibling); + var L = r(o, x, a[C], u); + if (null === L) { + null === x && (x = j); + break; + } + (s && x && null === L.alternate && b(o, x), + (i = f(L, i, C)), + null === w ? (_ = L) : (w.sibling = L), + (w = L), + (x = j)); + } + if (C === a.length) return (c(o, x), Fn && tg(o, C), _); + if (null === x) { + for (; C < a.length; C++) + null !== (x = q(o, a[C], u)) && + ((i = f(x, i, C)), null === w ? (_ = x) : (w.sibling = x), (w = x)); + return (Fn && tg(o, C), _); + } + for (x = d(o, x); C < a.length; C++) + null !== (j = y(x, o, C, a[C], u)) && + (s && null !== j.alternate && x.delete(null === j.key ? C : j.key), + (i = f(j, i, C)), + null === w ? (_ = j) : (w.sibling = j), + (w = j)); + return ( + s && + x.forEach(function (s) { + return b(o, s); + }), + Fn && tg(o, C), + _ + ); + } + function t(o, i, a, u) { + var _ = Ka(a); + if ('function' != typeof _) throw Error(p(150)); + if (null == (a = _.call(a))) throw Error(p(151)); + for ( + var w = (_ = null), x = i, C = (i = 0), j = null, L = a.next(); + null !== x && !L.done; + C++, L = a.next() + ) { + x.index > C ? ((j = x), (x = null)) : (j = x.sibling); + var B = r(o, x, L.value, u); if (null === B) { - null === C && (C = L); + null === x && (x = j); 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)); + (s && x && null === B.alternate && b(o, x), + (i = f(B, i, C)), + null === w ? (_ = B) : (w.sibling = B), + (w = B), + (x = j)); } - 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); + if (L.done) return (c(o, x), Fn && tg(o, C), _); + if (null === x) { + for (; !L.done; C++, L = a.next()) + null !== (L = q(o, L.value, u)) && + ((i = f(L, i, C)), null === w ? (_ = L) : (w.sibling = L), (w = L)); + return (Fn && tg(o, C), _); } - 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)); + for (x = d(o, x); !L.done; C++, L = a.next()) + null !== (L = y(x, o, C, L.value, u)) && + (s && null !== L.alternate && x.delete(null === L.key ? C : L.key), + (i = f(L, i, C)), + null === w ? (_ = L) : (w.sibling = L), + (w = L)); return ( s && - C.forEach(function (s) { + x.forEach(function (s) { return b(o, s); }), - Fn && tg(o, j), - w + Fn && tg(o, C), + _ ); } - 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) { + return function J(s, o, i, a) { if ( ('object' == typeof i && null !== i && - i.type === ee && + i.type === Z && null === i.key && (i = i.props.children), 'object' == typeof i && null !== i) ) { switch (i.$$typeof) { - case Y: + case z: 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)); + for (var u = i.key, _ = o; null !== _; ) { + if (_.key === u) { + if ((u = i.type) === Z) { + if (7 === _.tag) { + (c(s, _.sibling), ((o = e(_, i.props.children)).return = s), (s = o)); break e; } } else if ( - w.elementType === _ || - ('object' == typeof _ && - null !== _ && - _.$$typeof === be && - Ng(_) === w.type) + _.elementType === u || + ('object' == typeof u && + null !== u && + u.$$typeof === ye && + Ng(u) === _.type) ) { - (c(s, w.sibling), - ((o = e(w, i.props)).ref = Lg(s, w, i)), + (c(s, _.sibling), + ((o = e(_, i.props)).ref = Lg(s, _, i)), (o.return = s), (s = o)); break e; } - c(s, w); + c(s, _); break; } - (b(s, w), (w = w.sibling)); + (b(s, _), (_ = _.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)); + i.type === Z + ? (((o = Tg(i.props.children, s.mode, a, i.key)).return = s), (s = o)) + : (((a = Rg(i.type, i.key, i.props, null, s.mode, a)).ref = Lg(s, o, i)), + (a.return = s), + (s = a)); } return g(s); - case Z: + case Y: e: { - for (w = i.key; null !== o; ) { - if (o.key === w) { + for (_ = i.key; null !== o; ) { + if (o.key === _) { if ( 4 === o.tag && o.stateNode.containerInfo === i.containerInfo && @@ -18889,45 +18271,45 @@ } (b(s, o), (o = o.sibling)); } - (((o = Sg(i, s.mode, u)).return = s), (s = o)); + (((o = Sg(i, s.mode, a)).return = s), (s = o)); } return g(s); - case be: - return J(s, o, (w = i._init)(i._payload), u); + case ye: + return J(s, o, (_ = i._init)(i._payload), a); } - if (Te(i)) return n(s, o, i, u); - if (Ka(i)) return t(s, o, i, u); + if (Pe(i)) return n(s, o, i, a); + if (Ka(i)) return t(s, o, i, a); 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)), + : (c(s, o), ((o = Qg(i, s.mode, a)).return = s), (s = o)), g(s)) : c(s, o); }; } - var Vn = Og(!0), + var qn = Og(!0), Un = Og(!1), - zn = Uf(null), + Vn = Uf(null), + zn = null, Wn = null, - Kn = null, - Hn = null; + Jn = null; function $g() { - Hn = Kn = Wn = null; + Jn = Wn = zn = null; } function ah(s) { - var o = zn.current; - (E(zn), (s._currentValue = o)); + var o = Vn.current; + (E(Vn), (s._currentValue = o)); } function bh(s, o, i) { for (; null !== s; ) { - var u = s.alternate; + var a = 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.childLanes |= o), null !== a && (a.childLanes |= o)) + : null !== a && (a.childLanes & o) !== o && (a.childLanes |= o), s === i) ) break; @@ -18935,31 +18317,31 @@ } } function ch(s, o) { - ((Wn = s), - (Hn = Kn = null), + ((zn = s), + (Jn = Wn = null), null !== (s = s.dependencies) && null !== s.firstContext && - (!!(s.lanes & o) && (_s = !0), (s.firstContext = null))); + (!!(s.lanes & o) && (bs = !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; + if (Jn !== s) + if (((s = { context: s, memoizedValue: o, next: null }), null === Wn)) { + if (null === zn) throw Error(p(308)); + ((Wn = s), (zn.dependencies = { lanes: 0, firstContext: s })); + } else Wn = Wn.next = s; return o; } - var Jn = null; + var Hn = null; function gh(s) { - null === Jn ? (Jn = [s]) : Jn.push(s); + null === Hn ? (Hn = [s]) : Hn.push(s); } - function hh(s, o, i, u) { - var _ = o.interleaved; + function hh(s, o, i, a) { + var u = o.interleaved; return ( - null === _ ? ((i.next = i), gh(o)) : ((i.next = _.next), (_.next = i)), + null === u ? ((i.next = i), gh(o)) : ((i.next = u.next), (u.next = i)), (o.interleaved = i), - ih(s, u) + ih(s, a) ); } function ih(s, o) { @@ -18972,7 +18354,7 @@ (s = s.return)); return 3 === i.tag ? i.stateNode : null; } - var Gn = !1; + var Kn = !1; function kh(s) { s.updateQueue = { baseState: s.memoizedState, @@ -18997,39 +18379,39 @@ 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; + var a = s.updateQueue; + if (null === a) return null; + if (((a = a.shared), 2 & Ds)) { + var u = a.pending; return ( - null === _ ? (o.next = o) : ((o.next = _.next), (_.next = o)), - (u.pending = o), + null === u ? (o.next = o) : ((o.next = u.next), (u.next = o)), + (a.pending = o), ih(s, i) ); } return ( - null === (_ = u.interleaved) - ? ((o.next = o), gh(u)) - : ((o.next = _.next), (_.next = o)), - (u.interleaved = o), + null === (u = a.interleaved) + ? ((o.next = o), gh(a)) + : ((o.next = u.next), (u.next = o)), + (a.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)); + var a = o.lanes; + ((i |= a &= 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; + a = s.alternate; + if (null !== a && i === (a = a.updateQueue)) { + var u = null, + _ = null; if (null !== (i = i.firstBaseUpdate)) { do { - var x = { + var w = { eventTime: i.eventTime, lane: i.lane, tag: i.tag, @@ -19037,17 +18419,17 @@ callback: i.callback, next: null }; - (null === w ? (_ = w = x) : (w = w.next = x), (i = i.next)); + (null === _ ? (u = _ = w) : (_ = _.next = w), (i = i.next)); } while (null !== i); - null === w ? (_ = w = o) : (w = w.next = o); - } else _ = w = o; + null === _ ? (u = _ = o) : (_ = _.next = o); + } else u = _ = o; return ( (i = { - baseState: u.baseState, - firstBaseUpdate: _, - lastBaseUpdate: w, - shared: u.shared, - effects: u.effects + baseState: a.baseState, + firstBaseUpdate: u, + lastBaseUpdate: _, + shared: a.shared, + effects: a.effects }), void (s.updateQueue = i) ); @@ -19055,121 +18437,121 @@ (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)); + function qh(s, o, i, a) { + var u = s.updateQueue; + Kn = !1; + var _ = u.firstBaseUpdate, + w = u.lastBaseUpdate, + x = u.shared.pending; + if (null !== x) { + u.shared.pending = null; + var C = x, + j = C.next; + ((C.next = null), null === w ? (_ = j) : (w.next = j), (w = C)); + var L = s.alternate; + null !== L && + (x = (L = L.updateQueue).lastBaseUpdate) !== w && + (null === x ? (L.firstBaseUpdate = j) : (x.next = j), (L.lastBaseUpdate = C)); } - 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 = + if (null !== _) { + var B = u.baseState; + for (w = 0, L = j = C = null, x = _; ; ) { + var $ = x.lane, + U = x.eventTime; + if ((a & $) === $) { + null !== L && + (L = L.next = { eventTime: U, lane: 0, - tag: C.tag, - payload: C.payload, - callback: C.callback, + tag: x.tag, + payload: x.payload, + callback: x.callback, next: null }); e: { - var z = s, - Y = C; - switch (((V = o), (U = i), Y.tag)) { + var V = s, + z = x; + switch ((($ = o), (U = i), z.tag)) { case 1: - if ('function' == typeof (z = Y.payload)) { - $ = z.call(U, $, V); + if ('function' == typeof (V = z.payload)) { + B = V.call(U, B, $); break e; } - $ = z; + B = V; break e; case 3: - z.flags = (-65537 & z.flags) | 128; + V.flags = (-65537 & V.flags) | 128; case 0: if ( - null == (V = 'function' == typeof (z = Y.payload) ? z.call(U, $, V) : z) + null == ($ = 'function' == typeof (V = z.payload) ? V.call(U, B, $) : V) ) break e; - $ = xe({}, $, V); + B = we({}, B, $); break e; case 2: - Gn = !0; + Kn = !0; } } - null !== C.callback && - 0 !== C.lane && - ((s.flags |= 64), null === (V = _.effects) ? (_.effects = [C]) : V.push(C)); + null !== x.callback && + 0 !== x.lane && + ((s.flags |= 64), null === ($ = u.effects) ? (u.effects = [x]) : $.push(x)); } else ((U = { eventTime: U, - lane: V, - tag: C.tag, - payload: C.payload, - callback: C.callback, + lane: $, + tag: x.tag, + payload: x.payload, + callback: x.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)); + null === L ? ((j = L = U), (C = B)) : (L = L.next = U), + (w |= $)); + if (null === (x = x.next)) { + if (null === (x = u.shared.pending)) break; + ((x = ($ = x).next), + ($.next = null), + (u.lastBaseUpdate = $), + (u.shared.pending = null)); } } if ( - (null === B && (j = $), - (_.baseState = j), - (_.firstBaseUpdate = L), - (_.lastBaseUpdate = B), - null !== (o = _.shared.interleaved)) + (null === L && (C = B), + (u.baseState = C), + (u.firstBaseUpdate = j), + (u.lastBaseUpdate = L), + null !== (o = u.shared.interleaved)) ) { - _ = o; + u = o; do { - ((x |= _.lane), (_ = _.next)); - } while (_ !== o); - } else null === w && (_.shared.lanes = 0); - ((Ks |= x), (s.lanes = x), (s.memoizedState = $)); + ((w |= u.lane), (u = u.next)); + } while (u !== o); + } else null === _ && (u.shared.lanes = 0); + ((zs |= w), (s.lanes = w), (s.memoizedState = B)); } } 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 a = s[o], + u = a.callback; + if (null !== u) { + if (((a.callback = null), (a = i), 'function' != typeof u)) + throw Error(p(191, u)); + u.call(a); } } } - var Yn = {}, - Xn = Uf(Yn), - Zn = Uf(Yn), - Qn = Uf(Yn); + var Gn = {}, + Yn = Uf(Gn), + Xn = Uf(Gn), + Qn = Uf(Gn); function xh(s) { - if (s === Yn) throw Error(p(174)); + if (s === Gn) throw Error(p(174)); return s; } function yh(s, o) { - switch ((G(Qn, o), G(Zn, s), G(Xn, Yn), (s = o.nodeType))) { + switch ((G(Qn, o), G(Xn, s), G(Yn, Gn), (s = o.nodeType))) { case 9: case 11: o = (o = o.documentElement) ? o.namespaceURI : lb(null, ''); @@ -19180,21 +18562,21 @@ (s = s.tagName) ); } - (E(Xn), G(Xn, o)); + (E(Yn), G(Yn, o)); } function zh() { - (E(Xn), E(Zn), E(Qn)); + (E(Yn), E(Xn), E(Qn)); } function Ah(s) { xh(Qn.current); - var o = xh(Xn.current), + var o = xh(Yn.current), i = lb(o, s.type); - o !== i && (G(Zn, s), G(Xn, i)); + o !== i && (G(Xn, s), G(Yn, i)); } function Bh(s) { - Zn.current === s && (E(Xn), E(Zn)); + Xn.current === s && (E(Yn), E(Xn)); } - var es = Uf(0); + var Zn = Uf(0); function Ch(s) { for (var o = s; null !== o; ) { if (13 === o.tag) { @@ -19219,55 +18601,55 @@ } return null; } - var ts = []; + var es = []; function Eh() { - for (var s = 0; s < ts.length; s++) ts[s]._workInProgressVersionPrimary = null; - ts.length = 0; + for (var s = 0; s < es.length; s++) es[s]._workInProgressVersionPrimary = null; + es.length = 0; } - var rs = z.ReactCurrentDispatcher, - ns = z.ReactCurrentBatchConfig, - ss = 0, + var ts = V.ReactCurrentDispatcher, + rs = V.ReactCurrentBatchConfig, + ns = 0, + ss = null, os = null, as = null, - ls = null, cs = !1, - us = !1, - ps = 0, - hs = 0; + ls = !1, + us = 0, + ps = 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; + for (var i = 0; i < o.length && i < s.length; i++) if (!Dr(s[i], o[i])) return !1; return !0; } - function Nh(s, o, i, u, _, w) { + function Nh(s, o, i, a, u, _) { if ( - ((ss = w), - (os = o), + ((ns = _), + (ss = o), (o.memoizedState = null), (o.updateQueue = null), (o.lanes = 0), - (rs.current = null === s || null === s.memoizedState ? fs : ms), - (s = i(u, _)), - us) + (ts.current = null === s || null === s.memoizedState ? ds : fs), + (s = i(a, u)), + ls) ) { - w = 0; + _ = 0; do { - if (((us = !1), (ps = 0), 25 <= w)) throw Error(p(301)); - ((w += 1), - (ls = as = null), + if (((ls = !1), (us = 0), 25 <= _)) throw Error(p(301)); + ((_ += 1), + (as = os = null), (o.updateQueue = null), - (rs.current = gs), - (s = i(u, _))); - } while (us); + (ts.current = ms), + (s = i(a, u))); + } while (ls); } if ( - ((rs.current = ds), - (o = null !== as && null !== as.next), - (ss = 0), - (ls = as = os = null), + ((ts.current = hs), + (o = null !== os && null !== os.next), + (ns = 0), + (as = os = ss = null), (cs = !1), o) ) @@ -19275,8 +18657,8 @@ return s; } function Sh() { - var s = 0 !== ps; - return ((ps = 0), s); + var s = 0 !== us; + return ((us = 0), s); } function Th() { var s = { @@ -19286,27 +18668,27 @@ queue: null, next: null }; - return (null === ls ? (os.memoizedState = ls = s) : (ls = ls.next = s), ls); + return (null === as ? (ss.memoizedState = as = s) : (as = as.next = s), as); } function Uh() { - if (null === as) { - var s = os.alternate; + if (null === os) { + var s = ss.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 s = os.next; + var o = null === as ? ss.memoizedState : as.next; + if (null !== o) ((as = o), (os = s)); else { if (null === s) throw Error(p(310)); ((s = { - memoizedState: (as = s).memoizedState, - baseState: as.baseState, - baseQueue: as.baseQueue, - queue: as.queue, + memoizedState: (os = s).memoizedState, + baseState: os.baseState, + baseQueue: os.baseQueue, + queue: os.queue, next: null }), - null === ls ? (os.memoizedState = ls = s) : (ls = ls.next = s)); + null === as ? (ss.memoizedState = as = s) : (as = as.next = s)); } - return ls; + return as; } function Vh(s, o) { return 'function' == typeof o ? o(s) : o; @@ -19316,61 +18698,61 @@ 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)); - } + var a = os, + u = a.baseQueue, + _ = i.pending; if (null !== _) { - ((w = _.next), (u = u.baseState)); - var C = (x = null), - j = null, - L = w; + if (null !== u) { + var w = u.next; + ((u.next = _.next), (_.next = w)); + } + ((a.baseQueue = u = _), (i.pending = null)); + } + if (null !== u) { + ((_ = u.next), (a = a.baseState)); + var x = (w = null), + C = null, + j = _; do { - var B = L.lane; - if ((ss & B) === B) - (null !== j && - (j = j.next = + var L = j.lane; + if ((ns & L) === L) + (null !== C && + (C = C.next = { lane: 0, - action: L.action, - hasEagerState: L.hasEagerState, - eagerState: L.eagerState, + action: j.action, + hasEagerState: j.hasEagerState, + eagerState: j.eagerState, next: null }), - (u = L.hasEagerState ? L.eagerState : s(u, L.action))); + (a = j.hasEagerState ? j.eagerState : s(a, j.action))); else { - var $ = { - lane: B, - action: L.action, - hasEagerState: L.hasEagerState, - eagerState: L.eagerState, + var B = { + lane: L, + action: j.action, + hasEagerState: j.hasEagerState, + eagerState: j.eagerState, next: null }; - (null === j ? ((C = j = $), (x = u)) : (j = j.next = $), - (os.lanes |= B), - (Ks |= B)); + (null === C ? ((x = C = B), (w = a)) : (C = C.next = B), + (ss.lanes |= L), + (zs |= L)); } - 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)); + j = j.next; + } while (null !== j && j !== _); + (null === C ? (w = a) : (C.next = x), + Dr(a, o.memoizedState) || (bs = !0), + (o.memoizedState = a), + (o.baseState = w), + (o.baseQueue = C), + (i.lastRenderedState = a)); } if (null !== (s = i.interleaved)) { - _ = s; + u = s; do { - ((w = _.lane), (os.lanes |= w), (Ks |= w), (_ = _.next)); - } while (_ !== s); - } else null === _ && (i.lanes = 0); + ((_ = u.lane), (ss.lanes |= _), (zs |= _), (u = u.next)); + } while (u !== s); + } else null === u && (i.lanes = 0); return [o.memoizedState, i.dispatch]; } function Xh(s) { @@ -19378,51 +18760,51 @@ i = o.queue; if (null === i) throw Error(p(311)); i.lastRenderedReducer = s; - var u = i.dispatch, - _ = i.pending, - w = o.memoizedState; - if (null !== _) { + var a = i.dispatch, + u = i.pending, + _ = o.memoizedState; + if (null !== u) { i.pending = null; - var x = (_ = _.next); + var w = (u = u.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)); + ((_ = s(_, w.action)), (w = w.next)); + } while (w !== u); + (Dr(_, o.memoizedState) || (bs = !0), + (o.memoizedState = _), + null === o.baseQueue && (o.baseState = _), + (i.lastRenderedState = _)); } - return [w, u]; + return [_, a]; } function Yh() {} function Zh(s, o) { - var i = os, - u = Uh(), - _ = o(), - w = !Lr(u.memoizedState, _); + var i = ss, + a = Uh(), + u = o(), + _ = !Dr(a.memoizedState, u); 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)) + (_ && ((a.memoizedState = u), (bs = !0)), + (a = a.queue), + $h(ai.bind(null, i, a, s), [s]), + a.getSnapshot !== o || _ || (null !== as && 1 & as.memoizedState.tag)) ) { - if (((i.flags |= 2048), bi(9, ci.bind(null, i, u, _, o), void 0, null), null === Fs)) + if (((i.flags |= 2048), bi(9, ci.bind(null, i, a, u, o), void 0, null), null === Ls)) throw Error(p(349)); - 30 & ss || di(i, o, _); + 30 & ns || di(i, o, u); } - return _; + return u; } 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 === (o = ss.updateQueue) + ? ((o = { lastEffect: null, stores: null }), (ss.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 ci(s, o, i, a) { + ((o.value = i), (o.getSnapshot = a), ei(o) && fi(s)); } function ai(s, o, i) { return i(function () { @@ -19434,7 +18816,7 @@ s = s.value; try { var i = o(); - return !Lr(s, i); + return !Dr(s, i); } catch (s) { return !0; } @@ -19457,40 +18839,40 @@ lastRenderedState: s }), (o.queue = s), - (s = s.dispatch = ii.bind(null, os, s)), + (s = s.dispatch = ii.bind(null, ss, s)), [o.memoizedState, s] ); } - function bi(s, o, i, u) { + function bi(s, o, i, a) { return ( - (s = { tag: s, create: o, destroy: i, deps: u, next: null }), - null === (o = os.updateQueue) + (s = { tag: s, create: o, destroy: i, deps: a, next: null }), + null === (o = ss.updateQueue) ? ((o = { lastEffect: null, stores: null }), - (os.updateQueue = o), + (ss.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)), + : ((a = i.next), (i.next = s), (s.next = a), (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 ki(s, o, i, a) { + var u = Th(); + ((ss.flags |= s), (u.memoizedState = bi(1 | o, i, void 0, void 0 === a ? null : a))); } - 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)); + function li(s, o, i, a) { + var u = Uh(); + a = void 0 === a ? null : a; + var _ = void 0; + if (null !== os) { + var w = os.memoizedState; + if (((_ = w.destroy), null !== a && Mh(a, w.deps))) + return void (u.memoizedState = bi(o, i, _, a)); } - ((os.flags |= s), (_.memoizedState = bi(1 | o, i, w, u))); + ((ss.flags |= s), (u.memoizedState = bi(1 | o, i, _, a))); } function mi(s, o) { return ki(8390656, 8, s, o); @@ -19526,87 +18908,87 @@ 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); + var a = i.memoizedState; + return null !== a && null !== o && Mh(o, a[1]) ? a[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] + var a = i.memoizedState; + return null !== a && null !== o && Mh(o, a[1]) + ? a[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)); + return 21 & ns + ? (Dr(i, o) || ((i = yc()), (ss.lanes |= i), (zs |= i), (s.baseState = !0)), o) + : (s.baseState && ((s.baseState = !1), (bs = !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 = {}; + var a = rs.transition; + rs.transition = {}; try { (s(!1), o()); } finally { - ((At = i), (ns.transition = u)); + ((At = i), (rs.transition = a)); } } function wi() { return Uh().memoizedState; } function xi(s, o, i) { - var u = yi(s); + var a = yi(s); if ( - ((i = { lane: u, action: i, hasEagerState: !1, eagerState: null, next: null }), zi(s)) + ((i = { lane: a, 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)); + else if (null !== (i = hh(s, o, i, a))) { + (gi(i, s, a, R()), Bi(i, o, a)); } } function ii(s, o, i) { - var u = yi(s), - _ = { lane: u, action: i, hasEagerState: !1, eagerState: null, next: null }; - if (zi(s)) Ai(o, _); + var a = yi(s), + u = { lane: a, action: i, hasEagerState: !1, eagerState: null, next: null }; + if (zi(s)) Ai(o, u); else { - var w = s.alternate; + var _ = s.alternate; if ( 0 === s.lanes && - (null === w || 0 === w.lanes) && - null !== (w = o.lastRenderedReducer) + (null === _ || 0 === _.lanes) && + null !== (_ = o.lastRenderedReducer) ) try { - var x = o.lastRenderedState, - C = w(x, i); - if (((_.hasEagerState = !0), (_.eagerState = C), Lr(C, x))) { - var j = o.interleaved; + var w = o.lastRenderedState, + x = _(w, i); + if (((u.hasEagerState = !0), (u.eagerState = x), Dr(x, w))) { + var C = o.interleaved; return ( - null === j ? ((_.next = _), gh(o)) : ((_.next = j.next), (j.next = _)), - void (o.interleaved = _) + null === C ? ((u.next = u), gh(o)) : ((u.next = C.next), (C.next = u)), + void (o.interleaved = u) ); } } catch (s) {} - null !== (i = hh(s, o, _, u)) && (gi(i, s, u, (_ = R())), Bi(i, o, u)); + null !== (i = hh(s, o, u, a)) && (gi(i, s, a, (u = R())), Bi(i, o, a)); } } function zi(s) { var o = s.alternate; - return s === os || (null !== o && o === os); + return s === ss || (null !== o && o === ss); } function Ai(s, o) { - us = cs = !0; + ls = 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 a = o.lanes; + ((i |= a &= s.pendingLanes), (o.lanes = i), Cc(s, i)); } } - var ds = { + var hs = { readContext: eh, useCallback: P, useContext: P, @@ -19626,7 +19008,7 @@ useId: P, unstable_isNewReconciler: !1 }, - fs = { + ds = { readContext: eh, useCallback: function (s, o) { return ((Th().memoizedState = [s, void 0 === o ? null : o]), s); @@ -19650,10 +19032,10 @@ return ((o = void 0 === o ? null : o), (s = s()), (i.memoizedState = [s, o]), s); }, useReducer: function (s, o, i) { - var u = Th(); + var a = Th(); return ( (o = void 0 !== i ? i(o) : o), - (u.memoizedState = u.baseState = o), + (a.memoizedState = a.baseState = o), (s = { pending: null, interleaved: null, @@ -19662,9 +19044,9 @@ lastRenderedReducer: s, lastRenderedState: o }), - (u.queue = s), - (s = s.dispatch = xi.bind(null, os, s)), - [u.memoizedState, s] + (a.queue = s), + (s = s.dispatch = xi.bind(null, ss, s)), + [a.memoizedState, s] ); }, useRef: function (s) { @@ -19682,39 +19064,39 @@ }, useMutableSource: function () {}, useSyncExternalStore: function (s, o, i) { - var u = os, - _ = Th(); + var a = ss, + u = 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); + if (((i = o()), null === Ls)) throw Error(p(349)); + 30 & ns || di(a, o, i); } - _.memoizedState = i; - var w = { value: i, getSnapshot: o }; + u.memoizedState = i; + var _ = { 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), + (u.queue = _), + mi(ai.bind(null, a, _, s), [s]), + (a.flags |= 2048), + bi(9, ci.bind(null, a, _, i, o), void 0, null), i ); }, useId: function () { var s = Th(), - o = Fs.identifierPrefix; + o = Ls.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)), + var i = Rn; + ((o = ':' + o + 'R' + (i = (Mn & ~(1 << (32 - Et(Mn) - 1))).toString(32) + i)), + 0 < (i = us++) && (o += 'H' + i.toString(32)), (o += ':')); - } else o = ':' + o + 'r' + (i = hs++).toString(32) + ':'; + } else o = ':' + o + 'r' + (i = ps++).toString(32) + ':'; return (s.memoizedState = o); }, unstable_isNewReconciler: !1 }, - ms = { + fs = { readContext: eh, useCallback: si, useContext: eh, @@ -19730,7 +19112,7 @@ }, useDebugValue: ri, useDeferredValue: function (s) { - return ui(Uh(), as.memoizedState, s); + return ui(Uh(), os.memoizedState, s); }, useTransition: function () { return [Wh(Vh)[0], Uh().memoizedState]; @@ -19740,7 +19122,7 @@ useId: wi, unstable_isNewReconciler: !1 }, - gs = { + ms = { readContext: eh, useCallback: si, useContext: eh, @@ -19757,7 +19139,7 @@ useDebugValue: ri, useDeferredValue: function (s) { var o = Uh(); - return null === as ? (o.memoizedState = s) : ui(o, as.memoizedState, s); + return null === os ? (o.memoizedState = s) : ui(o, os.memoizedState, s); }, useTransition: function () { return [Xh(Vh)[0], Uh().memoizedState]; @@ -19769,116 +19151,116 @@ }; function Ci(s, o) { if (s && s.defaultProps) { - for (var i in ((o = xe({}, o)), (s = s.defaultProps))) + for (var i in ((o = we({}, 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)), + function Di(s, o, i, a) { + ((i = null == (i = i(a, (o = s.memoizedState))) ? o : we({}, o, i)), (s.memoizedState = i), 0 === s.lanes && (s.updateQueue.baseState = i)); } - var ys = { + var gs = { 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, _))); + var a = R(), + u = yi(s), + _ = mh(a, u); + ((_.payload = o), + null != i && (_.callback = i), + null !== (o = nh(s, _, u)) && (gi(o, s, u, a), oh(o, s, u))); }, 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, _))); + var a = R(), + u = yi(s), + _ = mh(a, u); + ((_.tag = 1), + (_.payload = o), + null != i && (_.callback = i), + null !== (o = nh(s, _, u)) && (gi(o, s, u, a), oh(o, s, u))); }, 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))); + a = yi(s), + u = mh(i, a); + ((u.tag = 2), + null != o && (u.callback = o), + null !== (o = nh(s, u, a)) && (gi(o, s, a, i), oh(o, s, a))); } }; - function Fi(s, o, i, u, _, w, x) { + function Fi(s, o, i, a, u, _, w) { return 'function' == typeof (s = s.stateNode).shouldComponentUpdate - ? s.shouldComponentUpdate(u, w, x) - : !o.prototype || !o.prototype.isPureReactComponent || !Ie(i, u) || !Ie(_, w); + ? s.shouldComponentUpdate(a, _, w) + : !o.prototype || !o.prototype.isPureReactComponent || !Ie(i, a) || !Ie(u, _); } function Gi(s, o, i) { - var u = !1, - _ = En, - w = o.contextType; + var a = !1, + u = Sn, + _ = 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)), + 'object' == typeof _ && null !== _ + ? (_ = eh(_)) + : ((u = Zf(o) ? wn : _n.current), + (_ = (a = null != (a = o.contextTypes)) ? Yf(s, u) : Sn)), + (o = new o(i, _)), (s.memoizedState = null !== o.state && void 0 !== o.state ? o.state : null), - (o.updater = ys), + (o.updater = gs), (s.stateNode = o), (o._reactInternals = s), - u && - (((s = s.stateNode).__reactInternalMemoizedUnmaskedChildContext = _), - (s.__reactInternalMemoizedMaskedChildContext = w)), + a && + (((s = s.stateNode).__reactInternalMemoizedUnmaskedChildContext = u), + (s.__reactInternalMemoizedMaskedChildContext = _)), o ); } - function Hi(s, o, i, u) { + function Hi(s, o, i, a) { ((s = o.state), - 'function' == typeof o.componentWillReceiveProps && o.componentWillReceiveProps(i, u), + 'function' == typeof o.componentWillReceiveProps && o.componentWillReceiveProps(i, a), 'function' == typeof o.UNSAFE_componentWillReceiveProps && - o.UNSAFE_componentWillReceiveProps(i, u), - o.state !== s && ys.enqueueReplaceState(o, o.state, null)); + o.UNSAFE_componentWillReceiveProps(i, a), + o.state !== s && gs.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 Ii(s, o, i, a) { + var u = s.stateNode; + ((u.props = i), (u.state = s.memoizedState), (u.refs = {}), kh(s)); + var _ = o.contextType; + ('object' == typeof _ && null !== _ + ? (u.context = eh(_)) + : ((_ = Zf(o) ? wn : _n.current), (u.context = Yf(s, _))), + (u.state = s.memoizedState), + 'function' == typeof (_ = o.getDerivedStateFromProps) && + (Di(s, o, _, i), (u.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' == typeof u.getSnapshotBeforeUpdate || + ('function' != typeof u.UNSAFE_componentWillMount && + 'function' != typeof u.componentWillMount) || + ((o = u.state), + 'function' == typeof u.componentWillMount && u.componentWillMount(), + 'function' == typeof u.UNSAFE_componentWillMount && u.UNSAFE_componentWillMount(), + o !== u.state && gs.enqueueReplaceState(u, u.state, null), + qh(s, i, u, a), + (u.state = s.memoizedState)), + 'function' == typeof u.componentDidMount && (s.flags |= 4194308)); } function Ji(s, o) { try { var i = '', - u = o; + a = o; do { - ((i += Pa(u)), (u = u.return)); - } while (u); - var _ = i; + ((i += Pa(a)), (a = a.return)); + } while (a); + var u = i; } catch (s) { - _ = '\nError generating stack: ' + s.message + '\n' + s.stack; + u = '\nError generating stack: ' + s.message + '\n' + s.stack; } - return { value: s, source: o, stack: _, digest: null }; + return { value: s, source: o, stack: u, digest: null }; } function Ki(s, o, i) { return { @@ -19897,37 +19279,37 @@ }); } } - var vs = 'function' == typeof WeakMap ? WeakMap : Map; + var ys = 'function' == typeof WeakMap ? WeakMap : Map; function Ni(s, o, i) { (((i = mh(-1, i)).tag = 3), (i.payload = { element: null })); - var u = o.value; + var a = o.value; return ( (i.callback = function () { - (eo || ((eo = !0), (to = u)), Li(0, o)); + (Qs || ((Qs = !0), (Zs = a)), 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; + var a = s.type.getDerivedStateFromError; + if ('function' == typeof a) { + var u = o.value; ((i.payload = function () { - return u(_); + return a(u); }), (i.callback = function () { Li(0, o); })); } - var w = s.stateNode; + var _ = s.stateNode; return ( - null !== w && - 'function' == typeof w.componentDidCatch && + null !== _ && + 'function' == typeof _.componentDidCatch && (i.callback = function () { (Li(0, o), - 'function' != typeof u && - (null === ro ? (ro = new Set([this])) : ro.add(this))); + 'function' != typeof a && + (null === eo ? (eo = new Set([this])) : eo.add(this))); var s = o.stack; this.componentDidCatch(o.value, { componentStack: null !== s ? s : '' }); }), @@ -19935,13 +19317,13 @@ ); } 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)); + var a = s.pingCache; + if (null === a) { + a = s.pingCache = new ys(); + var u = new Set(); + a.set(o, u); + } else void 0 === (u = a.get(o)) && ((u = new Set()), a.set(o, u)); + u.has(i) || (u.add(i), (s = Ti.bind(null, s, o, i)), o.then(s, s)); } function Ui(s) { do { @@ -19956,9 +19338,9 @@ } while (null !== s); return null; } - function Vi(s, o, i, u, _) { + function Vi(s, o, i, a, u) { return 1 & s.mode - ? ((s.flags |= 65536), (s.lanes = _), s) + ? ((s.flags |= 65536), (s.lanes = u), s) : (s === o ? (s.flags |= 65536) : ((s.flags |= 128), @@ -19971,219 +19353,219 @@ (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); + var vs = V.ReactCurrentOwner, + bs = !1; + function Xi(s, o, i, a) { + o.child = null === s ? Un(o, null, i, a) : qn(o, s.child, i, a); } - function Yi(s, o, i, u, _) { + function Yi(s, o, i, a, u) { i = i.render; - var w = o.ref; + var _ = o.ref; return ( - ch(o, _), - (u = Nh(s, o, i, u, w, _)), + ch(o, u), + (a = Nh(s, o, i, a, _, u)), (i = Sh()), - null === s || _s - ? (Fn && i && vg(o), (o.flags |= 1), Xi(s, o, u, _), o.child) + null === s || bs + ? (Fn && i && vg(o), (o.flags |= 1), Xi(s, o, a, u), o.child) : ((o.updateQueue = s.updateQueue), (o.flags &= -2053), - (s.lanes &= ~_), - Zi(s, o, _)) + (s.lanes &= ~u), + Zi(s, o, u)) ); } - function $i(s, o, i, u, _) { + function $i(s, o, i, a, u) { if (null === s) { - var w = i.type; - return 'function' != typeof w || - aj(w) || - void 0 !== w.defaultProps || + var _ = i.type; + return 'function' != typeof _ || + aj(_) || + void 0 !== _.defaultProps || null !== i.compare || void 0 !== i.defaultProps - ? (((s = Rg(i.type, null, u, o, o.mode, _)).ref = o.ref), + ? (((s = Rg(i.type, null, a, o, o.mode, u)).ref = o.ref), (s.return = o), (o.child = s)) - : ((o.tag = 15), (o.type = w), bj(s, o, w, u, _)); + : ((o.tag = 15), (o.type = _), bj(s, o, _, a, 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, _); + if (((_ = s.child), !(s.lanes & u))) { + var w = _.memoizedProps; + if ((i = null !== (i = i.compare) ? i : Ie)(w, a) && s.ref === o.ref) + return Zi(s, o, u); } - return ((o.flags |= 1), ((s = Pg(w, u)).ref = o.ref), (s.return = o), (o.child = s)); + return ((o.flags |= 1), ((s = Pg(_, a)).ref = o.ref), (s.return = o), (o.child = s)); } - function bj(s, o, i, u, _) { + function bj(s, o, i, a, 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); + var _ = s.memoizedProps; + if (Ie(_, a) && s.ref === o.ref) { + if (((bs = !1), (o.pendingProps = a = _), !(s.lanes & u))) + return ((o.lanes = s.lanes), Zi(s, o, u)); + 131072 & s.flags && (bs = !0); } } - return cj(s, o, i, u, _); + return cj(s, o, i, a, u); } function dj(s, o, i) { - var u = o.pendingProps, - _ = u.children, - w = null !== s ? s.memoizedState : null; - if ('hidden' === u.mode) + var a = o.pendingProps, + u = a.children, + _ = null !== s ? s.memoizedState : null; + if ('hidden' === a.mode) if (1 & o.mode) { if (!(1073741824 & i)) return ( - (s = null !== w ? w.baseLanes | i : i), + (s = null !== _ ? _.baseLanes | i : i), (o.lanes = o.childLanes = 1073741824), (o.memoizedState = { baseLanes: s, cachePool: null, transitions: null }), (o.updateQueue = null), - G(Us, Vs), - (Vs |= s), + G(qs, $s), + ($s |= s), null ); ((o.memoizedState = { baseLanes: 0, cachePool: null, transitions: null }), - (u = null !== w ? w.baseLanes : i), - G(Us, Vs), - (Vs |= u)); + (a = null !== _ ? _.baseLanes : i), + G(qs, $s), + ($s |= a)); } else ((o.memoizedState = { baseLanes: 0, cachePool: null, transitions: null }), - G(Us, Vs), - (Vs |= i)); + G(qs, $s), + ($s |= 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); + (null !== _ ? ((a = _.baseLanes | i), (o.memoizedState = null)) : (a = i), + G(qs, $s), + ($s |= a)); + return (Xi(s, o, u, 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; + function cj(s, o, i, a, u) { + var _ = Zf(i) ? wn : _n.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) + (_ = Yf(o, _)), + ch(o, u), + (i = Nh(s, o, i, a, _, u)), + (a = Sh()), + null === s || bs + ? (Fn && a && vg(o), (o.flags |= 1), Xi(s, o, i, u), o.child) : ((o.updateQueue = s.updateQueue), (o.flags &= -2053), - (s.lanes &= ~_), - Zi(s, o, _)) + (s.lanes &= ~u), + Zi(s, o, u)) ); } - function hj(s, o, i, u, _) { + function hj(s, o, i, a, u) { if (Zf(i)) { - var w = !0; + var _ = !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 _ = !1; + if ((ch(o, u), null === o.stateNode)) (ij(s, o), Gi(o, i, a), Ii(o, i, a, u), (a = !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))); + var w = o.stateNode, + x = o.memoizedProps; + w.props = x; + var C = w.context, + j = i.contextType; + 'object' == typeof j && null !== j + ? (j = eh(j)) + : (j = Yf(o, (j = Zf(i) ? wn : _n.current))); + var L = i.getDerivedStateFromProps, + B = 'function' == typeof L || 'function' == typeof w.getSnapshotBeforeUpdate; + (B || + ('function' != typeof w.UNSAFE_componentWillReceiveProps && + 'function' != typeof w.componentWillReceiveProps) || + ((x !== a || C !== j) && Hi(o, w, a, j)), + (Kn = !1)); + var $ = o.memoizedState; + ((w.state = $), + qh(o, a, w, u), + (C = o.memoizedState), + x !== a || $ !== C || En.current || Kn + ? ('function' == typeof L && (Di(o, i, L, a), (C = o.memoizedState)), + (x = Kn || Fi(o, i, x, a, $, C, j)) + ? (B || + ('function' != typeof w.UNSAFE_componentWillMount && + 'function' != typeof w.componentWillMount) || + ('function' == typeof w.componentWillMount && w.componentWillMount(), + 'function' == typeof w.UNSAFE_componentWillMount && + w.UNSAFE_componentWillMount()), + 'function' == typeof w.componentDidMount && (o.flags |= 4194308)) + : ('function' == typeof w.componentDidMount && (o.flags |= 4194308), + (o.memoizedProps = a), + (o.memoizedState = C)), + (w.props = a), + (w.state = C), + (w.context = j), + (a = x)) + : ('function' == typeof w.componentDidMount && (o.flags |= 4194308), (a = !1))); } else { - ((x = o.stateNode), + ((w = 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)))); + (x = o.memoizedProps), + (j = o.type === o.elementType ? x : Ci(o.type, x)), + (w.props = j), + (B = o.pendingProps), + ($ = w.context), + 'object' == typeof (C = i.contextType) && null !== C + ? (C = eh(C)) + : (C = Yf(o, (C = Zf(i) ? wn : _n.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) || + ((L = 'function' == typeof U || 'function' == typeof w.getSnapshotBeforeUpdate) || + ('function' != typeof w.UNSAFE_componentWillReceiveProps && + 'function' != typeof w.componentWillReceiveProps) || + ((x !== B || $ !== C) && Hi(o, w, a, C)), + (Kn = !1), + ($ = o.memoizedState), + (w.state = $), + qh(o, a, w, u)); + var V = o.memoizedState; + x !== B || $ !== V || En.current || Kn + ? ('function' == typeof U && (Di(o, i, U, a), (V = o.memoizedState)), + (j = Kn || Fi(o, i, j, a, $, V, C) || !1) + ? (L || + ('function' != typeof w.UNSAFE_componentWillUpdate && + 'function' != typeof w.componentWillUpdate) || + ('function' == typeof w.componentWillUpdate && + w.componentWillUpdate(a, V, C), + 'function' == typeof w.UNSAFE_componentWillUpdate && + w.UNSAFE_componentWillUpdate(a, V, C)), + 'function' == typeof w.componentDidUpdate && (o.flags |= 4), + 'function' == typeof w.getSnapshotBeforeUpdate && (o.flags |= 1024)) + : ('function' != typeof w.componentDidUpdate || + (x === s.memoizedProps && $ === s.memoizedState) || (o.flags |= 4), - 'function' != typeof x.getSnapshotBeforeUpdate || - (C === s.memoizedProps && V === s.memoizedState) || + 'function' != typeof w.getSnapshotBeforeUpdate || + (x === s.memoizedProps && $ === 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.memoizedProps = a), + (o.memoizedState = V)), + (w.props = a), + (w.state = V), + (w.context = C), + (a = j)) + : ('function' != typeof w.componentDidUpdate || + (x === s.memoizedProps && $ === s.memoizedState) || (o.flags |= 4), - 'function' != typeof x.getSnapshotBeforeUpdate || - (C === s.memoizedProps && V === s.memoizedState) || + 'function' != typeof w.getSnapshotBeforeUpdate || + (x === s.memoizedProps && $ === s.memoizedState) || (o.flags |= 1024), - (u = !1)); + (a = !1)); } - return jj(s, o, i, u, w, _); + return jj(s, o, i, a, _, u); } - function jj(s, o, i, u, _, w) { + function jj(s, o, i, a, u, _) { 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(); + var w = !!(128 & o.flags); + if (!a && !w) return (u && dg(o, i, !1), Zi(s, o, _)); + ((a = o.stateNode), (vs.current = o)); + var x = w && 'function' != typeof i.getDerivedStateFromError ? null : a.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), + null !== s && w + ? ((o.child = qn(o, s.child, null, _)), (o.child = qn(o, null, x, _))) + : Xi(s, o, x, _), + (o.memoizedState = a.state), + u && dg(o, i, !0), o.child ); } @@ -20194,29 +19576,28 @@ : 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); + function lj(s, o, i, a, u) { + return (Ig(), Jg(u), (o.flags |= 256), Xi(s, o, i, a), o.child); } - var Es, - ws, - Ss, - xs, - ks = { dehydrated: null, treeContext: null, retryLane: 0 }; + var Ss, + _s, + Es, + ws = { 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); + var a, + u = o.pendingProps, + _ = Zn.current, + w = !1, + x = !!(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), + ((a = x) || (a = (null === s || null !== s.memoizedState) && !!(2 & _)), + a + ? ((w = !0), (o.flags &= -129)) + : (null !== s && null === s.memoizedState) || (_ |= 1), + G(Zn, 1 & _), null === s) ) return ( @@ -20228,57 +19609,57 @@ : (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), + : ((x = u.children), + (s = u.fallback), + w + ? ((u = o.mode), + (w = o.child), + (x = { mode: 'hidden', children: x }), + 1 & u || null === w + ? (w = pj(x, u, 0, null)) + : ((w.childLanes = 0), (w.pendingProps = x)), + (s = Tg(s, u, i, null)), + (w.return = o), (s.return = o), - (x.sibling = s), - (o.child = x), + (w.sibling = s), + (o.child = w), (o.child.memoizedState = nj(i)), - (o.memoizedState = ks), + (o.memoizedState = ws), s) - : qj(o, C)) + : qj(o, x)) ); - if (null !== (w = s.memoizedState) && null !== (u = w.dehydrated)) - return (function rj(s, o, i, u, _, w, x) { + if (null !== (_ = s.memoizedState) && null !== (a = _.dehydrated)) + return (function rj(s, o, i, a, u, _, w) { if (i) return 256 & o.flags - ? ((o.flags &= -257), sj(s, o, x, (u = Ki(Error(p(422)))))) + ? ((o.flags &= -257), sj(s, o, w, (a = 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)))); + : ((_ = a.fallback), + (u = o.mode), + (a = pj({ mode: 'visible', children: a.children }, u, 0, null)), + ((_ = Tg(_, u, w, null)).flags |= 2), + (a.return = o), + (_.return = o), + (a.sibling = _), + (o.child = a), + 1 & o.mode && qn(o, s.child, null, w), + (o.child.memoizedState = nj(w)), + (o.memoizedState = ws), + _); + if (!(1 & o.mode)) return sj(s, o, w, null); + if ('$!' === u.data) { + if ((a = u.nextSibling && u.nextSibling.dataset)) var x = a.dgst; + return ((a = x), sj(s, o, w, (a = Ki((_ = Error(p(419))), a, void 0)))); } - if (((C = !!(x & s.childLanes)), _s || C)) { - if (null !== (u = Fs)) { - switch (x & -x) { + if (((x = !!(w & s.childLanes)), bs || x)) { + if (null !== (a = Ls)) { + switch (w & -w) { case 4: - _ = 2; + u = 2; break; case 16: - _ = 8; + u = 8; break; case 64: case 128: @@ -20301,77 +19682,77 @@ case 16777216: case 33554432: case 67108864: - _ = 32; + u = 32; break; case 536870912: - _ = 268435456; + u = 268435456; break; default: - _ = 0; + u = 0; } - 0 !== (_ = _ & (u.suspendedLanes | x) ? 0 : _) && - _ !== w.retryLane && - ((w.retryLane = _), ih(s, _), gi(u, s, _, -1)); + 0 !== (u = u & (a.suspendedLanes | w) ? 0 : u) && + u !== _.retryLane && + ((_.retryLane = u), ih(s, u), gi(a, s, u, -1)); } - return (tj(), sj(s, o, x, (u = Ki(Error(p(421)))))); + return (tj(), sj(s, o, w, (a = Ki(Error(p(421)))))); } - return '$?' === _.data + return '$?' === u.data ? ((o.flags |= 128), (o.child = s.child), (o = uj.bind(null, s)), - (_._reactRetry = o), + (u._reactRetry = o), null) - : ((s = w.treeContext), - (Bn = Lf(_.nextSibling)), - (Ln = o), + : ((s = _.treeContext), + (Ln = Lf(u.nextSibling)), + (Dn = o), (Fn = !0), - (qn = null), + (Bn = null), null !== s && - ((Mn[Tn++] = Rn), - (Mn[Tn++] = Dn), - (Mn[Tn++] = Nn), - (Rn = s.id), - (Dn = s.overflow), + ((In[Tn++] = Mn), + (In[Tn++] = Rn), + (In[Tn++] = Nn), + (Mn = s.id), + (Rn = s.overflow), (Nn = o)), - (o = qj(o, u.children)), + (o = qj(o, a.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 }; + })(s, o, x, u, a, _, i); + if (w) { + ((w = u.fallback), (x = o.mode), (a = (_ = s.child).sibling)); + var C = { mode: 'hidden', children: u.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) + 1 & x || o.child === _ + ? ((u = Pg(_, C)).subtreeFlags = 14680064 & _.subtreeFlags) + : (((u = o.child).childLanes = 0), (u.pendingProps = C), (o.deletions = null)), + null !== a ? (w = Pg(a, w)) : ((w = Tg(w, x, i, null)).flags |= 2), + (w.return = o), + (u.return = o), + (u.sibling = w), + (o.child = u), + (u = w), + (w = o.child), + (x = + null === (x = 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), - _ + : { baseLanes: x.baseLanes | i, cachePool: null, transitions: x.transitions }), + (w.memoizedState = x), + (w.childLanes = s.childLanes & ~i), + (o.memoizedState = ws), + u ); } return ( - (s = (x = s.child).sibling), - (_ = Pg(x, { mode: 'visible', children: _.children })), - !(1 & o.mode) && (_.lanes = i), - (_.return = o), - (_.sibling = null), + (s = (w = s.child).sibling), + (u = Pg(w, { mode: 'visible', children: u.children })), + !(1 & o.mode) && (u.lanes = i), + (u.return = o), + (u.sibling = null), null !== s && (null === (i = o.deletions) ? ((o.deletions = [s]), (o.flags |= 16)) : i.push(s)), - (o.child = _), + (o.child = u), (o.memoizedState = null), - _ + u ); } function qj(s, o) { @@ -20380,10 +19761,10 @@ (s.child = o) ); } - function sj(s, o, i, u) { + function sj(s, o, i, a) { return ( - null !== u && Jg(u), - Vn(o, s.child, null, i), + null !== a && Jg(a), + qn(o, s.child, null, i), ((s = qj(o, o.pendingProps.children)).flags |= 2), (o.memoizedState = null), s @@ -20391,33 +19772,33 @@ } function vj(s, o, i) { s.lanes |= o; - var u = s.alternate; - (null !== u && (u.lanes |= o), bh(s.return, o, i)); + var a = s.alternate; + (null !== a && (a.lanes |= o), bh(s.return, o, i)); } - function wj(s, o, i, u, _) { - var w = s.memoizedState; - null === w + function wj(s, o, i, a, u) { + var _ = s.memoizedState; + null === _ ? (s.memoizedState = { isBackwards: o, rendering: null, renderingStartTime: 0, - last: u, + last: a, tail: i, - tailMode: _ + tailMode: u }) - : ((w.isBackwards = o), - (w.rendering = null), - (w.renderingStartTime = 0), - (w.last = u), - (w.tail = i), - (w.tailMode = _)); + : ((_.isBackwards = o), + (_.rendering = null), + (_.renderingStartTime = 0), + (_.last = a), + (_.tail = i), + (_.tailMode = u)); } 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)); + var a = o.pendingProps, + u = a.revealOrder, + _ = a.tail; + if ((Xi(s, o, a.children, i), 2 & (a = Zn.current))) + ((a = (1 & a) | 2), (o.flags |= 128)); else { if (null !== s && 128 & s.flags) e: for (s = o.child; null !== s; ) { @@ -20434,27 +19815,27 @@ } ((s.sibling.return = s.return), (s = s.sibling)); } - u &= 1; + a &= 1; } - if ((G(es, u), 1 & o.mode)) - switch (_) { + if ((G(Zn, a), 1 & o.mode)) + switch (u) { 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)); + for (i = o.child, u = null; null !== i; ) + (null !== (s = i.alternate) && null === Ch(s) && (u = i), (i = i.sibling)); + (null === (i = u) + ? ((u = o.child), (o.child = null)) + : ((u = i.sibling), (i.sibling = null)), + wj(o, !1, u, i, _)); break; case 'backwards': - for (i = null, _ = o.child, o.child = null; null !== _; ) { - if (null !== (s = _.alternate) && null === Ch(s)) { - o.child = _; + for (i = null, u = o.child, o.child = null; null !== u; ) { + if (null !== (s = u.alternate) && null === Ch(s)) { + o.child = u; break; } - ((s = _.sibling), (_.sibling = i), (i = _), (_ = s)); + ((s = u.sibling), (u.sibling = i), (i = u), (u = s)); } - wj(o, !0, i, null, w); + wj(o, !0, i, null, _); break; case 'together': wj(o, !1, null, null, void 0); @@ -20473,7 +19854,7 @@ function Zi(s, o, i) { if ( (null !== s && (o.dependencies = s.dependencies), - (Ks |= o.lanes), + (zs |= o.lanes), !(i & o.childLanes)) ) return null; @@ -20499,37 +19880,37 @@ break; case 'collapsed': i = s.tail; - for (var u = null; null !== i; ) - (null !== i.alternate && (u = i), (i = i.sibling)); - null === u + for (var a = null; null !== i; ) + (null !== i.alternate && (a = i), (i = i.sibling)); + null === a ? o || null === s.tail ? (s.tail = null) : (s.tail.sibling = null) - : (u.sibling = null); + : (a.sibling = null); } } function S(s) { var o = null !== s.alternate && s.alternate.child === s.child, i = 0, - u = 0; + a = 0; if (o) - for (var _ = s.child; null !== _; ) - ((i |= _.lanes | _.childLanes), - (u |= 14680064 & _.subtreeFlags), - (u |= 14680064 & _.flags), - (_.return = s), - (_ = _.sibling)); + for (var u = s.child; null !== u; ) + ((i |= u.lanes | u.childLanes), + (a |= 14680064 & u.subtreeFlags), + (a |= 14680064 & u.flags), + (u.return = s), + (u = u.sibling)); else - for (_ = s.child; null !== _; ) - ((i |= _.lanes | _.childLanes), - (u |= _.subtreeFlags), - (u |= _.flags), - (_.return = s), - (_ = _.sibling)); - return ((s.subtreeFlags |= u), (s.childLanes = i), o); + for (u = s.child; null !== u; ) + ((i |= u.lanes | u.childLanes), + (a |= u.subtreeFlags), + (a |= u.flags), + (u.return = s), + (u = u.sibling)); + return ((s.subtreeFlags |= a), (s.childLanes = i), o); } function Ej(s, o, i) { - var u = o.pendingProps; + var a = o.pendingProps; switch ((wg(o), o.tag)) { case 2: case 16: @@ -20547,357 +19928,356 @@ return (Zf(o.type) && $f(), S(o), null); case 3: return ( - (u = o.stateNode), + (a = o.stateNode), zh(), - E(Sn), - E(wn), + E(En), + E(_n), Eh(), - u.pendingContext && ((u.context = u.pendingContext), (u.pendingContext = null)), + a.pendingContext && ((a.context = a.pendingContext), (a.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), + ((o.flags |= 1024), null !== Bn && (Fj(Bn), (Bn = null)))), S(o), null ); case 5: Bh(o); - var _ = xh(Qn.current); + var u = 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))); + (_s(s, o, i, a), s.ref !== o.ref && ((o.flags |= 512), (o.flags |= 2097152))); else { - if (!u) { + if (!a) { 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)) { + if (((s = xh(Yn.current)), Gg(o))) { + ((a = o.stateNode), (i = o.type)); + var _ = o.memoizedProps; + switch (((a[hn] = o), (a[dn] = _), (s = !!(1 & o.mode)), i)) { case 'dialog': - (D('cancel', u), D('close', u)); + (D('cancel', a), D('close', a)); break; case 'iframe': case 'object': case 'embed': - D('load', u); + D('load', a); break; case 'video': case 'audio': - for (_ = 0; _ < en.length; _++) D(en[_], u); + for (u = 0; u < Zr.length; u++) D(Zr[u], a); break; case 'source': - D('error', u); + D('error', a); break; case 'img': case 'image': case 'link': - (D('error', u), D('load', u)); + (D('error', a), D('load', a)); break; case 'details': - D('toggle', u); + D('toggle', a); break; case 'input': - (Za(u, w), D('invalid', u)); + (Za(a, _), D('invalid', a)); break; case 'select': - ((u._wrapperState = { wasMultiple: !!w.multiple }), D('invalid', u)); + ((a._wrapperState = { wasMultiple: !!_.multiple }), D('invalid', a)); break; case 'textarea': - (hb(u, w), D('invalid', u)); + (hb(a, _), D('invalid', a)); } - 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); + for (var x in (ub(i, _), (u = null), _)) + if (_.hasOwnProperty(x)) { + var C = _[x]; + 'children' === x + ? 'string' == typeof C + ? a.textContent !== C && + (!0 !== _.suppressHydrationWarning && Af(a.textContent, C, s), + (u = ['children', C])) + : 'number' == typeof C && + a.textContent !== '' + C && + (!0 !== _.suppressHydrationWarning && Af(a.textContent, C, s), + (u = ['children', '' + C])) + : w.hasOwnProperty(x) && null != C && 'onScroll' === x && D('scroll', a); } switch (i) { case 'input': - (Va(u), db(u, w, !0)); + (Va(a), db(a, _, !0)); break; case 'textarea': - (Va(u), jb(u)); + (Va(a), jb(a)); break; case 'select': case 'option': break; default: - 'function' == typeof w.onClick && (u.onclick = Bf); + 'function' == typeof _.onClick && (a.onclick = Bf); } - ((u = _), (o.updateQueue = u), null !== u && (o.flags |= 4)); + ((a = u), (o.updateQueue = a), null !== a && (o.flags |= 4)); } else { - ((C = 9 === _.nodeType ? _ : _.ownerDocument), + ((x = 9 === u.nodeType ? u : u.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 = x.createElement('div')).innerHTML = ' @@ -763,6 +768,25 @@ /> + + +
+
+ + {$i18n.t('File Extensions')} + +
+ +
{/if} diff --git a/src/lib/components/admin/Settings/Models.svelte b/src/lib/components/admin/Settings/Models.svelte index 42828b6c53..cacb5b5e73 100644 --- a/src/lib/components/admin/Settings/Models.svelte +++ b/src/lib/components/admin/Settings/Models.svelte @@ -350,12 +350,12 @@ window.addEventListener('keydown', onKeyDown); window.addEventListener('keyup', onKeyUp); - window.addEventListener('blur-sm', onBlur); + window.addEventListener('blur', onBlur); return () => { window.removeEventListener('keydown', onKeyDown); window.removeEventListener('keyup', onKeyUp); - window.removeEventListener('blur-sm', onBlur); + window.removeEventListener('blur', onBlur); }; }); diff --git a/src/lib/components/admin/Settings/Models/ModelSettingsModal.svelte b/src/lib/components/admin/Settings/Models/ModelSettingsModal.svelte index 14b6e6d34c..1b6b0a3f31 100644 --- a/src/lib/components/admin/Settings/Models/ModelSettingsModal.svelte +++ b/src/lib/components/admin/Settings/Models/ModelSettingsModal.svelte @@ -234,7 +234,7 @@ tooltip={$i18n.t( 'Set the default models that are automatically selected for all users when a new chat is created.' )} - models={$models} + models={$models.filter((model) => !(model?.info?.meta?.hidden ?? false))} bind:modelIds={defaultModelIds} /> @@ -245,7 +245,7 @@ tooltip={$i18n.t( 'Set the models that are automatically pinned to the sidebar for all users.' )} - models={$models} + models={$models.filter((model) => !(model?.info?.meta?.hidden ?? false))} bind:modelIds={defaultPinnedModelIds} /> @@ -366,7 +366,7 @@ {#if showDefaultParams}
- +
{/if} diff --git a/src/lib/components/admin/Settings/WebSearch.svelte b/src/lib/components/admin/Settings/WebSearch.svelte index cac70fc55a..4f9929dc60 100644 --- a/src/lib/components/admin/Settings/WebSearch.svelte +++ b/src/lib/components/admin/Settings/WebSearch.svelte @@ -39,7 +39,8 @@ 'firecrawl', 'external', 'yandex', - 'youcom' + 'youcom', + 'linkup' ]; let webLoaderEngines = ['playwright', 'firecrawl', 'tavily', 'external']; @@ -78,8 +79,15 @@ webConfig.PLAYWRIGHT_TIMEOUT = webConfig.PLAYWRIGHT_TIMEOUT.toString(); } + // Convert Linkup params JSON string to object before sending + const linkupParams = + typeof webConfig.LINKUP_SEARCH_PARAMS === 'string' && + webConfig.LINKUP_SEARCH_PARAMS.trim() !== '' + ? JSON.parse(webConfig.LINKUP_SEARCH_PARAMS) + : (webConfig.LINKUP_SEARCH_PARAMS ?? {}); + const res = await updateRAGConfig(localStorage.token, { - web: webConfig + web: { ...webConfig, LINKUP_SEARCH_PARAMS: linkupParams } }); // Convert arrays back to strings for display @@ -123,6 +131,12 @@ webConfig.PLAYWRIGHT_TIMEOUT = parsed; } } + + // Convert Linkup params object to JSON string for textarea display + webConfig.LINKUP_SEARCH_PARAMS = + typeof webConfig.LINKUP_SEARCH_PARAMS === 'object' + ? JSON.stringify(webConfig.LINKUP_SEARCH_PARAMS ?? {}, null, 2) + : (webConfig.LINKUP_SEARCH_PARAMS ?? ''); } }); @@ -834,6 +848,30 @@ /> + {:else if webConfig.WEB_SEARCH_ENGINE === 'linkup'} +
+
+
+ {$i18n.t('Linkup API Key')} +
+ + +
+ +
+
+ {$i18n.t('Parameters')} +
+ +