mirror of
https://github.com/open-webui/open-webui.git
synced 2025-12-12 12:25:20 +00:00
Merge branch 'open-webui:main' into universal_file_deletion
This commit is contained in:
commit
f79a061113
189 changed files with 7810 additions and 3257 deletions
13
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
13
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
|
|
@ -11,7 +11,7 @@ body:
|
||||||
|
|
||||||
## Important Notes
|
## Important Notes
|
||||||
|
|
||||||
- **Before submitting a bug report**: Please check the [Issues](https://github.com/open-webui/open-webui/issues) or [Discussions](https://github.com/open-webui/open-webui/discussions) sections to see if a similar issue has already been reported. If unsure, start a discussion first, as this helps us efficiently focus on improving the project.
|
- **Before submitting a bug report**: Please check the [Issues](https://github.com/open-webui/open-webui/issues) and [Discussions](https://github.com/open-webui/open-webui/discussions) sections to see if a similar issue has already been reported. If unsure, start a discussion first, as this helps us efficiently focus on improving the project. Duplicates may be closed without notice. **Please search for existing issues and discussions.**
|
||||||
|
|
||||||
- **Respectful collaboration**: Open WebUI is a volunteer-driven project with a single maintainer and contributors who also have full-time jobs. Please be constructive and respectful in your communication.
|
- **Respectful collaboration**: Open WebUI is a volunteer-driven project with a single maintainer and contributors who also have full-time jobs. Please be constructive and respectful in your communication.
|
||||||
|
|
||||||
|
|
@ -25,7 +25,9 @@ body:
|
||||||
label: Check Existing Issues
|
label: Check Existing Issues
|
||||||
description: Confirm that you’ve checked for existing reports before submitting a new one.
|
description: Confirm that you’ve checked for existing reports before submitting a new one.
|
||||||
options:
|
options:
|
||||||
- label: I have searched the existing issues and discussions.
|
- label: I have searched for any existing and/or related issues.
|
||||||
|
required: true
|
||||||
|
- label: I have searched for any existing and/or related discussions.
|
||||||
required: true
|
required: true
|
||||||
- label: I am using the latest version of Open WebUI.
|
- label: I am using the latest version of Open WebUI.
|
||||||
required: true
|
required: true
|
||||||
|
|
@ -47,7 +49,7 @@ body:
|
||||||
id: open-webui-version
|
id: open-webui-version
|
||||||
attributes:
|
attributes:
|
||||||
label: Open WebUI Version
|
label: Open WebUI Version
|
||||||
description: Specify the version (e.g., v0.3.11)
|
description: Specify the version (e.g., v0.6.26)
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
|
|
||||||
|
|
@ -63,7 +65,7 @@ body:
|
||||||
id: operating-system
|
id: operating-system
|
||||||
attributes:
|
attributes:
|
||||||
label: Operating System
|
label: Operating System
|
||||||
description: Specify the OS (e.g., Windows 10, macOS Sonoma, Ubuntu 22.04)
|
description: Specify the OS (e.g., Windows 10, macOS Sonoma, Ubuntu 22.04, Debian 12)
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
|
|
||||||
|
|
@ -126,6 +128,7 @@ body:
|
||||||
description: |
|
description: |
|
||||||
Please provide a **very detailed, step-by-step guide** to reproduce the issue. Your instructions should be so clear and precise that anyone can follow them without guesswork. Include every relevant detail—settings, configuration options, exact commands used, values entered, and any prerequisites or environment variables.
|
Please provide a **very detailed, step-by-step guide** to reproduce the issue. Your instructions should be so clear and precise that anyone can follow them without guesswork. Include every relevant detail—settings, configuration options, exact commands used, values entered, and any prerequisites or environment variables.
|
||||||
**If full reproduction steps and all relevant settings are not provided, your issue may not be addressed.**
|
**If full reproduction steps and all relevant settings are not provided, your issue may not be addressed.**
|
||||||
|
**If your steps to reproduction are incomplete, lacking detail or not reproducible, your issue can not be addressed.**
|
||||||
|
|
||||||
placeholder: |
|
placeholder: |
|
||||||
Example (include every detail):
|
Example (include every detail):
|
||||||
|
|
@ -163,5 +166,5 @@ body:
|
||||||
attributes:
|
attributes:
|
||||||
value: |
|
value: |
|
||||||
## Note
|
## Note
|
||||||
If the bug report is incomplete or does not follow instructions, it may not be addressed. Ensure that you've followed all the **README.md** and **troubleshooting.md** guidelines, and provide all necessary information for us to reproduce the issue.
|
**If the bug report is incomplete, does not follow instructions or is lacking details it may not be addressed.** Ensure that you've followed all the **README.md** and **troubleshooting.md** guidelines, and provide all necessary information for us to reproduce the issue.
|
||||||
Thank you for contributing to Open WebUI!
|
Thank you for contributing to Open WebUI!
|
||||||
|
|
|
||||||
6
.github/dependabot.yml
vendored
6
.github/dependabot.yml
vendored
|
|
@ -12,12 +12,6 @@ updates:
|
||||||
interval: monthly
|
interval: monthly
|
||||||
target-branch: 'dev'
|
target-branch: 'dev'
|
||||||
|
|
||||||
- package-ecosystem: npm
|
|
||||||
directory: '/'
|
|
||||||
schedule:
|
|
||||||
interval: monthly
|
|
||||||
target-branch: 'dev'
|
|
||||||
|
|
||||||
- package-ecosystem: 'github-actions'
|
- package-ecosystem: 'github-actions'
|
||||||
directory: '/'
|
directory: '/'
|
||||||
schedule:
|
schedule:
|
||||||
|
|
|
||||||
2
.github/workflows/build-release.yml
vendored
2
.github/workflows/build-release.yml
vendored
|
|
@ -11,7 +11,7 @@ jobs:
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v5
|
||||||
|
|
||||||
- name: Check for changes in package.json
|
- name: Check for changes in package.json
|
||||||
run: |
|
run: |
|
||||||
|
|
|
||||||
2
.github/workflows/deploy-to-hf-spaces.yml
vendored
2
.github/workflows/deploy-to-hf-spaces.yml
vendored
|
|
@ -27,7 +27,7 @@ jobs:
|
||||||
HF_TOKEN: ${{ secrets.HF_TOKEN }}
|
HF_TOKEN: ${{ secrets.HF_TOKEN }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v5
|
||||||
with:
|
with:
|
||||||
lfs: true
|
lfs: true
|
||||||
|
|
||||||
|
|
|
||||||
174
.github/workflows/docker-build.yaml
vendored
174
.github/workflows/docker-build.yaml
vendored
|
|
@ -43,7 +43,7 @@ jobs:
|
||||||
echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV
|
echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV
|
||||||
|
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v5
|
||||||
|
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@v3
|
uses: docker/setup-qemu-action@v3
|
||||||
|
|
@ -142,7 +142,7 @@ jobs:
|
||||||
echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV
|
echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV
|
||||||
|
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v5
|
||||||
|
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@v3
|
uses: docker/setup-qemu-action@v3
|
||||||
|
|
@ -244,7 +244,7 @@ jobs:
|
||||||
echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV
|
echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV
|
||||||
|
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v5
|
||||||
|
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@v3
|
uses: docker/setup-qemu-action@v3
|
||||||
|
|
@ -347,7 +347,7 @@ jobs:
|
||||||
echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV
|
echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV
|
||||||
|
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v5
|
||||||
|
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@v3
|
uses: docker/setup-qemu-action@v3
|
||||||
|
|
@ -419,6 +419,108 @@ jobs:
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
retention-days: 1
|
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
|
||||||
|
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:
|
merge-main-images:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs: [build-main-image]
|
needs: [build-main-image]
|
||||||
|
|
@ -433,7 +535,7 @@ jobs:
|
||||||
IMAGE_NAME: '${{ github.repository }}'
|
IMAGE_NAME: '${{ github.repository }}'
|
||||||
|
|
||||||
- name: Download digests
|
- name: Download digests
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v5
|
||||||
with:
|
with:
|
||||||
pattern: digests-main-*
|
pattern: digests-main-*
|
||||||
path: /tmp/digests
|
path: /tmp/digests
|
||||||
|
|
@ -487,7 +589,7 @@ jobs:
|
||||||
IMAGE_NAME: '${{ github.repository }}'
|
IMAGE_NAME: '${{ github.repository }}'
|
||||||
|
|
||||||
- name: Download digests
|
- name: Download digests
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v5
|
||||||
with:
|
with:
|
||||||
pattern: digests-cuda-*
|
pattern: digests-cuda-*
|
||||||
path: /tmp/digests
|
path: /tmp/digests
|
||||||
|
|
@ -543,7 +645,7 @@ jobs:
|
||||||
IMAGE_NAME: '${{ github.repository }}'
|
IMAGE_NAME: '${{ github.repository }}'
|
||||||
|
|
||||||
- name: Download digests
|
- name: Download digests
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v5
|
||||||
with:
|
with:
|
||||||
pattern: digests-cuda126-*
|
pattern: digests-cuda126-*
|
||||||
path: /tmp/digests
|
path: /tmp/digests
|
||||||
|
|
@ -599,7 +701,7 @@ jobs:
|
||||||
IMAGE_NAME: '${{ github.repository }}'
|
IMAGE_NAME: '${{ github.repository }}'
|
||||||
|
|
||||||
- name: Download digests
|
- name: Download digests
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v5
|
||||||
with:
|
with:
|
||||||
pattern: digests-ollama-*
|
pattern: digests-ollama-*
|
||||||
path: /tmp/digests
|
path: /tmp/digests
|
||||||
|
|
@ -640,3 +742,59 @@ jobs:
|
||||||
- name: Inspect image
|
- name: Inspect image
|
||||||
run: |
|
run: |
|
||||||
docker buildx imagetools inspect ${{ env.FULL_IMAGE_NAME }}:${{ steps.meta.outputs.version }}
|
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 }}
|
||||||
|
|
|
||||||
2
.github/workflows/format-backend.yaml
vendored
2
.github/workflows/format-backend.yaml
vendored
|
|
@ -30,7 +30,7 @@ jobs:
|
||||||
- 3.12.x
|
- 3.12.x
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v5
|
||||||
|
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
uses: actions/setup-python@v5
|
uses: actions/setup-python@v5
|
||||||
|
|
|
||||||
4
.github/workflows/format-build-frontend.yaml
vendored
4
.github/workflows/format-build-frontend.yaml
vendored
|
|
@ -24,7 +24,7 @@ jobs:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout Repository
|
- name: Checkout Repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v5
|
||||||
|
|
||||||
- name: Setup Node.js
|
- name: Setup Node.js
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
|
|
@ -51,7 +51,7 @@ jobs:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout Repository
|
- name: Checkout Repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v5
|
||||||
|
|
||||||
- name: Setup Node.js
|
- name: Setup Node.js
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
|
|
|
||||||
2
.github/workflows/release-pypi.yml
vendored
2
.github/workflows/release-pypi.yml
vendored
|
|
@ -16,7 +16,7 @@ jobs:
|
||||||
id-token: write
|
id-token: write
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v5
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
- name: Install Git
|
- name: Install Git
|
||||||
|
|
|
||||||
96
CHANGELOG.md
96
CHANGELOG.md
|
|
@ -5,6 +5,102 @@ 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/),
|
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).
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
## [0.6.27] - 2025-09-09
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- 📁 Emoji folder icons were added, allowing users to personalize workspace organization with visual cues, including improved chevron display. [Commit](https://github.com/open-webui/open-webui/pull/17070/commits/1588f42fe777ad5d807e3f2fc8dbbc47a8db87c0), [Commit](https://github.com/open-webui/open-webui/pull/17070/commits/b70c0f36c0f5bbfc2a767429984d6fba1a7bb26c), [Commit](https://github.com/open-webui/open-webui/pull/17070/commits/11dea8795bfce42aa5d8d58ef316ded05173bd87), [Commit](https://github.com/open-webui/open-webui/pull/17070/commits/c0a47169fa059154d5f5a9ea6b94f9a66d82f255)
|
||||||
|
- 📁 The 'Search Collection' input field now dynamically displays the total number of files within the knowledge base. [Commit](https://github.com/open-webui/open-webui/pull/17070/commits/fbbe1117ae4c9c8fec6499d790eee275818eccc5)
|
||||||
|
- ☁️ A provider toggle in connection settings now allows users to manually specify Azure OpenAI deployments. [Commit](https://github.com/open-webui/open-webui/pull/17070/commits/5bdd334b74fbd154085f2d590f4afdba32469c8a)
|
||||||
|
- ⚡ Model list caching performance was optimized by fixing cache key generation to reduce redundant API calls. [#17158](https://github.com/open-webui/open-webui/pull/17158)
|
||||||
|
- 🎨 Azure OpenAI image generation is now supported, with configurations for IMAGES_OPENAI_API_VERSION via environment variable and admin UI. [#17147](https://github.com/open-webui/open-webui/pull/17147), [#16274](https://github.com/open-webui/open-webui/discussions/16274), [Docs:#679](https://github.com/open-webui/docs/pull/679)
|
||||||
|
- ⚡ Comprehensive N+1 query performance is optimized by reducing database queries from 1+N to 1+1 patterns across major listing endpoints. [#17165](https://github.com/open-webui/open-webui/pull/17165), [#17160](https://github.com/open-webui/open-webui/pull/17160), [#17161](https://github.com/open-webui/open-webui/pull/17161), [#17162](https://github.com/open-webui/open-webui/pull/17162), [#17159](https://github.com/open-webui/open-webui/pull/17159), [#17166](https://github.com/open-webui/open-webui/pull/17166)
|
||||||
|
- ⚡ The PDF.js library is now dynamically loaded, significantly reducing initial page load size and improving responsiveness. [#17222](https://github.com/open-webui/open-webui/pull/17222)
|
||||||
|
- ⚡ The heic2any library is now dynamically loaded across various message input components, including channels, for faster page loads. [#17225](https://github.com/open-webui/open-webui/pull/17225), [#17229](https://github.com/open-webui/open-webui/pull/17229)
|
||||||
|
- 📚 The knowledge API now supports a "delete_file" query parameter, allowing configurable file deletion behavior. [Commit](https://github.com/open-webui/open-webui/pull/17070/commits/22c4ef4fb096498066b73befe993ae3a82f7a8e7)
|
||||||
|
- 📊 Llama.cpp timing statistics are now integrated into the usage field for comprehensive model performance metrics. [Commit](https://github.com/open-webui/open-webui/pull/17070/commits/e830b4959ecd4b2795e29e53026984a58a7696a9)
|
||||||
|
- 🗄️ The PGVECTOR_CREATE_EXTENSION environment variable now allows control over automatic pgvector extension creation. [Commit](https://github.com/open-webui/open-webui/pull/17070/commits/c2b4976c82d335ed524bd80dc914b5e2f5bfbd9e), [Commit](https://github.com/open-webui/open-webui/pull/17070/commits/b45219c8b15b48d5ee3d42983e1107bbcefbab01), [Docs:#672](https://github.com/open-webui/docs/pull/672)
|
||||||
|
- 🔒 Comprehensive server-side OAuth token management was implemented, securely storing encrypted tokens in a new database table and introducing an automatic refresh mechanism, enabling seamless and secure forwarding of valid user-specific OAuth tokens to downstream services, including OpenAI-compatible endpoints and external tool servers via the new "system_oauth" authentication type, resolving long-standing issues such as large token size limitations, stale/expired tokens, and reliable token propagation, and enhancing overall security by minimizing client-side token exposure, configurable via "ENABLE_OAUTH_ID_TOKEN_COOKIE" and "OAUTH_SESSION_TOKEN_ENCRYPTION_KEY" environment variables. [Docs:#683](https://github.com/open-webui/docs/pull/683), [#17210](https://github.com/open-webui/open-webui/pull/17210), [#8957](https://github.com/open-webui/open-webui/discussions/8957), [#11029](https://github.com/open-webui/open-webui/discussions/11029), [#17178](https://github.com/open-webui/open-webui/issues/17178), [#17183](https://github.com/open-webui/open-webui/issues/17183), [Commit](https://github.com/open-webui/open-webui/commit/217f4daef09b36d3d4cc4681e11d3ebd9984a1a5), [Commit](https://github.com/open-webui/open-webui/commit/fc11e4384fe98fac659e10596f67c23483578867), [Commit](https://github.com/open-webui/open-webui/commit/f11bdc6ab5dd5682bb3e27166e77581f5b8af3e0), [Commit](https://github.com/open-webui/open-webui/commit/f71834720e623761d972d4d740e9bbd90a3a86c6), [Commit](https://github.com/open-webui/open-webui/commit/b5bb6ae177dcdc4e8274d7e5ffa50bc8099fd466), [Commit](https://github.com/open-webui/open-webui/commit/b786d1e3f3308ef4f0f95d7130ddbcaaca4fc927), [Commit](https://github.com/open-webui/open-webui/commit/8a9f8627017bd0a74cbd647891552b26e56aabb7), [Commit](https://github.com/open-webui/open-webui/commit/30d1dc2c60e303756120fe1c5538968c4e6139f4), [Commit](https://github.com/open-webui/open-webui/commit/2b2d123531eb3f42c0e940593832a64e2806240d), [Commit](https://github.com/open-webui/open-webui/commit/6f6412dd16c63c2bb4df79a96b814bf69cb3f880)
|
||||||
|
- 🔒 Conditional Permission Hardening for OpenShift Deployments: Added a build argument to enable optional permission hardening for OpenShift and container environments. [Commit](https://github.com/open-webui/open-webui/pull/17070/commits/0ebe4f8f8490451ac8e85a4846f010854d9b54e5)
|
||||||
|
- 👥 Regex pattern support is added for OAuth blocked groups, allowing more flexible group filtering rules. [Commit](https://github.com/open-webui/open-webui/pull/17070/commits/df66e21472646648d008ebb22b0e8d5424d491df)
|
||||||
|
- 💬 Web search result display was enhanced to include titles and favicons, providing a clearer overview of search sources. [Commit](https://github.com/open-webui/open-webui/pull/17070/commits/33f04a771455e3fabf8f0e8ebb994ae7f41b8ed4), [Commit](https://github.com/open-webui/open-webui/pull/17070/commits/0a85dd4bca23022729eafdbc82c8c139fa365af2), [Commit](https://github.com/open-webui/open-webui/pull/17070/commits/16090bc2721fde492afa2c4af5927e2b668527e1), [#17197](https://github.com/open-webui/open-webui/pull/17197), [#14179](https://github.com/open-webui/open-webui/issues/14179), [Commit](https://github.com/open-webui/open-webui/pull/17070/commits/1cdb7aed1ee9bf81f2fd0404be52dcfa64f8ed4f), [Commit](https://github.com/open-webui/open-webui/pull/17070/commits/f2525ebc447c008cf7269ef20ce04fa456f302c4), [Commit](https://github.com/open-webui/open-webui/pull/17070/commits/7f523de408ede4075349d8de71ae0214b7e1a62e), [Commit](https://github.com/open-webui/open-webui/pull/17070/commits/3d37e4a42d344051ae715ab59bd7b5718e46c343), [Commit](https://github.com/open-webui/open-webui/pull/17070/commits/cd5e2be27b613314aadda6107089331783987985), [Commit](https://github.com/open-webui/open-webui/pull/17070/commits/6dc0df247347aede2762fe2065cf30275fd137ae)
|
||||||
|
- 💬 A new setting was added to control whether clicking a suggested prompt automatically sends the message or only inserts the text. [#17192](https://github.com/open-webui/open-webui/issues/17192), [Commit](https://github.com/open-webui/open-webui/commit/e023a98f11fc52feb21e4065ec707cc98e50c7d3)
|
||||||
|
- 🔄 Various improvements were implemented across the frontend and backend to enhance performance, stability, and security.
|
||||||
|
- 🌐 Translations for Portuguese (Brazil), Simplified Chinese, Catalan, and Spanish were enhanced and expanded.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- 🔍 Hybrid search functionality now correctly handles lexical-semantic weight labels and avoids errors when BM25 weight is zero. [#17049](https://github.com/open-webui/open-webui/pull/17049), [#17046](https://github.com/open-webui/open-webui/issues/17046)
|
||||||
|
- 🛑 Task stopping errors are prevented by gracefully handling multiple stop requests for the same task. [#17195](https://github.com/open-webui/open-webui/pull/17195)
|
||||||
|
- 🐍 Code execution package detection precision is improved in Pyodide to prevent unnecessary package inclusions. [Commit](https://github.com/open-webui/open-webui/pull/17070/commits/bbe116795860a81a647d9567e0d9cb1950650095)
|
||||||
|
- 🛠️ Tool message format API compliance is fixed by ensuring content fields in tool call responses contain valid string values instead of null. [Commit](https://github.com/open-webui/open-webui/pull/17070/commits/37bf0087e5b8a324009c9d06b304027df351ea6b)
|
||||||
|
- 📱 Mobile app config API authentication now supports Authorization header token verification with cookie fallback for iOS and Android requests. [#17175](https://github.com/open-webui/open-webui/pull/17175)
|
||||||
|
- 💾 Knowledge file save race conditions are prevented by serializing API calls and adding an "isSaving" guard. [#17137](https://github.com/open-webui/open-webui/pull/17137), [Commit](https://github.com/open-webui/open-webui/pull/17070/commits/4ca936f0bf9813bee11ec8aea41d7e34fb6b16a9)
|
||||||
|
- 🔐 The SSO login button visibility is restored for OIDC PKCE authentication without a client secret. [#17012](https://github.com/open-webui/open-webui/pull/17012)
|
||||||
|
- 🔊 Text-to-Speech (TTS) API requests now use proper URL joining methods, ensuring reliable functionality regardless of trailing slashes in the base URL. [#17061](https://github.com/open-webui/open-webui/pull/17061)
|
||||||
|
- 🛡️ Admin account creation on Hugging Face Spaces now correctly detects the configured port, resolving issues with custom port deployments. [#17064](https://github.com/open-webui/open-webui/pull/17064)
|
||||||
|
- 📁 Unicode filename support is improved for external document loaders by properly URL-encoding filenames in HTTP headers. [#17013](https://github.com/open-webui/open-webui/pull/17013), [#17000](https://github.com/open-webui/open-webui/issues/17000)
|
||||||
|
- 🔗 Web page and YouTube attachments are now correctly processed by setting their type as "text" and using collection names for accurate content retrieval. [Commit](https://github.com/open-webui/open-webui/pull/17070/commits/487979859a6ffcfd60468f523822cdf838fbef5b)
|
||||||
|
- ✍️ Message input composition event handling is fixed to properly manage text input for multilingual users using Input Method Editors (IME). [#17085](https://github.com/open-webui/open-webui/pull/17085)
|
||||||
|
- 💬 Follow-up tooltip duplication is removed, streamlining the user interface and preventing visual clutter. [#17186](https://github.com/open-webui/open-webui/pull/17186)
|
||||||
|
- 🎨 Chat button text display is corrected by preventing clipping of descending characters and removing unnecessary capitalization. [#17191](https://github.com/open-webui/open-webui/pull/17191)
|
||||||
|
- 🧠 RAG Loop/Error with Gemma 3.1 2B Instruct is fixed by correctly unwrapping unexpected single-item list responses from models. [Commit](https://github.com/open-webui/open-webui/pull/17070/commits/1bc9711afd2b72cd07c4e539a83783868733767c), [#17213](https://github.com/open-webui/open-webui/issues/17213)
|
||||||
|
- 🖼️ HEIC conversion failures are resolved, improving robustness of image handling. [#17225](https://github.com/open-webui/open-webui/pull/17225)
|
||||||
|
- 📦 The slim Docker image size regression has been fixed by refining the build process to correctly exclude components when USE_SLIM=true. [#16997](https://github.com/open-webui/open-webui/issues/16997), [Commit](https://github.com/open-webui/open-webui/commit/be373e9fd42ac73b0302bdb487e16dbeae178b4e), [Commit](https://github.com/open-webui/open-webui/commit/0ebe4f8f8490451ac8e85a4846f010854d9b54e5)
|
||||||
|
- 📁 Knowledge base update validation errors are resolved, ensuring seamless management via UI or API. [#17244](https://github.com/open-webui/open-webui/issues/17244), [Commit](https://github.com/open-webui/open-webui/commit/9aac1489080a5c9441e89b1a56de0d3a672bc5fb)
|
||||||
|
- 🔐 Resolved a security issue where a global web search setting overrode model-specific restrictions, ensuring model-level settings are now correctly prioritized. [#17151](https://github.com/open-webui/open-webui/issues/17151), [Commit](https://github.com/open-webui/open-webui/commit/9368d0ac751ec3072d5a96712b80a9b20a642ce6)
|
||||||
|
- 🔐 OAuth redirect reliability is improved by robustly preserving the intended redirect path using session storage. [#17235](https://github.com/open-webui/open-webui/issues/17235), [Commit](https://github.com/open-webui/open-webui/pull/17070/commits/4f2b821088367da18374027919594365c7a3f459), [#15575](https://github.com/open-webui/open-webui/pull/15575), [Commit](https://github.com/open-webui/open-webui/pull/17070/commits/d9f97c832c556fae4b116759da0177bf4fe619de)
|
||||||
|
- 🔐 Fixed a security vulnerability where knowledge base access within chat folders persisted after permissions were revoked. [#17182](https://github.com/open-webui/open-webui/issues/17182), [Commit](https://github.com/open-webui/open-webui/commit/40e40d1dddf9ca937e99af41c8ca038dbc93a7e6)
|
||||||
|
- 🔒 OIDC access denied errors are now displayed as user-friendly toast notifications instead of raw JSON. [#17208](https://github.com/open-webui/open-webui/issues/17208), [Commit](https://github.com/open-webui/open-webui/commit/3d6d050ad82d360adc42d6e9f42e8faf8d13c9f4)
|
||||||
|
- 💬 Chat exception handling is enhanced to prevent system instability during message generation and ensure graceful error recovery. [Commit](https://github.com/open-webui/open-webui/pull/17070/commits/f56889c5c7f0cf1a501c05d35dfa614e4f8b6958)
|
||||||
|
- 🔒 Static asset authentication is improved by adding crossorigin="use-credentials" attributes to all link elements, enabling proper cookie forwarding for proxy environments and authenticated requests to favicon, manifest, and stylesheet resources. [#17280](https://github.com/open-webui/open-webui/pull/17280), [Commit](https://github.com/open-webui/open-webui/commit/f17d8b5d19e1a05df7d63f53e939c99772a59c1e)
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- 🛠️ Renamed "Tools" to "External Tools" across the UI for clearer distinction between built-in and external functionalities. [Commit](https://github.com/open-webui/open-webui/pull/17070/commits/0bca4e230ef276bec468889e3be036242ad11086f)
|
||||||
|
- 🛡️ Default permission validation for message regeneration and deletion actions is enhanced to provide more restrictive access controls, improving chat security and user data protection. [#17285](https://github.com/open-webui/open-webui/pull/17285)
|
||||||
|
|
||||||
|
## [0.6.26] - 2025-08-28
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- 🛂 **Granular Chat Interaction Permissions**: Added fine-grained permission controls for individual chat actions including "Continue Response", "Regenerate Response", "Rate Response", and "Delete Messages". Administrators can now configure these permissions per user group or set system defaults via environment variables, providing enhanced security and governance by preventing potential system prompt leakage through response continuation and enabling precise control over user interactions with AI responses.
|
||||||
|
- 🧠 **Custom Reasoning Tags Configuration**: Added configurable reasoning tag detection for AI model responses, allowing administrators and users to customize how the system identifies and processes reasoning content. Users can now define custom reasoning tag pairs, use default tags like "think" and "reasoning", or disable reasoning detection entirely through the Advanced Parameters interface, providing enhanced control over AI thought process visibility.
|
||||||
|
- 📱 **Pull-to-Refresh Support**: Added pull-to-refresh functionality allowing user to easily refresh the interface by pulling down on the navbar area. This resolves timeout issues that occurred when temporarily switching away from the app during long AI response generations, eliminating the need to close and relaunch the PWA.
|
||||||
|
- 📁 **Configurable File Upload Processing Mode**: Added "process_in_background" query parameter to the file upload API endpoint, allowing clients to choose between asynchronous (default) and synchronous file processing. Setting "process_in_background=false" forces the upload request to wait until extraction and embedding complete, returning immediately usable files and simplifying integration for backend API consumers that prefer blocking calls over polling workflows.
|
||||||
|
- 🔐 **Azure Document Intelligence DefaultAzureCredential Support**: Added support for authenticating with Azure Document Intelligence using DefaultAzureCredential in addition to API key authentication, enabling seamless integration with Azure Entra ID and managed identity authentication for enterprise Azure environments.
|
||||||
|
- 🔐 **Authentication Bootstrapping Enhancements**: Added "ENABLE_INITIAL_ADMIN_SIGNUP" environment variable and "?form=true" URL parameter to enable initial admin user creation and forced login form display in SSO-only deployments. This resolves bootstrap issues where administrators couldn't create the first user when login forms were disabled, allowing proper initialization of SSO-configured deployments without requiring temporary configuration changes.
|
||||||
|
- ⚡ **Query Generation Caching**: Added "ENABLE_QUERIES_CACHE" environment variable to enable request-scoped caching of generated search queries. When both web search and file retrieval are active, queries generated for web search are automatically reused for file retrieval, eliminating duplicate LLM API calls and reducing token usage and costs while maintaining search quality.
|
||||||
|
- 🔧 **Configurable Tool Call Retry Limit**: Added "CHAT_RESPONSE_MAX_TOOL_CALL_RETRIES" environment variable to control the maximum number of sequential tool calls allowed before safety stopping a chat session. This replaces the previous hardcoded limit of 10, enabling administrators to configure higher limits for complex workflows requiring extensive tool interactions.
|
||||||
|
- 📦 **Slim Docker Image Variant**: Added new slim Docker image option via "USE_SLIM" build argument that excludes embedded AI models and Ollama installation, reducing image size by approximately 1GB. This variant enables faster image pulls and deployments for environments where AI models are managed externally, particularly beneficial for auto-scaling clusters and distributed deployments.
|
||||||
|
- 🗂️ **Shift-to-Delete Functionality for Workspace Prompts**: Added keyboard shortcut support for quick prompt deletion on the Workspace Prompts page. Hold Shift and hover over any prompt to reveal a trash icon for instant deletion, bringing consistent interaction patterns across all workspace sections (Models, Tools, Functions, and now Prompts) and streamlining prompt management workflows.
|
||||||
|
- ♿ **Accessibility Enhancements**: Enhanced user interface accessibility with improved keyboard navigation, ARIA labels, and screen reader compatibility across key platform components.
|
||||||
|
- 📄 **Optimized PDF Export for Smaller File Size**: PDF exports are now significantly optimized, producing much smaller files for faster downloads and easier sharing or archiving of your chats and documents.
|
||||||
|
- 📦 **Slimmed Default Install with Optional Full Dependencies**: Installing Open WebUI via pip now defaults to a slimmer package; PostgreSQL support is no longer included by default—simply use 'pip install open-webui[all]' to include all optional dependencies for full feature compatibility.
|
||||||
|
- 🔄 **General Backend Refactoring**: Implemented various backend improvements to enhance performance, stability, and security, ensuring a more resilient and reliable platform for all users.
|
||||||
|
- 🌐 **Localization & Internationalization Improvements**: Enhanced and expanded translations for Finnish, Spanish, Japanese, Polish, Portuguese (Brazil), and Chinese, including missing translations and typo corrections, providing a more natural and professional user experience for speakers of these languages across the entire interface.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- ⚠️ **Chat Error Feedback Restored**: Fixed an issue where various backend errors (tool server failures, API connection issues, malformed responses) would cause chats to load indefinitely without providing user feedback. The system now properly displays error messages when failures occur during chat generation, allowing users to understand issues and retry as needed instead of waiting indefinitely.
|
||||||
|
- 🖼️ **Image Generation Steps Setting Visibility Fixed**: Fixed a UI issue where the "Set Steps" configuration option was incorrectly displayed for OpenAI and Gemini image generation engines that don't support this parameter. The setting is now only visible for compatible engines like ComfyUI and Automatic1111, eliminating user confusion about non-functional configuration options.
|
||||||
|
- 📄 **Datalab Marker API Document Loader Fixed**: Fixed broken Datalab Marker API document loader functionality by correcting URL path handling for both hosted Datalab services and self-hosted Marker servers. Removed hardcoded "/marker" paths from the loader code and restored proper default URL structure, ensuring PDF and document processing works correctly with both deployment types.
|
||||||
|
- 📋 **Citation Error Handling Improved**: Fixed an issue where malformed citation or source objects from external tools, pipes, or filters would cause JavaScript errors and make the chat interface completely unresponsive. The system now gracefully handles missing or undefined citation properties, allowing conversations to load properly even when tools generate defective citation events.
|
||||||
|
- 👥 **Group User Add API Endpoint Fixed**: Fixed an issue where the "/api/v1/groups/id/{group_id}/users/add" API endpoint would accept requests without errors but fail to actually add users to groups. The system now properly initializes and deduplicates user ID lists, ensuring users are correctly added to and removed from groups via API calls.
|
||||||
|
- 🛠️ **External Tool Server Error Handling Improved**: Fixed an issue where unreachable or misconfigured external tool servers would cause JavaScript errors and prevent the interface from loading properly. The system now gracefully handles connection failures, displays appropriate error messages, and filters out inaccessible servers while maintaining full functionality for working connections.
|
||||||
|
- 📋 **Code Block Copy Button Content Fixed**: Fixed an issue where the copy button in code blocks would copy the original AI-generated code instead of any user-edited content, ensuring the copy function always captures the currently displayed code as modified by users.
|
||||||
|
- 📄 **PDF Export Content Mismatch Fixed**: Resolved an issue where exporting a PDF of one chat while viewing another chat would incorrectly generate the PDF using the currently viewed chat's content instead of the intended chat's content. Additionally optimized the PDF generation algorithm with improved canvas slicing, better memory management, and enhanced image quality, while removing the problematic PDF export option from individual chat menus to prevent further confusion.
|
||||||
|
- 🖱️ **Windows Sidebar Cursor Icon Corrected**: Fixed confusing cursor icons on Windows systems where sidebar toggle buttons displayed resize cursors (ew-resize) instead of appropriate pointer cursors. The sidebar buttons now show standard pointer cursors on Windows, eliminating user confusion about whether the buttons expand/collapse the sidebar or resize it.
|
||||||
|
- 📝 **Safari IME Composition Bug Fixed**: Resolved an issue where pressing Enter while composing Chinese text using Input Method Editors (IMEs) on Safari would prematurely send messages instead of completing text composition. The system now properly detects composition states and ignores keydown events that occur immediately after composition ends, ensuring smooth multilingual text input across all browsers.
|
||||||
|
- 🔍 **Hybrid Search Parameter Handling Fixed**: Fixed an issue where the "hybrid" parameter in collection query requests was not being properly evaluated, causing the system to ignore user-specified hybrid search preferences and only check global configuration. Additionally resolved a division by zero error that occurred in hybrid search when BM25Retriever was called with empty document lists, ensuring robust search functionality across all collection states.
|
||||||
|
- 💬 **RTL Text Orientation in Messages Fixed**: Fixed text alignment issues in user messages and AI responses for Right-to-Left languages, ensuring proper text direction based on user language settings. Code blocks now consistently use Left-to-Right orientation regardless of the user's language preference, maintaining code readability across all supported languages.
|
||||||
|
- 📁 **File Content Preview in Modal Restored**: Fixed an issue where clicking on uploaded files would display an empty preview modal, even when the files were successfully processed and available for AI context. File content now displays correctly in the preview modal, ensuring users can verify and review their uploaded documents as intended.
|
||||||
|
- 🌐 **Playwright Timeout Configuration Corrected**: Fixed an issue where Playwright timeout values were incorrectly converted from milliseconds to seconds with an additional 1000x multiplier, causing excessively long web loading timeouts. The timeout parameter now correctly uses the configured millisecond values as intended, ensuring responsive web search and document loading operations.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- 🔄 **Follow-Up Question Language Constraint Removed**: Follow-up question suggestions no longer strictly adhere to the chat's primary language setting, allowing for more flexible and diverse suggestion generation that may include questions in different languages based on conversation context and relevance rather than enforced language matching.
|
||||||
|
|
||||||
## [0.6.25] - 2025-08-22
|
## [0.6.25] - 2025-08-22
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
|
||||||
19
Dockerfile
19
Dockerfile
|
|
@ -3,6 +3,8 @@
|
||||||
# use build args in the docker build command with --build-arg="BUILDARG=true"
|
# use build args in the docker build command with --build-arg="BUILDARG=true"
|
||||||
ARG USE_CUDA=false
|
ARG USE_CUDA=false
|
||||||
ARG USE_OLLAMA=false
|
ARG USE_OLLAMA=false
|
||||||
|
ARG USE_SLIM=false
|
||||||
|
ARG USE_PERMISSION_HARDENING=false
|
||||||
# Tested with cu117 for CUDA 11 and cu121 for CUDA 12 (default)
|
# Tested with cu117 for CUDA 11 and cu121 for CUDA 12 (default)
|
||||||
ARG USE_CUDA_VER=cu128
|
ARG USE_CUDA_VER=cu128
|
||||||
# any sentence transformer model; models to use can be found at https://huggingface.co/models?library=sentence-transformers
|
# any sentence transformer model; models to use can be found at https://huggingface.co/models?library=sentence-transformers
|
||||||
|
|
@ -24,6 +26,9 @@ ARG GID=0
|
||||||
FROM --platform=$BUILDPLATFORM node:22-alpine3.20 AS build
|
FROM --platform=$BUILDPLATFORM node:22-alpine3.20 AS build
|
||||||
ARG BUILD_HASH
|
ARG BUILD_HASH
|
||||||
|
|
||||||
|
# Set Node.js options (heap limit Allocation failed - JavaScript heap out of memory)
|
||||||
|
# ENV NODE_OPTIONS="--max-old-space-size=4096"
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
# to store git revision in build
|
# to store git revision in build
|
||||||
|
|
@ -43,6 +48,8 @@ FROM python:3.11-slim-bookworm AS base
|
||||||
ARG USE_CUDA
|
ARG USE_CUDA
|
||||||
ARG USE_OLLAMA
|
ARG USE_OLLAMA
|
||||||
ARG USE_CUDA_VER
|
ARG USE_CUDA_VER
|
||||||
|
ARG USE_SLIM
|
||||||
|
ARG USE_PERMISSION_HARDENING
|
||||||
ARG USE_EMBEDDING_MODEL
|
ARG USE_EMBEDDING_MODEL
|
||||||
ARG USE_RERANKING_MODEL
|
ARG USE_RERANKING_MODEL
|
||||||
ARG UID
|
ARG UID
|
||||||
|
|
@ -54,6 +61,7 @@ ENV ENV=prod \
|
||||||
# pass build args to the build
|
# pass build args to the build
|
||||||
USE_OLLAMA_DOCKER=${USE_OLLAMA} \
|
USE_OLLAMA_DOCKER=${USE_OLLAMA} \
|
||||||
USE_CUDA_DOCKER=${USE_CUDA} \
|
USE_CUDA_DOCKER=${USE_CUDA} \
|
||||||
|
USE_SLIM_DOCKER=${USE_SLIM} \
|
||||||
USE_CUDA_DOCKER_VER=${USE_CUDA_VER} \
|
USE_CUDA_DOCKER_VER=${USE_CUDA_VER} \
|
||||||
USE_EMBEDDING_MODEL_DOCKER=${USE_EMBEDDING_MODEL} \
|
USE_EMBEDDING_MODEL_DOCKER=${USE_EMBEDDING_MODEL} \
|
||||||
USE_RERANKING_MODEL_DOCKER=${USE_RERANKING_MODEL}
|
USE_RERANKING_MODEL_DOCKER=${USE_RERANKING_MODEL}
|
||||||
|
|
@ -130,11 +138,14 @@ RUN pip3 install --no-cache-dir uv && \
|
||||||
else \
|
else \
|
||||||
pip3 install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cpu --no-cache-dir && \
|
pip3 install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cpu --no-cache-dir && \
|
||||||
uv pip install --system -r requirements.txt --no-cache-dir && \
|
uv pip install --system -r requirements.txt --no-cache-dir && \
|
||||||
|
if [ "$USE_SLIM" != "true" ]; then \
|
||||||
python -c "import os; from sentence_transformers import SentenceTransformer; SentenceTransformer(os.environ['RAG_EMBEDDING_MODEL'], device='cpu')" && \
|
python -c "import os; from sentence_transformers import SentenceTransformer; SentenceTransformer(os.environ['RAG_EMBEDDING_MODEL'], device='cpu')" && \
|
||||||
python -c "import os; from faster_whisper import WhisperModel; WhisperModel(os.environ['WHISPER_MODEL'], device='cpu', compute_type='int8', download_root=os.environ['WHISPER_MODEL_DIR'])"; \
|
python -c "import os; from faster_whisper import WhisperModel; WhisperModel(os.environ['WHISPER_MODEL'], device='cpu', compute_type='int8', download_root=os.environ['WHISPER_MODEL_DIR'])"; \
|
||||||
python -c "import os; import tiktoken; tiktoken.get_encoding(os.environ['TIKTOKEN_ENCODING_NAME'])"; \
|
python -c "import os; import tiktoken; tiktoken.get_encoding(os.environ['TIKTOKEN_ENCODING_NAME'])"; \
|
||||||
fi; \
|
fi; \
|
||||||
chown -R $UID:$GID /app/backend/data/
|
fi; \
|
||||||
|
mkdir -p /app/backend/data && chown -R $UID:$GID /app/backend/data/ && \
|
||||||
|
rm -rf /var/lib/apt/lists/*;
|
||||||
|
|
||||||
# Install Ollama if requested
|
# Install Ollama if requested
|
||||||
RUN if [ "$USE_OLLAMA" = "true" ]; then \
|
RUN if [ "$USE_OLLAMA" = "true" ]; then \
|
||||||
|
|
@ -163,11 +174,13 @@ HEALTHCHECK CMD curl --silent --fail http://localhost:${PORT:-8080}/health | jq
|
||||||
# Minimal, atomic permission hardening for OpenShift (arbitrary UID):
|
# Minimal, atomic permission hardening for OpenShift (arbitrary UID):
|
||||||
# - Group 0 owns /app and /root
|
# - Group 0 owns /app and /root
|
||||||
# - Directories are group-writable and have SGID so new files inherit GID 0
|
# - Directories are group-writable and have SGID so new files inherit GID 0
|
||||||
RUN set -eux; \
|
RUN if [ "$USE_PERMISSION_HARDENING" = "true" ]; then \
|
||||||
|
set -eux; \
|
||||||
chgrp -R 0 /app /root || true; \
|
chgrp -R 0 /app /root || true; \
|
||||||
chmod -R g+rwX /app /root || true; \
|
chmod -R g+rwX /app /root || true; \
|
||||||
find /app -type d -exec chmod g+s {} + || true; \
|
find /app -type d -exec chmod g+s {} + || true; \
|
||||||
find /root -type d -exec chmod g+s {} + || true
|
find /root -type d -exec chmod g+s {} + || true; \
|
||||||
|
fi
|
||||||
|
|
||||||
USER $UID:$GID
|
USER $UID:$GID
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -313,7 +313,7 @@ JWT_EXPIRES_IN = PersistentConfig(
|
||||||
####################################
|
####################################
|
||||||
|
|
||||||
ENABLE_OAUTH_PERSISTENT_CONFIG = (
|
ENABLE_OAUTH_PERSISTENT_CONFIG = (
|
||||||
os.environ.get("ENABLE_OAUTH_PERSISTENT_CONFIG", "True").lower() == "true"
|
os.environ.get("ENABLE_OAUTH_PERSISTENT_CONFIG", "False").lower() == "true"
|
||||||
)
|
)
|
||||||
|
|
||||||
ENABLE_OAUTH_SIGNUP = PersistentConfig(
|
ENABLE_OAUTH_SIGNUP = PersistentConfig(
|
||||||
|
|
@ -660,7 +660,7 @@ def load_oauth_providers():
|
||||||
|
|
||||||
if (
|
if (
|
||||||
OAUTH_CLIENT_ID.value
|
OAUTH_CLIENT_ID.value
|
||||||
and OAUTH_CLIENT_SECRET.value
|
and (OAUTH_CLIENT_SECRET.value or OAUTH_CODE_CHALLENGE_METHOD.value)
|
||||||
and OPENID_PROVIDER_URL.value
|
and OPENID_PROVIDER_URL.value
|
||||||
):
|
):
|
||||||
|
|
||||||
|
|
@ -1208,6 +1208,23 @@ USER_PERMISSIONS_CHAT_DELETE = (
|
||||||
os.environ.get("USER_PERMISSIONS_CHAT_DELETE", "True").lower() == "true"
|
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 = (
|
USER_PERMISSIONS_CHAT_EDIT = (
|
||||||
os.environ.get("USER_PERMISSIONS_CHAT_EDIT", "True").lower() == "true"
|
os.environ.get("USER_PERMISSIONS_CHAT_EDIT", "True").lower() == "true"
|
||||||
)
|
)
|
||||||
|
|
@ -1290,6 +1307,10 @@ DEFAULT_USER_PERMISSIONS = {
|
||||||
"params": USER_PERMISSIONS_CHAT_PARAMS,
|
"params": USER_PERMISSIONS_CHAT_PARAMS,
|
||||||
"file_upload": USER_PERMISSIONS_CHAT_FILE_UPLOAD,
|
"file_upload": USER_PERMISSIONS_CHAT_FILE_UPLOAD,
|
||||||
"delete": USER_PERMISSIONS_CHAT_DELETE,
|
"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,
|
"edit": USER_PERMISSIONS_CHAT_EDIT,
|
||||||
"share": USER_PERMISSIONS_CHAT_SHARE,
|
"share": USER_PERMISSIONS_CHAT_SHARE,
|
||||||
"export": USER_PERMISSIONS_CHAT_EXPORT,
|
"export": USER_PERMISSIONS_CHAT_EXPORT,
|
||||||
|
|
@ -1576,7 +1597,7 @@ FOLLOW_UP_GENERATION_PROMPT_TEMPLATE = PersistentConfig(
|
||||||
)
|
)
|
||||||
|
|
||||||
DEFAULT_FOLLOW_UP_GENERATION_PROMPT_TEMPLATE = """### Task:
|
DEFAULT_FOLLOW_UP_GENERATION_PROMPT_TEMPLATE = """### Task:
|
||||||
Suggest 3-5 relevant follow-up questions or prompts in the chat's primary language 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.
|
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:
|
### Guidelines:
|
||||||
- Write all follow-up questions from the user’s point of view, directed to the assistant.
|
- 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).
|
- Make questions concise, clear, and directly related to the discussed topic(s).
|
||||||
|
|
@ -1977,6 +1998,9 @@ PGVECTOR_INITIALIZE_MAX_VECTOR_LENGTH = int(
|
||||||
os.environ.get("PGVECTOR_INITIALIZE_MAX_VECTOR_LENGTH", "1536")
|
os.environ.get("PGVECTOR_INITIALIZE_MAX_VECTOR_LENGTH", "1536")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
PGVECTOR_CREATE_EXTENSION = (
|
||||||
|
os.getenv("PGVECTOR_CREATE_EXTENSION", "true").lower() == "true"
|
||||||
|
)
|
||||||
PGVECTOR_PGCRYPTO = os.getenv("PGVECTOR_PGCRYPTO", "false").lower() == "true"
|
PGVECTOR_PGCRYPTO = os.getenv("PGVECTOR_PGCRYPTO", "false").lower() == "true"
|
||||||
PGVECTOR_PGCRYPTO_KEY = os.getenv("PGVECTOR_PGCRYPTO_KEY", None)
|
PGVECTOR_PGCRYPTO_KEY = os.getenv("PGVECTOR_PGCRYPTO_KEY", None)
|
||||||
if PGVECTOR_PGCRYPTO and not PGVECTOR_PGCRYPTO_KEY:
|
if PGVECTOR_PGCRYPTO and not PGVECTOR_PGCRYPTO_KEY:
|
||||||
|
|
@ -2208,6 +2232,18 @@ DOCLING_SERVER_URL = PersistentConfig(
|
||||||
os.getenv("DOCLING_SERVER_URL", "http://docling:5001"),
|
os.getenv("DOCLING_SERVER_URL", "http://docling:5001"),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
DOCLING_DO_OCR = PersistentConfig(
|
||||||
|
"DOCLING_DO_OCR",
|
||||||
|
"rag.docling_do_ocr",
|
||||||
|
os.getenv("DOCLING_DO_OCR", "True").lower() == "true",
|
||||||
|
)
|
||||||
|
|
||||||
|
DOCLING_FORCE_OCR = PersistentConfig(
|
||||||
|
"DOCLING_FORCE_OCR",
|
||||||
|
"rag.docling_force_ocr",
|
||||||
|
os.getenv("DOCLING_FORCE_OCR", "False").lower() == "true",
|
||||||
|
)
|
||||||
|
|
||||||
DOCLING_OCR_ENGINE = PersistentConfig(
|
DOCLING_OCR_ENGINE = PersistentConfig(
|
||||||
"DOCLING_OCR_ENGINE",
|
"DOCLING_OCR_ENGINE",
|
||||||
"rag.docling_ocr_engine",
|
"rag.docling_ocr_engine",
|
||||||
|
|
@ -2220,6 +2256,24 @@ DOCLING_OCR_LANG = PersistentConfig(
|
||||||
os.getenv("DOCLING_OCR_LANG", "eng,fra,deu,spa"),
|
os.getenv("DOCLING_OCR_LANG", "eng,fra,deu,spa"),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
DOCLING_PDF_BACKEND = PersistentConfig(
|
||||||
|
"DOCLING_PDF_BACKEND",
|
||||||
|
"rag.docling_pdf_backend",
|
||||||
|
os.getenv("DOCLING_PDF_BACKEND", "dlparse_v4"),
|
||||||
|
)
|
||||||
|
|
||||||
|
DOCLING_TABLE_MODE = PersistentConfig(
|
||||||
|
"DOCLING_TABLE_MODE",
|
||||||
|
"rag.docling_table_mode",
|
||||||
|
os.getenv("DOCLING_TABLE_MODE", "accurate"),
|
||||||
|
)
|
||||||
|
|
||||||
|
DOCLING_PIPELINE = PersistentConfig(
|
||||||
|
"DOCLING_PIPELINE",
|
||||||
|
"rag.docling_pipeline",
|
||||||
|
os.getenv("DOCLING_PIPELINE", "standard"),
|
||||||
|
)
|
||||||
|
|
||||||
DOCLING_DO_PICTURE_DESCRIPTION = PersistentConfig(
|
DOCLING_DO_PICTURE_DESCRIPTION = PersistentConfig(
|
||||||
"DOCLING_DO_PICTURE_DESCRIPTION",
|
"DOCLING_DO_PICTURE_DESCRIPTION",
|
||||||
"rag.docling_do_picture_description",
|
"rag.docling_do_picture_description",
|
||||||
|
|
@ -3076,6 +3130,12 @@ IMAGES_OPENAI_API_BASE_URL = PersistentConfig(
|
||||||
"image_generation.openai.api_base_url",
|
"image_generation.openai.api_base_url",
|
||||||
os.getenv("IMAGES_OPENAI_API_BASE_URL", 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 = PersistentConfig(
|
||||||
"IMAGES_OPENAI_API_KEY",
|
"IMAGES_OPENAI_API_KEY",
|
||||||
"image_generation.openai.api_key",
|
"image_generation.openai.api_key",
|
||||||
|
|
|
||||||
|
|
@ -362,6 +362,8 @@ ENABLE_REALTIME_CHAT_SAVE = (
|
||||||
os.environ.get("ENABLE_REALTIME_CHAT_SAVE", "False").lower() == "true"
|
os.environ.get("ENABLE_REALTIME_CHAT_SAVE", "False").lower() == "true"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
ENABLE_QUERIES_CACHE = os.environ.get("ENABLE_QUERIES_CACHE", "False").lower() == "true"
|
||||||
|
|
||||||
####################################
|
####################################
|
||||||
# REDIS
|
# REDIS
|
||||||
####################################
|
####################################
|
||||||
|
|
@ -402,6 +404,10 @@ except ValueError:
|
||||||
####################################
|
####################################
|
||||||
|
|
||||||
WEBUI_AUTH = os.environ.get("WEBUI_AUTH", "True").lower() == "true"
|
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 = (
|
ENABLE_SIGNUP_PASSWORD_CONFIRMATION = (
|
||||||
os.environ.get("ENABLE_SIGNUP_PASSWORD_CONFIRMATION", "False").lower() == "true"
|
os.environ.get("ENABLE_SIGNUP_PASSWORD_CONFIRMATION", "False").lower() == "true"
|
||||||
)
|
)
|
||||||
|
|
@ -459,6 +465,19 @@ ENABLE_COMPRESSION_MIDDLEWARE = (
|
||||||
os.environ.get("ENABLE_COMPRESSION_MIDDLEWARE", "True").lower() == "true"
|
os.environ.get("ENABLE_COMPRESSION_MIDDLEWARE", "True").lower() == "true"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
####################################
|
||||||
|
# OAUTH Configuration
|
||||||
|
####################################
|
||||||
|
|
||||||
|
|
||||||
|
ENABLE_OAUTH_ID_TOKEN_COOKIE = (
|
||||||
|
os.environ.get("ENABLE_OAUTH_ID_TOKEN_COOKIE", "True").lower() == "true"
|
||||||
|
)
|
||||||
|
|
||||||
|
OAUTH_SESSION_TOKEN_ENCRYPTION_KEY = os.environ.get(
|
||||||
|
"OAUTH_SESSION_TOKEN_ENCRYPTION_KEY", WEBUI_SECRET_KEY
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
####################################
|
####################################
|
||||||
# SCIM Configuration
|
# SCIM Configuration
|
||||||
|
|
@ -527,6 +546,19 @@ else:
|
||||||
CHAT_RESPONSE_STREAM_DELTA_CHUNK_SIZE = 1
|
CHAT_RESPONSE_STREAM_DELTA_CHUNK_SIZE = 1
|
||||||
|
|
||||||
|
|
||||||
|
CHAT_RESPONSE_MAX_TOOL_CALL_RETRIES = os.environ.get(
|
||||||
|
"CHAT_RESPONSE_MAX_TOOL_CALL_RETRIES", "10"
|
||||||
|
)
|
||||||
|
|
||||||
|
if CHAT_RESPONSE_MAX_TOOL_CALL_RETRIES == "":
|
||||||
|
CHAT_RESPONSE_MAX_TOOL_CALL_RETRIES = 10
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
CHAT_RESPONSE_MAX_TOOL_CALL_RETRIES = int(CHAT_RESPONSE_MAX_TOOL_CALL_RETRIES)
|
||||||
|
except Exception:
|
||||||
|
CHAT_RESPONSE_MAX_TOOL_CALL_RETRIES = 10
|
||||||
|
|
||||||
|
|
||||||
####################################
|
####################################
|
||||||
# WEBSOCKET SUPPORT
|
# WEBSOCKET SUPPORT
|
||||||
####################################
|
####################################
|
||||||
|
|
|
||||||
|
|
@ -219,6 +219,15 @@ async def generate_function_chat_completion(
|
||||||
__task__ = metadata.get("task", None)
|
__task__ = metadata.get("task", None)
|
||||||
__task_body__ = metadata.get("task_body", None)
|
__task_body__ = metadata.get("task_body", None)
|
||||||
|
|
||||||
|
oauth_token = None
|
||||||
|
try:
|
||||||
|
oauth_token = request.app.state.oauth_manager.get_oauth_token(
|
||||||
|
user.id,
|
||||||
|
request.cookies.get("oauth_session_id", None),
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
log.error(f"Error getting OAuth token: {e}")
|
||||||
|
|
||||||
extra_params = {
|
extra_params = {
|
||||||
"__event_emitter__": __event_emitter__,
|
"__event_emitter__": __event_emitter__,
|
||||||
"__event_call__": __event_call__,
|
"__event_call__": __event_call__,
|
||||||
|
|
@ -230,9 +239,10 @@ async def generate_function_chat_completion(
|
||||||
"__files__": files,
|
"__files__": files,
|
||||||
"__user__": user.model_dump() if isinstance(user, UserModel) else {},
|
"__user__": user.model_dump() if isinstance(user, UserModel) else {},
|
||||||
"__metadata__": metadata,
|
"__metadata__": metadata,
|
||||||
|
"__oauth_token__": oauth_token,
|
||||||
"__request__": request,
|
"__request__": request,
|
||||||
}
|
}
|
||||||
extra_params["__tools__"] = get_tools(
|
extra_params["__tools__"] = await get_tools(
|
||||||
request,
|
request,
|
||||||
tool_ids,
|
tool_ids,
|
||||||
user,
|
user,
|
||||||
|
|
|
||||||
|
|
@ -158,6 +158,7 @@ from open_webui.config import (
|
||||||
IMAGE_SIZE,
|
IMAGE_SIZE,
|
||||||
IMAGE_STEPS,
|
IMAGE_STEPS,
|
||||||
IMAGES_OPENAI_API_BASE_URL,
|
IMAGES_OPENAI_API_BASE_URL,
|
||||||
|
IMAGES_OPENAI_API_VERSION,
|
||||||
IMAGES_OPENAI_API_KEY,
|
IMAGES_OPENAI_API_KEY,
|
||||||
IMAGES_GEMINI_API_BASE_URL,
|
IMAGES_GEMINI_API_BASE_URL,
|
||||||
IMAGES_GEMINI_API_KEY,
|
IMAGES_GEMINI_API_KEY,
|
||||||
|
|
@ -244,8 +245,13 @@ from open_webui.config import (
|
||||||
EXTERNAL_DOCUMENT_LOADER_API_KEY,
|
EXTERNAL_DOCUMENT_LOADER_API_KEY,
|
||||||
TIKA_SERVER_URL,
|
TIKA_SERVER_URL,
|
||||||
DOCLING_SERVER_URL,
|
DOCLING_SERVER_URL,
|
||||||
|
DOCLING_DO_OCR,
|
||||||
|
DOCLING_FORCE_OCR,
|
||||||
DOCLING_OCR_ENGINE,
|
DOCLING_OCR_ENGINE,
|
||||||
DOCLING_OCR_LANG,
|
DOCLING_OCR_LANG,
|
||||||
|
DOCLING_PDF_BACKEND,
|
||||||
|
DOCLING_TABLE_MODE,
|
||||||
|
DOCLING_PIPELINE,
|
||||||
DOCLING_DO_PICTURE_DESCRIPTION,
|
DOCLING_DO_PICTURE_DESCRIPTION,
|
||||||
DOCLING_PICTURE_DESCRIPTION_MODE,
|
DOCLING_PICTURE_DESCRIPTION_MODE,
|
||||||
DOCLING_PICTURE_DESCRIPTION_LOCAL,
|
DOCLING_PICTURE_DESCRIPTION_LOCAL,
|
||||||
|
|
@ -592,6 +598,7 @@ app = FastAPI(
|
||||||
)
|
)
|
||||||
|
|
||||||
oauth_manager = OAuthManager(app)
|
oauth_manager = OAuthManager(app)
|
||||||
|
app.state.oauth_manager = oauth_manager
|
||||||
|
|
||||||
app.state.instance_id = None
|
app.state.instance_id = None
|
||||||
app.state.config = AppConfig(
|
app.state.config = AppConfig(
|
||||||
|
|
@ -811,8 +818,13 @@ app.state.config.EXTERNAL_DOCUMENT_LOADER_URL = EXTERNAL_DOCUMENT_LOADER_URL
|
||||||
app.state.config.EXTERNAL_DOCUMENT_LOADER_API_KEY = EXTERNAL_DOCUMENT_LOADER_API_KEY
|
app.state.config.EXTERNAL_DOCUMENT_LOADER_API_KEY = EXTERNAL_DOCUMENT_LOADER_API_KEY
|
||||||
app.state.config.TIKA_SERVER_URL = TIKA_SERVER_URL
|
app.state.config.TIKA_SERVER_URL = TIKA_SERVER_URL
|
||||||
app.state.config.DOCLING_SERVER_URL = DOCLING_SERVER_URL
|
app.state.config.DOCLING_SERVER_URL = DOCLING_SERVER_URL
|
||||||
|
app.state.config.DOCLING_DO_OCR = DOCLING_DO_OCR
|
||||||
|
app.state.config.DOCLING_FORCE_OCR = DOCLING_FORCE_OCR
|
||||||
app.state.config.DOCLING_OCR_ENGINE = DOCLING_OCR_ENGINE
|
app.state.config.DOCLING_OCR_ENGINE = DOCLING_OCR_ENGINE
|
||||||
app.state.config.DOCLING_OCR_LANG = DOCLING_OCR_LANG
|
app.state.config.DOCLING_OCR_LANG = DOCLING_OCR_LANG
|
||||||
|
app.state.config.DOCLING_PDF_BACKEND = DOCLING_PDF_BACKEND
|
||||||
|
app.state.config.DOCLING_TABLE_MODE = DOCLING_TABLE_MODE
|
||||||
|
app.state.config.DOCLING_PIPELINE = DOCLING_PIPELINE
|
||||||
app.state.config.DOCLING_DO_PICTURE_DESCRIPTION = DOCLING_DO_PICTURE_DESCRIPTION
|
app.state.config.DOCLING_DO_PICTURE_DESCRIPTION = DOCLING_DO_PICTURE_DESCRIPTION
|
||||||
app.state.config.DOCLING_PICTURE_DESCRIPTION_MODE = DOCLING_PICTURE_DESCRIPTION_MODE
|
app.state.config.DOCLING_PICTURE_DESCRIPTION_MODE = DOCLING_PICTURE_DESCRIPTION_MODE
|
||||||
app.state.config.DOCLING_PICTURE_DESCRIPTION_LOCAL = DOCLING_PICTURE_DESCRIPTION_LOCAL
|
app.state.config.DOCLING_PICTURE_DESCRIPTION_LOCAL = DOCLING_PICTURE_DESCRIPTION_LOCAL
|
||||||
|
|
@ -1020,6 +1032,7 @@ app.state.config.ENABLE_IMAGE_GENERATION = ENABLE_IMAGE_GENERATION
|
||||||
app.state.config.ENABLE_IMAGE_PROMPT_GENERATION = ENABLE_IMAGE_PROMPT_GENERATION
|
app.state.config.ENABLE_IMAGE_PROMPT_GENERATION = ENABLE_IMAGE_PROMPT_GENERATION
|
||||||
|
|
||||||
app.state.config.IMAGES_OPENAI_API_BASE_URL = IMAGES_OPENAI_API_BASE_URL
|
app.state.config.IMAGES_OPENAI_API_BASE_URL = IMAGES_OPENAI_API_BASE_URL
|
||||||
|
app.state.config.IMAGES_OPENAI_API_VERSION = IMAGES_OPENAI_API_VERSION
|
||||||
app.state.config.IMAGES_OPENAI_API_KEY = IMAGES_OPENAI_API_KEY
|
app.state.config.IMAGES_OPENAI_API_KEY = IMAGES_OPENAI_API_KEY
|
||||||
|
|
||||||
app.state.config.IMAGES_GEMINI_API_BASE_URL = IMAGES_GEMINI_API_BASE_URL
|
app.state.config.IMAGES_GEMINI_API_BASE_URL = IMAGES_GEMINI_API_BASE_URL
|
||||||
|
|
@ -1407,6 +1420,14 @@ async def chat_completion(
|
||||||
model_item = form_data.pop("model_item", {})
|
model_item = form_data.pop("model_item", {})
|
||||||
tasks = form_data.pop("background_tasks", None)
|
tasks = form_data.pop("background_tasks", None)
|
||||||
|
|
||||||
|
oauth_token = None
|
||||||
|
try:
|
||||||
|
oauth_token = request.app.state.oauth_manager.get_oauth_token(
|
||||||
|
user.id, request.cookies.get("oauth_session_id", None)
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
log.error(f"Error getting OAuth token: {e}")
|
||||||
|
|
||||||
metadata = {}
|
metadata = {}
|
||||||
try:
|
try:
|
||||||
if not model_item.get("direct", False):
|
if not model_item.get("direct", False):
|
||||||
|
|
@ -1439,11 +1460,15 @@ async def chat_completion(
|
||||||
stream_delta_chunk_size = form_data.get("params", {}).get(
|
stream_delta_chunk_size = form_data.get("params", {}).get(
|
||||||
"stream_delta_chunk_size"
|
"stream_delta_chunk_size"
|
||||||
)
|
)
|
||||||
|
reasoning_tags = form_data.get("params", {}).get("reasoning_tags")
|
||||||
|
|
||||||
# Model Params
|
# Model Params
|
||||||
if model_info_params.get("stream_delta_chunk_size"):
|
if model_info_params.get("stream_delta_chunk_size"):
|
||||||
stream_delta_chunk_size = model_info_params.get("stream_delta_chunk_size")
|
stream_delta_chunk_size = model_info_params.get("stream_delta_chunk_size")
|
||||||
|
|
||||||
|
if model_info_params.get("reasoning_tags") is not None:
|
||||||
|
reasoning_tags = model_info_params.get("reasoning_tags")
|
||||||
|
|
||||||
metadata = {
|
metadata = {
|
||||||
"user_id": user.id,
|
"user_id": user.id,
|
||||||
"chat_id": form_data.pop("chat_id", None),
|
"chat_id": form_data.pop("chat_id", None),
|
||||||
|
|
@ -1459,6 +1484,7 @@ async def chat_completion(
|
||||||
"direct": model_item.get("direct", False),
|
"direct": model_item.get("direct", False),
|
||||||
"params": {
|
"params": {
|
||||||
"stream_delta_chunk_size": stream_delta_chunk_size,
|
"stream_delta_chunk_size": stream_delta_chunk_size,
|
||||||
|
"reasoning_tags": reasoning_tags,
|
||||||
"function_calling": (
|
"function_calling": (
|
||||||
"native"
|
"native"
|
||||||
if (
|
if (
|
||||||
|
|
@ -1516,7 +1542,7 @@ async def chat_completion(
|
||||||
try:
|
try:
|
||||||
event_emitter = get_event_emitter(metadata)
|
event_emitter = get_event_emitter(metadata)
|
||||||
await event_emitter(
|
await event_emitter(
|
||||||
{"type": "task-cancelled"},
|
{"type": "chat:tasks:cancel"},
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
pass
|
pass
|
||||||
|
|
@ -1532,14 +1558,21 @@ async def chat_completion(
|
||||||
"error": {"content": str(e)},
|
"error": {"content": str(e)},
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
event_emitter = get_event_emitter(metadata)
|
||||||
|
await event_emitter(
|
||||||
|
{
|
||||||
|
"type": "chat:message:error",
|
||||||
|
"data": {"error": {"content": str(e)}},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
await event_emitter(
|
||||||
|
{"type": "chat:tasks:cancel"},
|
||||||
|
)
|
||||||
|
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=status.HTTP_400_BAD_REQUEST,
|
|
||||||
detail=str(e),
|
|
||||||
)
|
|
||||||
|
|
||||||
if (
|
if (
|
||||||
metadata.get("session_id")
|
metadata.get("session_id")
|
||||||
and metadata.get("chat_id")
|
and metadata.get("chat_id")
|
||||||
|
|
@ -1639,8 +1672,18 @@ async def list_tasks_by_chat_id_endpoint(
|
||||||
@app.get("/api/config")
|
@app.get("/api/config")
|
||||||
async def get_app_config(request: Request):
|
async def get_app_config(request: Request):
|
||||||
user = None
|
user = None
|
||||||
if "token" in request.cookies:
|
token = None
|
||||||
|
|
||||||
|
auth_header = request.headers.get("Authorization")
|
||||||
|
if auth_header:
|
||||||
|
cred = get_http_authorization_cred(auth_header)
|
||||||
|
if cred:
|
||||||
|
token = cred.credentials
|
||||||
|
|
||||||
|
if not token and "token" in request.cookies:
|
||||||
token = request.cookies.get("token")
|
token = request.cookies.get("token")
|
||||||
|
|
||||||
|
if token:
|
||||||
try:
|
try:
|
||||||
data = decode_token(token)
|
data = decode_token(token)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,52 @@
|
||||||
|
"""Add oauth_session table
|
||||||
|
|
||||||
|
Revision ID: 38d63c18f30f
|
||||||
|
Revises: 3af16a1c9fb6
|
||||||
|
Create Date: 2025-09-08 14:19:59.583921
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
from typing import Sequence, Union
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision: str = "38d63c18f30f"
|
||||||
|
down_revision: Union[str, None] = "3af16a1c9fb6"
|
||||||
|
branch_labels: Union[str, Sequence[str], None] = None
|
||||||
|
depends_on: Union[str, Sequence[str], None] = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
# Create oauth_session table
|
||||||
|
op.create_table(
|
||||||
|
"oauth_session",
|
||||||
|
sa.Column("id", sa.Text(), nullable=False),
|
||||||
|
sa.Column("user_id", sa.Text(), 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),
|
||||||
|
sa.PrimaryKeyConstraint("id"),
|
||||||
|
sa.ForeignKeyConstraint(["user_id"], ["user.id"], ondelete="CASCADE"),
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create indexes for better performance
|
||||||
|
op.create_index("idx_oauth_session_user_id", "oauth_session", ["user_id"])
|
||||||
|
op.create_index("idx_oauth_session_expires_at", "oauth_session", ["expires_at"])
|
||||||
|
op.create_index(
|
||||||
|
"idx_oauth_session_user_provider", "oauth_session", ["user_id", "provider"]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
# Drop indexes first
|
||||||
|
op.drop_index("idx_oauth_session_user_provider", table_name="oauth_session")
|
||||||
|
op.drop_index("idx_oauth_session_expires_at", table_name="oauth_session")
|
||||||
|
op.drop_index("idx_oauth_session_user_id", table_name="oauth_session")
|
||||||
|
|
||||||
|
# Drop the table
|
||||||
|
op.drop_table("oauth_session")
|
||||||
|
|
@ -147,6 +147,15 @@ class FilesTable:
|
||||||
with get_db() as db:
|
with get_db() as db:
|
||||||
return [FileModel.model_validate(file) for file in db.query(File).all()]
|
return [FileModel.model_validate(file) for file in db.query(File).all()]
|
||||||
|
|
||||||
|
def check_access_by_user_id(self, id, user_id, permission="write") -> bool:
|
||||||
|
file = self.get_file_by_id(id)
|
||||||
|
if not file:
|
||||||
|
return False
|
||||||
|
if file.user_id == user_id:
|
||||||
|
return True
|
||||||
|
# Implement additional access control logic here as needed
|
||||||
|
return False
|
||||||
|
|
||||||
def get_files_by_ids(self, ids: list[str]) -> list[FileModel]:
|
def get_files_by_ids(self, ids: list[str]) -> list[FileModel]:
|
||||||
with get_db() as db:
|
with get_db() as db:
|
||||||
return [
|
return [
|
||||||
|
|
|
||||||
|
|
@ -58,6 +58,14 @@ class FolderModel(BaseModel):
|
||||||
class FolderForm(BaseModel):
|
class FolderForm(BaseModel):
|
||||||
name: str
|
name: str
|
||||||
data: Optional[dict] = None
|
data: Optional[dict] = None
|
||||||
|
meta: Optional[dict] = None
|
||||||
|
model_config = ConfigDict(extra="allow")
|
||||||
|
|
||||||
|
|
||||||
|
class FolderUpdateForm(BaseModel):
|
||||||
|
name: Optional[str] = None
|
||||||
|
data: Optional[dict] = None
|
||||||
|
meta: Optional[dict] = None
|
||||||
model_config = ConfigDict(extra="allow")
|
model_config = ConfigDict(extra="allow")
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -197,7 +205,7 @@ class FolderTable:
|
||||||
return
|
return
|
||||||
|
|
||||||
def update_folder_by_id_and_user_id(
|
def update_folder_by_id_and_user_id(
|
||||||
self, id: str, user_id: str, form_data: FolderForm
|
self, id: str, user_id: str, form_data: FolderUpdateForm
|
||||||
) -> Optional[FolderModel]:
|
) -> Optional[FolderModel]:
|
||||||
try:
|
try:
|
||||||
with get_db() as db:
|
with get_db() as db:
|
||||||
|
|
@ -228,8 +236,13 @@ class FolderTable:
|
||||||
**form_data["data"],
|
**form_data["data"],
|
||||||
}
|
}
|
||||||
|
|
||||||
folder.updated_at = int(time.time())
|
if "meta" in form_data:
|
||||||
|
folder.meta = {
|
||||||
|
**(folder.meta or {}),
|
||||||
|
**form_data["meta"],
|
||||||
|
}
|
||||||
|
|
||||||
|
folder.updated_at = int(time.time())
|
||||||
db.commit()
|
db.commit()
|
||||||
|
|
||||||
return FolderModel.model_validate(folder)
|
return FolderModel.model_validate(folder)
|
||||||
|
|
|
||||||
|
|
@ -54,6 +54,22 @@ class FunctionModel(BaseModel):
|
||||||
model_config = ConfigDict(from_attributes=True)
|
model_config = ConfigDict(from_attributes=True)
|
||||||
|
|
||||||
|
|
||||||
|
class FunctionWithValvesModel(BaseModel):
|
||||||
|
id: str
|
||||||
|
user_id: str
|
||||||
|
name: str
|
||||||
|
type: str
|
||||||
|
content: str
|
||||||
|
meta: FunctionMeta
|
||||||
|
valves: Optional[dict] = None
|
||||||
|
is_active: bool = False
|
||||||
|
is_global: bool = False
|
||||||
|
updated_at: int # timestamp in epoch
|
||||||
|
created_at: int # timestamp in epoch
|
||||||
|
|
||||||
|
model_config = ConfigDict(from_attributes=True)
|
||||||
|
|
||||||
|
|
||||||
####################
|
####################
|
||||||
# Forms
|
# Forms
|
||||||
####################
|
####################
|
||||||
|
|
@ -111,8 +127,8 @@ class FunctionsTable:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def sync_functions(
|
def sync_functions(
|
||||||
self, user_id: str, functions: list[FunctionModel]
|
self, user_id: str, functions: list[FunctionWithValvesModel]
|
||||||
) -> list[FunctionModel]:
|
) -> list[FunctionWithValvesModel]:
|
||||||
# Synchronize functions for a user by updating existing ones, inserting new ones, and removing those that are no longer present.
|
# Synchronize functions for a user by updating existing ones, inserting new ones, and removing those that are no longer present.
|
||||||
try:
|
try:
|
||||||
with get_db() as db:
|
with get_db() as db:
|
||||||
|
|
@ -166,17 +182,24 @@ class FunctionsTable:
|
||||||
except Exception:
|
except Exception:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def get_functions(self, active_only=False) -> list[FunctionModel]:
|
def get_functions(
|
||||||
|
self, active_only=False, include_valves=False
|
||||||
|
) -> list[FunctionModel | FunctionWithValvesModel]:
|
||||||
with get_db() as db:
|
with get_db() as db:
|
||||||
if active_only:
|
if active_only:
|
||||||
|
functions = db.query(Function).filter_by(is_active=True).all()
|
||||||
|
|
||||||
|
else:
|
||||||
|
functions = db.query(Function).all()
|
||||||
|
|
||||||
|
if include_valves:
|
||||||
return [
|
return [
|
||||||
FunctionModel.model_validate(function)
|
FunctionWithValvesModel.model_validate(function)
|
||||||
for function in db.query(Function).filter_by(is_active=True).all()
|
for function in functions
|
||||||
]
|
]
|
||||||
else:
|
else:
|
||||||
return [
|
return [
|
||||||
FunctionModel.model_validate(function)
|
FunctionModel.model_validate(function) for function in functions
|
||||||
for function in db.query(Function).all()
|
|
||||||
]
|
]
|
||||||
|
|
||||||
def get_functions_by_type(
|
def get_functions_by_type(
|
||||||
|
|
|
||||||
|
|
@ -288,13 +288,17 @@ class GroupTable:
|
||||||
if not group:
|
if not group:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
if not group.user_ids:
|
group_user_ids = group.user_ids
|
||||||
group.user_ids = []
|
if not group_user_ids or not isinstance(group_user_ids, list):
|
||||||
|
group_user_ids = []
|
||||||
|
|
||||||
|
group_user_ids = list(set(group_user_ids)) # Deduplicate
|
||||||
|
|
||||||
for user_id in user_ids:
|
for user_id in user_ids:
|
||||||
if user_id not in group.user_ids:
|
if user_id not in group_user_ids:
|
||||||
group.user_ids.append(user_id)
|
group_user_ids.append(user_id)
|
||||||
|
|
||||||
|
group.user_ids = group_user_ids
|
||||||
group.updated_at = int(time.time())
|
group.updated_at = int(time.time())
|
||||||
db.commit()
|
db.commit()
|
||||||
db.refresh(group)
|
db.refresh(group)
|
||||||
|
|
@ -312,14 +316,20 @@ class GroupTable:
|
||||||
if not group:
|
if not group:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
if not group.user_ids:
|
group_user_ids = group.user_ids
|
||||||
|
|
||||||
|
if not group_user_ids or not isinstance(group_user_ids, list):
|
||||||
return GroupModel.model_validate(group)
|
return GroupModel.model_validate(group)
|
||||||
|
|
||||||
for user_id in user_ids:
|
group_user_ids = list(set(group_user_ids)) # Deduplicate
|
||||||
if user_id in group.user_ids:
|
|
||||||
group.user_ids.remove(user_id)
|
|
||||||
|
|
||||||
|
for user_id in user_ids:
|
||||||
|
if user_id in group_user_ids:
|
||||||
|
group_user_ids.remove(user_id)
|
||||||
|
|
||||||
|
group.user_ids = group_user_ids
|
||||||
group.updated_at = int(time.time())
|
group.updated_at = int(time.time())
|
||||||
|
|
||||||
db.commit()
|
db.commit()
|
||||||
db.refresh(group)
|
db.refresh(group)
|
||||||
return GroupModel.model_validate(group)
|
return GroupModel.model_validate(group)
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ from open_webui.internal.db import Base, get_db
|
||||||
from open_webui.env import SRC_LOG_LEVELS
|
from open_webui.env import SRC_LOG_LEVELS
|
||||||
|
|
||||||
from open_webui.models.files import FileMetadataResponse
|
from open_webui.models.files import FileMetadataResponse
|
||||||
|
from open_webui.models.groups import Groups
|
||||||
from open_webui.models.users import Users, UserResponse
|
from open_webui.models.users import Users, UserResponse
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -128,11 +129,18 @@ class KnowledgeTable:
|
||||||
|
|
||||||
def get_knowledge_bases(self) -> list[KnowledgeUserModel]:
|
def get_knowledge_bases(self) -> list[KnowledgeUserModel]:
|
||||||
with get_db() as db:
|
with get_db() as db:
|
||||||
knowledge_bases = []
|
all_knowledge = (
|
||||||
for knowledge in (
|
|
||||||
db.query(Knowledge).order_by(Knowledge.updated_at.desc()).all()
|
db.query(Knowledge).order_by(Knowledge.updated_at.desc()).all()
|
||||||
):
|
)
|
||||||
user = Users.get_user_by_id(knowledge.user_id)
|
|
||||||
|
user_ids = list(set(knowledge.user_id for knowledge in all_knowledge))
|
||||||
|
|
||||||
|
users = Users.get_users_by_user_ids(user_ids) if user_ids else []
|
||||||
|
users_dict = {user.id: user for user in users}
|
||||||
|
|
||||||
|
knowledge_bases = []
|
||||||
|
for knowledge in all_knowledge:
|
||||||
|
user = users_dict.get(knowledge.user_id)
|
||||||
knowledge_bases.append(
|
knowledge_bases.append(
|
||||||
KnowledgeUserModel.model_validate(
|
KnowledgeUserModel.model_validate(
|
||||||
{
|
{
|
||||||
|
|
@ -143,15 +151,27 @@ class KnowledgeTable:
|
||||||
)
|
)
|
||||||
return knowledge_bases
|
return knowledge_bases
|
||||||
|
|
||||||
|
def check_access_by_user_id(self, id, user_id, permission="write") -> bool:
|
||||||
|
knowledge = self.get_knowledge_by_id(id)
|
||||||
|
if not knowledge:
|
||||||
|
return False
|
||||||
|
if knowledge.user_id == user_id:
|
||||||
|
return True
|
||||||
|
user_group_ids = {group.id for group in Groups.get_groups_by_member_id(user_id)}
|
||||||
|
return has_access(user_id, permission, knowledge.access_control, user_group_ids)
|
||||||
|
|
||||||
def get_knowledge_bases_by_user_id(
|
def get_knowledge_bases_by_user_id(
|
||||||
self, user_id: str, permission: str = "write"
|
self, user_id: str, permission: str = "write"
|
||||||
) -> list[KnowledgeUserModel]:
|
) -> list[KnowledgeUserModel]:
|
||||||
knowledge_bases = self.get_knowledge_bases()
|
knowledge_bases = self.get_knowledge_bases()
|
||||||
|
user_group_ids = {group.id for group in Groups.get_groups_by_member_id(user_id)}
|
||||||
return [
|
return [
|
||||||
knowledge_base
|
knowledge_base
|
||||||
for knowledge_base in knowledge_bases
|
for knowledge_base in knowledge_bases
|
||||||
if knowledge_base.user_id == user_id
|
if knowledge_base.user_id == user_id
|
||||||
or has_access(user_id, permission, knowledge_base.access_control)
|
or has_access(
|
||||||
|
user_id, permission, knowledge_base.access_control, user_group_ids
|
||||||
|
)
|
||||||
]
|
]
|
||||||
|
|
||||||
def get_knowledge_by_id(self, id: str) -> Optional[KnowledgeModel]:
|
def get_knowledge_by_id(self, id: str) -> Optional[KnowledgeModel]:
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ from typing import Optional
|
||||||
from open_webui.internal.db import Base, JSONField, get_db
|
from open_webui.internal.db import Base, JSONField, get_db
|
||||||
from open_webui.env import SRC_LOG_LEVELS
|
from open_webui.env import SRC_LOG_LEVELS
|
||||||
|
|
||||||
|
from open_webui.models.groups import Groups
|
||||||
from open_webui.models.users import Users, UserResponse
|
from open_webui.models.users import Users, UserResponse
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -175,9 +176,16 @@ class ModelsTable:
|
||||||
|
|
||||||
def get_models(self) -> list[ModelUserResponse]:
|
def get_models(self) -> list[ModelUserResponse]:
|
||||||
with get_db() as db:
|
with get_db() as db:
|
||||||
|
all_models = db.query(Model).filter(Model.base_model_id != None).all()
|
||||||
|
|
||||||
|
user_ids = list(set(model.user_id for model in all_models))
|
||||||
|
|
||||||
|
users = Users.get_users_by_user_ids(user_ids) if user_ids else []
|
||||||
|
users_dict = {user.id: user for user in users}
|
||||||
|
|
||||||
models = []
|
models = []
|
||||||
for model in db.query(Model).filter(Model.base_model_id != None).all():
|
for model in all_models:
|
||||||
user = Users.get_user_by_id(model.user_id)
|
user = users_dict.get(model.user_id)
|
||||||
models.append(
|
models.append(
|
||||||
ModelUserResponse.model_validate(
|
ModelUserResponse.model_validate(
|
||||||
{
|
{
|
||||||
|
|
@ -199,11 +207,12 @@ class ModelsTable:
|
||||||
self, user_id: str, permission: str = "write"
|
self, user_id: str, permission: str = "write"
|
||||||
) -> list[ModelUserResponse]:
|
) -> list[ModelUserResponse]:
|
||||||
models = self.get_models()
|
models = self.get_models()
|
||||||
|
user_group_ids = {group.id for group in Groups.get_groups_by_member_id(user_id)}
|
||||||
return [
|
return [
|
||||||
model
|
model
|
||||||
for model in models
|
for model in models
|
||||||
if model.user_id == user_id
|
if model.user_id == user_id
|
||||||
or has_access(user_id, permission, model.access_control)
|
or has_access(user_id, permission, model.access_control, user_group_ids)
|
||||||
]
|
]
|
||||||
|
|
||||||
def get_model_by_id(self, id: str) -> Optional[ModelModel]:
|
def get_model_by_id(self, id: str) -> Optional[ModelModel]:
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import uuid
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from open_webui.internal.db import Base, get_db
|
from open_webui.internal.db import Base, get_db
|
||||||
|
from open_webui.models.groups import Groups
|
||||||
from open_webui.utils.access_control import has_access
|
from open_webui.utils.access_control import has_access
|
||||||
from open_webui.models.users import Users, UserResponse
|
from open_webui.models.users import Users, UserResponse
|
||||||
|
|
||||||
|
|
@ -105,11 +106,12 @@ class NoteTable:
|
||||||
self, user_id: str, permission: str = "write"
|
self, user_id: str, permission: str = "write"
|
||||||
) -> list[NoteModel]:
|
) -> list[NoteModel]:
|
||||||
notes = self.get_notes()
|
notes = self.get_notes()
|
||||||
|
user_group_ids = {group.id for group in Groups.get_groups_by_member_id(user_id)}
|
||||||
return [
|
return [
|
||||||
note
|
note
|
||||||
for note in notes
|
for note in notes
|
||||||
if note.user_id == user_id
|
if note.user_id == user_id
|
||||||
or has_access(user_id, permission, note.access_control)
|
or has_access(user_id, permission, note.access_control, user_group_ids)
|
||||||
]
|
]
|
||||||
|
|
||||||
def get_note_by_id(self, id: str) -> Optional[NoteModel]:
|
def get_note_by_id(self, id: str) -> Optional[NoteModel]:
|
||||||
|
|
|
||||||
246
backend/open_webui/models/oauth_sessions.py
Normal file
246
backend/open_webui/models/oauth_sessions.py
Normal file
|
|
@ -0,0 +1,246 @@
|
||||||
|
import time
|
||||||
|
import logging
|
||||||
|
import uuid
|
||||||
|
from typing import Optional, List
|
||||||
|
import base64
|
||||||
|
import hashlib
|
||||||
|
import json
|
||||||
|
|
||||||
|
from cryptography.fernet import Fernet
|
||||||
|
|
||||||
|
from open_webui.internal.db import Base, get_db
|
||||||
|
from open_webui.env import SRC_LOG_LEVELS, OAUTH_SESSION_TOKEN_ENCRYPTION_KEY
|
||||||
|
|
||||||
|
from pydantic import BaseModel, ConfigDict
|
||||||
|
from sqlalchemy import BigInteger, Column, String, Text, Index
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
log.setLevel(SRC_LOG_LEVELS["MODELS"])
|
||||||
|
|
||||||
|
####################
|
||||||
|
# DB MODEL
|
||||||
|
####################
|
||||||
|
|
||||||
|
|
||||||
|
class OAuthSession(Base):
|
||||||
|
__tablename__ = "oauth_session"
|
||||||
|
|
||||||
|
id = Column(Text, primary_key=True)
|
||||||
|
user_id = Column(Text, nullable=False)
|
||||||
|
provider = Column(Text, nullable=False)
|
||||||
|
token = Column(
|
||||||
|
Text, nullable=False
|
||||||
|
) # JSON with access_token, id_token, refresh_token
|
||||||
|
expires_at = Column(BigInteger, nullable=False)
|
||||||
|
created_at = Column(BigInteger, nullable=False)
|
||||||
|
updated_at = Column(BigInteger, nullable=False)
|
||||||
|
|
||||||
|
# Add indexes for better performance
|
||||||
|
__table_args__ = (
|
||||||
|
Index("idx_oauth_session_user_id", "user_id"),
|
||||||
|
Index("idx_oauth_session_expires_at", "expires_at"),
|
||||||
|
Index("idx_oauth_session_user_provider", "user_id", "provider"),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class OAuthSessionModel(BaseModel):
|
||||||
|
id: str
|
||||||
|
user_id: str
|
||||||
|
provider: str
|
||||||
|
token: dict
|
||||||
|
expires_at: int # timestamp in epoch
|
||||||
|
created_at: int # timestamp in epoch
|
||||||
|
updated_at: int # timestamp in epoch
|
||||||
|
|
||||||
|
model_config = ConfigDict(from_attributes=True)
|
||||||
|
|
||||||
|
|
||||||
|
####################
|
||||||
|
# Forms
|
||||||
|
####################
|
||||||
|
|
||||||
|
|
||||||
|
class OAuthSessionResponse(BaseModel):
|
||||||
|
id: str
|
||||||
|
user_id: str
|
||||||
|
provider: str
|
||||||
|
expires_at: int
|
||||||
|
|
||||||
|
|
||||||
|
class OAuthSessionTable:
|
||||||
|
def __init__(self):
|
||||||
|
self.encryption_key = OAUTH_SESSION_TOKEN_ENCRYPTION_KEY
|
||||||
|
if not self.encryption_key:
|
||||||
|
raise Exception("OAUTH_SESSION_TOKEN_ENCRYPTION_KEY is not set")
|
||||||
|
|
||||||
|
# check if encryption key is in the right format for Fernet (32 url-safe base64-encoded bytes)
|
||||||
|
if len(self.encryption_key) != 44:
|
||||||
|
key_bytes = hashlib.sha256(self.encryption_key.encode()).digest()
|
||||||
|
self.encryption_key = base64.urlsafe_b64encode(key_bytes)
|
||||||
|
else:
|
||||||
|
self.encryption_key = self.encryption_key.encode()
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.fernet = Fernet(self.encryption_key)
|
||||||
|
except Exception as e:
|
||||||
|
log.error(f"Error initializing Fernet with provided key: {e}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
def _encrypt_token(self, token) -> str:
|
||||||
|
"""Encrypt OAuth tokens for storage"""
|
||||||
|
try:
|
||||||
|
token_json = json.dumps(token)
|
||||||
|
encrypted = self.fernet.encrypt(token_json.encode()).decode()
|
||||||
|
return encrypted
|
||||||
|
except Exception as e:
|
||||||
|
log.error(f"Error encrypting tokens: {e}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
def _decrypt_token(self, token: str):
|
||||||
|
"""Decrypt OAuth tokens from storage"""
|
||||||
|
try:
|
||||||
|
decrypted = self.fernet.decrypt(token.encode()).decode()
|
||||||
|
return json.loads(decrypted)
|
||||||
|
except Exception as e:
|
||||||
|
log.error(f"Error decrypting tokens: {e}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
def create_session(
|
||||||
|
self,
|
||||||
|
user_id: str,
|
||||||
|
provider: str,
|
||||||
|
token: dict,
|
||||||
|
) -> Optional[OAuthSessionModel]:
|
||||||
|
"""Create a new OAuth session"""
|
||||||
|
try:
|
||||||
|
with get_db() as db:
|
||||||
|
current_time = int(time.time())
|
||||||
|
id = str(uuid.uuid4())
|
||||||
|
|
||||||
|
result = OAuthSession(
|
||||||
|
**{
|
||||||
|
"id": id,
|
||||||
|
"user_id": user_id,
|
||||||
|
"provider": provider,
|
||||||
|
"token": self._encrypt_token(token),
|
||||||
|
"expires_at": token.get("expires_at"),
|
||||||
|
"created_at": current_time,
|
||||||
|
"updated_at": current_time,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
db.add(result)
|
||||||
|
db.commit()
|
||||||
|
db.refresh(result)
|
||||||
|
|
||||||
|
if result:
|
||||||
|
result.token = token # Return decrypted token
|
||||||
|
return OAuthSessionModel.model_validate(result)
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
except Exception as e:
|
||||||
|
log.error(f"Error creating OAuth session: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_session_by_id(self, session_id: str) -> Optional[OAuthSessionModel]:
|
||||||
|
"""Get OAuth session by ID"""
|
||||||
|
try:
|
||||||
|
with get_db() as db:
|
||||||
|
session = db.query(OAuthSession).filter_by(id=session_id).first()
|
||||||
|
if session:
|
||||||
|
session.token = self._decrypt_token(session.token)
|
||||||
|
return OAuthSessionModel.model_validate(session)
|
||||||
|
|
||||||
|
return None
|
||||||
|
except Exception as e:
|
||||||
|
log.error(f"Error getting OAuth session by ID: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_session_by_id_and_user_id(
|
||||||
|
self, session_id: str, user_id: str
|
||||||
|
) -> Optional[OAuthSessionModel]:
|
||||||
|
"""Get OAuth session by ID and user ID"""
|
||||||
|
try:
|
||||||
|
with get_db() as db:
|
||||||
|
session = (
|
||||||
|
db.query(OAuthSession)
|
||||||
|
.filter_by(id=session_id, user_id=user_id)
|
||||||
|
.first()
|
||||||
|
)
|
||||||
|
if session:
|
||||||
|
session.token = self._decrypt_token(session.token)
|
||||||
|
return OAuthSessionModel.model_validate(session)
|
||||||
|
|
||||||
|
return None
|
||||||
|
except Exception as e:
|
||||||
|
log.error(f"Error getting OAuth session by ID: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_sessions_by_user_id(self, user_id: str) -> List[OAuthSessionModel]:
|
||||||
|
"""Get all OAuth sessions for a user"""
|
||||||
|
try:
|
||||||
|
with get_db() as db:
|
||||||
|
sessions = db.query(OAuthSession).filter_by(user_id=user_id).all()
|
||||||
|
|
||||||
|
results = []
|
||||||
|
for session in sessions:
|
||||||
|
session.token = self._decrypt_token(session.token)
|
||||||
|
results.append(OAuthSessionModel.model_validate(session))
|
||||||
|
|
||||||
|
return results
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
log.error(f"Error getting OAuth sessions by user ID: {e}")
|
||||||
|
return []
|
||||||
|
|
||||||
|
def update_session_by_id(
|
||||||
|
self, session_id: str, token: dict
|
||||||
|
) -> Optional[OAuthSessionModel]:
|
||||||
|
"""Update OAuth session tokens"""
|
||||||
|
try:
|
||||||
|
with get_db() as db:
|
||||||
|
current_time = int(time.time())
|
||||||
|
|
||||||
|
db.query(OAuthSession).filter_by(id=session_id).update(
|
||||||
|
{
|
||||||
|
"token": self._encrypt_token(token),
|
||||||
|
"expires_at": token.get("expires_at"),
|
||||||
|
"updated_at": current_time,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
db.commit()
|
||||||
|
session = db.query(OAuthSession).filter_by(id=session_id).first()
|
||||||
|
|
||||||
|
if session:
|
||||||
|
session.token = self._decrypt_token(session.token)
|
||||||
|
return OAuthSessionModel.model_validate(session)
|
||||||
|
|
||||||
|
return None
|
||||||
|
except Exception as e:
|
||||||
|
log.error(f"Error updating OAuth session tokens: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def delete_session_by_id(self, session_id: str) -> bool:
|
||||||
|
"""Delete an OAuth session"""
|
||||||
|
try:
|
||||||
|
with get_db() as db:
|
||||||
|
result = db.query(OAuthSession).filter_by(id=session_id).delete()
|
||||||
|
db.commit()
|
||||||
|
return result > 0
|
||||||
|
except Exception as e:
|
||||||
|
log.error(f"Error deleting OAuth session: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def delete_sessions_by_user_id(self, user_id: str) -> bool:
|
||||||
|
"""Delete all OAuth sessions for a user"""
|
||||||
|
try:
|
||||||
|
with get_db() as db:
|
||||||
|
result = db.query(OAuthSession).filter_by(user_id=user_id).delete()
|
||||||
|
db.commit()
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
log.error(f"Error deleting OAuth sessions by user ID: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
OAuthSessions = OAuthSessionTable()
|
||||||
|
|
@ -2,6 +2,7 @@ import time
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from open_webui.internal.db import Base, get_db
|
from open_webui.internal.db import Base, get_db
|
||||||
|
from open_webui.models.groups import Groups
|
||||||
from open_webui.models.users import Users, UserResponse
|
from open_webui.models.users import Users, UserResponse
|
||||||
|
|
||||||
from pydantic import BaseModel, ConfigDict
|
from pydantic import BaseModel, ConfigDict
|
||||||
|
|
@ -103,10 +104,16 @@ class PromptsTable:
|
||||||
|
|
||||||
def get_prompts(self) -> list[PromptUserResponse]:
|
def get_prompts(self) -> list[PromptUserResponse]:
|
||||||
with get_db() as db:
|
with get_db() as db:
|
||||||
prompts = []
|
all_prompts = db.query(Prompt).order_by(Prompt.timestamp.desc()).all()
|
||||||
|
|
||||||
for prompt in db.query(Prompt).order_by(Prompt.timestamp.desc()).all():
|
user_ids = list(set(prompt.user_id for prompt in all_prompts))
|
||||||
user = Users.get_user_by_id(prompt.user_id)
|
|
||||||
|
users = Users.get_users_by_user_ids(user_ids) if user_ids else []
|
||||||
|
users_dict = {user.id: user for user in users}
|
||||||
|
|
||||||
|
prompts = []
|
||||||
|
for prompt in all_prompts:
|
||||||
|
user = users_dict.get(prompt.user_id)
|
||||||
prompts.append(
|
prompts.append(
|
||||||
PromptUserResponse.model_validate(
|
PromptUserResponse.model_validate(
|
||||||
{
|
{
|
||||||
|
|
@ -122,12 +129,13 @@ class PromptsTable:
|
||||||
self, user_id: str, permission: str = "write"
|
self, user_id: str, permission: str = "write"
|
||||||
) -> list[PromptUserResponse]:
|
) -> list[PromptUserResponse]:
|
||||||
prompts = self.get_prompts()
|
prompts = self.get_prompts()
|
||||||
|
user_group_ids = {group.id for group in Groups.get_groups_by_member_id(user_id)}
|
||||||
|
|
||||||
return [
|
return [
|
||||||
prompt
|
prompt
|
||||||
for prompt in prompts
|
for prompt in prompts
|
||||||
if prompt.user_id == user_id
|
if prompt.user_id == user_id
|
||||||
or has_access(user_id, permission, prompt.access_control)
|
or has_access(user_id, permission, prompt.access_control, user_group_ids)
|
||||||
]
|
]
|
||||||
|
|
||||||
def update_prompt_by_command(
|
def update_prompt_by_command(
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,8 @@ from typing import Optional
|
||||||
|
|
||||||
from open_webui.internal.db import Base, JSONField, get_db
|
from open_webui.internal.db import Base, JSONField, get_db
|
||||||
from open_webui.models.users import Users, UserResponse
|
from open_webui.models.users import Users, UserResponse
|
||||||
|
from open_webui.models.groups import Groups
|
||||||
|
|
||||||
from open_webui.env import SRC_LOG_LEVELS
|
from open_webui.env import SRC_LOG_LEVELS
|
||||||
from pydantic import BaseModel, ConfigDict
|
from pydantic import BaseModel, ConfigDict
|
||||||
from sqlalchemy import BigInteger, Column, String, Text, JSON
|
from sqlalchemy import BigInteger, Column, String, Text, JSON
|
||||||
|
|
@ -144,9 +146,16 @@ class ToolsTable:
|
||||||
|
|
||||||
def get_tools(self) -> list[ToolUserModel]:
|
def get_tools(self) -> list[ToolUserModel]:
|
||||||
with get_db() as db:
|
with get_db() as db:
|
||||||
|
all_tools = db.query(Tool).order_by(Tool.updated_at.desc()).all()
|
||||||
|
|
||||||
|
user_ids = list(set(tool.user_id for tool in all_tools))
|
||||||
|
|
||||||
|
users = Users.get_users_by_user_ids(user_ids) if user_ids else []
|
||||||
|
users_dict = {user.id: user for user in users}
|
||||||
|
|
||||||
tools = []
|
tools = []
|
||||||
for tool in db.query(Tool).order_by(Tool.updated_at.desc()).all():
|
for tool in all_tools:
|
||||||
user = Users.get_user_by_id(tool.user_id)
|
user = users_dict.get(tool.user_id)
|
||||||
tools.append(
|
tools.append(
|
||||||
ToolUserModel.model_validate(
|
ToolUserModel.model_validate(
|
||||||
{
|
{
|
||||||
|
|
@ -161,12 +170,13 @@ class ToolsTable:
|
||||||
self, user_id: str, permission: str = "write"
|
self, user_id: str, permission: str = "write"
|
||||||
) -> list[ToolUserModel]:
|
) -> list[ToolUserModel]:
|
||||||
tools = self.get_tools()
|
tools = self.get_tools()
|
||||||
|
user_group_ids = {group.id for group in Groups.get_groups_by_member_id(user_id)}
|
||||||
|
|
||||||
return [
|
return [
|
||||||
tool
|
tool
|
||||||
for tool in tools
|
for tool in tools
|
||||||
if tool.user_id == user_id
|
if tool.user_id == user_id
|
||||||
or has_access(user_id, permission, tool.access_control)
|
or has_access(user_id, permission, tool.access_control, user_group_ids)
|
||||||
]
|
]
|
||||||
|
|
||||||
def get_tool_valves_by_id(self, id: str) -> Optional[dict]:
|
def get_tool_valves_by_id(self, id: str) -> Optional[dict]:
|
||||||
|
|
|
||||||
|
|
@ -64,7 +64,7 @@ class DatalabMarkerLoader:
|
||||||
return mime_map.get(ext, "application/octet-stream")
|
return mime_map.get(ext, "application/octet-stream")
|
||||||
|
|
||||||
def check_marker_request_status(self, request_id: str) -> dict:
|
def check_marker_request_status(self, request_id: str) -> dict:
|
||||||
url = f"{self.api_base_url}/marker/{request_id}"
|
url = f"{self.api_base_url}/{request_id}"
|
||||||
headers = {"X-Api-Key": self.api_key}
|
headers = {"X-Api-Key": self.api_key}
|
||||||
try:
|
try:
|
||||||
response = requests.get(url, headers=headers)
|
response = requests.get(url, headers=headers)
|
||||||
|
|
@ -111,7 +111,7 @@ class DatalabMarkerLoader:
|
||||||
with open(self.file_path, "rb") as f:
|
with open(self.file_path, "rb") as f:
|
||||||
files = {"file": (filename, f, mime_type)}
|
files = {"file": (filename, f, mime_type)}
|
||||||
response = requests.post(
|
response = requests.post(
|
||||||
f"{self.api_base_url}/marker",
|
f"{self.api_base_url}",
|
||||||
data=form_data,
|
data=form_data,
|
||||||
files=files,
|
files=files,
|
||||||
headers=headers,
|
headers=headers,
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import requests
|
import requests
|
||||||
import logging, os
|
import logging, os
|
||||||
from typing import Iterator, List, Union
|
from typing import Iterator, List, Union
|
||||||
|
from urllib.parse import quote
|
||||||
|
|
||||||
from langchain_core.document_loaders import BaseLoader
|
from langchain_core.document_loaders import BaseLoader
|
||||||
from langchain_core.documents import Document
|
from langchain_core.documents import Document
|
||||||
|
|
@ -37,7 +38,7 @@ class ExternalDocumentLoader(BaseLoader):
|
||||||
headers["Authorization"] = f"Bearer {self.api_key}"
|
headers["Authorization"] = f"Bearer {self.api_key}"
|
||||||
|
|
||||||
try:
|
try:
|
||||||
headers["X-Filename"] = os.path.basename(self.file_path)
|
headers["X-Filename"] = quote(os.path.basename(self.file_path))
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import ftfy
|
||||||
import sys
|
import sys
|
||||||
import json
|
import json
|
||||||
|
|
||||||
|
from azure.identity import DefaultAzureCredential
|
||||||
from langchain_community.document_loaders import (
|
from langchain_community.document_loaders import (
|
||||||
AzureAIDocumentIntelligenceLoader,
|
AzureAIDocumentIntelligenceLoader,
|
||||||
BSHTMLLoader,
|
BSHTMLLoader,
|
||||||
|
|
@ -147,7 +148,7 @@ class DoclingLoader:
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
params = {"image_export_mode": "placeholder", "table_mode": "accurate"}
|
params = {"image_export_mode": "placeholder"}
|
||||||
|
|
||||||
if self.params:
|
if self.params:
|
||||||
if self.params.get("do_picture_description"):
|
if self.params.get("do_picture_description"):
|
||||||
|
|
@ -173,7 +174,15 @@ class DoclingLoader:
|
||||||
self.params.get("picture_description_api", {})
|
self.params.get("picture_description_api", {})
|
||||||
)
|
)
|
||||||
|
|
||||||
if self.params.get("ocr_engine") and self.params.get("ocr_lang"):
|
params["do_ocr"] = self.params.get("do_ocr")
|
||||||
|
|
||||||
|
params["force_ocr"] = self.params.get("force_ocr")
|
||||||
|
|
||||||
|
if (
|
||||||
|
self.params.get("do_ocr")
|
||||||
|
and self.params.get("ocr_engine")
|
||||||
|
and self.params.get("ocr_lang")
|
||||||
|
):
|
||||||
params["ocr_engine"] = self.params.get("ocr_engine")
|
params["ocr_engine"] = self.params.get("ocr_engine")
|
||||||
params["ocr_lang"] = [
|
params["ocr_lang"] = [
|
||||||
lang.strip()
|
lang.strip()
|
||||||
|
|
@ -181,6 +190,15 @@ class DoclingLoader:
|
||||||
if lang.strip()
|
if lang.strip()
|
||||||
]
|
]
|
||||||
|
|
||||||
|
if self.params.get("pdf_backend"):
|
||||||
|
params["pdf_backend"] = self.params.get("pdf_backend")
|
||||||
|
|
||||||
|
if self.params.get("table_mode"):
|
||||||
|
params["table_mode"] = self.params.get("table_mode")
|
||||||
|
|
||||||
|
if self.params.get("pipeline"):
|
||||||
|
params["pipeline"] = self.params.get("pipeline")
|
||||||
|
|
||||||
endpoint = f"{self.url}/v1/convert/file"
|
endpoint = f"{self.url}/v1/convert/file"
|
||||||
r = requests.post(endpoint, files=files, data=params)
|
r = requests.post(endpoint, files=files, data=params)
|
||||||
|
|
||||||
|
|
@ -283,7 +301,7 @@ class Loader:
|
||||||
):
|
):
|
||||||
api_base_url = self.kwargs.get("DATALAB_MARKER_API_BASE_URL", "")
|
api_base_url = self.kwargs.get("DATALAB_MARKER_API_BASE_URL", "")
|
||||||
if not api_base_url or api_base_url.strip() == "":
|
if not api_base_url or api_base_url.strip() == "":
|
||||||
api_base_url = "https://www.datalab.to/api/v1"
|
api_base_url = "https://www.datalab.to/api/v1/marker" # https://github.com/open-webui/open-webui/pull/16867#issuecomment-3218424349
|
||||||
|
|
||||||
loader = DatalabMarkerLoader(
|
loader = DatalabMarkerLoader(
|
||||||
file_path=file_path,
|
file_path=file_path,
|
||||||
|
|
@ -327,7 +345,6 @@ class Loader:
|
||||||
elif (
|
elif (
|
||||||
self.engine == "document_intelligence"
|
self.engine == "document_intelligence"
|
||||||
and self.kwargs.get("DOCUMENT_INTELLIGENCE_ENDPOINT") != ""
|
and self.kwargs.get("DOCUMENT_INTELLIGENCE_ENDPOINT") != ""
|
||||||
and self.kwargs.get("DOCUMENT_INTELLIGENCE_KEY") != ""
|
|
||||||
and (
|
and (
|
||||||
file_ext in ["pdf", "xls", "xlsx", "docx", "ppt", "pptx"]
|
file_ext in ["pdf", "xls", "xlsx", "docx", "ppt", "pptx"]
|
||||||
or file_content_type
|
or file_content_type
|
||||||
|
|
@ -340,11 +357,18 @@ class Loader:
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
):
|
):
|
||||||
|
if self.kwargs.get("DOCUMENT_INTELLIGENCE_KEY") != "":
|
||||||
loader = AzureAIDocumentIntelligenceLoader(
|
loader = AzureAIDocumentIntelligenceLoader(
|
||||||
file_path=file_path,
|
file_path=file_path,
|
||||||
api_endpoint=self.kwargs.get("DOCUMENT_INTELLIGENCE_ENDPOINT"),
|
api_endpoint=self.kwargs.get("DOCUMENT_INTELLIGENCE_ENDPOINT"),
|
||||||
api_key=self.kwargs.get("DOCUMENT_INTELLIGENCE_KEY"),
|
api_key=self.kwargs.get("DOCUMENT_INTELLIGENCE_KEY"),
|
||||||
)
|
)
|
||||||
|
else:
|
||||||
|
loader = AzureAIDocumentIntelligenceLoader(
|
||||||
|
file_path=file_path,
|
||||||
|
api_endpoint=self.kwargs.get("DOCUMENT_INTELLIGENCE_ENDPOINT"),
|
||||||
|
azure_credential=DefaultAzureCredential(),
|
||||||
|
)
|
||||||
elif (
|
elif (
|
||||||
self.engine == "mistral_ocr"
|
self.engine == "mistral_ocr"
|
||||||
and self.kwargs.get("MISTRAL_OCR_API_KEY") != ""
|
and self.kwargs.get("MISTRAL_OCR_API_KEY") != ""
|
||||||
|
|
|
||||||
|
|
@ -98,10 +98,9 @@ class YoutubeLoader:
|
||||||
else:
|
else:
|
||||||
youtube_proxies = None
|
youtube_proxies = None
|
||||||
|
|
||||||
|
transcript_api = YouTubeTranscriptApi(proxy_config=youtube_proxies)
|
||||||
try:
|
try:
|
||||||
transcript_list = YouTubeTranscriptApi.list_transcripts(
|
transcript_list = transcript_api.list(self.video_id)
|
||||||
self.video_id, proxies=youtube_proxies
|
|
||||||
)
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.exception("Loading YouTube transcript failed")
|
log.exception("Loading YouTube transcript failed")
|
||||||
return []
|
return []
|
||||||
|
|
|
||||||
|
|
@ -124,9 +124,12 @@ def query_doc_with_hybrid_search(
|
||||||
hybrid_bm25_weight: float,
|
hybrid_bm25_weight: float,
|
||||||
) -> dict:
|
) -> dict:
|
||||||
try:
|
try:
|
||||||
# BM_25 required only if weight is greater than 0
|
if not collection_result.documents[0]:
|
||||||
if hybrid_bm25_weight > 0:
|
log.warning(f"query_doc_with_hybrid_search:no_docs {collection_name}")
|
||||||
|
return {"documents": [], "metadatas": [], "distances": []}
|
||||||
|
|
||||||
log.debug(f"query_doc_with_hybrid_search:doc {collection_name}")
|
log.debug(f"query_doc_with_hybrid_search:doc {collection_name}")
|
||||||
|
|
||||||
bm25_retriever = BM25Retriever.from_texts(
|
bm25_retriever = BM25Retriever.from_texts(
|
||||||
texts=collection_result.documents[0],
|
texts=collection_result.documents[0],
|
||||||
metadatas=collection_result.metadatas[0],
|
metadatas=collection_result.metadatas[0],
|
||||||
|
|
@ -339,8 +342,6 @@ def query_collection_with_hybrid_search(
|
||||||
# Fetch collection data once per collection sequentially
|
# Fetch collection data once per collection sequentially
|
||||||
# Avoid fetching the same data multiple times later
|
# Avoid fetching the same data multiple times later
|
||||||
collection_results = {}
|
collection_results = {}
|
||||||
# Only retrieve entire collection if bm_25 calculation is required
|
|
||||||
if hybrid_bm25_weight > 0:
|
|
||||||
for collection_name in collection_names:
|
for collection_name in collection_names:
|
||||||
try:
|
try:
|
||||||
log.debug(
|
log.debug(
|
||||||
|
|
@ -352,9 +353,7 @@ def query_collection_with_hybrid_search(
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.exception(f"Failed to fetch collection {collection_name}: {e}")
|
log.exception(f"Failed to fetch collection {collection_name}: {e}")
|
||||||
collection_results[collection_name] = None
|
collection_results[collection_name] = None
|
||||||
else:
|
|
||||||
for collection_name in collection_names:
|
|
||||||
collection_results[collection_name] = []
|
|
||||||
log.info(
|
log.info(
|
||||||
f"Starting hybrid search for {len(queries)} queries in {len(collection_names)} collections..."
|
f"Starting hybrid search for {len(queries)} queries in {len(collection_names)} collections..."
|
||||||
)
|
)
|
||||||
|
|
@ -489,17 +488,18 @@ def get_sources_from_items(
|
||||||
|
|
||||||
if item.get("type") == "text":
|
if item.get("type") == "text":
|
||||||
# Raw Text
|
# Raw Text
|
||||||
# Used during temporary chat file uploads
|
# Used during temporary chat file uploads or web page & youtube attachements
|
||||||
|
|
||||||
if item.get("file"):
|
if item.get("collection_name"):
|
||||||
|
# If item has a collection name, use it
|
||||||
|
collection_names.append(item.get("collection_name"))
|
||||||
|
elif item.get("file"):
|
||||||
# if item has file data, use it
|
# if item has file data, use it
|
||||||
query_result = {
|
query_result = {
|
||||||
"documents": [
|
"documents": [
|
||||||
[item.get("file", {}).get("data", {}).get("content")]
|
[item.get("file", {}).get("data", {}).get("content")]
|
||||||
],
|
],
|
||||||
"metadatas": [
|
"metadatas": [[item.get("file", {}).get("meta", {})]],
|
||||||
[item.get("file", {}).get("data", {}).get("meta", {})]
|
|
||||||
],
|
|
||||||
}
|
}
|
||||||
else:
|
else:
|
||||||
# Fallback to item content
|
# Fallback to item content
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,7 @@ from open_webui.retrieval.vector.main import (
|
||||||
from open_webui.config import (
|
from open_webui.config import (
|
||||||
PGVECTOR_DB_URL,
|
PGVECTOR_DB_URL,
|
||||||
PGVECTOR_INITIALIZE_MAX_VECTOR_LENGTH,
|
PGVECTOR_INITIALIZE_MAX_VECTOR_LENGTH,
|
||||||
|
PGVECTOR_CREATE_EXTENSION,
|
||||||
PGVECTOR_PGCRYPTO,
|
PGVECTOR_PGCRYPTO,
|
||||||
PGVECTOR_PGCRYPTO_KEY,
|
PGVECTOR_PGCRYPTO_KEY,
|
||||||
PGVECTOR_POOL_SIZE,
|
PGVECTOR_POOL_SIZE,
|
||||||
|
|
@ -112,6 +113,7 @@ class PgvectorClient(VectorDBBase):
|
||||||
try:
|
try:
|
||||||
# Ensure the pgvector extension is available
|
# Ensure the pgvector extension is available
|
||||||
# Use a conditional check to avoid permission issues on Azure PostgreSQL
|
# Use a conditional check to avoid permission issues on Azure PostgreSQL
|
||||||
|
if PGVECTOR_CREATE_EXTENSION:
|
||||||
self.session.execute(
|
self.session.execute(
|
||||||
text(
|
text(
|
||||||
"""
|
"""
|
||||||
|
|
|
||||||
|
|
@ -517,6 +517,7 @@ class SafeWebBaseLoader(WebBaseLoader):
|
||||||
async with session.get(
|
async with session.get(
|
||||||
url,
|
url,
|
||||||
**(self.requests_kwargs | kwargs),
|
**(self.requests_kwargs | kwargs),
|
||||||
|
allow_redirects=False,
|
||||||
) as response:
|
) as response:
|
||||||
if self.raise_for_status:
|
if self.raise_for_status:
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
|
|
@ -614,7 +615,7 @@ def get_web_loader(
|
||||||
WebLoaderClass = SafeWebBaseLoader
|
WebLoaderClass = SafeWebBaseLoader
|
||||||
if WEB_LOADER_ENGINE.value == "playwright":
|
if WEB_LOADER_ENGINE.value == "playwright":
|
||||||
WebLoaderClass = SafePlaywrightURLLoader
|
WebLoaderClass = SafePlaywrightURLLoader
|
||||||
web_loader_args["playwright_timeout"] = PLAYWRIGHT_TIMEOUT.value * 1000
|
web_loader_args["playwright_timeout"] = PLAYWRIGHT_TIMEOUT.value
|
||||||
if PLAYWRIGHT_WS_URL.value:
|
if PLAYWRIGHT_WS_URL.value:
|
||||||
web_loader_args["playwright_ws_url"] = PLAYWRIGHT_WS_URL.value
|
web_loader_args["playwright_ws_url"] = PLAYWRIGHT_WS_URL.value
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,6 @@ import logging
|
||||||
import os
|
import os
|
||||||
import uuid
|
import uuid
|
||||||
from functools import lru_cache
|
from functools import lru_cache
|
||||||
from pathlib import Path
|
|
||||||
from pydub import AudioSegment
|
from pydub import AudioSegment
|
||||||
from pydub.silence import split_on_silence
|
from pydub.silence import split_on_silence
|
||||||
from concurrent.futures import ThreadPoolExecutor
|
from concurrent.futures import ThreadPoolExecutor
|
||||||
|
|
@ -15,7 +14,7 @@ import aiohttp
|
||||||
import aiofiles
|
import aiofiles
|
||||||
import requests
|
import requests
|
||||||
import mimetypes
|
import mimetypes
|
||||||
from urllib.parse import quote
|
from urllib.parse import urljoin, quote
|
||||||
|
|
||||||
from fastapi import (
|
from fastapi import (
|
||||||
Depends,
|
Depends,
|
||||||
|
|
@ -338,7 +337,10 @@ async def speech(request: Request, user=Depends(get_verified_user)):
|
||||||
timeout=timeout, trust_env=True
|
timeout=timeout, trust_env=True
|
||||||
) as session:
|
) as session:
|
||||||
r = await session.post(
|
r = await session.post(
|
||||||
url=f"{request.app.state.config.TTS_OPENAI_API_BASE_URL}/audio/speech",
|
url=urljoin(
|
||||||
|
request.app.state.config.TTS_OPENAI_API_BASE_URL,
|
||||||
|
"/audio/speech",
|
||||||
|
),
|
||||||
json=payload,
|
json=payload,
|
||||||
headers={
|
headers={
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
|
|
@ -466,8 +468,10 @@ async def speech(request: Request, user=Depends(get_verified_user)):
|
||||||
timeout=timeout, trust_env=True
|
timeout=timeout, trust_env=True
|
||||||
) as session:
|
) as session:
|
||||||
async with session.post(
|
async with session.post(
|
||||||
(base_url or f"https://{region}.tts.speech.microsoft.com")
|
urljoin(
|
||||||
+ "/cognitiveservices/v1",
|
base_url or f"https://{region}.tts.speech.microsoft.com",
|
||||||
|
"/cognitiveservices/v1",
|
||||||
|
),
|
||||||
headers={
|
headers={
|
||||||
"Ocp-Apim-Subscription-Key": request.app.state.config.TTS_API_KEY,
|
"Ocp-Apim-Subscription-Key": request.app.state.config.TTS_API_KEY,
|
||||||
"Content-Type": "application/ssml+xml",
|
"Content-Type": "application/ssml+xml",
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ from open_webui.models.auths import (
|
||||||
)
|
)
|
||||||
from open_webui.models.users import Users, UpdateProfileForm
|
from open_webui.models.users import Users, UpdateProfileForm
|
||||||
from open_webui.models.groups import Groups
|
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.constants import ERROR_MESSAGES, WEBHOOK_MESSAGES
|
||||||
from open_webui.env import (
|
from open_webui.env import (
|
||||||
|
|
@ -29,6 +30,7 @@ from open_webui.env import (
|
||||||
WEBUI_AUTH_COOKIE_SAME_SITE,
|
WEBUI_AUTH_COOKIE_SAME_SITE,
|
||||||
WEBUI_AUTH_COOKIE_SECURE,
|
WEBUI_AUTH_COOKIE_SECURE,
|
||||||
WEBUI_AUTH_SIGNOUT_REDIRECT_URL,
|
WEBUI_AUTH_SIGNOUT_REDIRECT_URL,
|
||||||
|
ENABLE_INITIAL_ADMIN_SIGNUP,
|
||||||
SRC_LOG_LEVELS,
|
SRC_LOG_LEVELS,
|
||||||
)
|
)
|
||||||
from fastapi import APIRouter, Depends, HTTPException, Request, status
|
from fastapi import APIRouter, Depends, HTTPException, Request, status
|
||||||
|
|
@ -569,6 +571,7 @@ async def signup(request: Request, response: Response, form_data: SignupForm):
|
||||||
not request.app.state.config.ENABLE_SIGNUP
|
not request.app.state.config.ENABLE_SIGNUP
|
||||||
or not request.app.state.config.ENABLE_LOGIN_FORM
|
or not request.app.state.config.ENABLE_LOGIN_FORM
|
||||||
):
|
):
|
||||||
|
if has_users or not ENABLE_INITIAL_ADMIN_SIGNUP:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.ACCESS_PROHIBITED
|
status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.ACCESS_PROHIBITED
|
||||||
)
|
)
|
||||||
|
|
@ -674,19 +677,29 @@ async def signup(request: Request, response: Response, form_data: SignupForm):
|
||||||
async def signout(request: Request, response: Response):
|
async def signout(request: Request, response: Response):
|
||||||
response.delete_cookie("token")
|
response.delete_cookie("token")
|
||||||
response.delete_cookie("oui-session")
|
response.delete_cookie("oui-session")
|
||||||
|
|
||||||
if ENABLE_OAUTH_SIGNUP.value:
|
|
||||||
oauth_id_token = request.cookies.get("oauth_id_token")
|
|
||||||
if oauth_id_token and OPENID_PROVIDER_URL.value:
|
|
||||||
try:
|
|
||||||
async with ClientSession(trust_env=True) as session:
|
|
||||||
async with session.get(OPENID_PROVIDER_URL.value) as resp:
|
|
||||||
if resp.status == 200:
|
|
||||||
openid_data = await resp.json()
|
|
||||||
logout_url = openid_data.get("end_session_endpoint")
|
|
||||||
if logout_url:
|
|
||||||
response.delete_cookie("oauth_id_token")
|
response.delete_cookie("oauth_id_token")
|
||||||
|
|
||||||
|
oauth_session_id = request.cookies.get("oauth_session_id")
|
||||||
|
if oauth_session_id:
|
||||||
|
response.delete_cookie("oauth_session_id")
|
||||||
|
|
||||||
|
session = OAuthSessions.get_session_by_id(oauth_session_id)
|
||||||
|
oauth_server_metadata_url = (
|
||||||
|
request.app.state.oauth_manager.get_server_metadata_url(session.provider)
|
||||||
|
if session
|
||||||
|
else None
|
||||||
|
) or OPENID_PROVIDER_URL.value
|
||||||
|
|
||||||
|
if session and oauth_server_metadata_url:
|
||||||
|
oauth_id_token = session.token.get("id_token")
|
||||||
|
try:
|
||||||
|
async with ClientSession(trust_env=True) as session:
|
||||||
|
async with session.get(oauth_server_metadata_url) as r:
|
||||||
|
if r.status == 200:
|
||||||
|
openid_data = await r.json()
|
||||||
|
logout_url = openid_data.get("end_session_endpoint")
|
||||||
|
|
||||||
|
if logout_url:
|
||||||
return JSONResponse(
|
return JSONResponse(
|
||||||
status_code=200,
|
status_code=200,
|
||||||
content={
|
content={
|
||||||
|
|
@ -701,15 +714,14 @@ async def signout(request: Request, response: Response):
|
||||||
headers=response.headers,
|
headers=response.headers,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
raise HTTPException(
|
raise Exception("Failed to fetch OpenID configuration")
|
||||||
status_code=resp.status,
|
|
||||||
detail="Failed to fetch OpenID configuration",
|
|
||||||
)
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.error(f"OpenID signout error: {str(e)}")
|
log.error(f"OpenID signout error: {str(e)}")
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=500,
|
status_code=500,
|
||||||
detail="Failed to sign out from the OpenID provider.",
|
detail="Failed to sign out from the OpenID provider.",
|
||||||
|
headers=response.headers,
|
||||||
)
|
)
|
||||||
|
|
||||||
if WEBUI_AUTH_SIGNOUT_REDIRECT_URL:
|
if WEBUI_AUTH_SIGNOUT_REDIRECT_URL:
|
||||||
|
|
|
||||||
|
|
@ -143,9 +143,18 @@ def upload_file(
|
||||||
file: UploadFile = File(...),
|
file: UploadFile = File(...),
|
||||||
metadata: Optional[dict | str] = Form(None),
|
metadata: Optional[dict | str] = Form(None),
|
||||||
process: bool = Query(True),
|
process: bool = Query(True),
|
||||||
|
process_in_background: bool = Query(True),
|
||||||
user=Depends(get_verified_user),
|
user=Depends(get_verified_user),
|
||||||
):
|
):
|
||||||
return upload_file_handler(request, file, metadata, process, user, background_tasks)
|
return upload_file_handler(
|
||||||
|
request,
|
||||||
|
file=file,
|
||||||
|
metadata=metadata,
|
||||||
|
process=process,
|
||||||
|
process_in_background=process_in_background,
|
||||||
|
user=user,
|
||||||
|
background_tasks=background_tasks,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def upload_file_handler(
|
def upload_file_handler(
|
||||||
|
|
@ -153,6 +162,7 @@ def upload_file_handler(
|
||||||
file: UploadFile = File(...),
|
file: UploadFile = File(...),
|
||||||
metadata: Optional[dict | str] = Form(None),
|
metadata: Optional[dict | str] = Form(None),
|
||||||
process: bool = Query(True),
|
process: bool = Query(True),
|
||||||
|
process_in_background: bool = Query(True),
|
||||||
user=Depends(get_verified_user),
|
user=Depends(get_verified_user),
|
||||||
background_tasks: Optional[BackgroundTasks] = None,
|
background_tasks: Optional[BackgroundTasks] = None,
|
||||||
):
|
):
|
||||||
|
|
@ -225,7 +235,7 @@ def upload_file_handler(
|
||||||
)
|
)
|
||||||
|
|
||||||
if process:
|
if process:
|
||||||
if background_tasks:
|
if background_tasks and process_in_background:
|
||||||
background_tasks.add_task(
|
background_tasks.add_task(
|
||||||
process_uploaded_file,
|
process_uploaded_file,
|
||||||
request,
|
request,
|
||||||
|
|
@ -401,6 +411,7 @@ async def get_file_process_status(
|
||||||
MAX_FILE_PROCESSING_DURATION = 3600 * 2
|
MAX_FILE_PROCESSING_DURATION = 3600 * 2
|
||||||
|
|
||||||
async def event_stream(file_item):
|
async def event_stream(file_item):
|
||||||
|
if file_item:
|
||||||
for _ in range(MAX_FILE_PROCESSING_DURATION):
|
for _ in range(MAX_FILE_PROCESSING_DURATION):
|
||||||
file_item = Files.get_file_by_id(file_item.id)
|
file_item = Files.get_file_by_id(file_item.id)
|
||||||
if file_item:
|
if file_item:
|
||||||
|
|
@ -420,6 +431,8 @@ async def get_file_process_status(
|
||||||
break
|
break
|
||||||
|
|
||||||
await asyncio.sleep(0.5)
|
await asyncio.sleep(0.5)
|
||||||
|
else:
|
||||||
|
yield f"data: {json.dumps({'status': 'not_found'})}\n\n"
|
||||||
|
|
||||||
return StreamingResponse(
|
return StreamingResponse(
|
||||||
event_stream(file),
|
event_stream(file),
|
||||||
|
|
|
||||||
|
|
@ -10,10 +10,14 @@ import mimetypes
|
||||||
|
|
||||||
from open_webui.models.folders import (
|
from open_webui.models.folders import (
|
||||||
FolderForm,
|
FolderForm,
|
||||||
|
FolderUpdateForm,
|
||||||
FolderModel,
|
FolderModel,
|
||||||
Folders,
|
Folders,
|
||||||
)
|
)
|
||||||
from open_webui.models.chats import Chats
|
from open_webui.models.chats import Chats
|
||||||
|
from open_webui.models.files import Files
|
||||||
|
from open_webui.models.knowledge import Knowledges
|
||||||
|
|
||||||
|
|
||||||
from open_webui.config import UPLOAD_DIR
|
from open_webui.config import UPLOAD_DIR
|
||||||
from open_webui.env import SRC_LOG_LEVELS
|
from open_webui.env import SRC_LOG_LEVELS
|
||||||
|
|
@ -44,6 +48,31 @@ router = APIRouter()
|
||||||
async def get_folders(user=Depends(get_verified_user)):
|
async def get_folders(user=Depends(get_verified_user)):
|
||||||
folders = Folders.get_folders_by_user_id(user.id)
|
folders = Folders.get_folders_by_user_id(user.id)
|
||||||
|
|
||||||
|
# Verify folder data integrity
|
||||||
|
for folder in folders:
|
||||||
|
if folder.data:
|
||||||
|
if "files" in folder.data:
|
||||||
|
valid_files = []
|
||||||
|
for file in folder.data["files"]:
|
||||||
|
|
||||||
|
if file.get("type") == "file":
|
||||||
|
if Files.check_access_by_user_id(
|
||||||
|
file.get("id"), user.id, "read"
|
||||||
|
):
|
||||||
|
valid_files.append(file)
|
||||||
|
elif file.get("type") == "collection":
|
||||||
|
if Knowledges.check_access_by_user_id(
|
||||||
|
file.get("id"), user.id, "read"
|
||||||
|
):
|
||||||
|
valid_files.append(file)
|
||||||
|
else:
|
||||||
|
valid_files.append(file)
|
||||||
|
|
||||||
|
folder.data["files"] = valid_files
|
||||||
|
Folders.update_folder_by_id_and_user_id(
|
||||||
|
folder.id, user.id, FolderUpdateForm(data=folder.data)
|
||||||
|
)
|
||||||
|
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
**folder.model_dump(),
|
**folder.model_dump(),
|
||||||
|
|
@ -113,10 +142,13 @@ async def get_folder_by_id(id: str, user=Depends(get_verified_user)):
|
||||||
|
|
||||||
@router.post("/{id}/update")
|
@router.post("/{id}/update")
|
||||||
async def update_folder_name_by_id(
|
async def update_folder_name_by_id(
|
||||||
id: str, form_data: FolderForm, user=Depends(get_verified_user)
|
id: str, form_data: FolderUpdateForm, user=Depends(get_verified_user)
|
||||||
):
|
):
|
||||||
folder = Folders.get_folder_by_id_and_user_id(id, user.id)
|
folder = Folders.get_folder_by_id_and_user_id(id, user.id)
|
||||||
if folder:
|
if folder:
|
||||||
|
|
||||||
|
if form_data.name is not None:
|
||||||
|
# Check if folder with same name exists
|
||||||
existing_folder = Folders.get_folder_by_parent_id_and_user_id_and_name(
|
existing_folder = Folders.get_folder_by_parent_id_and_user_id_and_name(
|
||||||
folder.parent_id, user.id, form_data.name
|
folder.parent_id, user.id, form_data.name
|
||||||
)
|
)
|
||||||
|
|
@ -128,7 +160,6 @@ async def update_folder_name_by_id(
|
||||||
|
|
||||||
try:
|
try:
|
||||||
folder = Folders.update_folder_by_id_and_user_id(id, user.id, form_data)
|
folder = Folders.update_folder_by_id_and_user_id(id, user.id, form_data)
|
||||||
|
|
||||||
return folder
|
return folder
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.exception(e)
|
log.exception(e)
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ from open_webui.models.functions import (
|
||||||
FunctionForm,
|
FunctionForm,
|
||||||
FunctionModel,
|
FunctionModel,
|
||||||
FunctionResponse,
|
FunctionResponse,
|
||||||
|
FunctionWithValvesModel,
|
||||||
Functions,
|
Functions,
|
||||||
)
|
)
|
||||||
from open_webui.utils.plugin import (
|
from open_webui.utils.plugin import (
|
||||||
|
|
@ -46,9 +47,9 @@ async def get_functions(user=Depends(get_verified_user)):
|
||||||
############################
|
############################
|
||||||
|
|
||||||
|
|
||||||
@router.get("/export", response_model=list[FunctionModel])
|
@router.get("/export", response_model=list[FunctionModel | FunctionWithValvesModel])
|
||||||
async def get_functions(user=Depends(get_admin_user)):
|
async def get_functions(include_valves: bool = False, user=Depends(get_admin_user)):
|
||||||
return Functions.get_functions()
|
return Functions.get_functions(include_valves=include_valves)
|
||||||
|
|
||||||
|
|
||||||
############################
|
############################
|
||||||
|
|
@ -132,10 +133,10 @@ async def load_function_from_url(
|
||||||
|
|
||||||
|
|
||||||
class SyncFunctionsForm(BaseModel):
|
class SyncFunctionsForm(BaseModel):
|
||||||
functions: list[FunctionModel] = []
|
functions: list[FunctionWithValvesModel] = []
|
||||||
|
|
||||||
|
|
||||||
@router.post("/sync", response_model=list[FunctionModel])
|
@router.post("/sync", response_model=list[FunctionWithValvesModel])
|
||||||
async def sync_functions(
|
async def sync_functions(
|
||||||
request: Request, form_data: SyncFunctionsForm, user=Depends(get_admin_user)
|
request: Request, form_data: SyncFunctionsForm, user=Depends(get_admin_user)
|
||||||
):
|
):
|
||||||
|
|
|
||||||
|
|
@ -48,6 +48,7 @@ async def get_config(request: Request, user=Depends(get_admin_user)):
|
||||||
"prompt_generation": request.app.state.config.ENABLE_IMAGE_PROMPT_GENERATION,
|
"prompt_generation": request.app.state.config.ENABLE_IMAGE_PROMPT_GENERATION,
|
||||||
"openai": {
|
"openai": {
|
||||||
"OPENAI_API_BASE_URL": request.app.state.config.IMAGES_OPENAI_API_BASE_URL,
|
"OPENAI_API_BASE_URL": request.app.state.config.IMAGES_OPENAI_API_BASE_URL,
|
||||||
|
"OPENAI_API_VERSION": request.app.state.config.IMAGES_OPENAI_API_VERSION,
|
||||||
"OPENAI_API_KEY": request.app.state.config.IMAGES_OPENAI_API_KEY,
|
"OPENAI_API_KEY": request.app.state.config.IMAGES_OPENAI_API_KEY,
|
||||||
},
|
},
|
||||||
"automatic1111": {
|
"automatic1111": {
|
||||||
|
|
@ -72,6 +73,7 @@ async def get_config(request: Request, user=Depends(get_admin_user)):
|
||||||
|
|
||||||
class OpenAIConfigForm(BaseModel):
|
class OpenAIConfigForm(BaseModel):
|
||||||
OPENAI_API_BASE_URL: str
|
OPENAI_API_BASE_URL: str
|
||||||
|
OPENAI_API_VERSION: str
|
||||||
OPENAI_API_KEY: str
|
OPENAI_API_KEY: str
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -119,6 +121,9 @@ async def update_config(
|
||||||
request.app.state.config.IMAGES_OPENAI_API_BASE_URL = (
|
request.app.state.config.IMAGES_OPENAI_API_BASE_URL = (
|
||||||
form_data.openai.OPENAI_API_BASE_URL
|
form_data.openai.OPENAI_API_BASE_URL
|
||||||
)
|
)
|
||||||
|
request.app.state.config.IMAGES_OPENAI_API_VERSION = (
|
||||||
|
form_data.openai.OPENAI_API_VERSION
|
||||||
|
)
|
||||||
request.app.state.config.IMAGES_OPENAI_API_KEY = form_data.openai.OPENAI_API_KEY
|
request.app.state.config.IMAGES_OPENAI_API_KEY = form_data.openai.OPENAI_API_KEY
|
||||||
|
|
||||||
request.app.state.config.IMAGES_GEMINI_API_BASE_URL = (
|
request.app.state.config.IMAGES_GEMINI_API_BASE_URL = (
|
||||||
|
|
@ -165,6 +170,7 @@ async def update_config(
|
||||||
"prompt_generation": request.app.state.config.ENABLE_IMAGE_PROMPT_GENERATION,
|
"prompt_generation": request.app.state.config.ENABLE_IMAGE_PROMPT_GENERATION,
|
||||||
"openai": {
|
"openai": {
|
||||||
"OPENAI_API_BASE_URL": request.app.state.config.IMAGES_OPENAI_API_BASE_URL,
|
"OPENAI_API_BASE_URL": request.app.state.config.IMAGES_OPENAI_API_BASE_URL,
|
||||||
|
"OPENAI_API_VERSION": request.app.state.config.IMAGES_OPENAI_API_VERSION,
|
||||||
"OPENAI_API_KEY": request.app.state.config.IMAGES_OPENAI_API_KEY,
|
"OPENAI_API_KEY": request.app.state.config.IMAGES_OPENAI_API_KEY,
|
||||||
},
|
},
|
||||||
"automatic1111": {
|
"automatic1111": {
|
||||||
|
|
@ -544,10 +550,16 @@ async def image_generations(
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
api_version_query_param = ""
|
||||||
|
if request.app.state.config.IMAGES_OPENAI_API_VERSION:
|
||||||
|
api_version_query_param = (
|
||||||
|
f"?api-version={request.app.state.config.IMAGES_OPENAI_API_VERSION}"
|
||||||
|
)
|
||||||
|
|
||||||
# Use asyncio.to_thread for the requests.post call
|
# Use asyncio.to_thread for the requests.post call
|
||||||
r = await asyncio.to_thread(
|
r = await asyncio.to_thread(
|
||||||
requests.post,
|
requests.post,
|
||||||
url=f"{request.app.state.config.IMAGES_OPENAI_API_BASE_URL}/images/generations",
|
url=f"{request.app.state.config.IMAGES_OPENAI_API_BASE_URL}/images/generations{api_version_query_param}",
|
||||||
json=data,
|
json=data,
|
||||||
headers=headers,
|
headers=headers,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
from typing import List, Optional
|
from typing import List, Optional
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
from fastapi import APIRouter, Depends, HTTPException, status, Request
|
from fastapi import APIRouter, Depends, HTTPException, status, Request, Query
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from open_webui.models.knowledge import (
|
from open_webui.models.knowledge import (
|
||||||
|
|
@ -151,6 +151,18 @@ async def create_new_knowledge(
|
||||||
detail=ERROR_MESSAGES.UNAUTHORIZED,
|
detail=ERROR_MESSAGES.UNAUTHORIZED,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Check if user can share publicly
|
||||||
|
if (
|
||||||
|
user.role != "admin"
|
||||||
|
and form_data.access_control == None
|
||||||
|
and not has_permission(
|
||||||
|
user.id,
|
||||||
|
"sharing.public_knowledge",
|
||||||
|
request.app.state.config.USER_PERMISSIONS,
|
||||||
|
)
|
||||||
|
):
|
||||||
|
form_data.access_control = {}
|
||||||
|
|
||||||
knowledge = Knowledges.insert_new_knowledge(user.id, form_data)
|
knowledge = Knowledges.insert_new_knowledge(user.id, form_data)
|
||||||
|
|
||||||
if knowledge:
|
if knowledge:
|
||||||
|
|
@ -285,6 +297,7 @@ async def get_knowledge_by_id(id: str, user=Depends(get_verified_user)):
|
||||||
|
|
||||||
@router.post("/{id}/update", response_model=Optional[KnowledgeFilesResponse])
|
@router.post("/{id}/update", response_model=Optional[KnowledgeFilesResponse])
|
||||||
async def update_knowledge_by_id(
|
async def update_knowledge_by_id(
|
||||||
|
request: Request,
|
||||||
id: str,
|
id: str,
|
||||||
form_data: KnowledgeForm,
|
form_data: KnowledgeForm,
|
||||||
user=Depends(get_verified_user),
|
user=Depends(get_verified_user),
|
||||||
|
|
@ -306,10 +319,22 @@ async def update_knowledge_by_id(
|
||||||
detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
|
detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Check if user can share publicly
|
||||||
|
if (
|
||||||
|
user.role != "admin"
|
||||||
|
and form_data.access_control == None
|
||||||
|
and not has_permission(
|
||||||
|
user.id,
|
||||||
|
"sharing.public_knowledge",
|
||||||
|
request.app.state.config.USER_PERMISSIONS,
|
||||||
|
)
|
||||||
|
):
|
||||||
|
form_data.access_control = {}
|
||||||
|
|
||||||
knowledge = Knowledges.update_knowledge_by_id(id=id, form_data=form_data)
|
knowledge = Knowledges.update_knowledge_by_id(id=id, form_data=form_data)
|
||||||
if knowledge:
|
if knowledge:
|
||||||
file_ids = knowledge.data.get("file_ids", []) if knowledge.data else []
|
file_ids = knowledge.data.get("file_ids", []) if knowledge.data else []
|
||||||
files = Files.get_files_by_ids(file_ids)
|
files = Files.get_file_metadatas_by_ids(file_ids)
|
||||||
|
|
||||||
return KnowledgeFilesResponse(
|
return KnowledgeFilesResponse(
|
||||||
**knowledge.model_dump(),
|
**knowledge.model_dump(),
|
||||||
|
|
@ -492,6 +517,7 @@ def update_file_from_knowledge_by_id(
|
||||||
def remove_file_from_knowledge_by_id(
|
def remove_file_from_knowledge_by_id(
|
||||||
id: str,
|
id: str,
|
||||||
form_data: KnowledgeFileIdForm,
|
form_data: KnowledgeFileIdForm,
|
||||||
|
delete_file: bool = Query(True),
|
||||||
user=Depends(get_verified_user),
|
user=Depends(get_verified_user),
|
||||||
):
|
):
|
||||||
knowledge = Knowledges.get_knowledge_by_id(id=id)
|
knowledge = Knowledges.get_knowledge_by_id(id=id)
|
||||||
|
|
@ -528,6 +554,7 @@ def remove_file_from_knowledge_by_id(
|
||||||
log.debug(e)
|
log.debug(e)
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
if delete_file:
|
||||||
try:
|
try:
|
||||||
# Remove the file's collection from vector database
|
# Remove the file's collection from vector database
|
||||||
file_collection = f"file-{form_data.file_id}"
|
file_collection = f"file-{form_data.file_id}"
|
||||||
|
|
|
||||||
|
|
@ -329,7 +329,8 @@ def merge_ollama_models_lists(model_lists):
|
||||||
for idx, model_list in enumerate(model_lists):
|
for idx, model_list in enumerate(model_lists):
|
||||||
if model_list is not None:
|
if model_list is not None:
|
||||||
for model in model_list:
|
for model in model_list:
|
||||||
id = model["model"]
|
id = model.get("model")
|
||||||
|
if id is not None:
|
||||||
if id not in merged_models:
|
if id not in merged_models:
|
||||||
model["urls"] = [idx]
|
model["urls"] = [idx]
|
||||||
merged_models[id] = model
|
merged_models[id] = model
|
||||||
|
|
@ -339,7 +340,10 @@ def merge_ollama_models_lists(model_lists):
|
||||||
return list(merged_models.values())
|
return list(merged_models.values())
|
||||||
|
|
||||||
|
|
||||||
@cached(ttl=MODELS_CACHE_TTL)
|
@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):
|
||||||
log.info("get_all_models()")
|
log.info("get_all_models()")
|
||||||
if request.app.state.config.ENABLE_OLLAMA_API:
|
if request.app.state.config.ENABLE_OLLAMA_API:
|
||||||
|
|
|
||||||
|
|
@ -119,6 +119,74 @@ def openai_reasoning_model_handler(payload):
|
||||||
return payload
|
return payload
|
||||||
|
|
||||||
|
|
||||||
|
def get_headers_and_cookies(
|
||||||
|
request: Request,
|
||||||
|
url,
|
||||||
|
key=None,
|
||||||
|
config=None,
|
||||||
|
metadata: Optional[dict] = None,
|
||||||
|
user: UserModel = None,
|
||||||
|
):
|
||||||
|
cookies = {}
|
||||||
|
headers = {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
**(
|
||||||
|
{
|
||||||
|
"HTTP-Referer": "https://openwebui.com/",
|
||||||
|
"X-Title": "Open WebUI",
|
||||||
|
}
|
||||||
|
if "openrouter.ai" in url
|
||||||
|
else {}
|
||||||
|
),
|
||||||
|
**(
|
||||||
|
{
|
||||||
|
"X-OpenWebUI-User-Name": quote(user.name, safe=" "),
|
||||||
|
"X-OpenWebUI-User-Id": user.id,
|
||||||
|
"X-OpenWebUI-User-Email": user.email,
|
||||||
|
"X-OpenWebUI-User-Role": user.role,
|
||||||
|
**(
|
||||||
|
{"X-OpenWebUI-Chat-Id": metadata.get("chat_id")}
|
||||||
|
if metadata and metadata.get("chat_id")
|
||||||
|
else {}
|
||||||
|
),
|
||||||
|
}
|
||||||
|
if ENABLE_FORWARD_USER_INFO_HEADERS
|
||||||
|
else {}
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
token = None
|
||||||
|
auth_type = config.get("auth_type")
|
||||||
|
|
||||||
|
if auth_type == "bearer" or auth_type is None:
|
||||||
|
# Default to bearer if not specified
|
||||||
|
token = f"{key}"
|
||||||
|
elif auth_type == "none":
|
||||||
|
token = None
|
||||||
|
elif auth_type == "session":
|
||||||
|
cookies = request.cookies
|
||||||
|
token = request.state.token.credentials
|
||||||
|
elif auth_type == "system_oauth":
|
||||||
|
cookies = request.cookies
|
||||||
|
|
||||||
|
oauth_token = None
|
||||||
|
try:
|
||||||
|
oauth_token = request.app.state.oauth_manager.get_oauth_token(
|
||||||
|
user.id,
|
||||||
|
request.cookies.get("oauth_session_id", None),
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
log.error(f"Error getting OAuth token: {e}")
|
||||||
|
|
||||||
|
if oauth_token:
|
||||||
|
token = f"{oauth_token.get('access_token', '')}"
|
||||||
|
|
||||||
|
if token:
|
||||||
|
headers["Authorization"] = f"Bearer {token}"
|
||||||
|
|
||||||
|
return headers, cookies
|
||||||
|
|
||||||
|
|
||||||
##########################################
|
##########################################
|
||||||
#
|
#
|
||||||
# API routes
|
# API routes
|
||||||
|
|
@ -210,34 +278,23 @@ async def speech(request: Request, user=Depends(get_verified_user)):
|
||||||
return FileResponse(file_path)
|
return FileResponse(file_path)
|
||||||
|
|
||||||
url = request.app.state.config.OPENAI_API_BASE_URLS[idx]
|
url = request.app.state.config.OPENAI_API_BASE_URLS[idx]
|
||||||
|
key = request.app.state.config.OPENAI_API_KEYS[idx]
|
||||||
|
api_config = request.app.state.config.OPENAI_API_CONFIGS.get(
|
||||||
|
str(idx),
|
||||||
|
request.app.state.config.OPENAI_API_CONFIGS.get(url, {}), # Legacy support
|
||||||
|
)
|
||||||
|
|
||||||
|
headers, cookies = get_headers_and_cookies(
|
||||||
|
request, url, key, api_config, user=user
|
||||||
|
)
|
||||||
|
|
||||||
r = None
|
r = None
|
||||||
try:
|
try:
|
||||||
r = requests.post(
|
r = requests.post(
|
||||||
url=f"{url}/audio/speech",
|
url=f"{url}/audio/speech",
|
||||||
data=body,
|
data=body,
|
||||||
headers={
|
headers=headers,
|
||||||
"Content-Type": "application/json",
|
cookies=cookies,
|
||||||
"Authorization": f"Bearer {request.app.state.config.OPENAI_API_KEYS[idx]}",
|
|
||||||
**(
|
|
||||||
{
|
|
||||||
"HTTP-Referer": "https://openwebui.com/",
|
|
||||||
"X-Title": "Open WebUI",
|
|
||||||
}
|
|
||||||
if "openrouter.ai" in url
|
|
||||||
else {}
|
|
||||||
),
|
|
||||||
**(
|
|
||||||
{
|
|
||||||
"X-OpenWebUI-User-Name": quote(user.name, safe=" "),
|
|
||||||
"X-OpenWebUI-User-Id": user.id,
|
|
||||||
"X-OpenWebUI-User-Email": user.email,
|
|
||||||
"X-OpenWebUI-User-Role": user.role,
|
|
||||||
}
|
|
||||||
if ENABLE_FORWARD_USER_INFO_HEADERS
|
|
||||||
else {}
|
|
||||||
),
|
|
||||||
},
|
|
||||||
stream=True,
|
stream=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -401,7 +458,10 @@ async def get_filtered_models(models, user):
|
||||||
return filtered_models
|
return filtered_models
|
||||||
|
|
||||||
|
|
||||||
@cached(ttl=MODELS_CACHE_TTL)
|
@cached(
|
||||||
|
ttl=MODELS_CACHE_TTL,
|
||||||
|
key=lambda _, user: f"openai_all_models_{user.id}" if user else "openai_all_models",
|
||||||
|
)
|
||||||
async def get_all_models(request: Request, user: UserModel) -> dict[str, list]:
|
async def get_all_models(request: Request, user: UserModel) -> dict[str, list]:
|
||||||
log.info("get_all_models()")
|
log.info("get_all_models()")
|
||||||
|
|
||||||
|
|
@ -489,19 +549,9 @@ async def get_models(
|
||||||
timeout=aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST),
|
timeout=aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST),
|
||||||
) as session:
|
) as session:
|
||||||
try:
|
try:
|
||||||
headers = {
|
headers, cookies = get_headers_and_cookies(
|
||||||
"Content-Type": "application/json",
|
request, url, key, api_config, user=user
|
||||||
**(
|
)
|
||||||
{
|
|
||||||
"X-OpenWebUI-User-Name": quote(user.name, safe=" "),
|
|
||||||
"X-OpenWebUI-User-Id": user.id,
|
|
||||||
"X-OpenWebUI-User-Email": user.email,
|
|
||||||
"X-OpenWebUI-User-Role": user.role,
|
|
||||||
}
|
|
||||||
if ENABLE_FORWARD_USER_INFO_HEADERS
|
|
||||||
else {}
|
|
||||||
),
|
|
||||||
}
|
|
||||||
|
|
||||||
if api_config.get("azure", False):
|
if api_config.get("azure", False):
|
||||||
models = {
|
models = {
|
||||||
|
|
@ -509,11 +559,10 @@ async def get_models(
|
||||||
"object": "list",
|
"object": "list",
|
||||||
}
|
}
|
||||||
else:
|
else:
|
||||||
headers["Authorization"] = f"Bearer {key}"
|
|
||||||
|
|
||||||
async with session.get(
|
async with session.get(
|
||||||
f"{url}/models",
|
f"{url}/models",
|
||||||
headers=headers,
|
headers=headers,
|
||||||
|
cookies=cookies,
|
||||||
ssl=AIOHTTP_CLIENT_SESSION_SSL,
|
ssl=AIOHTTP_CLIENT_SESSION_SSL,
|
||||||
) as r:
|
) as r:
|
||||||
if r.status != 200:
|
if r.status != 200:
|
||||||
|
|
@ -572,7 +621,9 @@ class ConnectionVerificationForm(BaseModel):
|
||||||
|
|
||||||
@router.post("/verify")
|
@router.post("/verify")
|
||||||
async def verify_connection(
|
async def verify_connection(
|
||||||
form_data: ConnectionVerificationForm, user=Depends(get_admin_user)
|
request: Request,
|
||||||
|
form_data: ConnectionVerificationForm,
|
||||||
|
user=Depends(get_admin_user),
|
||||||
):
|
):
|
||||||
url = form_data.url
|
url = form_data.url
|
||||||
key = form_data.key
|
key = form_data.key
|
||||||
|
|
@ -584,19 +635,9 @@ async def verify_connection(
|
||||||
timeout=aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST),
|
timeout=aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST),
|
||||||
) as session:
|
) as session:
|
||||||
try:
|
try:
|
||||||
headers = {
|
headers, cookies = get_headers_and_cookies(
|
||||||
"Content-Type": "application/json",
|
request, url, key, api_config, user=user
|
||||||
**(
|
)
|
||||||
{
|
|
||||||
"X-OpenWebUI-User-Name": quote(user.name, safe=" "),
|
|
||||||
"X-OpenWebUI-User-Id": user.id,
|
|
||||||
"X-OpenWebUI-User-Email": user.email,
|
|
||||||
"X-OpenWebUI-User-Role": user.role,
|
|
||||||
}
|
|
||||||
if ENABLE_FORWARD_USER_INFO_HEADERS
|
|
||||||
else {}
|
|
||||||
),
|
|
||||||
}
|
|
||||||
|
|
||||||
if api_config.get("azure", False):
|
if api_config.get("azure", False):
|
||||||
headers["api-key"] = key
|
headers["api-key"] = key
|
||||||
|
|
@ -605,6 +646,7 @@ async def verify_connection(
|
||||||
async with session.get(
|
async with session.get(
|
||||||
url=f"{url}/openai/models?api-version={api_version}",
|
url=f"{url}/openai/models?api-version={api_version}",
|
||||||
headers=headers,
|
headers=headers,
|
||||||
|
cookies=cookies,
|
||||||
ssl=AIOHTTP_CLIENT_SESSION_SSL,
|
ssl=AIOHTTP_CLIENT_SESSION_SSL,
|
||||||
) as r:
|
) as r:
|
||||||
try:
|
try:
|
||||||
|
|
@ -624,11 +666,10 @@ async def verify_connection(
|
||||||
|
|
||||||
return response_data
|
return response_data
|
||||||
else:
|
else:
|
||||||
headers["Authorization"] = f"Bearer {key}"
|
|
||||||
|
|
||||||
async with session.get(
|
async with session.get(
|
||||||
f"{url}/models",
|
f"{url}/models",
|
||||||
headers=headers,
|
headers=headers,
|
||||||
|
cookies=cookies,
|
||||||
ssl=AIOHTTP_CLIENT_SESSION_SSL,
|
ssl=AIOHTTP_CLIENT_SESSION_SSL,
|
||||||
) as r:
|
) as r:
|
||||||
try:
|
try:
|
||||||
|
|
@ -836,32 +877,9 @@ async def generate_chat_completion(
|
||||||
convert_logit_bias_input_to_json(payload["logit_bias"])
|
convert_logit_bias_input_to_json(payload["logit_bias"])
|
||||||
)
|
)
|
||||||
|
|
||||||
headers = {
|
headers, cookies = get_headers_and_cookies(
|
||||||
"Content-Type": "application/json",
|
request, url, key, api_config, metadata, user=user
|
||||||
**(
|
)
|
||||||
{
|
|
||||||
"HTTP-Referer": "https://openwebui.com/",
|
|
||||||
"X-Title": "Open WebUI",
|
|
||||||
}
|
|
||||||
if "openrouter.ai" in url
|
|
||||||
else {}
|
|
||||||
),
|
|
||||||
**(
|
|
||||||
{
|
|
||||||
"X-OpenWebUI-User-Name": quote(user.name, safe=" "),
|
|
||||||
"X-OpenWebUI-User-Id": user.id,
|
|
||||||
"X-OpenWebUI-User-Email": user.email,
|
|
||||||
"X-OpenWebUI-User-Role": user.role,
|
|
||||||
**(
|
|
||||||
{"X-OpenWebUI-Chat-Id": metadata.get("chat_id")}
|
|
||||||
if metadata and metadata.get("chat_id")
|
|
||||||
else {}
|
|
||||||
),
|
|
||||||
}
|
|
||||||
if ENABLE_FORWARD_USER_INFO_HEADERS
|
|
||||||
else {}
|
|
||||||
),
|
|
||||||
}
|
|
||||||
|
|
||||||
if api_config.get("azure", False):
|
if api_config.get("azure", False):
|
||||||
api_version = api_config.get("api_version", "2023-03-15-preview")
|
api_version = api_config.get("api_version", "2023-03-15-preview")
|
||||||
|
|
@ -871,7 +889,6 @@ async def generate_chat_completion(
|
||||||
request_url = f"{request_url}/chat/completions?api-version={api_version}"
|
request_url = f"{request_url}/chat/completions?api-version={api_version}"
|
||||||
else:
|
else:
|
||||||
request_url = f"{url}/chat/completions"
|
request_url = f"{url}/chat/completions"
|
||||||
headers["Authorization"] = f"Bearer {key}"
|
|
||||||
|
|
||||||
payload = json.dumps(payload)
|
payload = json.dumps(payload)
|
||||||
|
|
||||||
|
|
@ -890,6 +907,7 @@ async def generate_chat_completion(
|
||||||
url=request_url,
|
url=request_url,
|
||||||
data=payload,
|
data=payload,
|
||||||
headers=headers,
|
headers=headers,
|
||||||
|
cookies=cookies,
|
||||||
ssl=AIOHTTP_CLIENT_SESSION_SSL,
|
ssl=AIOHTTP_CLIENT_SESSION_SSL,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -951,31 +969,27 @@ async def embeddings(request: Request, form_data: dict, user):
|
||||||
models = request.app.state.OPENAI_MODELS
|
models = request.app.state.OPENAI_MODELS
|
||||||
if model_id in models:
|
if model_id in models:
|
||||||
idx = models[model_id]["urlIdx"]
|
idx = models[model_id]["urlIdx"]
|
||||||
|
|
||||||
url = request.app.state.config.OPENAI_API_BASE_URLS[idx]
|
url = request.app.state.config.OPENAI_API_BASE_URLS[idx]
|
||||||
key = request.app.state.config.OPENAI_API_KEYS[idx]
|
key = request.app.state.config.OPENAI_API_KEYS[idx]
|
||||||
|
api_config = request.app.state.config.OPENAI_API_CONFIGS.get(
|
||||||
|
str(idx),
|
||||||
|
request.app.state.config.OPENAI_API_CONFIGS.get(url, {}), # Legacy support
|
||||||
|
)
|
||||||
|
|
||||||
r = None
|
r = None
|
||||||
session = None
|
session = None
|
||||||
streaming = False
|
streaming = False
|
||||||
|
|
||||||
|
headers, cookies = get_headers_and_cookies(request, url, key, api_config, user=user)
|
||||||
try:
|
try:
|
||||||
session = aiohttp.ClientSession(trust_env=True)
|
session = aiohttp.ClientSession(trust_env=True)
|
||||||
r = await session.request(
|
r = await session.request(
|
||||||
method="POST",
|
method="POST",
|
||||||
url=f"{url}/embeddings",
|
url=f"{url}/embeddings",
|
||||||
data=body,
|
data=body,
|
||||||
headers={
|
headers=headers,
|
||||||
"Authorization": f"Bearer {key}",
|
cookies=cookies,
|
||||||
"Content-Type": "application/json",
|
|
||||||
**(
|
|
||||||
{
|
|
||||||
"X-OpenWebUI-User-Name": quote(user.name, safe=" "),
|
|
||||||
"X-OpenWebUI-User-Id": user.id,
|
|
||||||
"X-OpenWebUI-User-Email": user.email,
|
|
||||||
"X-OpenWebUI-User-Role": user.role,
|
|
||||||
}
|
|
||||||
if ENABLE_FORWARD_USER_INFO_HEADERS and user
|
|
||||||
else {}
|
|
||||||
),
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if "text/event-stream" in r.headers.get("Content-Type", ""):
|
if "text/event-stream" in r.headers.get("Content-Type", ""):
|
||||||
|
|
@ -1037,19 +1051,9 @@ async def proxy(path: str, request: Request, user=Depends(get_verified_user)):
|
||||||
streaming = False
|
streaming = False
|
||||||
|
|
||||||
try:
|
try:
|
||||||
headers = {
|
headers, cookies = get_headers_and_cookies(
|
||||||
"Content-Type": "application/json",
|
request, url, key, api_config, user=user
|
||||||
**(
|
)
|
||||||
{
|
|
||||||
"X-OpenWebUI-User-Name": quote(user.name, safe=" "),
|
|
||||||
"X-OpenWebUI-User-Id": user.id,
|
|
||||||
"X-OpenWebUI-User-Email": user.email,
|
|
||||||
"X-OpenWebUI-User-Role": user.role,
|
|
||||||
}
|
|
||||||
if ENABLE_FORWARD_USER_INFO_HEADERS
|
|
||||||
else {}
|
|
||||||
),
|
|
||||||
}
|
|
||||||
|
|
||||||
if api_config.get("azure", False):
|
if api_config.get("azure", False):
|
||||||
api_version = api_config.get("api_version", "2023-03-15-preview")
|
api_version = api_config.get("api_version", "2023-03-15-preview")
|
||||||
|
|
@ -1062,7 +1066,6 @@ async def proxy(path: str, request: Request, user=Depends(get_verified_user)):
|
||||||
|
|
||||||
request_url = f"{url}/{path}?api-version={api_version}"
|
request_url = f"{url}/{path}?api-version={api_version}"
|
||||||
else:
|
else:
|
||||||
headers["Authorization"] = f"Bearer {key}"
|
|
||||||
request_url = f"{url}/{path}"
|
request_url = f"{url}/{path}"
|
||||||
|
|
||||||
session = aiohttp.ClientSession(trust_env=True)
|
session = aiohttp.ClientSession(trust_env=True)
|
||||||
|
|
@ -1071,6 +1074,7 @@ async def proxy(path: str, request: Request, user=Depends(get_verified_user)):
|
||||||
url=request_url,
|
url=request_url,
|
||||||
data=body,
|
data=body,
|
||||||
headers=headers,
|
headers=headers,
|
||||||
|
cookies=cookies,
|
||||||
ssl=AIOHTTP_CLIENT_SESSION_SSL,
|
ssl=AIOHTTP_CLIENT_SESSION_SSL,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -426,8 +426,13 @@ async def get_rag_config(request: Request, user=Depends(get_admin_user)):
|
||||||
"EXTERNAL_DOCUMENT_LOADER_API_KEY": request.app.state.config.EXTERNAL_DOCUMENT_LOADER_API_KEY,
|
"EXTERNAL_DOCUMENT_LOADER_API_KEY": request.app.state.config.EXTERNAL_DOCUMENT_LOADER_API_KEY,
|
||||||
"TIKA_SERVER_URL": request.app.state.config.TIKA_SERVER_URL,
|
"TIKA_SERVER_URL": request.app.state.config.TIKA_SERVER_URL,
|
||||||
"DOCLING_SERVER_URL": request.app.state.config.DOCLING_SERVER_URL,
|
"DOCLING_SERVER_URL": request.app.state.config.DOCLING_SERVER_URL,
|
||||||
|
"DOCLING_DO_OCR": request.app.state.config.DOCLING_DO_OCR,
|
||||||
|
"DOCLING_FORCE_OCR": request.app.state.config.DOCLING_FORCE_OCR,
|
||||||
"DOCLING_OCR_ENGINE": request.app.state.config.DOCLING_OCR_ENGINE,
|
"DOCLING_OCR_ENGINE": request.app.state.config.DOCLING_OCR_ENGINE,
|
||||||
"DOCLING_OCR_LANG": request.app.state.config.DOCLING_OCR_LANG,
|
"DOCLING_OCR_LANG": request.app.state.config.DOCLING_OCR_LANG,
|
||||||
|
"DOCLING_PDF_BACKEND": request.app.state.config.DOCLING_PDF_BACKEND,
|
||||||
|
"DOCLING_TABLE_MODE": request.app.state.config.DOCLING_TABLE_MODE,
|
||||||
|
"DOCLING_PIPELINE": request.app.state.config.DOCLING_PIPELINE,
|
||||||
"DOCLING_DO_PICTURE_DESCRIPTION": request.app.state.config.DOCLING_DO_PICTURE_DESCRIPTION,
|
"DOCLING_DO_PICTURE_DESCRIPTION": request.app.state.config.DOCLING_DO_PICTURE_DESCRIPTION,
|
||||||
"DOCLING_PICTURE_DESCRIPTION_MODE": request.app.state.config.DOCLING_PICTURE_DESCRIPTION_MODE,
|
"DOCLING_PICTURE_DESCRIPTION_MODE": request.app.state.config.DOCLING_PICTURE_DESCRIPTION_MODE,
|
||||||
"DOCLING_PICTURE_DESCRIPTION_LOCAL": request.app.state.config.DOCLING_PICTURE_DESCRIPTION_LOCAL,
|
"DOCLING_PICTURE_DESCRIPTION_LOCAL": request.app.state.config.DOCLING_PICTURE_DESCRIPTION_LOCAL,
|
||||||
|
|
@ -596,8 +601,13 @@ class ConfigForm(BaseModel):
|
||||||
|
|
||||||
TIKA_SERVER_URL: Optional[str] = None
|
TIKA_SERVER_URL: Optional[str] = None
|
||||||
DOCLING_SERVER_URL: Optional[str] = None
|
DOCLING_SERVER_URL: Optional[str] = None
|
||||||
|
DOCLING_DO_OCR: Optional[bool] = None
|
||||||
|
DOCLING_FORCE_OCR: Optional[bool] = None
|
||||||
DOCLING_OCR_ENGINE: Optional[str] = None
|
DOCLING_OCR_ENGINE: Optional[str] = None
|
||||||
DOCLING_OCR_LANG: Optional[str] = None
|
DOCLING_OCR_LANG: Optional[str] = None
|
||||||
|
DOCLING_PDF_BACKEND: Optional[str] = None
|
||||||
|
DOCLING_TABLE_MODE: Optional[str] = None
|
||||||
|
DOCLING_PIPELINE: Optional[str] = None
|
||||||
DOCLING_DO_PICTURE_DESCRIPTION: Optional[bool] = None
|
DOCLING_DO_PICTURE_DESCRIPTION: Optional[bool] = None
|
||||||
DOCLING_PICTURE_DESCRIPTION_MODE: Optional[str] = None
|
DOCLING_PICTURE_DESCRIPTION_MODE: Optional[str] = None
|
||||||
DOCLING_PICTURE_DESCRIPTION_LOCAL: Optional[dict] = None
|
DOCLING_PICTURE_DESCRIPTION_LOCAL: Optional[dict] = None
|
||||||
|
|
@ -767,6 +777,16 @@ async def update_rag_config(
|
||||||
if form_data.DOCLING_SERVER_URL is not None
|
if form_data.DOCLING_SERVER_URL is not None
|
||||||
else request.app.state.config.DOCLING_SERVER_URL
|
else request.app.state.config.DOCLING_SERVER_URL
|
||||||
)
|
)
|
||||||
|
request.app.state.config.DOCLING_DO_OCR = (
|
||||||
|
form_data.DOCLING_DO_OCR
|
||||||
|
if form_data.DOCLING_DO_OCR is not None
|
||||||
|
else request.app.state.config.DOCLING_DO_OCR
|
||||||
|
)
|
||||||
|
request.app.state.config.DOCLING_FORCE_OCR = (
|
||||||
|
form_data.DOCLING_FORCE_OCR
|
||||||
|
if form_data.DOCLING_FORCE_OCR is not None
|
||||||
|
else request.app.state.config.DOCLING_FORCE_OCR
|
||||||
|
)
|
||||||
request.app.state.config.DOCLING_OCR_ENGINE = (
|
request.app.state.config.DOCLING_OCR_ENGINE = (
|
||||||
form_data.DOCLING_OCR_ENGINE
|
form_data.DOCLING_OCR_ENGINE
|
||||||
if form_data.DOCLING_OCR_ENGINE is not None
|
if form_data.DOCLING_OCR_ENGINE is not None
|
||||||
|
|
@ -777,7 +797,21 @@ async def update_rag_config(
|
||||||
if form_data.DOCLING_OCR_LANG is not None
|
if form_data.DOCLING_OCR_LANG is not None
|
||||||
else request.app.state.config.DOCLING_OCR_LANG
|
else request.app.state.config.DOCLING_OCR_LANG
|
||||||
)
|
)
|
||||||
|
request.app.state.config.DOCLING_PDF_BACKEND = (
|
||||||
|
form_data.DOCLING_PDF_BACKEND
|
||||||
|
if form_data.DOCLING_PDF_BACKEND is not None
|
||||||
|
else request.app.state.config.DOCLING_PDF_BACKEND
|
||||||
|
)
|
||||||
|
request.app.state.config.DOCLING_TABLE_MODE = (
|
||||||
|
form_data.DOCLING_TABLE_MODE
|
||||||
|
if form_data.DOCLING_TABLE_MODE is not None
|
||||||
|
else request.app.state.config.DOCLING_TABLE_MODE
|
||||||
|
)
|
||||||
|
request.app.state.config.DOCLING_PIPELINE = (
|
||||||
|
form_data.DOCLING_PIPELINE
|
||||||
|
if form_data.DOCLING_PIPELINE is not None
|
||||||
|
else request.app.state.config.DOCLING_PIPELINE
|
||||||
|
)
|
||||||
request.app.state.config.DOCLING_DO_PICTURE_DESCRIPTION = (
|
request.app.state.config.DOCLING_DO_PICTURE_DESCRIPTION = (
|
||||||
form_data.DOCLING_DO_PICTURE_DESCRIPTION
|
form_data.DOCLING_DO_PICTURE_DESCRIPTION
|
||||||
if form_data.DOCLING_DO_PICTURE_DESCRIPTION is not None
|
if form_data.DOCLING_DO_PICTURE_DESCRIPTION is not None
|
||||||
|
|
@ -1062,8 +1096,13 @@ async def update_rag_config(
|
||||||
"EXTERNAL_DOCUMENT_LOADER_API_KEY": request.app.state.config.EXTERNAL_DOCUMENT_LOADER_API_KEY,
|
"EXTERNAL_DOCUMENT_LOADER_API_KEY": request.app.state.config.EXTERNAL_DOCUMENT_LOADER_API_KEY,
|
||||||
"TIKA_SERVER_URL": request.app.state.config.TIKA_SERVER_URL,
|
"TIKA_SERVER_URL": request.app.state.config.TIKA_SERVER_URL,
|
||||||
"DOCLING_SERVER_URL": request.app.state.config.DOCLING_SERVER_URL,
|
"DOCLING_SERVER_URL": request.app.state.config.DOCLING_SERVER_URL,
|
||||||
|
"DOCLING_DO_OCR": request.app.state.config.DOCLING_DO_OCR,
|
||||||
|
"DOCLING_FORCE_OCR": request.app.state.config.DOCLING_FORCE_OCR,
|
||||||
"DOCLING_OCR_ENGINE": request.app.state.config.DOCLING_OCR_ENGINE,
|
"DOCLING_OCR_ENGINE": request.app.state.config.DOCLING_OCR_ENGINE,
|
||||||
"DOCLING_OCR_LANG": request.app.state.config.DOCLING_OCR_LANG,
|
"DOCLING_OCR_LANG": request.app.state.config.DOCLING_OCR_LANG,
|
||||||
|
"DOCLING_PDF_BACKEND": request.app.state.config.DOCLING_PDF_BACKEND,
|
||||||
|
"DOCLING_TABLE_MODE": request.app.state.config.DOCLING_TABLE_MODE,
|
||||||
|
"DOCLING_PIPELINE": request.app.state.config.DOCLING_PIPELINE,
|
||||||
"DOCLING_DO_PICTURE_DESCRIPTION": request.app.state.config.DOCLING_DO_PICTURE_DESCRIPTION,
|
"DOCLING_DO_PICTURE_DESCRIPTION": request.app.state.config.DOCLING_DO_PICTURE_DESCRIPTION,
|
||||||
"DOCLING_PICTURE_DESCRIPTION_MODE": request.app.state.config.DOCLING_PICTURE_DESCRIPTION_MODE,
|
"DOCLING_PICTURE_DESCRIPTION_MODE": request.app.state.config.DOCLING_PICTURE_DESCRIPTION_MODE,
|
||||||
"DOCLING_PICTURE_DESCRIPTION_LOCAL": request.app.state.config.DOCLING_PICTURE_DESCRIPTION_LOCAL,
|
"DOCLING_PICTURE_DESCRIPTION_LOCAL": request.app.state.config.DOCLING_PICTURE_DESCRIPTION_LOCAL,
|
||||||
|
|
@ -1453,8 +1492,13 @@ def process_file(
|
||||||
TIKA_SERVER_URL=request.app.state.config.TIKA_SERVER_URL,
|
TIKA_SERVER_URL=request.app.state.config.TIKA_SERVER_URL,
|
||||||
DOCLING_SERVER_URL=request.app.state.config.DOCLING_SERVER_URL,
|
DOCLING_SERVER_URL=request.app.state.config.DOCLING_SERVER_URL,
|
||||||
DOCLING_PARAMS={
|
DOCLING_PARAMS={
|
||||||
|
"do_ocr": request.app.state.config.DOCLING_DO_OCR,
|
||||||
|
"force_ocr": request.app.state.config.DOCLING_FORCE_OCR,
|
||||||
"ocr_engine": request.app.state.config.DOCLING_OCR_ENGINE,
|
"ocr_engine": request.app.state.config.DOCLING_OCR_ENGINE,
|
||||||
"ocr_lang": request.app.state.config.DOCLING_OCR_LANG,
|
"ocr_lang": request.app.state.config.DOCLING_OCR_LANG,
|
||||||
|
"pdf_backend": request.app.state.config.DOCLING_PDF_BACKEND,
|
||||||
|
"table_mode": request.app.state.config.DOCLING_TABLE_MODE,
|
||||||
|
"pipeline": request.app.state.config.DOCLING_PIPELINE,
|
||||||
"do_picture_description": request.app.state.config.DOCLING_DO_PICTURE_DESCRIPTION,
|
"do_picture_description": request.app.state.config.DOCLING_DO_PICTURE_DESCRIPTION,
|
||||||
"picture_description_mode": request.app.state.config.DOCLING_PICTURE_DESCRIPTION_MODE,
|
"picture_description_mode": request.app.state.config.DOCLING_PICTURE_DESCRIPTION_MODE,
|
||||||
"picture_description_local": request.app.state.config.DOCLING_PICTURE_DESCRIPTION_LOCAL,
|
"picture_description_local": request.app.state.config.DOCLING_PICTURE_DESCRIPTION_LOCAL,
|
||||||
|
|
@ -1945,6 +1989,8 @@ async def process_web_search(
|
||||||
):
|
):
|
||||||
|
|
||||||
urls = []
|
urls = []
|
||||||
|
result_items = []
|
||||||
|
|
||||||
try:
|
try:
|
||||||
logging.info(
|
logging.info(
|
||||||
f"trying to web search with {request.app.state.config.WEB_SEARCH_ENGINE, form_data.queries}"
|
f"trying to web search with {request.app.state.config.WEB_SEARCH_ENGINE, form_data.queries}"
|
||||||
|
|
@ -1966,6 +2012,7 @@ async def process_web_search(
|
||||||
if result:
|
if result:
|
||||||
for item in result:
|
for item in result:
|
||||||
if item and item.link:
|
if item and item.link:
|
||||||
|
result_items.append(item)
|
||||||
urls.append(item.link)
|
urls.append(item.link)
|
||||||
|
|
||||||
urls = list(dict.fromkeys(urls))
|
urls = list(dict.fromkeys(urls))
|
||||||
|
|
@ -2010,12 +2057,16 @@ async def process_web_search(
|
||||||
urls = [
|
urls = [
|
||||||
doc.metadata.get("source") for doc in docs if doc.metadata.get("source")
|
doc.metadata.get("source") for doc in docs if doc.metadata.get("source")
|
||||||
] # only keep the urls returned by the loader
|
] # only keep the urls returned by the loader
|
||||||
|
result_items = [
|
||||||
|
dict(item) for item in result_items if item.link in urls
|
||||||
|
] # only keep the search results that have been loaded
|
||||||
|
|
||||||
if request.app.state.config.BYPASS_WEB_SEARCH_EMBEDDING_AND_RETRIEVAL:
|
if request.app.state.config.BYPASS_WEB_SEARCH_EMBEDDING_AND_RETRIEVAL:
|
||||||
return {
|
return {
|
||||||
"status": True,
|
"status": True,
|
||||||
"collection_name": None,
|
"collection_name": None,
|
||||||
"filenames": urls,
|
"filenames": urls,
|
||||||
|
"items": result_items,
|
||||||
"docs": [
|
"docs": [
|
||||||
{
|
{
|
||||||
"content": doc.page_content,
|
"content": doc.page_content,
|
||||||
|
|
@ -2048,6 +2099,7 @@ async def process_web_search(
|
||||||
return {
|
return {
|
||||||
"status": True,
|
"status": True,
|
||||||
"collection_names": [collection_name],
|
"collection_names": [collection_name],
|
||||||
|
"items": result_items,
|
||||||
"filenames": urls,
|
"filenames": urls,
|
||||||
"loaded_count": len(docs),
|
"loaded_count": len(docs),
|
||||||
}
|
}
|
||||||
|
|
@ -2075,7 +2127,9 @@ def query_doc_handler(
|
||||||
user=Depends(get_verified_user),
|
user=Depends(get_verified_user),
|
||||||
):
|
):
|
||||||
try:
|
try:
|
||||||
if request.app.state.config.ENABLE_RAG_HYBRID_SEARCH:
|
if request.app.state.config.ENABLE_RAG_HYBRID_SEARCH and (
|
||||||
|
form_data.hybrid is None or form_data.hybrid
|
||||||
|
):
|
||||||
collection_results = {}
|
collection_results = {}
|
||||||
collection_results[form_data.collection_name] = VECTOR_DB_CLIENT.get(
|
collection_results[form_data.collection_name] = VECTOR_DB_CLIENT.get(
|
||||||
collection_name=form_data.collection_name
|
collection_name=form_data.collection_name
|
||||||
|
|
@ -2145,7 +2199,9 @@ def query_collection_handler(
|
||||||
user=Depends(get_verified_user),
|
user=Depends(get_verified_user),
|
||||||
):
|
):
|
||||||
try:
|
try:
|
||||||
if request.app.state.config.ENABLE_RAG_HYBRID_SEARCH:
|
if request.app.state.config.ENABLE_RAG_HYBRID_SEARCH and (
|
||||||
|
form_data.hybrid is None or form_data.hybrid
|
||||||
|
):
|
||||||
return query_collection_with_hybrid_search(
|
return query_collection_with_hybrid_search(
|
||||||
collection_names=form_data.collection_names,
|
collection_names=form_data.collection_names,
|
||||||
queries=[form_data.query],
|
queries=[form_data.query],
|
||||||
|
|
|
||||||
|
|
@ -470,6 +470,10 @@ async def generate_queries(
|
||||||
detail=f"Query generation is disabled",
|
detail=f"Query generation is disabled",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if getattr(request.state, "cached_queries", None):
|
||||||
|
log.info(f"Reusing cached queries: {request.state.cached_queries}")
|
||||||
|
return request.state.cached_queries
|
||||||
|
|
||||||
if getattr(request.state, "direct", False) and hasattr(request.state, "model"):
|
if getattr(request.state, "direct", False) and hasattr(request.state, "model"):
|
||||||
models = {
|
models = {
|
||||||
request.state.model["id"]: request.state.model,
|
request.state.model["id"]: request.state.model,
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ from typing import Optional
|
||||||
import time
|
import time
|
||||||
import re
|
import re
|
||||||
import aiohttp
|
import aiohttp
|
||||||
|
from open_webui.models.groups import Groups
|
||||||
from pydantic import BaseModel, HttpUrl
|
from pydantic import BaseModel, HttpUrl
|
||||||
from fastapi import APIRouter, Depends, HTTPException, Request, status
|
from fastapi import APIRouter, Depends, HTTPException, Request, status
|
||||||
|
|
||||||
|
|
@ -71,11 +72,12 @@ async def get_tools(request: Request, user=Depends(get_verified_user)):
|
||||||
# Admin can see all tools
|
# Admin can see all tools
|
||||||
return tools
|
return tools
|
||||||
else:
|
else:
|
||||||
|
user_group_ids = {group.id for group in Groups.get_groups_by_member_id(user.id)}
|
||||||
tools = [
|
tools = [
|
||||||
tool
|
tool
|
||||||
for tool in tools
|
for tool in tools
|
||||||
if tool.user_id == user.id
|
if tool.user_id == user.id
|
||||||
or has_access(user.id, "read", tool.access_control)
|
or has_access(user.id, "read", tool.access_control, user_group_ids)
|
||||||
]
|
]
|
||||||
return tools
|
return tools
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,8 @@ from pydantic import BaseModel
|
||||||
|
|
||||||
|
|
||||||
from open_webui.models.auths import Auths
|
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.groups import Groups
|
||||||
from open_webui.models.chats import Chats
|
from open_webui.models.chats import Chats
|
||||||
from open_webui.models.users import (
|
from open_webui.models.users import (
|
||||||
|
|
@ -146,6 +148,10 @@ class ChatPermissions(BaseModel):
|
||||||
params: bool = True
|
params: bool = True
|
||||||
file_upload: bool = True
|
file_upload: bool = True
|
||||||
delete: bool = True
|
delete: bool = True
|
||||||
|
delete_message: bool = True
|
||||||
|
continue_response: bool = True
|
||||||
|
regenerate_response: bool = True
|
||||||
|
rate_response: bool = True
|
||||||
edit: bool = True
|
edit: bool = True
|
||||||
share: bool = True
|
share: bool = True
|
||||||
export: bool = True
|
export: bool = True
|
||||||
|
|
@ -336,6 +342,18 @@ async def get_user_by_id(user_id: str, user=Depends(get_verified_user)):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/{user_id}/oauth/sessions", response_model=Optional[dict])
|
||||||
|
async def get_user_oauth_sessions_by_id(user_id: str, user=Depends(get_admin_user)):
|
||||||
|
sessions = OAuthSessions.get_sessions_by_user_id(user_id)
|
||||||
|
if sessions and len(sessions) > 0:
|
||||||
|
return sessions
|
||||||
|
else:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
|
detail=ERROR_MESSAGES.USER_NOT_FOUND,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
############################
|
############################
|
||||||
# GetUserProfileImageById
|
# GetUserProfileImageById
|
||||||
############################
|
############################
|
||||||
|
|
|
||||||
|
|
@ -115,7 +115,7 @@ if WEBSOCKET_MANAGER == "redis":
|
||||||
|
|
||||||
clean_up_lock = RedisLock(
|
clean_up_lock = RedisLock(
|
||||||
redis_url=WEBSOCKET_REDIS_URL,
|
redis_url=WEBSOCKET_REDIS_URL,
|
||||||
lock_name="usage_cleanup_lock",
|
lock_name=f"{REDIS_KEY_PREFIX}:usage_cleanup_lock",
|
||||||
timeout_secs=WEBSOCKET_REDIS_LOCK_TIMEOUT,
|
timeout_secs=WEBSOCKET_REDIS_LOCK_TIMEOUT,
|
||||||
redis_sentinels=redis_sentinels,
|
redis_sentinels=redis_sentinels,
|
||||||
redis_cluster=WEBSOCKET_REDIS_CLUSTER,
|
redis_cluster=WEBSOCKET_REDIS_CLUSTER,
|
||||||
|
|
@ -705,6 +705,42 @@ def get_event_emitter(request_info, update_db=True):
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if "type" in event_data and event_data["type"] == "files":
|
||||||
|
message = Chats.get_message_by_id_and_message_id(
|
||||||
|
request_info["chat_id"],
|
||||||
|
request_info["message_id"],
|
||||||
|
)
|
||||||
|
|
||||||
|
files = event_data.get("data", {}).get("files", [])
|
||||||
|
files.extend(message.get("files", []))
|
||||||
|
|
||||||
|
Chats.upsert_message_to_chat_by_id_and_message_id(
|
||||||
|
request_info["chat_id"],
|
||||||
|
request_info["message_id"],
|
||||||
|
{
|
||||||
|
"files": files,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
if event_data.get("type") in ["source", "citation"]:
|
||||||
|
data = event_data.get("data", {})
|
||||||
|
if data.get("type") == None:
|
||||||
|
message = Chats.get_message_by_id_and_message_id(
|
||||||
|
request_info["chat_id"],
|
||||||
|
request_info["message_id"],
|
||||||
|
)
|
||||||
|
|
||||||
|
sources = message.get("sources", [])
|
||||||
|
sources.append(data)
|
||||||
|
|
||||||
|
Chats.upsert_message_to_chat_by_id_and_message_id(
|
||||||
|
request_info["chat_id"],
|
||||||
|
request_info["message_id"],
|
||||||
|
{
|
||||||
|
"sources": sources,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
return __event_emitter__
|
return __event_emitter__
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -153,9 +153,9 @@ async def stop_task(redis, task_id: str):
|
||||||
# Optionally check if task_id still in Redis a few moments later for feedback?
|
# Optionally check if task_id still in Redis a few moments later for feedback?
|
||||||
return {"status": True, "message": f"Stop signal sent for {task_id}"}
|
return {"status": True, "message": f"Stop signal sent for {task_id}"}
|
||||||
|
|
||||||
task = tasks.pop(task_id)
|
task = tasks.pop(task_id, None)
|
||||||
if not task:
|
if not task:
|
||||||
raise ValueError(f"Task with ID {task_id} not found.")
|
return {"status": False, "message": f"Task with ID {task_id} not found."}
|
||||||
|
|
||||||
task.cancel() # Request task cancellation
|
task.cancel() # Request task cancellation
|
||||||
try:
|
try:
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
from typing import Optional, Union, List, Dict, Any
|
from typing import Optional, Set, Union, List, Dict, Any
|
||||||
from open_webui.models.users import Users, UserModel
|
from open_webui.models.users import Users, UserModel
|
||||||
from open_webui.models.groups import Groups
|
from open_webui.models.groups import Groups
|
||||||
|
|
||||||
|
|
@ -109,12 +109,15 @@ def has_access(
|
||||||
user_id: str,
|
user_id: str,
|
||||||
type: str = "write",
|
type: str = "write",
|
||||||
access_control: Optional[dict] = None,
|
access_control: Optional[dict] = None,
|
||||||
|
user_group_ids: Optional[Set[str]] = None,
|
||||||
) -> bool:
|
) -> bool:
|
||||||
if access_control is None:
|
if access_control is None:
|
||||||
return type == "read"
|
return type == "read"
|
||||||
|
|
||||||
|
if user_group_ids is None:
|
||||||
user_groups = Groups.get_groups_by_member_id(user_id)
|
user_groups = Groups.get_groups_by_member_id(user_id)
|
||||||
user_group_ids = [group.id for group in user_groups]
|
user_group_ids = {group.id for group in user_groups}
|
||||||
|
|
||||||
permission_access = access_control.get(type, {})
|
permission_access = access_control.get(type, {})
|
||||||
permitted_group_ids = permission_access.get("group_ids", [])
|
permitted_group_ids = permission_access.get("group_ids", [])
|
||||||
permitted_user_ids = permission_access.get("user_ids", [])
|
permitted_user_ids = permission_access.get("user_ids", [])
|
||||||
|
|
|
||||||
|
|
@ -261,6 +261,8 @@ def get_current_user(
|
||||||
return user
|
return user
|
||||||
|
|
||||||
# auth by jwt token
|
# auth by jwt token
|
||||||
|
|
||||||
|
try:
|
||||||
try:
|
try:
|
||||||
data = decode_token(token)
|
data = decode_token(token)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|
@ -282,11 +284,6 @@ def get_current_user(
|
||||||
WEBUI_AUTH_TRUSTED_EMAIL_HEADER, ""
|
WEBUI_AUTH_TRUSTED_EMAIL_HEADER, ""
|
||||||
).lower()
|
).lower()
|
||||||
if trusted_email and user.email != trusted_email:
|
if trusted_email and user.email != trusted_email:
|
||||||
# Delete the token cookie
|
|
||||||
response.delete_cookie("token")
|
|
||||||
# Delete OAuth token if present
|
|
||||||
if request.cookies.get("oauth_id_token"):
|
|
||||||
response.delete_cookie("oauth_id_token")
|
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
detail="User mismatch. Please sign in again.",
|
detail="User mismatch. Please sign in again.",
|
||||||
|
|
@ -303,13 +300,28 @@ def get_current_user(
|
||||||
# Refresh the user's last active timestamp asynchronously
|
# Refresh the user's last active timestamp asynchronously
|
||||||
# to prevent blocking the request
|
# to prevent blocking the request
|
||||||
if background_tasks:
|
if background_tasks:
|
||||||
background_tasks.add_task(Users.update_user_last_active_by_id, user.id)
|
background_tasks.add_task(
|
||||||
|
Users.update_user_last_active_by_id, user.id
|
||||||
|
)
|
||||||
return user
|
return user
|
||||||
else:
|
else:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
detail=ERROR_MESSAGES.UNAUTHORIZED,
|
detail=ERROR_MESSAGES.UNAUTHORIZED,
|
||||||
)
|
)
|
||||||
|
except Exception as e:
|
||||||
|
# Delete the token cookie
|
||||||
|
if request.cookies.get("token"):
|
||||||
|
response.delete_cookie("token")
|
||||||
|
|
||||||
|
if request.cookies.get("oauth_id_token"):
|
||||||
|
response.delete_cookie("oauth_id_token")
|
||||||
|
|
||||||
|
# Delete OAuth session if present
|
||||||
|
if request.cookies.get("oauth_session_id"):
|
||||||
|
response.delete_cookie("oauth_session_id")
|
||||||
|
|
||||||
|
raise e
|
||||||
|
|
||||||
|
|
||||||
def get_current_user_by_api_key(api_key: str):
|
def get_current_user_by_api_key(api_key: str):
|
||||||
|
|
|
||||||
|
|
@ -98,8 +98,10 @@ from open_webui.env import (
|
||||||
SRC_LOG_LEVELS,
|
SRC_LOG_LEVELS,
|
||||||
GLOBAL_LOG_LEVEL,
|
GLOBAL_LOG_LEVEL,
|
||||||
CHAT_RESPONSE_STREAM_DELTA_CHUNK_SIZE,
|
CHAT_RESPONSE_STREAM_DELTA_CHUNK_SIZE,
|
||||||
|
CHAT_RESPONSE_MAX_TOOL_CALL_RETRIES,
|
||||||
BYPASS_MODEL_ACCESS_CONTROL,
|
BYPASS_MODEL_ACCESS_CONTROL,
|
||||||
ENABLE_REALTIME_CHAT_SAVE,
|
ENABLE_REALTIME_CHAT_SAVE,
|
||||||
|
ENABLE_QUERIES_CACHE,
|
||||||
)
|
)
|
||||||
from open_webui.constants import TASKS
|
from open_webui.constants import TASKS
|
||||||
|
|
||||||
|
|
@ -109,6 +111,20 @@ log = logging.getLogger(__name__)
|
||||||
log.setLevel(SRC_LOG_LEVELS["MAIN"])
|
log.setLevel(SRC_LOG_LEVELS["MAIN"])
|
||||||
|
|
||||||
|
|
||||||
|
DEFAULT_REASONING_TAGS = [
|
||||||
|
("<think>", "</think>"),
|
||||||
|
("<thinking>", "</thinking>"),
|
||||||
|
("<reason>", "</reason>"),
|
||||||
|
("<reasoning>", "</reasoning>"),
|
||||||
|
("<thought>", "</thought>"),
|
||||||
|
("<Thought>", "</Thought>"),
|
||||||
|
("<|begin_of_thought|>", "<|end_of_thought|>"),
|
||||||
|
("◁think▷", "◁/think▷"),
|
||||||
|
]
|
||||||
|
DEFAULT_SOLUTION_TAGS = [("<|begin_of_solution|>", "<|end_of_solution|>")]
|
||||||
|
DEFAULT_CODE_INTERPRETER_TAGS = [("<code_interpreter>", "</code_interpreter>")]
|
||||||
|
|
||||||
|
|
||||||
async def chat_completion_tools_handler(
|
async def chat_completion_tools_handler(
|
||||||
request: Request, body: dict, extra_params: dict, user: UserModel, models, tools
|
request: Request, body: dict, extra_params: dict, user: UserModel, models, tools
|
||||||
) -> tuple[dict, dict]:
|
) -> tuple[dict, dict]:
|
||||||
|
|
@ -353,7 +369,7 @@ async def chat_web_search_handler(
|
||||||
"type": "status",
|
"type": "status",
|
||||||
"data": {
|
"data": {
|
||||||
"action": "web_search",
|
"action": "web_search",
|
||||||
"description": "Generating search query",
|
"description": "Searching the web",
|
||||||
"done": False,
|
"done": False,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
@ -390,6 +406,9 @@ async def chat_web_search_handler(
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
queries = [response]
|
queries = [response]
|
||||||
|
|
||||||
|
if ENABLE_QUERIES_CACHE:
|
||||||
|
request.state.cached_queries = queries
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.exception(e)
|
log.exception(e)
|
||||||
queries = [user_message]
|
queries = [user_message]
|
||||||
|
|
@ -416,8 +435,8 @@ async def chat_web_search_handler(
|
||||||
{
|
{
|
||||||
"type": "status",
|
"type": "status",
|
||||||
"data": {
|
"data": {
|
||||||
"action": "web_search",
|
"action": "web_search_queries_generated",
|
||||||
"description": "Searching the web",
|
"queries": queries,
|
||||||
"done": False,
|
"done": False,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
@ -468,6 +487,7 @@ async def chat_web_search_handler(
|
||||||
"action": "web_search",
|
"action": "web_search",
|
||||||
"description": "Searched {{count}} sites",
|
"description": "Searched {{count}} sites",
|
||||||
"urls": results["filenames"],
|
"urls": results["filenames"],
|
||||||
|
"items": results.get("items", []),
|
||||||
"done": True,
|
"done": True,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
@ -510,7 +530,7 @@ async def chat_image_generation_handler(
|
||||||
await __event_emitter__(
|
await __event_emitter__(
|
||||||
{
|
{
|
||||||
"type": "status",
|
"type": "status",
|
||||||
"data": {"description": "Generating an image", "done": False},
|
"data": {"description": "Creating image", "done": False},
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -562,7 +582,7 @@ async def chat_image_generation_handler(
|
||||||
await __event_emitter__(
|
await __event_emitter__(
|
||||||
{
|
{
|
||||||
"type": "status",
|
"type": "status",
|
||||||
"data": {"description": "Generated an image", "done": True},
|
"data": {"description": "Image created", "done": True},
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -605,8 +625,9 @@ async def chat_image_generation_handler(
|
||||||
|
|
||||||
|
|
||||||
async def chat_completion_files_handler(
|
async def chat_completion_files_handler(
|
||||||
request: Request, body: dict, user: UserModel
|
request: Request, body: dict, extra_params: dict, user: UserModel
|
||||||
) -> tuple[dict, dict[str, list]]:
|
) -> tuple[dict, dict[str, list]]:
|
||||||
|
__event_emitter__ = extra_params["__event_emitter__"]
|
||||||
sources = []
|
sources = []
|
||||||
|
|
||||||
if files := body.get("metadata", {}).get("files", None):
|
if files := body.get("metadata", {}).get("files", None):
|
||||||
|
|
@ -642,6 +663,17 @@ async def chat_completion_files_handler(
|
||||||
if len(queries) == 0:
|
if len(queries) == 0:
|
||||||
queries = [get_last_user_message(body["messages"])]
|
queries = [get_last_user_message(body["messages"])]
|
||||||
|
|
||||||
|
await __event_emitter__(
|
||||||
|
{
|
||||||
|
"type": "status",
|
||||||
|
"data": {
|
||||||
|
"action": "queries_generated",
|
||||||
|
"queries": queries,
|
||||||
|
"done": False,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Offload get_sources_from_items to a separate thread
|
# Offload get_sources_from_items to a separate thread
|
||||||
loop = asyncio.get_running_loop()
|
loop = asyncio.get_running_loop()
|
||||||
|
|
@ -678,6 +710,38 @@ async def chat_completion_files_handler(
|
||||||
|
|
||||||
log.debug(f"rag_contexts:sources: {sources}")
|
log.debug(f"rag_contexts:sources: {sources}")
|
||||||
|
|
||||||
|
unique_ids = set()
|
||||||
|
|
||||||
|
for source in sources or []:
|
||||||
|
if not source or len(source.keys()) == 0:
|
||||||
|
continue
|
||||||
|
|
||||||
|
documents = source.get("document") or []
|
||||||
|
metadatas = source.get("metadata") or []
|
||||||
|
src_info = source.get("source") or {}
|
||||||
|
|
||||||
|
for index, _ in enumerate(documents):
|
||||||
|
metadata = metadatas[index] if index < len(metadatas) else None
|
||||||
|
_id = (
|
||||||
|
(metadata or {}).get("source")
|
||||||
|
or (src_info or {}).get("id")
|
||||||
|
or "N/A"
|
||||||
|
)
|
||||||
|
unique_ids.add(_id)
|
||||||
|
|
||||||
|
sources_count = len(unique_ids)
|
||||||
|
|
||||||
|
await __event_emitter__(
|
||||||
|
{
|
||||||
|
"type": "status",
|
||||||
|
"data": {
|
||||||
|
"action": "sources_retrieved",
|
||||||
|
"count": sources_count,
|
||||||
|
"done": True,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
return body, {"sources": sources}
|
return body, {"sources": sources}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -689,6 +753,7 @@ def apply_params_to_form_data(form_data, model):
|
||||||
"stream_response": bool,
|
"stream_response": bool,
|
||||||
"stream_delta_chunk_size": int,
|
"stream_delta_chunk_size": int,
|
||||||
"function_calling": str,
|
"function_calling": str,
|
||||||
|
"reasoning_tags": list,
|
||||||
"system": str,
|
"system": str,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -750,6 +815,15 @@ async def process_chat_payload(request, form_data, user, metadata, model):
|
||||||
event_emitter = get_event_emitter(metadata)
|
event_emitter = get_event_emitter(metadata)
|
||||||
event_call = get_event_call(metadata)
|
event_call = get_event_call(metadata)
|
||||||
|
|
||||||
|
oauth_token = None
|
||||||
|
try:
|
||||||
|
oauth_token = request.app.state.oauth_manager.get_oauth_token(
|
||||||
|
user.id,
|
||||||
|
request.cookies.get("oauth_session_id", None),
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
log.error(f"Error getting OAuth token: {e}")
|
||||||
|
|
||||||
extra_params = {
|
extra_params = {
|
||||||
"__event_emitter__": event_emitter,
|
"__event_emitter__": event_emitter,
|
||||||
"__event_call__": event_call,
|
"__event_call__": event_call,
|
||||||
|
|
@ -757,6 +831,7 @@ async def process_chat_payload(request, form_data, user, metadata, model):
|
||||||
"__metadata__": metadata,
|
"__metadata__": metadata,
|
||||||
"__request__": request,
|
"__request__": request,
|
||||||
"__model__": model,
|
"__model__": model,
|
||||||
|
"__oauth_token__": oauth_token,
|
||||||
}
|
}
|
||||||
|
|
||||||
# Initialize events to store additional event to be sent to the client
|
# Initialize events to store additional event to be sent to the client
|
||||||
|
|
@ -865,7 +940,7 @@ async def process_chat_payload(request, form_data, user, metadata, model):
|
||||||
extra_params=extra_params,
|
extra_params=extra_params,
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise Exception(f"Error: {e}")
|
raise Exception(f"{e}")
|
||||||
|
|
||||||
features = form_data.pop("features", None)
|
features = form_data.pop("features", None)
|
||||||
if features:
|
if features:
|
||||||
|
|
@ -961,7 +1036,9 @@ async def process_chat_payload(request, form_data, user, metadata, model):
|
||||||
log.exception(e)
|
log.exception(e)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
form_data, flags = await chat_completion_files_handler(request, form_data, user)
|
form_data, flags = await chat_completion_files_handler(
|
||||||
|
request, form_data, extra_params, user
|
||||||
|
)
|
||||||
sources.extend(flags.get("sources", []))
|
sources.extend(flags.get("sources", []))
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.exception(e)
|
log.exception(e)
|
||||||
|
|
@ -1264,7 +1341,11 @@ async def process_chat_response(
|
||||||
# Non-streaming response
|
# Non-streaming response
|
||||||
if not isinstance(response, StreamingResponse):
|
if not isinstance(response, StreamingResponse):
|
||||||
if event_emitter:
|
if event_emitter:
|
||||||
|
try:
|
||||||
if isinstance(response, dict) or isinstance(response, JSONResponse):
|
if isinstance(response, dict) or isinstance(response, JSONResponse):
|
||||||
|
if isinstance(response, list) and len(response) == 1:
|
||||||
|
# If the response is a single-item list, unwrap it #17213
|
||||||
|
response = response[0]
|
||||||
|
|
||||||
if isinstance(response, JSONResponse) and isinstance(
|
if isinstance(response, JSONResponse) and isinstance(
|
||||||
response.body, bytes
|
response.body, bytes
|
||||||
|
|
@ -1272,12 +1353,20 @@ async def process_chat_response(
|
||||||
try:
|
try:
|
||||||
response_data = json.loads(response.body.decode("utf-8"))
|
response_data = json.loads(response.body.decode("utf-8"))
|
||||||
except json.JSONDecodeError:
|
except json.JSONDecodeError:
|
||||||
response_data = {"error": {"detail": "Invalid JSON response"}}
|
response_data = {
|
||||||
|
"error": {"detail": "Invalid JSON response"}
|
||||||
|
}
|
||||||
else:
|
else:
|
||||||
response_data = response
|
response_data = response
|
||||||
|
|
||||||
if "error" in response_data:
|
if "error" in response_data:
|
||||||
error = response_data["error"].get("detail", response_data["error"])
|
error = response_data.get("error")
|
||||||
|
|
||||||
|
if isinstance(error, dict):
|
||||||
|
error = error.get("detail", error)
|
||||||
|
else:
|
||||||
|
error = str(error)
|
||||||
|
|
||||||
Chats.upsert_message_to_chat_by_id_and_message_id(
|
Chats.upsert_message_to_chat_by_id_and_message_id(
|
||||||
metadata["chat_id"],
|
metadata["chat_id"],
|
||||||
metadata["message_id"],
|
metadata["message_id"],
|
||||||
|
|
@ -1285,6 +1374,13 @@ async def process_chat_response(
|
||||||
"error": {"content": error},
|
"error": {"content": error},
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
if isinstance(error, str) or isinstance(error, dict):
|
||||||
|
await event_emitter(
|
||||||
|
{
|
||||||
|
"type": "chat:message:error",
|
||||||
|
"data": {"error": {"content": error}},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
if "selected_model_id" in response_data:
|
if "selected_model_id" in response_data:
|
||||||
Chats.upsert_message_to_chat_by_id_and_message_id(
|
Chats.upsert_message_to_chat_by_id_and_message_id(
|
||||||
|
|
@ -1370,6 +1466,10 @@ async def process_chat_response(
|
||||||
status_code=response.status_code,
|
status_code=response.status_code,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
log.debug(f"Error occurred while processing request: {e}")
|
||||||
|
pass
|
||||||
|
|
||||||
return response
|
return response
|
||||||
else:
|
else:
|
||||||
if events and isinstance(events, list) and isinstance(response, dict):
|
if events and isinstance(events, list) and isinstance(response, dict):
|
||||||
|
|
@ -1394,11 +1494,21 @@ async def process_chat_response(
|
||||||
):
|
):
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
oauth_token = None
|
||||||
|
try:
|
||||||
|
oauth_token = request.app.state.oauth_manager.get_oauth_token(
|
||||||
|
user.id,
|
||||||
|
request.cookies.get("oauth_session_id", None),
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
log.error(f"Error getting OAuth token: {e}")
|
||||||
|
|
||||||
extra_params = {
|
extra_params = {
|
||||||
"__event_emitter__": event_emitter,
|
"__event_emitter__": event_emitter,
|
||||||
"__event_call__": event_caller,
|
"__event_call__": event_caller,
|
||||||
"__user__": user.model_dump() if isinstance(user, UserModel) else {},
|
"__user__": user.model_dump() if isinstance(user, UserModel) else {},
|
||||||
"__metadata__": metadata,
|
"__metadata__": metadata,
|
||||||
|
"__oauth_token__": oauth_token,
|
||||||
"__request__": request,
|
"__request__": request,
|
||||||
"__model__": model,
|
"__model__": model,
|
||||||
}
|
}
|
||||||
|
|
@ -1468,7 +1578,7 @@ async def process_chat_response(
|
||||||
tool_result_files = result.get("files", None)
|
tool_result_files = result.get("files", None)
|
||||||
break
|
break
|
||||||
|
|
||||||
if tool_result:
|
if tool_result is not None:
|
||||||
tool_calls_display_content = f'{tool_calls_display_content}<details type="tool_calls" done="true" id="{tool_call_id}" name="{tool_name}" arguments="{html.escape(json.dumps(tool_arguments))}" result="{html.escape(json.dumps(tool_result, ensure_ascii=False))}" files="{html.escape(json.dumps(tool_result_files)) if tool_result_files else ""}">\n<summary>Tool Executed</summary>\n</details>\n'
|
tool_calls_display_content = f'{tool_calls_display_content}<details type="tool_calls" done="true" id="{tool_call_id}" name="{tool_name}" arguments="{html.escape(json.dumps(tool_arguments))}" result="{html.escape(json.dumps(tool_result, ensure_ascii=False))}" files="{html.escape(json.dumps(tool_result_files)) if tool_result_files else ""}">\n<summary>Tool Executed</summary>\n</details>\n'
|
||||||
else:
|
else:
|
||||||
tool_calls_display_content = f'{tool_calls_display_content}<details type="tool_calls" done="false" id="{tool_call_id}" name="{tool_name}" arguments="{html.escape(json.dumps(tool_arguments))}">\n<summary>Executing...</summary>\n</details>\n'
|
tool_calls_display_content = f'{tool_calls_display_content}<details type="tool_calls" done="false" id="{tool_call_id}" name="{tool_name}" arguments="{html.escape(json.dumps(tool_arguments))}">\n<summary>Executing...</summary>\n</details>\n'
|
||||||
|
|
@ -1583,7 +1693,7 @@ async def process_chat_response(
|
||||||
{
|
{
|
||||||
"role": "tool",
|
"role": "tool",
|
||||||
"tool_call_id": result["tool_call_id"],
|
"tool_call_id": result["tool_call_id"],
|
||||||
"content": result["content"],
|
"content": result.get("content", "") or "",
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
temp_blocks = []
|
temp_blocks = []
|
||||||
|
|
@ -1806,27 +1916,23 @@ async def process_chat_response(
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
# We might want to disable this by default
|
reasoning_tags_param = metadata.get("params", {}).get("reasoning_tags")
|
||||||
DETECT_REASONING = True
|
DETECT_REASONING_TAGS = reasoning_tags_param is not False
|
||||||
DETECT_SOLUTION = True
|
|
||||||
DETECT_CODE_INTERPRETER = metadata.get("features", {}).get(
|
DETECT_CODE_INTERPRETER = metadata.get("features", {}).get(
|
||||||
"code_interpreter", False
|
"code_interpreter", False
|
||||||
)
|
)
|
||||||
|
|
||||||
|
reasoning_tags = []
|
||||||
|
if DETECT_REASONING_TAGS:
|
||||||
|
if (
|
||||||
|
isinstance(reasoning_tags_param, list)
|
||||||
|
and len(reasoning_tags_param) == 2
|
||||||
|
):
|
||||||
reasoning_tags = [
|
reasoning_tags = [
|
||||||
("<think>", "</think>"),
|
(reasoning_tags_param[0], reasoning_tags_param[1])
|
||||||
("<thinking>", "</thinking>"),
|
|
||||||
("<reason>", "</reason>"),
|
|
||||||
("<reasoning>", "</reasoning>"),
|
|
||||||
("<thought>", "</thought>"),
|
|
||||||
("<Thought>", "</Thought>"),
|
|
||||||
("<|begin_of_thought|>", "<|end_of_thought|>"),
|
|
||||||
("◁think▷", "◁/think▷"),
|
|
||||||
]
|
]
|
||||||
|
else:
|
||||||
code_interpreter_tags = [("<code_interpreter>", "</code_interpreter>")]
|
reasoning_tags = DEFAULT_REASONING_TAGS
|
||||||
|
|
||||||
solution_tags = [("<|begin_of_solution|>", "<|end_of_solution|>")]
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
for event in events:
|
for event in events:
|
||||||
|
|
@ -1935,6 +2041,10 @@ async def process_chat_response(
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
usage = data.get("usage", {})
|
usage = data.get("usage", {})
|
||||||
|
usage.update(
|
||||||
|
data.get("timing", {})
|
||||||
|
) # llama.cpp
|
||||||
|
|
||||||
if usage:
|
if usage:
|
||||||
await event_emitter(
|
await event_emitter(
|
||||||
{
|
{
|
||||||
|
|
@ -2078,7 +2188,7 @@ async def process_chat_response(
|
||||||
content_blocks[-1]["content"] + value
|
content_blocks[-1]["content"] + value
|
||||||
)
|
)
|
||||||
|
|
||||||
if DETECT_REASONING:
|
if DETECT_REASONING_TAGS:
|
||||||
content, content_blocks, _ = (
|
content, content_blocks, _ = (
|
||||||
tag_content_handler(
|
tag_content_handler(
|
||||||
"reasoning",
|
"reasoning",
|
||||||
|
|
@ -2088,11 +2198,20 @@ async def process_chat_response(
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
content, content_blocks, _ = (
|
||||||
|
tag_content_handler(
|
||||||
|
"solution",
|
||||||
|
DEFAULT_SOLUTION_TAGS,
|
||||||
|
content,
|
||||||
|
content_blocks,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
if DETECT_CODE_INTERPRETER:
|
if DETECT_CODE_INTERPRETER:
|
||||||
content, content_blocks, end = (
|
content, content_blocks, end = (
|
||||||
tag_content_handler(
|
tag_content_handler(
|
||||||
"code_interpreter",
|
"code_interpreter",
|
||||||
code_interpreter_tags,
|
DEFAULT_CODE_INTERPRETER_TAGS,
|
||||||
content,
|
content,
|
||||||
content_blocks,
|
content_blocks,
|
||||||
)
|
)
|
||||||
|
|
@ -2101,16 +2220,6 @@ async def process_chat_response(
|
||||||
if end:
|
if end:
|
||||||
break
|
break
|
||||||
|
|
||||||
if DETECT_SOLUTION:
|
|
||||||
content, content_blocks, _ = (
|
|
||||||
tag_content_handler(
|
|
||||||
"solution",
|
|
||||||
solution_tags,
|
|
||||||
content,
|
|
||||||
content_blocks,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
if ENABLE_REALTIME_CHAT_SAVE:
|
if ENABLE_REALTIME_CHAT_SAVE:
|
||||||
# Save message in the database
|
# Save message in the database
|
||||||
Chats.upsert_message_to_chat_by_id_and_message_id(
|
Chats.upsert_message_to_chat_by_id_and_message_id(
|
||||||
|
|
@ -2185,10 +2294,12 @@ async def process_chat_response(
|
||||||
|
|
||||||
await stream_body_handler(response, form_data)
|
await stream_body_handler(response, form_data)
|
||||||
|
|
||||||
MAX_TOOL_CALL_RETRIES = 10
|
|
||||||
tool_call_retries = 0
|
tool_call_retries = 0
|
||||||
|
|
||||||
while len(tool_calls) > 0 and tool_call_retries < MAX_TOOL_CALL_RETRIES:
|
while (
|
||||||
|
len(tool_calls) > 0
|
||||||
|
and tool_call_retries < CHAT_RESPONSE_MAX_TOOL_CALL_RETRIES
|
||||||
|
):
|
||||||
|
|
||||||
tool_call_retries += 1
|
tool_call_retries += 1
|
||||||
|
|
||||||
|
|
@ -2307,7 +2418,7 @@ async def process_chat_response(
|
||||||
results.append(
|
results.append(
|
||||||
{
|
{
|
||||||
"tool_call_id": tool_call_id,
|
"tool_call_id": tool_call_id,
|
||||||
"content": tool_result,
|
"content": tool_result or "",
|
||||||
**(
|
**(
|
||||||
{"files": tool_result_files}
|
{"files": tool_result_files}
|
||||||
if tool_result_files
|
if tool_result_files
|
||||||
|
|
@ -2594,7 +2705,7 @@ async def process_chat_response(
|
||||||
await background_tasks_handler()
|
await background_tasks_handler()
|
||||||
except asyncio.CancelledError:
|
except asyncio.CancelledError:
|
||||||
log.warning("Task was cancelled!")
|
log.warning("Task was cancelled!")
|
||||||
await event_emitter({"type": "task-cancelled"})
|
await event_emitter({"type": "chat:tasks:cancel"})
|
||||||
|
|
||||||
if not ENABLE_REALTIME_CHAT_SAVE:
|
if not ENABLE_REALTIME_CHAT_SAVE:
|
||||||
# Save message in the database
|
# Save message in the database
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,11 @@ import mimetypes
|
||||||
import sys
|
import sys
|
||||||
import uuid
|
import uuid
|
||||||
import json
|
import json
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
|
import re
|
||||||
|
import fnmatch
|
||||||
|
import time
|
||||||
|
|
||||||
import aiohttp
|
import aiohttp
|
||||||
from authlib.integrations.starlette_client import OAuth
|
from authlib.integrations.starlette_client import OAuth
|
||||||
|
|
@ -14,8 +19,12 @@ from fastapi import (
|
||||||
)
|
)
|
||||||
from starlette.responses import RedirectResponse
|
from starlette.responses import RedirectResponse
|
||||||
|
|
||||||
|
|
||||||
from open_webui.models.auths import Auths
|
from open_webui.models.auths import Auths
|
||||||
|
from open_webui.models.oauth_sessions import OAuthSessions
|
||||||
from open_webui.models.users import Users
|
from open_webui.models.users import Users
|
||||||
|
|
||||||
|
|
||||||
from open_webui.models.groups import Groups, GroupModel, GroupUpdateForm, GroupForm
|
from open_webui.models.groups import Groups, GroupModel, GroupUpdateForm, GroupForm
|
||||||
from open_webui.config import (
|
from open_webui.config import (
|
||||||
DEFAULT_USER_ROLE,
|
DEFAULT_USER_ROLE,
|
||||||
|
|
@ -46,6 +55,7 @@ from open_webui.env import (
|
||||||
WEBUI_NAME,
|
WEBUI_NAME,
|
||||||
WEBUI_AUTH_COOKIE_SAME_SITE,
|
WEBUI_AUTH_COOKIE_SAME_SITE,
|
||||||
WEBUI_AUTH_COOKIE_SECURE,
|
WEBUI_AUTH_COOKIE_SECURE,
|
||||||
|
ENABLE_OAUTH_ID_TOKEN_COOKIE,
|
||||||
)
|
)
|
||||||
from open_webui.utils.misc import parse_duration
|
from open_webui.utils.misc import parse_duration
|
||||||
from open_webui.utils.auth import get_password_hash, create_token
|
from open_webui.utils.auth import get_password_hash, create_token
|
||||||
|
|
@ -79,15 +89,235 @@ auth_manager_config.JWT_EXPIRES_IN = JWT_EXPIRES_IN
|
||||||
auth_manager_config.OAUTH_UPDATE_PICTURE_ON_LOGIN = OAUTH_UPDATE_PICTURE_ON_LOGIN
|
auth_manager_config.OAUTH_UPDATE_PICTURE_ON_LOGIN = OAUTH_UPDATE_PICTURE_ON_LOGIN
|
||||||
|
|
||||||
|
|
||||||
|
def is_in_blocked_groups(group_name: str, groups: list) -> bool:
|
||||||
|
"""
|
||||||
|
Check if a group name matches any blocked pattern.
|
||||||
|
Supports exact matches, shell-style wildcards (*, ?), and regex patterns.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
group_name: The group name to check
|
||||||
|
groups: List of patterns to match against
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if the group is blocked, False otherwise
|
||||||
|
"""
|
||||||
|
if not groups:
|
||||||
|
return False
|
||||||
|
|
||||||
|
for group_pattern in groups:
|
||||||
|
if not group_pattern: # Skip empty patterns
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Exact match
|
||||||
|
if group_name == group_pattern:
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Try as regex pattern first if it contains regex-specific characters
|
||||||
|
if any(
|
||||||
|
char in group_pattern
|
||||||
|
for char in ["^", "$", "[", "]", "(", ")", "{", "}", "+", "\\", "|"]
|
||||||
|
):
|
||||||
|
try:
|
||||||
|
# Use the original pattern as-is for regex matching
|
||||||
|
if re.search(group_pattern, group_name):
|
||||||
|
return True
|
||||||
|
except re.error:
|
||||||
|
# If regex is invalid, fall through to wildcard check
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Shell-style wildcard match (supports * and ?)
|
||||||
|
if "*" in group_pattern or "?" in group_pattern:
|
||||||
|
if fnmatch.fnmatch(group_name, group_pattern):
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
class OAuthManager:
|
class OAuthManager:
|
||||||
def __init__(self, app):
|
def __init__(self, app):
|
||||||
self.oauth = OAuth()
|
self.oauth = OAuth()
|
||||||
self.app = app
|
self.app = app
|
||||||
|
|
||||||
|
self._clients = {}
|
||||||
for _, provider_config in OAUTH_PROVIDERS.items():
|
for _, provider_config in OAUTH_PROVIDERS.items():
|
||||||
provider_config["register"](self.oauth)
|
provider_config["register"](self.oauth)
|
||||||
|
|
||||||
def get_client(self, provider_name):
|
def get_client(self, provider_name):
|
||||||
return self.oauth.create_client(provider_name)
|
if provider_name not in self._clients:
|
||||||
|
self._clients[provider_name] = self.oauth.create_client(provider_name)
|
||||||
|
return self._clients[provider_name]
|
||||||
|
|
||||||
|
def get_server_metadata_url(self, provider_name):
|
||||||
|
if provider_name in self._clients:
|
||||||
|
client = self._clients[provider_name]
|
||||||
|
return (
|
||||||
|
client.server_metadata_url
|
||||||
|
if hasattr(client, "server_metadata_url")
|
||||||
|
else None
|
||||||
|
)
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_oauth_token(
|
||||||
|
self, user_id: str, session_id: str, force_refresh: bool = False
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Get a valid OAuth token for the user, automatically refreshing if needed.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
user_id: The user ID
|
||||||
|
provider: Optional provider name. If None, gets the most recent session.
|
||||||
|
force_refresh: Force token refresh even if current token appears valid
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: OAuth token data with access_token, or None if no valid token available
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# Get the OAuth session
|
||||||
|
session = OAuthSessions.get_session_by_id_and_user_id(session_id, user_id)
|
||||||
|
if not session:
|
||||||
|
log.warning(
|
||||||
|
f"No OAuth session found for user {user_id}, session {session_id}"
|
||||||
|
)
|
||||||
|
return None
|
||||||
|
|
||||||
|
if force_refresh or datetime.now() + timedelta(
|
||||||
|
minutes=5
|
||||||
|
) >= datetime.fromtimestamp(session.expires_at):
|
||||||
|
log.debug(
|
||||||
|
f"Token refresh needed for user {user_id}, provider {session.provider}"
|
||||||
|
)
|
||||||
|
refreshed_token = self._refresh_token(session)
|
||||||
|
if refreshed_token:
|
||||||
|
return refreshed_token
|
||||||
|
else:
|
||||||
|
log.warning(
|
||||||
|
f"Token refresh failed for user {user_id}, provider {session.provider}"
|
||||||
|
)
|
||||||
|
return None
|
||||||
|
return session.token
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
log.error(f"Error getting OAuth token for user {user_id}: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
async def _refresh_token(self, session) -> dict:
|
||||||
|
"""
|
||||||
|
Refresh an OAuth token if needed, with concurrency protection.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
session: The OAuth session object
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: Refreshed token data, or None if refresh failed
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# Perform the actual refresh
|
||||||
|
refreshed_token = await self._perform_token_refresh(session)
|
||||||
|
|
||||||
|
if refreshed_token:
|
||||||
|
# Update the session with new token data
|
||||||
|
session = OAuthSessions.update_session_by_id(
|
||||||
|
session.id, refreshed_token
|
||||||
|
)
|
||||||
|
log.info(f"Successfully refreshed token for session {session.id}")
|
||||||
|
return session.token
|
||||||
|
else:
|
||||||
|
log.error(f"Failed to refresh token for session {session.id}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
log.error(f"Error refreshing token for session {session.id}: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
async def _perform_token_refresh(self, session) -> dict:
|
||||||
|
"""
|
||||||
|
Perform the actual OAuth token refresh.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
session: The OAuth session object
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: New token data, or None if refresh failed
|
||||||
|
"""
|
||||||
|
provider = session.provider
|
||||||
|
token_data = session.token
|
||||||
|
|
||||||
|
if not token_data.get("refresh_token"):
|
||||||
|
log.warning(f"No refresh token available for session {session.id}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
client = self.get_client(provider)
|
||||||
|
if not client:
|
||||||
|
log.error(f"No OAuth client found for provider {provider}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
token_endpoint = None
|
||||||
|
async with aiohttp.ClientSession(trust_env=True) as session_http:
|
||||||
|
async with session_http.get(client.gserver_metadata_url) as r:
|
||||||
|
if r.status == 200:
|
||||||
|
openid_data = await r.json()
|
||||||
|
token_endpoint = openid_data.get("token_endpoint")
|
||||||
|
else:
|
||||||
|
log.error(
|
||||||
|
f"Failed to fetch OpenID configuration for provider {provider}"
|
||||||
|
)
|
||||||
|
if not token_endpoint:
|
||||||
|
log.error(f"No token endpoint found for provider {provider}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Prepare refresh request
|
||||||
|
refresh_data = {
|
||||||
|
"grant_type": "refresh_token",
|
||||||
|
"refresh_token": token_data["refresh_token"],
|
||||||
|
"client_id": client.client_id,
|
||||||
|
}
|
||||||
|
# Add client_secret if available (some providers require it)
|
||||||
|
if hasattr(client, "client_secret") and client.client_secret:
|
||||||
|
refresh_data["client_secret"] = client.client_secret
|
||||||
|
|
||||||
|
# Make refresh request
|
||||||
|
async with aiohttp.ClientSession(trust_env=True) as session_http:
|
||||||
|
async with session_http.post(
|
||||||
|
token_endpoint,
|
||||||
|
data=refresh_data,
|
||||||
|
headers={"Content-Type": "application/x-www-form-urlencoded"},
|
||||||
|
ssl=AIOHTTP_CLIENT_SESSION_SSL,
|
||||||
|
) as r:
|
||||||
|
if r.status == 200:
|
||||||
|
new_token_data = await r.json()
|
||||||
|
|
||||||
|
# Merge with existing token data (preserve refresh_token if not provided)
|
||||||
|
if "refresh_token" not in new_token_data:
|
||||||
|
new_token_data["refresh_token"] = token_data[
|
||||||
|
"refresh_token"
|
||||||
|
]
|
||||||
|
|
||||||
|
# Add timestamp for tracking
|
||||||
|
new_token_data["issued_at"] = datetime.now().timestamp()
|
||||||
|
|
||||||
|
# Calculate expires_at if we have expires_in
|
||||||
|
if (
|
||||||
|
"expires_in" in new_token_data
|
||||||
|
and "expires_at" not in new_token_data
|
||||||
|
):
|
||||||
|
new_token_data["expires_at"] = (
|
||||||
|
datetime.now().timestamp()
|
||||||
|
+ new_token_data["expires_in"]
|
||||||
|
)
|
||||||
|
|
||||||
|
log.debug(f"Token refresh successful for provider {provider}")
|
||||||
|
return new_token_data
|
||||||
|
else:
|
||||||
|
error_text = await r.text()
|
||||||
|
log.error(
|
||||||
|
f"Token refresh failed for provider {provider}: {r.status} - {error_text}"
|
||||||
|
)
|
||||||
|
return None
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
log.error(f"Exception during token refresh for provider {provider}: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
def get_user_role(self, user, user_data):
|
def get_user_role(self, user, user_data):
|
||||||
user_count = Users.get_num_users()
|
user_count = Users.get_num_users()
|
||||||
|
|
@ -238,7 +468,7 @@ class OAuthManager:
|
||||||
if (
|
if (
|
||||||
user_oauth_groups
|
user_oauth_groups
|
||||||
and group_model.name not in user_oauth_groups
|
and group_model.name not in user_oauth_groups
|
||||||
and group_model.name not in blocked_groups
|
and not is_in_blocked_groups(group_model.name, blocked_groups)
|
||||||
):
|
):
|
||||||
# Remove group from user
|
# Remove group from user
|
||||||
log.debug(
|
log.debug(
|
||||||
|
|
@ -269,7 +499,7 @@ class OAuthManager:
|
||||||
user_oauth_groups
|
user_oauth_groups
|
||||||
and group_model.name in user_oauth_groups
|
and group_model.name in user_oauth_groups
|
||||||
and not any(gm.name == group_model.name for gm in user_current_groups)
|
and not any(gm.name == group_model.name for gm in user_current_groups)
|
||||||
and group_model.name not in blocked_groups
|
and not is_in_blocked_groups(group_model.name, blocked_groups)
|
||||||
):
|
):
|
||||||
# Add user to group
|
# Add user to group
|
||||||
log.debug(
|
log.debug(
|
||||||
|
|
@ -354,12 +584,17 @@ class OAuthManager:
|
||||||
async def handle_callback(self, request, provider, response):
|
async def handle_callback(self, request, provider, response):
|
||||||
if provider not in OAUTH_PROVIDERS:
|
if provider not in OAUTH_PROVIDERS:
|
||||||
raise HTTPException(404)
|
raise HTTPException(404)
|
||||||
|
|
||||||
|
error_message = None
|
||||||
|
try:
|
||||||
client = self.get_client(provider)
|
client = self.get_client(provider)
|
||||||
try:
|
try:
|
||||||
token = await client.authorize_access_token(request)
|
token = await client.authorize_access_token(request)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.warning(f"OAuth callback error: {e}")
|
log.warning(f"OAuth callback error: {e}")
|
||||||
raise HTTPException(400, detail=ERROR_MESSAGES.INVALID_CRED)
|
raise HTTPException(400, detail=ERROR_MESSAGES.INVALID_CRED)
|
||||||
|
|
||||||
|
# Try to get userinfo from the token first, some providers include it there
|
||||||
user_data: UserInfo = token.get("userinfo")
|
user_data: UserInfo = token.get("userinfo")
|
||||||
if (
|
if (
|
||||||
(not user_data)
|
(not user_data)
|
||||||
|
|
@ -371,18 +606,19 @@ class OAuthManager:
|
||||||
log.warning(f"OAuth callback failed, user data is missing: {token}")
|
log.warning(f"OAuth callback failed, user data is missing: {token}")
|
||||||
raise HTTPException(400, detail=ERROR_MESSAGES.INVALID_CRED)
|
raise HTTPException(400, detail=ERROR_MESSAGES.INVALID_CRED)
|
||||||
|
|
||||||
|
# Extract the "sub" claim, using custom claim if configured
|
||||||
if auth_manager_config.OAUTH_SUB_CLAIM:
|
if auth_manager_config.OAUTH_SUB_CLAIM:
|
||||||
sub = user_data.get(auth_manager_config.OAUTH_SUB_CLAIM)
|
sub = user_data.get(auth_manager_config.OAUTH_SUB_CLAIM)
|
||||||
else:
|
else:
|
||||||
# Fallback to the default sub claim if not configured
|
# Fallback to the default sub claim if not configured
|
||||||
sub = user_data.get(OAUTH_PROVIDERS[provider].get("sub_claim", "sub"))
|
sub = user_data.get(OAUTH_PROVIDERS[provider].get("sub_claim", "sub"))
|
||||||
|
|
||||||
if not sub:
|
if not sub:
|
||||||
log.warning(f"OAuth callback failed, sub is missing: {user_data}")
|
log.warning(f"OAuth callback failed, sub is missing: {user_data}")
|
||||||
raise HTTPException(400, detail=ERROR_MESSAGES.INVALID_CRED)
|
raise HTTPException(400, detail=ERROR_MESSAGES.INVALID_CRED)
|
||||||
|
|
||||||
provider_sub = f"{provider}@{sub}"
|
provider_sub = f"{provider}@{sub}"
|
||||||
|
|
||||||
|
# Email extraction
|
||||||
email_claim = auth_manager_config.OAUTH_EMAIL_CLAIM
|
email_claim = auth_manager_config.OAUTH_EMAIL_CLAIM
|
||||||
email = user_data.get(email_claim, "")
|
email = user_data.get(email_claim, "")
|
||||||
# We currently mandate that email addresses are provided
|
# We currently mandate that email addresses are provided
|
||||||
|
|
@ -402,7 +638,11 @@ class OAuthManager:
|
||||||
emails = await resp.json()
|
emails = await resp.json()
|
||||||
# use the primary email as the user's email
|
# use the primary email as the user's email
|
||||||
primary_email = next(
|
primary_email = next(
|
||||||
(e["email"] for e in emails if e.get("primary")),
|
(
|
||||||
|
e["email"]
|
||||||
|
for e in emails
|
||||||
|
if e.get("primary")
|
||||||
|
),
|
||||||
None,
|
None,
|
||||||
)
|
)
|
||||||
if primary_email:
|
if primary_email:
|
||||||
|
|
@ -426,9 +666,12 @@ class OAuthManager:
|
||||||
log.warning(f"OAuth callback failed, email is missing: {user_data}")
|
log.warning(f"OAuth callback failed, email is missing: {user_data}")
|
||||||
raise HTTPException(400, detail=ERROR_MESSAGES.INVALID_CRED)
|
raise HTTPException(400, detail=ERROR_MESSAGES.INVALID_CRED)
|
||||||
email = email.lower()
|
email = email.lower()
|
||||||
|
|
||||||
|
# If allowed domains are configured, check if the email domain is in the list
|
||||||
if (
|
if (
|
||||||
"*" not in auth_manager_config.OAUTH_ALLOWED_DOMAINS
|
"*" not in auth_manager_config.OAUTH_ALLOWED_DOMAINS
|
||||||
and email.split("@")[-1] not in auth_manager_config.OAUTH_ALLOWED_DOMAINS
|
and email.split("@")[-1]
|
||||||
|
not in auth_manager_config.OAUTH_ALLOWED_DOMAINS
|
||||||
):
|
):
|
||||||
log.warning(
|
log.warning(
|
||||||
f"OAuth callback failed, e-mail domain is not in the list of allowed domains: {user_data}"
|
f"OAuth callback failed, e-mail domain is not in the list of allowed domains: {user_data}"
|
||||||
|
|
@ -437,7 +680,6 @@ class OAuthManager:
|
||||||
|
|
||||||
# Check if the user exists
|
# Check if the user exists
|
||||||
user = Users.get_user_by_oauth_sub(provider_sub)
|
user = Users.get_user_by_oauth_sub(provider_sub)
|
||||||
|
|
||||||
if not user:
|
if not user:
|
||||||
# If the user does not exist, check if merging is enabled
|
# If the user does not exist, check if merging is enabled
|
||||||
if auth_manager_config.OAUTH_MERGE_ACCOUNTS_BY_EMAIL:
|
if auth_manager_config.OAUTH_MERGE_ACCOUNTS_BY_EMAIL:
|
||||||
|
|
@ -451,13 +693,13 @@ class OAuthManager:
|
||||||
determined_role = self.get_user_role(user, user_data)
|
determined_role = self.get_user_role(user, user_data)
|
||||||
if user.role != determined_role:
|
if user.role != determined_role:
|
||||||
Users.update_user_role_by_id(user.id, determined_role)
|
Users.update_user_role_by_id(user.id, determined_role)
|
||||||
|
|
||||||
# Update profile picture if enabled and different from current
|
# Update profile picture if enabled and different from current
|
||||||
if auth_manager_config.OAUTH_UPDATE_PICTURE_ON_LOGIN:
|
if auth_manager_config.OAUTH_UPDATE_PICTURE_ON_LOGIN:
|
||||||
picture_claim = auth_manager_config.OAUTH_PICTURE_CLAIM
|
picture_claim = auth_manager_config.OAUTH_PICTURE_CLAIM
|
||||||
if picture_claim:
|
if picture_claim:
|
||||||
new_picture_url = user_data.get(
|
new_picture_url = user_data.get(
|
||||||
picture_claim, OAUTH_PROVIDERS[provider].get("picture_url", "")
|
picture_claim,
|
||||||
|
OAUTH_PROVIDERS[provider].get("picture_url", ""),
|
||||||
)
|
)
|
||||||
processed_picture_url = await self._process_picture_url(
|
processed_picture_url = await self._process_picture_url(
|
||||||
new_picture_url, token.get("access_token")
|
new_picture_url, token.get("access_token")
|
||||||
|
|
@ -467,8 +709,7 @@ class OAuthManager:
|
||||||
user.id, processed_picture_url
|
user.id, processed_picture_url
|
||||||
)
|
)
|
||||||
log.debug(f"Updated profile picture for user {user.email}")
|
log.debug(f"Updated profile picture for user {user.email}")
|
||||||
|
else:
|
||||||
if not user:
|
|
||||||
# If the user does not exist, check if signups are enabled
|
# If the user does not exist, check if signups are enabled
|
||||||
if auth_manager_config.ENABLE_OAUTH_SIGNUP:
|
if auth_manager_config.ENABLE_OAUTH_SIGNUP:
|
||||||
# Check if an existing user with the same email already exists
|
# Check if an existing user with the same email already exists
|
||||||
|
|
@ -479,14 +720,14 @@ class OAuthManager:
|
||||||
picture_claim = auth_manager_config.OAUTH_PICTURE_CLAIM
|
picture_claim = auth_manager_config.OAUTH_PICTURE_CLAIM
|
||||||
if picture_claim:
|
if picture_claim:
|
||||||
picture_url = user_data.get(
|
picture_url = user_data.get(
|
||||||
picture_claim, OAUTH_PROVIDERS[provider].get("picture_url", "")
|
picture_claim,
|
||||||
|
OAUTH_PROVIDERS[provider].get("picture_url", ""),
|
||||||
)
|
)
|
||||||
picture_url = await self._process_picture_url(
|
picture_url = await self._process_picture_url(
|
||||||
picture_url, token.get("access_token")
|
picture_url, token.get("access_token")
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
picture_url = "/user.png"
|
picture_url = "/user.png"
|
||||||
|
|
||||||
username_claim = auth_manager_config.OAUTH_USERNAME_CLAIM
|
username_claim = auth_manager_config.OAUTH_USERNAME_CLAIM
|
||||||
|
|
||||||
name = user_data.get(username_claim)
|
name = user_data.get(username_claim)
|
||||||
|
|
@ -494,8 +735,6 @@ class OAuthManager:
|
||||||
log.warning("Username claim is missing, using email as name")
|
log.warning("Username claim is missing, using email as name")
|
||||||
name = email
|
name = email
|
||||||
|
|
||||||
role = self.get_user_role(None, user_data)
|
|
||||||
|
|
||||||
user = Auths.insert_new_auth(
|
user = Auths.insert_new_auth(
|
||||||
email=email,
|
email=email,
|
||||||
password=get_password_hash(
|
password=get_password_hash(
|
||||||
|
|
@ -503,7 +742,7 @@ class OAuthManager:
|
||||||
), # Random password, not used
|
), # Random password, not used
|
||||||
name=name,
|
name=name,
|
||||||
profile_image_url=picture_url,
|
profile_image_url=picture_url,
|
||||||
role=role,
|
role=self.get_user_role(None, user_data),
|
||||||
oauth_sub=provider_sub,
|
oauth_sub=provider_sub,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -520,26 +759,41 @@ class OAuthManager:
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.ACCESS_PROHIBITED
|
status.HTTP_403_FORBIDDEN,
|
||||||
|
detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
|
||||||
)
|
)
|
||||||
|
|
||||||
jwt_token = create_token(
|
jwt_token = create_token(
|
||||||
data={"id": user.id},
|
data={"id": user.id},
|
||||||
expires_delta=parse_duration(auth_manager_config.JWT_EXPIRES_IN),
|
expires_delta=parse_duration(auth_manager_config.JWT_EXPIRES_IN),
|
||||||
)
|
)
|
||||||
|
if (
|
||||||
if auth_manager_config.ENABLE_OAUTH_GROUP_MANAGEMENT and user.role != "admin":
|
auth_manager_config.ENABLE_OAUTH_GROUP_MANAGEMENT
|
||||||
|
and user.role != "admin"
|
||||||
|
):
|
||||||
self.update_user_groups(
|
self.update_user_groups(
|
||||||
user=user,
|
user=user,
|
||||||
user_data=user_data,
|
user_data=user_data,
|
||||||
default_permissions=request.app.state.config.USER_PERMISSIONS,
|
default_permissions=request.app.state.config.USER_PERMISSIONS,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
log.error(f"Error during OAuth process: {e}")
|
||||||
|
error_message = (
|
||||||
|
e.detail
|
||||||
|
if isinstance(e, HTTPException) and e.detail
|
||||||
|
else ERROR_MESSAGES.DEFAULT("Error during OAuth process")
|
||||||
|
)
|
||||||
|
|
||||||
redirect_base_url = str(request.app.state.config.WEBUI_URL or request.base_url)
|
redirect_base_url = str(request.app.state.config.WEBUI_URL or request.base_url)
|
||||||
if redirect_base_url.endswith("/"):
|
if redirect_base_url.endswith("/"):
|
||||||
redirect_base_url = redirect_base_url[:-1]
|
redirect_base_url = redirect_base_url[:-1]
|
||||||
redirect_url = f"{redirect_base_url}/auth"
|
redirect_url = f"{redirect_base_url}/auth"
|
||||||
|
|
||||||
|
if error_message:
|
||||||
|
redirect_url = f"{redirect_url}?error={error_message}"
|
||||||
|
return RedirectResponse(url=redirect_url, headers=response.headers)
|
||||||
|
|
||||||
response = RedirectResponse(url=redirect_url, headers=response.headers)
|
response = RedirectResponse(url=redirect_url, headers=response.headers)
|
||||||
|
|
||||||
# Set the cookie token
|
# Set the cookie token
|
||||||
|
|
@ -552,13 +806,48 @@ class OAuthManager:
|
||||||
secure=WEBUI_AUTH_COOKIE_SECURE,
|
secure=WEBUI_AUTH_COOKIE_SECURE,
|
||||||
)
|
)
|
||||||
|
|
||||||
if ENABLE_OAUTH_SIGNUP.value:
|
# Legacy cookies for compatibility with older frontend versions
|
||||||
oauth_id_token = token.get("id_token")
|
if ENABLE_OAUTH_ID_TOKEN_COOKIE:
|
||||||
response.set_cookie(
|
response.set_cookie(
|
||||||
key="oauth_id_token",
|
key="oauth_id_token",
|
||||||
value=oauth_id_token,
|
value=token.get("id_token"),
|
||||||
httponly=True,
|
httponly=True,
|
||||||
samesite=WEBUI_AUTH_COOKIE_SAME_SITE,
|
samesite=WEBUI_AUTH_COOKIE_SAME_SITE,
|
||||||
secure=WEBUI_AUTH_COOKIE_SECURE,
|
secure=WEBUI_AUTH_COOKIE_SECURE,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Add timestamp for tracking
|
||||||
|
token["issued_at"] = datetime.now().timestamp()
|
||||||
|
|
||||||
|
# Calculate expires_at if we have expires_in
|
||||||
|
if "expires_in" in token and "expires_at" not in token:
|
||||||
|
token["expires_at"] = datetime.now().timestamp() + token["expires_in"]
|
||||||
|
|
||||||
|
# Clean up any existing sessions for this user/provider first
|
||||||
|
sessions = OAuthSessions.get_sessions_by_user_id(user.id)
|
||||||
|
for session in sessions:
|
||||||
|
if session.provider == provider:
|
||||||
|
OAuthSessions.delete_session_by_id(session.id)
|
||||||
|
|
||||||
|
session = OAuthSessions.create_session(
|
||||||
|
user_id=user.id,
|
||||||
|
provider=provider,
|
||||||
|
token=token,
|
||||||
|
)
|
||||||
|
|
||||||
|
response.set_cookie(
|
||||||
|
key="oauth_session_id",
|
||||||
|
value=session.id,
|
||||||
|
httponly=True,
|
||||||
|
samesite=WEBUI_AUTH_COOKIE_SAME_SITE,
|
||||||
|
secure=WEBUI_AUTH_COOKIE_SECURE,
|
||||||
|
)
|
||||||
|
|
||||||
|
log.info(
|
||||||
|
f"Stored OAuth session server-side for user {user.id}, provider {provider}"
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
log.error(f"Failed to store OAuth session server-side: {e}")
|
||||||
|
|
||||||
return response
|
return response
|
||||||
|
|
|
||||||
|
|
@ -63,6 +63,7 @@ def remove_open_webui_params(params: dict) -> dict:
|
||||||
"stream_response": bool,
|
"stream_response": bool,
|
||||||
"stream_delta_chunk_size": int,
|
"stream_delta_chunk_size": int,
|
||||||
"function_calling": str,
|
"function_calling": str,
|
||||||
|
"reasoning_tags": list,
|
||||||
"system": str,
|
"system": str,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -119,18 +119,38 @@ async def get_tools(
|
||||||
function_name = spec["name"]
|
function_name = spec["name"]
|
||||||
|
|
||||||
auth_type = tool_server_connection.get("auth_type", "bearer")
|
auth_type = tool_server_connection.get("auth_type", "bearer")
|
||||||
token = None
|
|
||||||
|
cookies = {}
|
||||||
|
headers = {}
|
||||||
|
|
||||||
if auth_type == "bearer":
|
if auth_type == "bearer":
|
||||||
token = tool_server_connection.get("key", "")
|
headers["Authorization"] = (
|
||||||
|
f"Bearer {tool_server_connection.get('key', '')}"
|
||||||
|
)
|
||||||
|
elif auth_type == "none":
|
||||||
|
# No authentication
|
||||||
|
pass
|
||||||
elif auth_type == "session":
|
elif auth_type == "session":
|
||||||
token = request.state.token.credentials
|
cookies = request.cookies
|
||||||
|
headers["Authorization"] = (
|
||||||
|
f"Bearer {request.state.token.credentials}"
|
||||||
|
)
|
||||||
|
elif auth_type == "system_oauth":
|
||||||
|
cookies = request.cookies
|
||||||
|
oauth_token = extra_params.get("__oauth_token__", None)
|
||||||
|
if oauth_token:
|
||||||
|
headers["Authorization"] = (
|
||||||
|
f"Bearer {oauth_token.get('access_token', '')}"
|
||||||
|
)
|
||||||
|
|
||||||
def make_tool_function(function_name, token, tool_server_data):
|
headers["Content-Type"] = "application/json"
|
||||||
|
|
||||||
|
def make_tool_function(function_name, tool_server_data, headers):
|
||||||
async def tool_function(**kwargs):
|
async def tool_function(**kwargs):
|
||||||
return await execute_tool_server(
|
return await execute_tool_server(
|
||||||
token=token,
|
|
||||||
url=tool_server_data["url"],
|
url=tool_server_data["url"],
|
||||||
|
headers=headers,
|
||||||
|
cookies=cookies,
|
||||||
name=function_name,
|
name=function_name,
|
||||||
params=kwargs,
|
params=kwargs,
|
||||||
server_data=tool_server_data,
|
server_data=tool_server_data,
|
||||||
|
|
@ -139,7 +159,7 @@ async def get_tools(
|
||||||
return tool_function
|
return tool_function
|
||||||
|
|
||||||
tool_function = make_tool_function(
|
tool_function = make_tool_function(
|
||||||
function_name, token, tool_server_data
|
function_name, tool_server_data, headers
|
||||||
)
|
)
|
||||||
|
|
||||||
callable = get_async_tool_function_and_apply_extra_params(
|
callable = get_async_tool_function_and_apply_extra_params(
|
||||||
|
|
@ -542,9 +562,7 @@ async def get_tool_server_data(token: str, url: str) -> Dict[str, Any]:
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
|
||||||
async def get_tool_servers_data(
|
async def get_tool_servers_data(servers: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
||||||
servers: List[Dict[str, Any]], session_token: Optional[str] = None
|
|
||||||
) -> List[Dict[str, Any]]:
|
|
||||||
# Prepare list of enabled servers along with their original index
|
# Prepare list of enabled servers along with their original index
|
||||||
server_entries = []
|
server_entries = []
|
||||||
for idx, server in enumerate(servers):
|
for idx, server in enumerate(servers):
|
||||||
|
|
@ -560,8 +578,9 @@ async def get_tool_servers_data(
|
||||||
|
|
||||||
if auth_type == "bearer":
|
if auth_type == "bearer":
|
||||||
token = server.get("key", "")
|
token = server.get("key", "")
|
||||||
elif auth_type == "session":
|
elif auth_type == "none":
|
||||||
token = session_token
|
# No authentication
|
||||||
|
pass
|
||||||
|
|
||||||
id = info.get("id")
|
id = info.get("id")
|
||||||
if not id:
|
if not id:
|
||||||
|
|
@ -610,7 +629,12 @@ async def get_tool_servers_data(
|
||||||
|
|
||||||
|
|
||||||
async def execute_tool_server(
|
async def execute_tool_server(
|
||||||
token: str, url: str, name: str, params: Dict[str, Any], server_data: Dict[str, Any]
|
url: str,
|
||||||
|
headers: Dict[str, str],
|
||||||
|
cookies: Dict[str, str],
|
||||||
|
name: str,
|
||||||
|
params: Dict[str, Any],
|
||||||
|
server_data: Dict[str, Any],
|
||||||
) -> Any:
|
) -> Any:
|
||||||
error = None
|
error = None
|
||||||
try:
|
try:
|
||||||
|
|
@ -671,11 +695,6 @@ async def execute_tool_server(
|
||||||
f"Request body expected for operation '{name}' but none found."
|
f"Request body expected for operation '{name}' but none found."
|
||||||
)
|
)
|
||||||
|
|
||||||
headers = {"Content-Type": "application/json"}
|
|
||||||
|
|
||||||
if token:
|
|
||||||
headers["Authorization"] = f"Bearer {token}"
|
|
||||||
|
|
||||||
async with aiohttp.ClientSession(
|
async with aiohttp.ClientSession(
|
||||||
trust_env=True, timeout=aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT)
|
trust_env=True, timeout=aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT)
|
||||||
) as session:
|
) as session:
|
||||||
|
|
@ -686,6 +705,7 @@ async def execute_tool_server(
|
||||||
final_url,
|
final_url,
|
||||||
json=body_params,
|
json=body_params,
|
||||||
headers=headers,
|
headers=headers,
|
||||||
|
cookies=cookies,
|
||||||
ssl=AIOHTTP_CLIENT_SESSION_TOOL_SERVER_SSL,
|
ssl=AIOHTTP_CLIENT_SESSION_TOOL_SERVER_SSL,
|
||||||
) as response:
|
) as response:
|
||||||
if response.status >= 400:
|
if response.status >= 400:
|
||||||
|
|
@ -702,6 +722,7 @@ async def execute_tool_server(
|
||||||
async with request_method(
|
async with request_method(
|
||||||
final_url,
|
final_url,
|
||||||
headers=headers,
|
headers=headers,
|
||||||
|
cookies=cookies,
|
||||||
ssl=AIOHTTP_CLIENT_SESSION_TOOL_SERVER_SSL,
|
ssl=AIOHTTP_CLIENT_SESSION_TOOL_SERVER_SSL,
|
||||||
) as response:
|
) as response:
|
||||||
if response.status >= 400:
|
if response.status >= 400:
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,7 @@ pymongo
|
||||||
redis
|
redis
|
||||||
boto3==1.40.5
|
boto3==1.40.5
|
||||||
|
|
||||||
argon2-cffi==23.1.0
|
argon2-cffi==25.1.0
|
||||||
APScheduler==3.10.4
|
APScheduler==3.10.4
|
||||||
|
|
||||||
pycrdt==0.12.25
|
pycrdt==0.12.25
|
||||||
|
|
@ -42,12 +42,12 @@ asgiref==3.8.1
|
||||||
# AI libraries
|
# AI libraries
|
||||||
openai
|
openai
|
||||||
anthropic
|
anthropic
|
||||||
google-genai==1.28.0
|
google-genai==1.32.0
|
||||||
google-generativeai==0.8.5
|
google-generativeai==0.8.5
|
||||||
tiktoken
|
tiktoken
|
||||||
|
|
||||||
langchain==0.3.26
|
langchain==0.3.26
|
||||||
langchain-community==0.3.26
|
langchain-community==0.3.27
|
||||||
|
|
||||||
fake-useragent==2.2.0
|
fake-useragent==2.2.0
|
||||||
chromadb==0.6.3
|
chromadb==0.6.3
|
||||||
|
|
@ -65,12 +65,12 @@ transformers
|
||||||
sentence-transformers==4.1.0
|
sentence-transformers==4.1.0
|
||||||
accelerate
|
accelerate
|
||||||
colbert-ai==0.2.21
|
colbert-ai==0.2.21
|
||||||
pyarrow==20.0.0
|
pyarrow==20.0.0 # fix: pin pyarrow version to 20 for rpi compatibility #15897
|
||||||
einops==0.8.1
|
einops==0.8.1
|
||||||
|
|
||||||
|
|
||||||
ftfy==6.2.3
|
ftfy==6.2.3
|
||||||
pypdf==4.3.1
|
pypdf==6.0.0
|
||||||
fpdf2==2.8.2
|
fpdf2==2.8.2
|
||||||
pymdown-extensions==10.14.2
|
pymdown-extensions==10.14.2
|
||||||
docx2txt==0.8
|
docx2txt==0.8
|
||||||
|
|
@ -99,11 +99,10 @@ onnxruntime==1.20.1
|
||||||
faster-whisper==1.1.1
|
faster-whisper==1.1.1
|
||||||
|
|
||||||
PyJWT[crypto]==2.10.1
|
PyJWT[crypto]==2.10.1
|
||||||
authlib==1.6.1
|
authlib==1.6.3
|
||||||
|
|
||||||
black==25.1.0
|
black==25.1.0
|
||||||
langfuse==2.44.0
|
youtube-transcript-api==1.2.2
|
||||||
youtube-transcript-api==1.1.0
|
|
||||||
pytube==15.0.0
|
pytube==15.0.0
|
||||||
|
|
||||||
pydub
|
pydub
|
||||||
|
|
@ -116,7 +115,7 @@ google-auth-oauthlib
|
||||||
|
|
||||||
## Tests
|
## Tests
|
||||||
docker~=7.1.0
|
docker~=7.1.0
|
||||||
pytest~=8.3.5
|
pytest~=8.4.1
|
||||||
pytest-docker~=3.1.1
|
pytest-docker~=3.1.1
|
||||||
|
|
||||||
googleapis-common-protos==1.63.2
|
googleapis-common-protos==1.63.2
|
||||||
|
|
|
||||||
|
|
@ -53,12 +53,12 @@ if [ -n "$SPACE_ID" ]; then
|
||||||
WEBUI_SECRET_KEY="$WEBUI_SECRET_KEY" uvicorn open_webui.main:app --host "$HOST" --port "$PORT" --forwarded-allow-ips '*' &
|
WEBUI_SECRET_KEY="$WEBUI_SECRET_KEY" uvicorn open_webui.main:app --host "$HOST" --port "$PORT" --forwarded-allow-ips '*' &
|
||||||
webui_pid=$!
|
webui_pid=$!
|
||||||
echo "Waiting for webui to start..."
|
echo "Waiting for webui to start..."
|
||||||
while ! curl -s http://localhost:8080/health > /dev/null; do
|
while ! curl -s "http://localhost:${PORT}/health" > /dev/null; do
|
||||||
sleep 1
|
sleep 1
|
||||||
done
|
done
|
||||||
echo "Creating admin user..."
|
echo "Creating admin user..."
|
||||||
curl \
|
curl \
|
||||||
-X POST "http://localhost:8080/api/v1/auths/signup" \
|
-X POST "http://localhost:${PORT}/api/v1/auths/signup" \
|
||||||
-H "accept: application/json" \
|
-H "accept: application/json" \
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
-d "{ \"email\": \"${ADMIN_USER_EMAIL}\", \"password\": \"${ADMIN_USER_PASSWORD}\", \"name\": \"Admin\" }"
|
-d "{ \"email\": \"${ADMIN_USER_EMAIL}\", \"password\": \"${ADMIN_USER_PASSWORD}\", \"name\": \"Admin\" }"
|
||||||
|
|
|
||||||
177
package-lock.json
generated
177
package-lock.json
generated
|
|
@ -1,12 +1,12 @@
|
||||||
{
|
{
|
||||||
"name": "open-webui",
|
"name": "open-webui",
|
||||||
"version": "0.6.25",
|
"version": "0.6.27",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "open-webui",
|
"name": "open-webui",
|
||||||
"version": "0.6.25",
|
"version": "0.6.27",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@azure/msal-browser": "^4.5.0",
|
"@azure/msal-browser": "^4.5.0",
|
||||||
"@codemirror/lang-javascript": "^6.2.2",
|
"@codemirror/lang-javascript": "^6.2.2",
|
||||||
|
|
@ -26,7 +26,7 @@
|
||||||
"@tiptap/extension-drag-handle": "^3.0.7",
|
"@tiptap/extension-drag-handle": "^3.0.7",
|
||||||
"@tiptap/extension-file-handler": "^3.0.7",
|
"@tiptap/extension-file-handler": "^3.0.7",
|
||||||
"@tiptap/extension-floating-menu": "^2.26.1",
|
"@tiptap/extension-floating-menu": "^2.26.1",
|
||||||
"@tiptap/extension-highlight": "^3.0.7",
|
"@tiptap/extension-highlight": "^3.3.0",
|
||||||
"@tiptap/extension-image": "^3.0.7",
|
"@tiptap/extension-image": "^3.0.7",
|
||||||
"@tiptap/extension-link": "^3.0.7",
|
"@tiptap/extension-link": "^3.0.7",
|
||||||
"@tiptap/extension-list": "^3.0.7",
|
"@tiptap/extension-list": "^3.0.7",
|
||||||
|
|
@ -46,7 +46,7 @@
|
||||||
"codemirror-lang-hcl": "^0.1.0",
|
"codemirror-lang-hcl": "^0.1.0",
|
||||||
"crc-32": "^1.2.2",
|
"crc-32": "^1.2.2",
|
||||||
"dayjs": "^1.11.10",
|
"dayjs": "^1.11.10",
|
||||||
"dompurify": "^3.2.5",
|
"dompurify": "^3.2.6",
|
||||||
"eventsource-parser": "^1.1.2",
|
"eventsource-parser": "^1.1.2",
|
||||||
"file-saver": "^2.0.5",
|
"file-saver": "^2.0.5",
|
||||||
"focus-trap": "^7.6.4",
|
"focus-trap": "^7.6.4",
|
||||||
|
|
@ -66,10 +66,10 @@
|
||||||
"leaflet": "^1.9.4",
|
"leaflet": "^1.9.4",
|
||||||
"lowlight": "^3.3.0",
|
"lowlight": "^3.3.0",
|
||||||
"marked": "^9.1.0",
|
"marked": "^9.1.0",
|
||||||
"mermaid": "^11.6.0",
|
"mermaid": "^11.10.1",
|
||||||
"paneforge": "^0.0.6",
|
"paneforge": "^0.0.6",
|
||||||
"panzoom": "^9.4.3",
|
"panzoom": "^9.4.3",
|
||||||
"pdfjs-dist": "^5.3.93",
|
"pdfjs-dist": "^5.4.149",
|
||||||
"prosemirror-collab": "^1.3.1",
|
"prosemirror-collab": "^1.3.1",
|
||||||
"prosemirror-commands": "^1.6.0",
|
"prosemirror-commands": "^1.6.0",
|
||||||
"prosemirror-example-setup": "^1.2.3",
|
"prosemirror-example-setup": "^1.2.3",
|
||||||
|
|
@ -82,10 +82,11 @@
|
||||||
"prosemirror-state": "^1.4.3",
|
"prosemirror-state": "^1.4.3",
|
||||||
"prosemirror-tables": "^1.7.1",
|
"prosemirror-tables": "^1.7.1",
|
||||||
"prosemirror-view": "^1.34.3",
|
"prosemirror-view": "^1.34.3",
|
||||||
"pyodide": "^0.27.3",
|
"pyodide": "^0.28.2",
|
||||||
"socket.io-client": "^4.2.0",
|
"socket.io-client": "^4.2.0",
|
||||||
"sortablejs": "^1.15.6",
|
"sortablejs": "^1.15.6",
|
||||||
"svelte-sonner": "^0.3.19",
|
"svelte-sonner": "^0.3.19",
|
||||||
|
"svelte-tiptap": "^3.0.0",
|
||||||
"tippy.js": "^6.3.7",
|
"tippy.js": "^6.3.7",
|
||||||
"turndown": "^7.2.0",
|
"turndown": "^7.2.0",
|
||||||
"turndown-plugin-gfm": "^1.0.2",
|
"turndown-plugin-gfm": "^1.0.2",
|
||||||
|
|
@ -2224,9 +2225,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@mermaid-js/parser": {
|
"node_modules/@mermaid-js/parser": {
|
||||||
"version": "0.4.0",
|
"version": "0.6.2",
|
||||||
"resolved": "https://registry.npmjs.org/@mermaid-js/parser/-/parser-0.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/@mermaid-js/parser/-/parser-0.6.2.tgz",
|
||||||
"integrity": "sha512-wla8XOWvQAwuqy+gxiZqY+c7FokraOTHRWMsbB4AgRx9Sy7zKslNyejy7E+a77qHfey5GXw/ik3IXv/NHMJgaA==",
|
"integrity": "sha512-+PO02uGF6L6Cs0Bw8RpGhikVvMWEysfAyl27qTlroUB8jSWr1lL0Sf6zi78ZxlSnmgSY2AMMKVgghnN9jTtwkQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"langium": "3.3.1"
|
"langium": "3.3.1"
|
||||||
|
|
@ -2238,9 +2239,9 @@
|
||||||
"integrity": "sha512-Y28PR25bHXUg88kCV7nivXrP2Nj2RueZ3/l/jdx6J9f8J4nsEGcgX0Qe6lt7Pa+J79+kPiJU3LguR6O/6zrLOw=="
|
"integrity": "sha512-Y28PR25bHXUg88kCV7nivXrP2Nj2RueZ3/l/jdx6J9f8J4nsEGcgX0Qe6lt7Pa+J79+kPiJU3LguR6O/6zrLOw=="
|
||||||
},
|
},
|
||||||
"node_modules/@napi-rs/canvas": {
|
"node_modules/@napi-rs/canvas": {
|
||||||
"version": "0.1.73",
|
"version": "0.1.78",
|
||||||
"resolved": "https://registry.npmjs.org/@napi-rs/canvas/-/canvas-0.1.73.tgz",
|
"resolved": "https://registry.npmjs.org/@napi-rs/canvas/-/canvas-0.1.78.tgz",
|
||||||
"integrity": "sha512-9iwPZrNlCK4rG+vWyDvyvGeYjck9MoP0NVQP6N60gqJNFA1GsN0imG05pzNsqfCvFxUxgiTYlR8ff0HC1HXJiw==",
|
"integrity": "sha512-YaBHJvT+T1DoP16puvWM6w46Lq3VhwKIJ8th5m1iEJyGh7mibk5dT7flBvMQ1EH1LYmMzXJ+OUhu+8wQ9I6u7g==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"workspaces": [
|
"workspaces": [
|
||||||
|
|
@ -2250,22 +2251,22 @@
|
||||||
"node": ">= 10"
|
"node": ">= 10"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"@napi-rs/canvas-android-arm64": "0.1.73",
|
"@napi-rs/canvas-android-arm64": "0.1.78",
|
||||||
"@napi-rs/canvas-darwin-arm64": "0.1.73",
|
"@napi-rs/canvas-darwin-arm64": "0.1.78",
|
||||||
"@napi-rs/canvas-darwin-x64": "0.1.73",
|
"@napi-rs/canvas-darwin-x64": "0.1.78",
|
||||||
"@napi-rs/canvas-linux-arm-gnueabihf": "0.1.73",
|
"@napi-rs/canvas-linux-arm-gnueabihf": "0.1.78",
|
||||||
"@napi-rs/canvas-linux-arm64-gnu": "0.1.73",
|
"@napi-rs/canvas-linux-arm64-gnu": "0.1.78",
|
||||||
"@napi-rs/canvas-linux-arm64-musl": "0.1.73",
|
"@napi-rs/canvas-linux-arm64-musl": "0.1.78",
|
||||||
"@napi-rs/canvas-linux-riscv64-gnu": "0.1.73",
|
"@napi-rs/canvas-linux-riscv64-gnu": "0.1.78",
|
||||||
"@napi-rs/canvas-linux-x64-gnu": "0.1.73",
|
"@napi-rs/canvas-linux-x64-gnu": "0.1.78",
|
||||||
"@napi-rs/canvas-linux-x64-musl": "0.1.73",
|
"@napi-rs/canvas-linux-x64-musl": "0.1.78",
|
||||||
"@napi-rs/canvas-win32-x64-msvc": "0.1.73"
|
"@napi-rs/canvas-win32-x64-msvc": "0.1.78"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@napi-rs/canvas-android-arm64": {
|
"node_modules/@napi-rs/canvas-android-arm64": {
|
||||||
"version": "0.1.73",
|
"version": "0.1.78",
|
||||||
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-android-arm64/-/canvas-android-arm64-0.1.73.tgz",
|
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-android-arm64/-/canvas-android-arm64-0.1.78.tgz",
|
||||||
"integrity": "sha512-s8dMhfYIHVv7gz8BXg3Nb6cFi950Y0xH5R/sotNZzUVvU9EVqHfkqiGJ4UIqu+15UhqguT6mI3Bv1mhpRkmMQw==",
|
"integrity": "sha512-N1ikxztjrRmh8xxlG5kYm1RuNr8ZW1EINEDQsLhhuy7t0pWI/e7SH91uFVLZKCMDyjel1tyWV93b5fdCAi7ggw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
|
|
@ -2279,9 +2280,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@napi-rs/canvas-darwin-arm64": {
|
"node_modules/@napi-rs/canvas-darwin-arm64": {
|
||||||
"version": "0.1.73",
|
"version": "0.1.78",
|
||||||
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-arm64/-/canvas-darwin-arm64-0.1.73.tgz",
|
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-arm64/-/canvas-darwin-arm64-0.1.78.tgz",
|
||||||
"integrity": "sha512-bLPCq8Yyq1vMdVdIpQAqmgf6VGUknk8e7NdSZXJJFOA9gxkJ1RGcHOwoXo7h0gzhHxSorg71hIxyxtwXpq10Rw==",
|
"integrity": "sha512-FA3aCU3G5yGc74BSmnLJTObnZRV+HW+JBTrsU+0WVVaNyVKlb5nMvYAQuieQlRVemsAA2ek2c6nYtHh6u6bwFw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
|
|
@ -2295,9 +2296,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@napi-rs/canvas-darwin-x64": {
|
"node_modules/@napi-rs/canvas-darwin-x64": {
|
||||||
"version": "0.1.73",
|
"version": "0.1.78",
|
||||||
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-x64/-/canvas-darwin-x64-0.1.73.tgz",
|
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-x64/-/canvas-darwin-x64-0.1.78.tgz",
|
||||||
"integrity": "sha512-GR1CcehDjdNYXN3bj8PIXcXfYLUUOQANjQpM+KNnmpRo7ojsuqPjT7ZVH+6zoG/aqRJWhiSo+ChQMRazZlRU9g==",
|
"integrity": "sha512-xVij69o9t/frixCDEoyWoVDKgE3ksLGdmE2nvBWVGmoLu94MWUlv2y4Qzf5oozBmydG5Dcm4pRHFBM7YWa1i6g==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
|
|
@ -2311,9 +2312,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@napi-rs/canvas-linux-arm-gnueabihf": {
|
"node_modules/@napi-rs/canvas-linux-arm-gnueabihf": {
|
||||||
"version": "0.1.73",
|
"version": "0.1.78",
|
||||||
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm-gnueabihf/-/canvas-linux-arm-gnueabihf-0.1.73.tgz",
|
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm-gnueabihf/-/canvas-linux-arm-gnueabihf-0.1.78.tgz",
|
||||||
"integrity": "sha512-cM7F0kBJVFio0+U2iKSW4fWSfYQ8CPg4/DRZodSum/GcIyfB8+UPJSRM1BvvlcWinKLfX1zUYOwonZX9IFRRcw==",
|
"integrity": "sha512-aSEXrLcIpBtXpOSnLhTg4jPsjJEnK7Je9KqUdAWjc7T8O4iYlxWxrXFIF8rV8J79h5jNdScgZpAUWYnEcutR3g==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm"
|
"arm"
|
||||||
],
|
],
|
||||||
|
|
@ -2327,9 +2328,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@napi-rs/canvas-linux-arm64-gnu": {
|
"node_modules/@napi-rs/canvas-linux-arm64-gnu": {
|
||||||
"version": "0.1.73",
|
"version": "0.1.78",
|
||||||
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-gnu/-/canvas-linux-arm64-gnu-0.1.73.tgz",
|
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-gnu/-/canvas-linux-arm64-gnu-0.1.78.tgz",
|
||||||
"integrity": "sha512-PMWNrMON9uz9klz1B8ZY/RXepQSC5dxxHQTowfw93Tb3fLtWO5oNX2k9utw7OM4ypT9BUZUWJnDQ5bfuXc/EUQ==",
|
"integrity": "sha512-dlEPRX1hLGKaY3UtGa1dtkA1uGgFITn2mDnfI6YsLlYyLJQNqHx87D1YTACI4zFCUuLr/EzQDzuX+vnp9YveVg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
|
|
@ -2343,9 +2344,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@napi-rs/canvas-linux-arm64-musl": {
|
"node_modules/@napi-rs/canvas-linux-arm64-musl": {
|
||||||
"version": "0.1.73",
|
"version": "0.1.78",
|
||||||
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-musl/-/canvas-linux-arm64-musl-0.1.73.tgz",
|
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-musl/-/canvas-linux-arm64-musl-0.1.78.tgz",
|
||||||
"integrity": "sha512-lX0z2bNmnk1PGZ+0a9OZwI2lPPvWjRYzPqvEitXX7lspyLFrOzh2kcQiLL7bhyODN23QvfriqwYqp5GreSzVvA==",
|
"integrity": "sha512-TsCfjOPZtm5Q/NO1EZHR5pwDPSPjPEttvnv44GL32Zn1uvudssjTLbvaG1jHq81Qxm16GTXEiYLmx4jOLZQYlg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
|
|
@ -2359,9 +2360,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@napi-rs/canvas-linux-riscv64-gnu": {
|
"node_modules/@napi-rs/canvas-linux-riscv64-gnu": {
|
||||||
"version": "0.1.73",
|
"version": "0.1.78",
|
||||||
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-riscv64-gnu/-/canvas-linux-riscv64-gnu-0.1.73.tgz",
|
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-riscv64-gnu/-/canvas-linux-riscv64-gnu-0.1.78.tgz",
|
||||||
"integrity": "sha512-QDQgMElwxAoADsSR3UYvdTTQk5XOyD9J5kq15Z8XpGwpZOZsSE0zZ/X1JaOtS2x+HEZL6z1S6MF/1uhZFZb5ig==",
|
"integrity": "sha512-+cpTTb0GDshEow/5Fy8TpNyzaPsYb3clQIjgWRmzRcuteLU+CHEU/vpYvAcSo7JxHYPJd8fjSr+qqh+nI5AtmA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"riscv64"
|
"riscv64"
|
||||||
],
|
],
|
||||||
|
|
@ -2375,9 +2376,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@napi-rs/canvas-linux-x64-gnu": {
|
"node_modules/@napi-rs/canvas-linux-x64-gnu": {
|
||||||
"version": "0.1.73",
|
"version": "0.1.78",
|
||||||
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-gnu/-/canvas-linux-x64-gnu-0.1.73.tgz",
|
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-gnu/-/canvas-linux-x64-gnu-0.1.78.tgz",
|
||||||
"integrity": "sha512-wbzLJrTalQrpyrU1YRrO6w6pdr5vcebbJa+Aut5QfTaW9eEmMb1WFG6l1V+cCa5LdHmRr8bsvl0nJDU/IYDsmw==",
|
"integrity": "sha512-wxRcvKfvYBgtrO0Uy8OmwvjlnTcHpY45LLwkwVNIWHPqHAsyoTyG/JBSfJ0p5tWRzMOPDCDqdhpIO4LOgXjeyg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
|
|
@ -2391,9 +2392,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@napi-rs/canvas-linux-x64-musl": {
|
"node_modules/@napi-rs/canvas-linux-x64-musl": {
|
||||||
"version": "0.1.73",
|
"version": "0.1.78",
|
||||||
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-musl/-/canvas-linux-x64-musl-0.1.73.tgz",
|
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-musl/-/canvas-linux-x64-musl-0.1.78.tgz",
|
||||||
"integrity": "sha512-xbfhYrUufoTAKvsEx2ZUN4jvACabIF0h1F5Ik1Rk4e/kQq6c+Dwa5QF0bGrfLhceLpzHT0pCMGMDeQKQrcUIyA==",
|
"integrity": "sha512-vQFOGwC9QDP0kXlhb2LU1QRw/humXgcbVp8mXlyBqzc/a0eijlLF9wzyarHC1EywpymtS63TAj8PHZnhTYN6hg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
|
|
@ -2407,9 +2408,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@napi-rs/canvas-win32-x64-msvc": {
|
"node_modules/@napi-rs/canvas-win32-x64-msvc": {
|
||||||
"version": "0.1.73",
|
"version": "0.1.78",
|
||||||
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-win32-x64-msvc/-/canvas-win32-x64-msvc-0.1.73.tgz",
|
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-win32-x64-msvc/-/canvas-win32-x64-msvc-0.1.78.tgz",
|
||||||
"integrity": "sha512-YQmHXBufFBdWqhx+ympeTPkMfs3RNxaOgWm59vyjpsub7Us07BwCcmu1N5kildhO8Fm0syoI2kHnzGkJBLSvsg==",
|
"integrity": "sha512-/eKlTZBtGUgpRKalzOzRr6h7KVSuziESWXgBcBnXggZmimwIJWPJlEcbrx5Tcwj8rPuZiANXQOG9pPgy9Q4LTQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
|
|
@ -3516,16 +3517,16 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@tiptap/extension-highlight": {
|
"node_modules/@tiptap/extension-highlight": {
|
||||||
"version": "3.0.7",
|
"version": "3.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-highlight/-/extension-highlight-3.0.7.tgz",
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-highlight/-/extension-highlight-3.3.0.tgz",
|
||||||
"integrity": "sha512-3oIRuXAg7l9+VPIMwHycXcqtZ7XJcC5vnLhPAQXIesYun6L9EoXmQox0225z8jpPG70N8zfl+YSd4qjsTMPaAg==",
|
"integrity": "sha512-G+mHVXkoQ4uG97JRFN56qL42iJVKbSeWgDGssmnjNZN/W4Nsc40LuNryNbQUOM9CJbEMIT5NGAwvc/RG0OpGGQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"funding": {
|
"funding": {
|
||||||
"type": "github",
|
"type": "github",
|
||||||
"url": "https://github.com/sponsors/ueberdosis"
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@tiptap/core": "^3.0.7"
|
"@tiptap/core": "^3.3.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@tiptap/extension-horizontal-rule": {
|
"node_modules/@tiptap/extension-horizontal-rule": {
|
||||||
|
|
@ -6767,9 +6768,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/dompurify": {
|
"node_modules/dompurify": {
|
||||||
"version": "3.2.5",
|
"version": "3.2.6",
|
||||||
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.5.tgz",
|
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.6.tgz",
|
||||||
"integrity": "sha512-mLPd29uoRe9HpvwP2TxClGQBzGXeEC/we/q+bFlmPPmj2p2Ugl3r6ATu/UU1v77DXNcehiBg9zsr1dREyA/dJQ==",
|
"integrity": "sha512-/2GogDQlohXPZe6D6NOgQvXLPSYBqIWMnZ8zzOhn09REE4eyAzb+Hed3jhoM9OkuaJ8P6ZGTTVWQKAi8ieIzfQ==",
|
||||||
"license": "(MPL-2.0 OR Apache-2.0)",
|
"license": "(MPL-2.0 OR Apache-2.0)",
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"@types/trusted-types": "^2.0.7"
|
"@types/trusted-types": "^2.0.7"
|
||||||
|
|
@ -9577,14 +9578,14 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/mermaid": {
|
"node_modules/mermaid": {
|
||||||
"version": "11.6.0",
|
"version": "11.10.1",
|
||||||
"resolved": "https://registry.npmjs.org/mermaid/-/mermaid-11.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/mermaid/-/mermaid-11.10.1.tgz",
|
||||||
"integrity": "sha512-PE8hGUy1LDlWIHWBP05SFdqUHGmRcCcK4IzpOKPE35eOw+G9zZgcnMpyunJVUEOgb//KBORPjysKndw8bFLuRg==",
|
"integrity": "sha512-0PdeADVWURz7VMAX0+MiMcgfxFKY4aweSGsjgFihe3XlMKNqmai/cugMrqTd3WNHM93V+K+AZL6Wu6tB5HmxRw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@braintree/sanitize-url": "^7.0.4",
|
"@braintree/sanitize-url": "^7.0.4",
|
||||||
"@iconify/utils": "^2.1.33",
|
"@iconify/utils": "^2.1.33",
|
||||||
"@mermaid-js/parser": "^0.4.0",
|
"@mermaid-js/parser": "^0.6.2",
|
||||||
"@types/d3": "^7.4.3",
|
"@types/d3": "^7.4.3",
|
||||||
"cytoscape": "^3.29.3",
|
"cytoscape": "^3.29.3",
|
||||||
"cytoscape-cose-bilkent": "^4.1.0",
|
"cytoscape-cose-bilkent": "^4.1.0",
|
||||||
|
|
@ -9593,11 +9594,11 @@
|
||||||
"d3-sankey": "^0.12.3",
|
"d3-sankey": "^0.12.3",
|
||||||
"dagre-d3-es": "7.0.11",
|
"dagre-d3-es": "7.0.11",
|
||||||
"dayjs": "^1.11.13",
|
"dayjs": "^1.11.13",
|
||||||
"dompurify": "^3.2.4",
|
"dompurify": "^3.2.5",
|
||||||
"katex": "^0.16.9",
|
"katex": "^0.16.22",
|
||||||
"khroma": "^2.1.0",
|
"khroma": "^2.1.0",
|
||||||
"lodash-es": "^4.17.21",
|
"lodash-es": "^4.17.21",
|
||||||
"marked": "^15.0.7",
|
"marked": "^16.0.0",
|
||||||
"roughjs": "^4.6.6",
|
"roughjs": "^4.6.6",
|
||||||
"stylis": "^4.3.6",
|
"stylis": "^4.3.6",
|
||||||
"ts-dedent": "^2.2.0",
|
"ts-dedent": "^2.2.0",
|
||||||
|
|
@ -9605,15 +9606,15 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/mermaid/node_modules/marked": {
|
"node_modules/mermaid/node_modules/marked": {
|
||||||
"version": "15.0.8",
|
"version": "16.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/marked/-/marked-15.0.8.tgz",
|
"resolved": "https://registry.npmjs.org/marked/-/marked-16.2.1.tgz",
|
||||||
"integrity": "sha512-rli4l2LyZqpQuRve5C0rkn6pj3hT8EWPC+zkAxFTAJLxRbENfTAhEQq9itrmf1Y81QtAX5D/MYlGlIomNgj9lA==",
|
"integrity": "sha512-r3UrXED9lMlHF97jJByry90cwrZBBvZmjG1L68oYfuPMW+uDTnuMbyJDymCWwbTE+f+3LhpNDKfpR3a3saFyjA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"bin": {
|
"bin": {
|
||||||
"marked": "bin/marked.js"
|
"marked": "bin/marked.js"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 18"
|
"node": ">= 20"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/mermaid/node_modules/uuid": {
|
"node_modules/mermaid/node_modules/uuid": {
|
||||||
|
|
@ -10252,15 +10253,15 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/pdfjs-dist": {
|
"node_modules/pdfjs-dist": {
|
||||||
"version": "5.3.93",
|
"version": "5.4.149",
|
||||||
"resolved": "https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-5.3.93.tgz",
|
"resolved": "https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-5.4.149.tgz",
|
||||||
"integrity": "sha512-w3fQKVL1oGn8FRyx5JUG5tnbblggDqyx2XzA5brsJ5hSuS+I0NdnJANhmeWKLjotdbPQucLBug5t0MeWr0AAdg==",
|
"integrity": "sha512-Xe8/1FMJEQPUVSti25AlDpwpUm2QAVmNOpFP0SIahaPIOKBKICaefbzogLdwey3XGGoaP4Lb9wqiw2e9Jqp0LA==",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=20.16.0 || >=22.3.0"
|
"node": ">=20.16.0 || >=22.3.0"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"@napi-rs/canvas": "^0.1.71"
|
"@napi-rs/canvas": "^0.1.77"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/pend": {
|
"node_modules/pend": {
|
||||||
|
|
@ -10905,9 +10906,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/pyodide": {
|
"node_modules/pyodide": {
|
||||||
"version": "0.27.7",
|
"version": "0.28.2",
|
||||||
"resolved": "https://registry.npmjs.org/pyodide/-/pyodide-0.27.7.tgz",
|
"resolved": "https://registry.npmjs.org/pyodide/-/pyodide-0.28.2.tgz",
|
||||||
"integrity": "sha512-RUSVJlhQdfWfgO9hVHCiXoG+nVZQRS5D9FzgpLJ/VcgGBLSAKoPL8kTiOikxbHQm1kRISeWUBdulEgO26qpSRA==",
|
"integrity": "sha512-2BrZHrALvhYZfIuTGDHOvyiirHNLziHfBiBb1tpBFzLgAvDBb2ACxNPFFROCOzLnqapORmgArDYY8mJmMWH1Eg==",
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"ws": "^8.5.0"
|
"ws": "^8.5.0"
|
||||||
|
|
@ -12502,6 +12503,26 @@
|
||||||
"svelte": "^3.0.0 || ^4.0.0 || ^5.0.0-next.1"
|
"svelte": "^3.0.0 || ^4.0.0 || ^5.0.0-next.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/svelte-tiptap": {
|
||||||
|
"version": "3.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/svelte-tiptap/-/svelte-tiptap-3.0.0.tgz",
|
||||||
|
"integrity": "sha512-digFHOJe16RX0HIU+u8hOaCS9sIgktTpYHSF9yJ6dgxPv/JWJdYCdwoX65lcHitFhhCG7xnolJng6PJa9M9h3w==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/sibiraj-s"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"@floating-ui/dom": "^1.0.0",
|
||||||
|
"@tiptap/core": "^3.0.0",
|
||||||
|
"@tiptap/extension-bubble-menu": "^3.0.0",
|
||||||
|
"@tiptap/extension-floating-menu": "^3.0.0",
|
||||||
|
"@tiptap/pm": "^3.0.0",
|
||||||
|
"svelte": "^5.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/svelte/node_modules/estree-walker": {
|
"node_modules/svelte/node_modules/estree-walker": {
|
||||||
"version": "3.0.3",
|
"version": "3.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz",
|
||||||
|
|
|
||||||
13
package.json
13
package.json
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "open-webui",
|
"name": "open-webui",
|
||||||
"version": "0.6.25",
|
"version": "0.6.27",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "npm run pyodide:fetch && vite dev --host",
|
"dev": "npm run pyodide:fetch && vite dev --host",
|
||||||
|
|
@ -70,7 +70,7 @@
|
||||||
"@tiptap/extension-drag-handle": "^3.0.7",
|
"@tiptap/extension-drag-handle": "^3.0.7",
|
||||||
"@tiptap/extension-file-handler": "^3.0.7",
|
"@tiptap/extension-file-handler": "^3.0.7",
|
||||||
"@tiptap/extension-floating-menu": "^2.26.1",
|
"@tiptap/extension-floating-menu": "^2.26.1",
|
||||||
"@tiptap/extension-highlight": "^3.0.7",
|
"@tiptap/extension-highlight": "^3.3.0",
|
||||||
"@tiptap/extension-image": "^3.0.7",
|
"@tiptap/extension-image": "^3.0.7",
|
||||||
"@tiptap/extension-link": "^3.0.7",
|
"@tiptap/extension-link": "^3.0.7",
|
||||||
"@tiptap/extension-list": "^3.0.7",
|
"@tiptap/extension-list": "^3.0.7",
|
||||||
|
|
@ -90,7 +90,7 @@
|
||||||
"codemirror-lang-hcl": "^0.1.0",
|
"codemirror-lang-hcl": "^0.1.0",
|
||||||
"crc-32": "^1.2.2",
|
"crc-32": "^1.2.2",
|
||||||
"dayjs": "^1.11.10",
|
"dayjs": "^1.11.10",
|
||||||
"dompurify": "^3.2.5",
|
"dompurify": "^3.2.6",
|
||||||
"eventsource-parser": "^1.1.2",
|
"eventsource-parser": "^1.1.2",
|
||||||
"file-saver": "^2.0.5",
|
"file-saver": "^2.0.5",
|
||||||
"focus-trap": "^7.6.4",
|
"focus-trap": "^7.6.4",
|
||||||
|
|
@ -110,10 +110,10 @@
|
||||||
"leaflet": "^1.9.4",
|
"leaflet": "^1.9.4",
|
||||||
"lowlight": "^3.3.0",
|
"lowlight": "^3.3.0",
|
||||||
"marked": "^9.1.0",
|
"marked": "^9.1.0",
|
||||||
"mermaid": "^11.6.0",
|
"mermaid": "^11.10.1",
|
||||||
"paneforge": "^0.0.6",
|
"paneforge": "^0.0.6",
|
||||||
"panzoom": "^9.4.3",
|
"panzoom": "^9.4.3",
|
||||||
"pdfjs-dist": "^5.3.93",
|
"pdfjs-dist": "^5.4.149",
|
||||||
"prosemirror-collab": "^1.3.1",
|
"prosemirror-collab": "^1.3.1",
|
||||||
"prosemirror-commands": "^1.6.0",
|
"prosemirror-commands": "^1.6.0",
|
||||||
"prosemirror-example-setup": "^1.2.3",
|
"prosemirror-example-setup": "^1.2.3",
|
||||||
|
|
@ -126,10 +126,11 @@
|
||||||
"prosemirror-state": "^1.4.3",
|
"prosemirror-state": "^1.4.3",
|
||||||
"prosemirror-tables": "^1.7.1",
|
"prosemirror-tables": "^1.7.1",
|
||||||
"prosemirror-view": "^1.34.3",
|
"prosemirror-view": "^1.34.3",
|
||||||
"pyodide": "^0.27.3",
|
"pyodide": "^0.28.2",
|
||||||
"socket.io-client": "^4.2.0",
|
"socket.io-client": "^4.2.0",
|
||||||
"sortablejs": "^1.15.6",
|
"sortablejs": "^1.15.6",
|
||||||
"svelte-sonner": "^0.3.19",
|
"svelte-sonner": "^0.3.19",
|
||||||
|
"svelte-tiptap": "^3.0.0",
|
||||||
"tippy.js": "^6.3.7",
|
"tippy.js": "^6.3.7",
|
||||||
"turndown": "^7.2.0",
|
"turndown": "^7.2.0",
|
||||||
"turndown-plugin-gfm": "^1.0.2",
|
"turndown-plugin-gfm": "^1.0.2",
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,10 @@ dependencies = [
|
||||||
"python-jose==3.4.0",
|
"python-jose==3.4.0",
|
||||||
"passlib[bcrypt]==1.7.4",
|
"passlib[bcrypt]==1.7.4",
|
||||||
"cryptography",
|
"cryptography",
|
||||||
|
"bcrypt==4.3.0",
|
||||||
|
"argon2-cffi==23.1.0",
|
||||||
|
"PyJWT[crypto]==2.10.1",
|
||||||
|
"authlib==1.6.3",
|
||||||
|
|
||||||
"requests==2.32.4",
|
"requests==2.32.4",
|
||||||
"aiohttp==3.12.15",
|
"aiohttp==3.12.15",
|
||||||
|
|
@ -28,34 +32,27 @@ dependencies = [
|
||||||
"alembic==1.14.0",
|
"alembic==1.14.0",
|
||||||
"peewee==3.18.1",
|
"peewee==3.18.1",
|
||||||
"peewee-migrate==1.12.2",
|
"peewee-migrate==1.12.2",
|
||||||
"psycopg2-binary==2.9.9",
|
|
||||||
"pgvector==0.4.0",
|
|
||||||
"PyMySQL==1.1.1",
|
|
||||||
"bcrypt==4.3.0",
|
|
||||||
|
|
||||||
"pymongo",
|
|
||||||
"redis",
|
|
||||||
"boto3==1.40.5",
|
|
||||||
|
|
||||||
"argon2-cffi==23.1.0",
|
|
||||||
"APScheduler==3.10.4",
|
|
||||||
|
|
||||||
"pycrdt==0.12.25",
|
"pycrdt==0.12.25",
|
||||||
|
"redis",
|
||||||
|
|
||||||
|
"PyMySQL==1.1.1",
|
||||||
|
"boto3==1.40.5",
|
||||||
|
|
||||||
|
"APScheduler==3.10.4",
|
||||||
"RestrictedPython==8.0",
|
"RestrictedPython==8.0",
|
||||||
|
|
||||||
"loguru==0.7.3",
|
"loguru==0.7.3",
|
||||||
"asgiref==3.8.1",
|
"asgiref==3.8.1",
|
||||||
|
|
||||||
|
"tiktoken",
|
||||||
"openai",
|
"openai",
|
||||||
"anthropic",
|
"anthropic",
|
||||||
"google-genai==1.28.0",
|
"google-genai==1.32.0",
|
||||||
"google-generativeai==0.8.5",
|
"google-generativeai==0.8.5",
|
||||||
"tiktoken",
|
|
||||||
|
|
||||||
"langchain==0.3.26",
|
"langchain==0.3.26",
|
||||||
"langchain-community==0.3.26",
|
"langchain-community==0.3.27",
|
||||||
|
|
||||||
"fake-useragent==2.2.0",
|
"fake-useragent==2.2.0",
|
||||||
"chromadb==0.6.3",
|
"chromadb==0.6.3",
|
||||||
|
|
@ -75,7 +72,7 @@ dependencies = [
|
||||||
"einops==0.8.1",
|
"einops==0.8.1",
|
||||||
|
|
||||||
"ftfy==6.2.3",
|
"ftfy==6.2.3",
|
||||||
"pypdf==4.3.1",
|
"pypdf==6.0.0",
|
||||||
"fpdf2==2.8.2",
|
"fpdf2==2.8.2",
|
||||||
"pymdown-extensions==10.14.2",
|
"pymdown-extensions==10.14.2",
|
||||||
"docx2txt==0.8",
|
"docx2txt==0.8",
|
||||||
|
|
@ -100,14 +97,9 @@ dependencies = [
|
||||||
"rank-bm25==0.2.2",
|
"rank-bm25==0.2.2",
|
||||||
|
|
||||||
"onnxruntime==1.20.1",
|
"onnxruntime==1.20.1",
|
||||||
|
|
||||||
"faster-whisper==1.1.1",
|
"faster-whisper==1.1.1",
|
||||||
|
|
||||||
"PyJWT[crypto]==2.10.1",
|
|
||||||
"authlib==1.6.1",
|
|
||||||
|
|
||||||
"black==25.1.0",
|
"black==25.1.0",
|
||||||
"langfuse==2.44.0",
|
|
||||||
"youtube-transcript-api==1.1.0",
|
"youtube-transcript-api==1.1.0",
|
||||||
"pytube==15.0.0",
|
"pytube==15.0.0",
|
||||||
|
|
||||||
|
|
@ -118,9 +110,7 @@ dependencies = [
|
||||||
"google-auth-httplib2",
|
"google-auth-httplib2",
|
||||||
"google-auth-oauthlib",
|
"google-auth-oauthlib",
|
||||||
|
|
||||||
"docker~=7.1.0",
|
|
||||||
"pytest~=8.3.2",
|
|
||||||
"pytest-docker~=3.1.1",
|
|
||||||
|
|
||||||
"googleapis-common-protos==1.63.2",
|
"googleapis-common-protos==1.63.2",
|
||||||
"google-cloud-storage==2.19.0",
|
"google-cloud-storage==2.19.0",
|
||||||
|
|
@ -131,12 +121,8 @@ dependencies = [
|
||||||
"ldap3==2.9.1",
|
"ldap3==2.9.1",
|
||||||
|
|
||||||
"firecrawl-py==1.12.0",
|
"firecrawl-py==1.12.0",
|
||||||
|
|
||||||
"tencentcloud-sdk-python==3.0.1336",
|
"tencentcloud-sdk-python==3.0.1336",
|
||||||
|
|
||||||
"gcp-storage-emulator>=2024.8.3",
|
|
||||||
|
|
||||||
"moto[s3]>=5.0.26",
|
|
||||||
"oracledb>=3.2.0",
|
"oracledb>=3.2.0",
|
||||||
"posthog==5.4.0",
|
"posthog==5.4.0",
|
||||||
|
|
||||||
|
|
@ -154,6 +140,23 @@ classifiers = [
|
||||||
"Topic :: Multimedia",
|
"Topic :: Multimedia",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[project.optional-dependencies]
|
||||||
|
postgres = [
|
||||||
|
"psycopg2-binary==2.9.9",
|
||||||
|
"pgvector==0.4.0",
|
||||||
|
]
|
||||||
|
|
||||||
|
all = [
|
||||||
|
"pymongo",
|
||||||
|
"psycopg2-binary==2.9.9",
|
||||||
|
"pgvector==0.4.0",
|
||||||
|
"moto[s3]>=5.0.26",
|
||||||
|
"gcp-storage-emulator>=2024.8.3",
|
||||||
|
"docker~=7.1.0",
|
||||||
|
"pytest~=8.3.2",
|
||||||
|
"pytest-docker~=3.1.1",
|
||||||
|
]
|
||||||
|
|
||||||
[project.scripts]
|
[project.scripts]
|
||||||
open-webui = "open_webui:app"
|
open-webui = "open_webui:app"
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -282,6 +282,14 @@ input[type='number'] {
|
||||||
outline: none;
|
outline: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.cm-gutters {
|
||||||
|
@apply !bg-white dark:!bg-black !border-none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cm-editor {
|
||||||
|
@apply bg-white dark:bg-black;
|
||||||
|
}
|
||||||
|
|
||||||
.tippy-box[data-theme~='dark'] {
|
.tippy-box[data-theme~='dark'] {
|
||||||
@apply rounded-lg bg-gray-950 text-xs border border-gray-900 shadow-xl;
|
@apply rounded-lg bg-gray-950 text-xs border border-gray-900 shadow-xl;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
38
src/app.html
38
src/app.html
|
|
@ -2,14 +2,35 @@
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<link rel="icon" type="image/png" href="/static/favicon.png" />
|
<link rel="icon" type="image/png" href="/static/favicon.png" crossorigin="use-credentials" />
|
||||||
<link rel="icon" type="image/png" href="/static/favicon-96x96.png" sizes="96x96" />
|
<link
|
||||||
<link rel="icon" type="image/svg+xml" href="/static/favicon.svg" />
|
rel="icon"
|
||||||
<link rel="shortcut icon" href="/static/favicon.ico" />
|
type="image/png"
|
||||||
<link rel="apple-touch-icon" sizes="180x180" href="/static/apple-touch-icon.png" />
|
href="/static/favicon-96x96.png"
|
||||||
|
sizes="96x96"
|
||||||
|
crossorigin="use-credentials"
|
||||||
|
/>
|
||||||
|
<link
|
||||||
|
rel="icon"
|
||||||
|
type="image/svg+xml"
|
||||||
|
href="/static/favicon.svg"
|
||||||
|
crossorigin="use-credentials"
|
||||||
|
/>
|
||||||
|
<link rel="shortcut icon" href="/static/favicon.ico" crossorigin="use-credentials" />
|
||||||
|
<link
|
||||||
|
rel="apple-touch-icon"
|
||||||
|
sizes="180x180"
|
||||||
|
href="/static/apple-touch-icon.png"
|
||||||
|
crossorigin="use-credentials"
|
||||||
|
/>
|
||||||
<meta name="apple-mobile-web-app-title" content="Open WebUI" />
|
<meta name="apple-mobile-web-app-title" content="Open WebUI" />
|
||||||
|
|
||||||
<link rel="manifest" href="/manifest.json" crossorigin="use-credentials" />
|
<link
|
||||||
|
rel="manifest"
|
||||||
|
href="/manifest.json"
|
||||||
|
crossorigin="use-credentials"
|
||||||
|
crossorigin="use-credentials"
|
||||||
|
/>
|
||||||
<meta
|
<meta
|
||||||
name="viewport"
|
name="viewport"
|
||||||
content="width=device-width, initial-scale=1, maximum-scale=1, viewport-fit=cover"
|
content="width=device-width, initial-scale=1, maximum-scale=1, viewport-fit=cover"
|
||||||
|
|
@ -22,9 +43,10 @@
|
||||||
type="application/opensearchdescription+xml"
|
type="application/opensearchdescription+xml"
|
||||||
title="Open WebUI"
|
title="Open WebUI"
|
||||||
href="/opensearch.xml"
|
href="/opensearch.xml"
|
||||||
|
crossorigin="use-credentials"
|
||||||
/>
|
/>
|
||||||
<script src="/static/loader.js" defer></script>
|
<script src="/static/loader.js" defer crossorigin="use-credentials"></script>
|
||||||
<link rel="stylesheet" href="/static/custom.css" />
|
<link rel="stylesheet" href="/static/custom.css" crossorigin="use-credentials" />
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
function resizeIframe(obj) {
|
function resizeIframe(obj) {
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,9 @@
|
||||||
import { WEBUI_API_BASE_URL } from '$lib/constants';
|
import { WEBUI_API_BASE_URL } from '$lib/constants';
|
||||||
|
|
||||||
type FolderForm = {
|
type FolderForm = {
|
||||||
name: string;
|
name?: string;
|
||||||
data?: Record<string, any>;
|
data?: Record<string, any>;
|
||||||
|
meta?: Record<string, any>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const createNewFolder = async (token: string, folderForm: FolderForm) => {
|
export const createNewFolder = async (token: string, folderForm: FolderForm) => {
|
||||||
|
|
|
||||||
|
|
@ -347,25 +347,31 @@ export const getToolServerData = async (token: string, url: string) => {
|
||||||
return data;
|
return data;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getToolServersData = async (i18n, servers: object[]) => {
|
export const getToolServersData = async (servers: object[]) => {
|
||||||
return (
|
return (
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
servers
|
servers
|
||||||
.filter((server) => server?.config?.enable)
|
.filter((server) => server?.config?.enable)
|
||||||
.map(async (server) => {
|
.map(async (server) => {
|
||||||
|
let error = null;
|
||||||
|
|
||||||
|
let toolServerToken = null;
|
||||||
|
const auth_type = server?.auth_type ?? 'bearer';
|
||||||
|
if (auth_type === 'bearer') {
|
||||||
|
toolServerToken = server?.key;
|
||||||
|
} else if (auth_type === 'none') {
|
||||||
|
// No authentication
|
||||||
|
} else if (auth_type === 'session') {
|
||||||
|
toolServerToken = localStorage.token;
|
||||||
|
}
|
||||||
|
|
||||||
const data = await getToolServerData(
|
const data = await getToolServerData(
|
||||||
(server?.auth_type ?? 'bearer') === 'bearer' ? server?.key : localStorage.token,
|
toolServerToken,
|
||||||
(server?.path ?? '').includes('://')
|
(server?.path ?? '').includes('://')
|
||||||
? server?.path
|
? server?.path
|
||||||
: `${server?.url}${(server?.path ?? '').startsWith('/') ? '' : '/'}${server?.path}`
|
: `${server?.url}${(server?.path ?? '').startsWith('/') ? '' : '/'}${server?.path}`
|
||||||
).catch((err) => {
|
).catch((err) => {
|
||||||
toast.error(
|
error = err;
|
||||||
$i18n.t(`Failed to connect to {{URL}} OpenAPI tool server`, {
|
|
||||||
URL: (server?.path ?? '').includes('://')
|
|
||||||
? server?.path
|
|
||||||
: `${server?.url}${(server?.path ?? '').startsWith('/') ? '' : '/'}${server?.path}`
|
|
||||||
})
|
|
||||||
);
|
|
||||||
return null;
|
return null;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -377,6 +383,13 @@ export const getToolServersData = async (i18n, servers: object[]) => {
|
||||||
info: info,
|
info: info,
|
||||||
specs: specs
|
specs: specs
|
||||||
};
|
};
|
||||||
|
} else if (error) {
|
||||||
|
return {
|
||||||
|
error,
|
||||||
|
url: server?.url
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -372,6 +372,7 @@ export const generateOpenAIChatCompletion = async (
|
||||||
Authorization: `Bearer ${token}`,
|
Authorization: `Bearer ${token}`,
|
||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json'
|
||||||
},
|
},
|
||||||
|
credentials: 'include',
|
||||||
body: JSON.stringify(body)
|
body: JSON.stringify(body)
|
||||||
})
|
})
|
||||||
.then(async (res) => {
|
.then(async (res) => {
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,7 @@
|
||||||
|
|
||||||
let url = '';
|
let url = '';
|
||||||
let key = '';
|
let key = '';
|
||||||
|
let auth_type = 'bearer';
|
||||||
|
|
||||||
let connectionType = 'external';
|
let connectionType = 'external';
|
||||||
let azure = false;
|
let azure = false;
|
||||||
|
|
@ -74,6 +75,7 @@
|
||||||
url,
|
url,
|
||||||
key,
|
key,
|
||||||
config: {
|
config: {
|
||||||
|
auth_type,
|
||||||
azure: azure,
|
azure: azure,
|
||||||
api_version: apiVersion
|
api_version: apiVersion
|
||||||
}
|
}
|
||||||
|
|
@ -146,6 +148,7 @@
|
||||||
prefix_id: prefixId,
|
prefix_id: prefixId,
|
||||||
model_ids: modelIds,
|
model_ids: modelIds,
|
||||||
connection_type: connectionType,
|
connection_type: connectionType,
|
||||||
|
auth_type,
|
||||||
...(!ollama && azure ? { azure: true, api_version: apiVersion } : {})
|
...(!ollama && azure ? { azure: true, api_version: apiVersion } : {})
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
@ -157,6 +160,7 @@
|
||||||
|
|
||||||
url = '';
|
url = '';
|
||||||
key = '';
|
key = '';
|
||||||
|
auth_type = 'bearer';
|
||||||
prefixId = '';
|
prefixId = '';
|
||||||
tags = [];
|
tags = [];
|
||||||
modelIds = [];
|
modelIds = [];
|
||||||
|
|
@ -167,6 +171,8 @@
|
||||||
url = connection.url;
|
url = connection.url;
|
||||||
key = connection.key;
|
key = connection.key;
|
||||||
|
|
||||||
|
auth_type = connection.config.auth_type ?? 'bearer';
|
||||||
|
|
||||||
enable = connection.config?.enable ?? true;
|
enable = connection.config?.enable ?? true;
|
||||||
tags = connection.config?.tags ?? [];
|
tags = connection.config?.tags ?? [];
|
||||||
prefixId = connection.config?.prefix_id ?? '';
|
prefixId = connection.config?.prefix_id ?? '';
|
||||||
|
|
@ -305,23 +311,63 @@
|
||||||
|
|
||||||
<div class="flex gap-2 mt-2">
|
<div class="flex gap-2 mt-2">
|
||||||
<div class="flex flex-col w-full">
|
<div class="flex flex-col w-full">
|
||||||
<div
|
<label
|
||||||
class={`mb-0.5 text-xs text-gray-500
|
for="select-bearer-or-session"
|
||||||
${($settings?.highContrastMode ?? false) ? 'text-gray-800 dark:text-gray-100' : ''}`}
|
class={`text-xs ${($settings?.highContrastMode ?? false) ? 'text-gray-800 dark:text-gray-100' : 'text-gray-500'}`}
|
||||||
|
>{$i18n.t('Auth')}</label
|
||||||
>
|
>
|
||||||
{$i18n.t('Key')}
|
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<div class="flex-shrink-0 self-start">
|
||||||
|
<select
|
||||||
|
id="select-bearer-or-session"
|
||||||
|
class={`w-full text-sm bg-transparent pr-5 ${($settings?.highContrastMode ?? false) ? 'placeholder:text-gray-700 dark:placeholder:text-gray-100' : 'outline-hidden placeholder:text-gray-300 dark:placeholder:text-gray-700'}`}
|
||||||
|
bind:value={auth_type}
|
||||||
|
>
|
||||||
|
<option value="none">{$i18n.t('None')}</option>
|
||||||
|
<option value="bearer">{$i18n.t('Bearer')}</option>
|
||||||
|
|
||||||
|
{#if !ollama}
|
||||||
|
<option value="session">{$i18n.t('Session')}</option>
|
||||||
|
{#if !direct}
|
||||||
|
<option value="system_oauth">{$i18n.t('OAuth')}</option>
|
||||||
|
{/if}
|
||||||
|
{/if}
|
||||||
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex-1">
|
<div class="flex flex-1 items-center">
|
||||||
|
{#if auth_type === 'bearer'}
|
||||||
<SensitiveInput
|
<SensitiveInput
|
||||||
inputClassName={`w-full text-sm bg-transparent ${($settings?.highContrastMode ?? false) ? 'placeholder:text-gray-700 dark:placeholder:text-gray-100' : 'outline-hidden placeholder:text-gray-300 dark:placeholder:text-gray-700'}`}
|
|
||||||
bind:value={key}
|
bind:value={key}
|
||||||
placeholder={$i18n.t('API Key')}
|
placeholder={$i18n.t('API Key')}
|
||||||
required={false}
|
required={false}
|
||||||
/>
|
/>
|
||||||
|
{:else if auth_type === 'none'}
|
||||||
|
<div
|
||||||
|
class={`text-xs self-center translate-y-[1px] ${($settings?.highContrastMode ?? false) ? 'text-gray-800 dark:text-gray-100' : 'text-gray-500'}`}
|
||||||
|
>
|
||||||
|
{$i18n.t('No authentication')}
|
||||||
|
</div>
|
||||||
|
{:else if auth_type === 'session'}
|
||||||
|
<div
|
||||||
|
class={`text-xs self-center translate-y-[1px] ${($settings?.highContrastMode ?? false) ? 'text-gray-800 dark:text-gray-100' : 'text-gray-500'}`}
|
||||||
|
>
|
||||||
|
{$i18n.t('Forwards system user session credentials to authenticate')}
|
||||||
|
</div>
|
||||||
|
{:else if auth_type === 'system_oauth'}
|
||||||
|
<div
|
||||||
|
class={`text-xs self-center translate-y-[1px] ${($settings?.highContrastMode ?? false) ? 'text-gray-800 dark:text-gray-100' : 'text-gray-500'}`}
|
||||||
|
>
|
||||||
|
{$i18n.t('Forwards system user OAuth access token to authenticate')}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="flex gap-2 mt-2">
|
||||||
<div class="flex flex-col w-full">
|
<div class="flex flex-col w-full">
|
||||||
<label
|
<label
|
||||||
for="prefix-id-input"
|
for="prefix-id-input"
|
||||||
|
|
@ -349,6 +395,29 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{#if !ollama && !direct}
|
||||||
|
<div class="flex flex-row justify-between items-center w-full mt-2">
|
||||||
|
<label
|
||||||
|
for="prefix-id-input"
|
||||||
|
class={`mb-0.5 text-xs text-gray-500
|
||||||
|
${($settings?.highContrastMode ?? false) ? 'text-gray-800 dark:text-gray-100' : ''}`}
|
||||||
|
>{$i18n.t('Provider Type')}</label
|
||||||
|
>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<button
|
||||||
|
on:click={() => {
|
||||||
|
azure = !azure;
|
||||||
|
}}
|
||||||
|
type="button"
|
||||||
|
class=" text-xs text-gray-700 dark:text-gray-300"
|
||||||
|
>
|
||||||
|
{azure ? $i18n.t('Azure OpenAI') : $i18n.t('OpenAI')}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
{#if azure}
|
{#if azure}
|
||||||
<div class="flex gap-2 mt-2">
|
<div class="flex gap-2 mt-2">
|
||||||
<div class="flex flex-col w-full">
|
<div class="flex flex-col w-full">
|
||||||
|
|
@ -374,36 +443,6 @@
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<div class="flex gap-2 mt-2">
|
|
||||||
<div class="flex flex-col w-full">
|
|
||||||
<div
|
|
||||||
class={`mb-0.5 text-xs text-gray-500
|
|
||||||
${($settings?.highContrastMode ?? false) ? 'text-gray-800 dark:text-gray-100' : ''}`}
|
|
||||||
>
|
|
||||||
{$i18n.t('Tags')}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex-1">
|
|
||||||
<Tags
|
|
||||||
bind:tags
|
|
||||||
on:add={(e) => {
|
|
||||||
tags = [
|
|
||||||
...tags,
|
|
||||||
{
|
|
||||||
name: e.detail
|
|
||||||
}
|
|
||||||
];
|
|
||||||
}}
|
|
||||||
on:delete={(e) => {
|
|
||||||
tags = tags.filter((tag) => tag.name !== e.detail);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<hr class=" border-gray-100 dark:border-gray-700/10 my-2.5 w-full" />
|
|
||||||
|
|
||||||
<div class="flex flex-col w-full">
|
<div class="flex flex-col w-full">
|
||||||
<div class="mb-1 flex justify-between">
|
<div class="mb-1 flex justify-between">
|
||||||
<div
|
<div
|
||||||
|
|
@ -489,6 +528,36 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<hr class=" border-gray-50 dark:border-gray-850 my-2.5 w-full" />
|
||||||
|
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<div class="flex flex-col w-full">
|
||||||
|
<div
|
||||||
|
class={`mb-0.5 text-xs text-gray-500
|
||||||
|
${($settings?.highContrastMode ?? false) ? 'text-gray-800 dark:text-gray-100' : ''}`}
|
||||||
|
>
|
||||||
|
{$i18n.t('Tags')}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex-1 mt-0.5">
|
||||||
|
<Tags
|
||||||
|
bind:tags
|
||||||
|
on:add={(e) => {
|
||||||
|
tags = [
|
||||||
|
...tags,
|
||||||
|
{
|
||||||
|
name: e.detail
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}}
|
||||||
|
on:delete={(e) => {
|
||||||
|
tags = tags.filter((tag) => tag.name !== e.detail);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="flex justify-end pt-3 text-sm font-medium gap-1.5">
|
<div class="flex justify-end pt-3 text-sm font-medium gap-1.5">
|
||||||
{#if edit}
|
{#if edit}
|
||||||
<button
|
<button
|
||||||
|
|
|
||||||
|
|
@ -283,8 +283,14 @@
|
||||||
class={`w-full text-sm bg-transparent pr-5 ${($settings?.highContrastMode ?? false) ? 'placeholder:text-gray-700 dark:placeholder:text-gray-100' : 'outline-hidden placeholder:text-gray-300 dark:placeholder:text-gray-700'}`}
|
class={`w-full text-sm bg-transparent pr-5 ${($settings?.highContrastMode ?? false) ? 'placeholder:text-gray-700 dark:placeholder:text-gray-100' : 'outline-hidden placeholder:text-gray-300 dark:placeholder:text-gray-700'}`}
|
||||||
bind:value={auth_type}
|
bind:value={auth_type}
|
||||||
>
|
>
|
||||||
|
<option value="none">{$i18n.t('None')}</option>
|
||||||
|
|
||||||
<option value="bearer">{$i18n.t('Bearer')}</option>
|
<option value="bearer">{$i18n.t('Bearer')}</option>
|
||||||
<option value="session">{$i18n.t('Session')}</option>
|
<option value="session">{$i18n.t('Session')}</option>
|
||||||
|
|
||||||
|
{#if !direct}
|
||||||
|
<option value="system_oauth">{$i18n.t('OAuth')}</option>
|
||||||
|
{/if}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -295,12 +301,24 @@
|
||||||
placeholder={$i18n.t('API Key')}
|
placeholder={$i18n.t('API Key')}
|
||||||
required={false}
|
required={false}
|
||||||
/>
|
/>
|
||||||
|
{:else if auth_type === 'none'}
|
||||||
|
<div
|
||||||
|
class={`text-xs self-center translate-y-[1px] ${($settings?.highContrastMode ?? false) ? 'text-gray-800 dark:text-gray-100' : 'text-gray-500'}`}
|
||||||
|
>
|
||||||
|
{$i18n.t('No authentication')}
|
||||||
|
</div>
|
||||||
{:else if auth_type === 'session'}
|
{:else if auth_type === 'session'}
|
||||||
<div
|
<div
|
||||||
class={`text-xs self-center translate-y-[1px] ${($settings?.highContrastMode ?? false) ? 'text-gray-800 dark:text-gray-100' : 'text-gray-500'}`}
|
class={`text-xs self-center translate-y-[1px] ${($settings?.highContrastMode ?? false) ? 'text-gray-800 dark:text-gray-100' : 'text-gray-500'}`}
|
||||||
>
|
>
|
||||||
{$i18n.t('Forwards system user session credentials to authenticate')}
|
{$i18n.t('Forwards system user session credentials to authenticate')}
|
||||||
</div>
|
</div>
|
||||||
|
{:else if auth_type === 'system_oauth'}
|
||||||
|
<div
|
||||||
|
class={`text-xs self-center translate-y-[1px] ${($settings?.highContrastMode ?? false) ? 'text-gray-800 dark:text-gray-100' : 'text-gray-500'}`}
|
||||||
|
>
|
||||||
|
{$i18n.t('Forwards system user OAuth access token to authenticate')}
|
||||||
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -204,7 +204,7 @@
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<div class=" self-center">{$i18n.t('Tools')}</div>
|
<div class=" self-center">{$i18n.t('External Tools')}</div>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
|
|
|
||||||
|
|
@ -261,10 +261,10 @@
|
||||||
<div class="flex flex-col gap-1.5 mt-1.5">
|
<div class="flex flex-col gap-1.5 mt-1.5">
|
||||||
{#each OPENAI_API_BASE_URLS as url, idx}
|
{#each OPENAI_API_BASE_URLS as url, idx}
|
||||||
<OpenAIConnection
|
<OpenAIConnection
|
||||||
pipeline={pipelineUrls[url] ? true : false}
|
{url}
|
||||||
bind:url
|
key={OPENAI_API_KEYS[idx]}
|
||||||
bind:key={OPENAI_API_KEYS[idx]}
|
|
||||||
bind:config={OPENAI_API_CONFIGS[idx]}
|
bind:config={OPENAI_API_CONFIGS[idx]}
|
||||||
|
pipeline={pipelineUrls[url] ? true : false}
|
||||||
onSubmit={() => {
|
onSubmit={() => {
|
||||||
updateOpenAIHandler();
|
updateOpenAIHandler();
|
||||||
}}
|
}}
|
||||||
|
|
@ -326,7 +326,7 @@
|
||||||
<div class="flex-1 flex flex-col gap-1.5 mt-1.5">
|
<div class="flex-1 flex flex-col gap-1.5 mt-1.5">
|
||||||
{#each OLLAMA_BASE_URLS as url, idx}
|
{#each OLLAMA_BASE_URLS as url, idx}
|
||||||
<OllamaConnection
|
<OllamaConnection
|
||||||
bind:url
|
{url}
|
||||||
bind:config={OLLAMA_API_CONFIGS[idx]}
|
bind:config={OLLAMA_API_CONFIGS[idx]}
|
||||||
{idx}
|
{idx}
|
||||||
onSubmit={() => {
|
onSubmit={() => {
|
||||||
|
|
|
||||||
|
|
@ -71,6 +71,7 @@
|
||||||
class="w-full text-sm bg-transparent outline-hidden"
|
class="w-full text-sm bg-transparent outline-hidden"
|
||||||
placeholder={$i18n.t('Enter URL (e.g. http://localhost:11434)')}
|
placeholder={$i18n.t('Enter URL (e.g. http://localhost:11434)')}
|
||||||
bind:value={url}
|
bind:value={url}
|
||||||
|
readonly={true}
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -69,6 +69,7 @@
|
||||||
placeholder={$i18n.t('API Base URL')}
|
placeholder={$i18n.t('API Base URL')}
|
||||||
bind:value={url}
|
bind:value={url}
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
|
readonly={true}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{#if pipeline}
|
{#if pipeline}
|
||||||
|
|
@ -94,13 +95,6 @@
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<SensitiveInput
|
|
||||||
inputClassName=" outline-hidden bg-transparent w-full"
|
|
||||||
placeholder={$i18n.t('API Key')}
|
|
||||||
required={false}
|
|
||||||
bind:value={key}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -153,6 +153,7 @@
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
RAGConfig.CONTENT_EXTRACTION_ENGINE === 'docling' &&
|
RAGConfig.CONTENT_EXTRACTION_ENGINE === 'docling' &&
|
||||||
|
RAGConfig.DOCLING_DO_OCR &&
|
||||||
((RAGConfig.DOCLING_OCR_ENGINE === '' && RAGConfig.DOCLING_OCR_LANG !== '') ||
|
((RAGConfig.DOCLING_OCR_ENGINE === '' && RAGConfig.DOCLING_OCR_LANG !== '') ||
|
||||||
(RAGConfig.DOCLING_OCR_ENGINE !== '' && RAGConfig.DOCLING_OCR_LANG === ''))
|
(RAGConfig.DOCLING_OCR_ENGINE !== '' && RAGConfig.DOCLING_OCR_LANG === ''))
|
||||||
) {
|
) {
|
||||||
|
|
@ -161,6 +162,14 @@
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (
|
||||||
|
RAGConfig.CONTENT_EXTRACTION_ENGINE === 'docling' &&
|
||||||
|
RAGConfig.DOCLING_DO_OCR === false &&
|
||||||
|
RAGConfig.DOCLING_FORCE_OCR === true
|
||||||
|
) {
|
||||||
|
toast.error($i18n.t('In order to force OCR, performing OCR must be enabled.'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
RAGConfig.CONTENT_EXTRACTION_ENGINE === 'datalab_marker' &&
|
RAGConfig.CONTENT_EXTRACTION_ENGINE === 'datalab_marker' &&
|
||||||
|
|
@ -185,10 +194,9 @@
|
||||||
|
|
||||||
if (
|
if (
|
||||||
RAGConfig.CONTENT_EXTRACTION_ENGINE === 'document_intelligence' &&
|
RAGConfig.CONTENT_EXTRACTION_ENGINE === 'document_intelligence' &&
|
||||||
(RAGConfig.DOCUMENT_INTELLIGENCE_ENDPOINT === '' ||
|
RAGConfig.DOCUMENT_INTELLIGENCE_ENDPOINT === ''
|
||||||
RAGConfig.DOCUMENT_INTELLIGENCE_KEY === '')
|
|
||||||
) {
|
) {
|
||||||
toast.error($i18n.t('Document Intelligence endpoint and key required.'));
|
toast.error($i18n.t('Document Intelligence endpoint required.'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
|
|
@ -546,6 +554,18 @@
|
||||||
bind:value={RAGConfig.DOCLING_SERVER_URL}
|
bind:value={RAGConfig.DOCLING_SERVER_URL}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="flex w-full mt-2">
|
||||||
|
<div class="flex-1 flex justify-between">
|
||||||
|
<div class=" self-center text-xs font-medium">
|
||||||
|
{$i18n.t('Perform OCR')}
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center relative">
|
||||||
|
<Switch bind:state={RAGConfig.DOCLING_DO_OCR} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{#if RAGConfig.DOCLING_DO_OCR}
|
||||||
<div class="flex w-full mt-2">
|
<div class="flex w-full mt-2">
|
||||||
<input
|
<input
|
||||||
class="flex-1 w-full text-sm bg-transparent outline-hidden"
|
class="flex-1 w-full text-sm bg-transparent outline-hidden"
|
||||||
|
|
@ -558,7 +578,67 @@
|
||||||
bind:value={RAGConfig.DOCLING_OCR_LANG}
|
bind:value={RAGConfig.DOCLING_OCR_LANG}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
{/if}
|
||||||
|
<div class="flex w-full mt-2">
|
||||||
|
<div class="flex-1 flex justify-between">
|
||||||
|
<div class=" self-center text-xs font-medium">
|
||||||
|
{$i18n.t('Force OCR')}
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center relative">
|
||||||
|
<Switch bind:state={RAGConfig.DOCLING_FORCE_OCR} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between w-full mt-2">
|
||||||
|
<div class="self-center text-xs font-medium">
|
||||||
|
<Tooltip content={''} placement="top-start">
|
||||||
|
{$i18n.t('PDF Backend')}
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
<div class="">
|
||||||
|
<select
|
||||||
|
class="dark:bg-gray-900 w-fit pr-8 rounded-sm px-2 text-xs bg-transparent outline-hidden text-right"
|
||||||
|
bind:value={RAGConfig.DOCLING_PDF_BACKEND}
|
||||||
|
>
|
||||||
|
<option value="pypdfium2">{$i18n.t('pypdfium2')}</option>
|
||||||
|
<option value="dlparse_v1">{$i18n.t('dlparse_v1')}</option>
|
||||||
|
<option value="dlparse_v2">{$i18n.t('dlparse_v2')}</option>
|
||||||
|
<option value="dlparse_v4">{$i18n.t('dlparse_v4')}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between w-full mt-2">
|
||||||
|
<div class="self-center text-xs font-medium">
|
||||||
|
<Tooltip content={''} placement="top-start">
|
||||||
|
{$i18n.t('Table Mode')}
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
<div class="">
|
||||||
|
<select
|
||||||
|
class="dark:bg-gray-900 w-fit pr-8 rounded-sm px-2 text-xs bg-transparent outline-hidden text-right"
|
||||||
|
bind:value={RAGConfig.DOCLING_TABLE_MODE}
|
||||||
|
>
|
||||||
|
<option value="fast">{$i18n.t('fast')}</option>
|
||||||
|
<option value="accurate">{$i18n.t('accurate')}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between w-full mt-2">
|
||||||
|
<div class="self-center text-xs font-medium">
|
||||||
|
<Tooltip content={''} placement="top-start">
|
||||||
|
{$i18n.t('Pipeline')}
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
<div class="">
|
||||||
|
<select
|
||||||
|
class="dark:bg-gray-900 w-fit pr-8 rounded-sm px-2 text-xs bg-transparent outline-hidden text-right"
|
||||||
|
bind:value={RAGConfig.DOCLING_PIPELINE}
|
||||||
|
>
|
||||||
|
<option value="standard">{$i18n.t('standard')}</option>
|
||||||
|
<option value="vlm">{$i18n.t('vlm')}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="flex w-full mt-2">
|
<div class="flex w-full mt-2">
|
||||||
<div class="flex-1 flex justify-between">
|
<div class="flex-1 flex justify-between">
|
||||||
<div class=" self-center text-xs font-medium">
|
<div class=" self-center text-xs font-medium">
|
||||||
|
|
@ -644,6 +724,7 @@
|
||||||
<SensitiveInput
|
<SensitiveInput
|
||||||
placeholder={$i18n.t('Enter Document Intelligence Key')}
|
placeholder={$i18n.t('Enter Document Intelligence Key')}
|
||||||
bind:value={RAGConfig.DOCUMENT_INTELLIGENCE_KEY}
|
bind:value={RAGConfig.DOCUMENT_INTELLIGENCE_KEY}
|
||||||
|
required={false}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{:else if RAGConfig.CONTENT_EXTRACTION_ENGINE === 'mistral_ocr'}
|
{:else if RAGConfig.CONTENT_EXTRACTION_ENGINE === 'mistral_ocr'}
|
||||||
|
|
@ -1104,10 +1185,10 @@
|
||||||
<div class="py-0.5">
|
<div class="py-0.5">
|
||||||
<div class="flex w-full justify-between">
|
<div class="flex w-full justify-between">
|
||||||
<div class=" text-left text-xs font-small">
|
<div class=" text-left text-xs font-small">
|
||||||
{$i18n.t('lexical')}
|
{$i18n.t('semantic')}
|
||||||
</div>
|
</div>
|
||||||
<div class=" text-right text-xs font-small">
|
<div class=" text-right text-xs font-small">
|
||||||
{$i18n.t('semantic')}
|
{$i18n.t('lexical')}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -599,22 +599,44 @@
|
||||||
{/if}
|
{/if}
|
||||||
{:else if config?.engine === 'openai'}
|
{:else if config?.engine === 'openai'}
|
||||||
<div>
|
<div>
|
||||||
<div class=" mb-1.5 text-sm font-medium">{$i18n.t('OpenAI API Config')}</div>
|
<div class=" mb-2 text-sm font-medium">{$i18n.t('OpenAI API Config')}</div>
|
||||||
|
<div class="flex w-full">
|
||||||
<div class="flex gap-2 mb-1">
|
<div class="flex-1 mr-2">
|
||||||
<input
|
<input
|
||||||
class="flex-1 w-full text-sm bg-transparent outline-hidden"
|
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden"
|
||||||
placeholder={$i18n.t('API Base URL')}
|
placeholder={$i18n.t('API Base URL')}
|
||||||
bind:value={config.openai.OPENAI_API_BASE_URL}
|
bind:value={config.openai.OPENAI_API_BASE_URL}
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div class=" mb-2 text-sm font-medium">{$i18n.t('API Key')}</div>
|
||||||
|
<div class="flex w-full">
|
||||||
|
<div class="flex-1 mr-2">
|
||||||
<SensitiveInput
|
<SensitiveInput
|
||||||
placeholder={$i18n.t('API Key')}
|
placeholder={$i18n.t('API Key')}
|
||||||
bind:value={config.openai.OPENAI_API_KEY}
|
bind:value={config.openai.OPENAI_API_KEY}
|
||||||
|
required
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div class=" mb-2 text-sm font-medium">{$i18n.t('API Version')}</div>
|
||||||
|
<div class="flex w-full">
|
||||||
|
<div class="flex-1 mr-2">
|
||||||
|
<input
|
||||||
|
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden"
|
||||||
|
placeholder={$i18n.t('API Version')}
|
||||||
|
bind:value={config.openai.OPENAI_API_VERSION}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
{:else if config?.engine === 'gemini'}
|
{:else if config?.engine === 'gemini'}
|
||||||
<div>
|
<div>
|
||||||
<div class=" mb-1.5 text-sm font-medium">{$i18n.t('Gemini API Config')}</div>
|
<div class=" mb-1.5 text-sm font-medium">{$i18n.t('Gemini API Config')}</div>
|
||||||
|
|
@ -682,6 +704,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{#if ['comfyui', 'automatic1111', ''].includes(config?.engine)}
|
||||||
<div>
|
<div>
|
||||||
<div class=" mb-2.5 text-sm font-medium">{$i18n.t('Set Steps')}</div>
|
<div class=" mb-2.5 text-sm font-medium">{$i18n.t('Set Steps')}</div>
|
||||||
<div class="flex w-full">
|
<div class="flex w-full">
|
||||||
|
|
@ -699,6 +722,7 @@
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex justify-end pt-3 text-sm font-medium">
|
<div class="flex justify-end pt-3 text-sm font-medium">
|
||||||
|
|
|
||||||
|
|
@ -71,6 +71,10 @@
|
||||||
params: true,
|
params: true,
|
||||||
file_upload: true,
|
file_upload: true,
|
||||||
delete: true,
|
delete: true,
|
||||||
|
delete_message: true,
|
||||||
|
continue_response: true,
|
||||||
|
regenerate_response: true,
|
||||||
|
rate_response: true,
|
||||||
edit: true,
|
edit: true,
|
||||||
share: true,
|
share: true,
|
||||||
export: true,
|
export: true,
|
||||||
|
|
|
||||||
|
|
@ -53,6 +53,10 @@
|
||||||
params: true,
|
params: true,
|
||||||
file_upload: true,
|
file_upload: true,
|
||||||
delete: true,
|
delete: true,
|
||||||
|
delete_message: true,
|
||||||
|
continue_response: true,
|
||||||
|
regenerate_response: true,
|
||||||
|
rate_response: true,
|
||||||
edit: true,
|
edit: true,
|
||||||
share: true,
|
share: true,
|
||||||
export: true,
|
export: true,
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
<script>
|
<script>
|
||||||
import { toast } from 'svelte-sonner';
|
import { toast } from 'svelte-sonner';
|
||||||
import { onMount, getContext } from 'svelte';
|
import { onMount, getContext } from 'svelte';
|
||||||
|
import { page } from '$app/stores';
|
||||||
|
|
||||||
const i18n = getContext('i18n');
|
const i18n = getContext('i18n');
|
||||||
|
|
||||||
|
|
@ -10,7 +11,6 @@
|
||||||
import User from '$lib/components/icons/User.svelte';
|
import User from '$lib/components/icons/User.svelte';
|
||||||
import UserCircleSolid from '$lib/components/icons/UserCircleSolid.svelte';
|
import UserCircleSolid from '$lib/components/icons/UserCircleSolid.svelte';
|
||||||
import GroupModal from './EditGroupModal.svelte';
|
import GroupModal from './EditGroupModal.svelte';
|
||||||
import { querystringValue } from '$lib/utils';
|
|
||||||
|
|
||||||
export let users = [];
|
export let users = [];
|
||||||
export let group = {
|
export let group = {
|
||||||
|
|
@ -47,7 +47,7 @@
|
||||||
};
|
};
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
const groupId = querystringValue('id');
|
const groupId = $page.url.searchParams.get('id');
|
||||||
if (groupId && groupId === group.id) {
|
if (groupId && groupId === group.id) {
|
||||||
showEdit = true;
|
showEdit = true;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,10 @@
|
||||||
params: true,
|
params: true,
|
||||||
file_upload: true,
|
file_upload: true,
|
||||||
delete: true,
|
delete: true,
|
||||||
|
delete_message: true,
|
||||||
|
continue_response: true,
|
||||||
|
regenerate_response: true,
|
||||||
|
rate_response: true,
|
||||||
edit: true,
|
edit: true,
|
||||||
share: true,
|
share: true,
|
||||||
export: true,
|
export: true,
|
||||||
|
|
@ -292,6 +296,14 @@
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
<div class=" flex w-full justify-between my-2 pr-2">
|
||||||
|
<div class=" self-center text-xs font-medium">
|
||||||
|
{$i18n.t('Allow Chat Edit')}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Switch bind:state={permissions.chat.edit} />
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class=" flex w-full justify-between my-2 pr-2">
|
<div class=" flex w-full justify-between my-2 pr-2">
|
||||||
<div class=" self-center text-xs font-medium">
|
<div class=" self-center text-xs font-medium">
|
||||||
{$i18n.t('Allow Chat Delete')}
|
{$i18n.t('Allow Chat Delete')}
|
||||||
|
|
@ -302,10 +314,34 @@
|
||||||
|
|
||||||
<div class=" flex w-full justify-between my-2 pr-2">
|
<div class=" flex w-full justify-between my-2 pr-2">
|
||||||
<div class=" self-center text-xs font-medium">
|
<div class=" self-center text-xs font-medium">
|
||||||
{$i18n.t('Allow Chat Edit')}
|
{$i18n.t('Allow Delete Messages')}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Switch bind:state={permissions.chat.edit} />
|
<Switch bind:state={permissions.chat.delete_message} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class=" flex w-full justify-between my-2 pr-2">
|
||||||
|
<div class=" self-center text-xs font-medium">
|
||||||
|
{$i18n.t('Allow Continue Response')}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Switch bind:state={permissions.chat.continue_response} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class=" flex w-full justify-between my-2 pr-2">
|
||||||
|
<div class=" self-center text-xs font-medium">
|
||||||
|
{$i18n.t('Allow Regenerate Response')}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Switch bind:state={permissions.chat.regenerate_response} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class=" flex w-full justify-between my-2 pr-2">
|
||||||
|
<div class=" self-center text-xs font-medium">
|
||||||
|
{$i18n.t('Allow Rate Response')}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Switch bind:state={permissions.chat.rate_response} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class=" flex w-full justify-between my-2 pr-2">
|
<div class=" flex w-full justify-between my-2 pr-2">
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { toast } from 'svelte-sonner';
|
import { toast } from 'svelte-sonner';
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
import heic2any from 'heic2any';
|
|
||||||
|
|
||||||
import { tick, getContext, onMount, onDestroy } from 'svelte';
|
import { tick, getContext, onMount, onDestroy } from 'svelte';
|
||||||
|
|
||||||
|
|
@ -9,7 +8,7 @@
|
||||||
|
|
||||||
import { config, mobile, settings, socket, user } from '$lib/stores';
|
import { config, mobile, settings, socket, user } from '$lib/stores';
|
||||||
import {
|
import {
|
||||||
blobToFile,
|
convertHeicToJpeg,
|
||||||
compressImage,
|
compressImage,
|
||||||
extractInputVariables,
|
extractInputVariables,
|
||||||
getAge,
|
getAge,
|
||||||
|
|
@ -377,11 +376,7 @@
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
reader.readAsDataURL(
|
reader.readAsDataURL(file['type'] === 'image/heic' ? await convertHeicToJpeg(file) : file);
|
||||||
file['type'] === 'image/heic'
|
|
||||||
? await heic2any({ blob: file, toType: 'image/jpeg' })
|
|
||||||
: file
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
uploadFileHandler(file);
|
uploadFileHandler(file);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -30,9 +30,10 @@
|
||||||
import ProfilePreview from './Message/ProfilePreview.svelte';
|
import ProfilePreview from './Message/ProfilePreview.svelte';
|
||||||
import ChatBubbleOvalEllipsis from '$lib/components/icons/ChatBubble.svelte';
|
import ChatBubbleOvalEllipsis from '$lib/components/icons/ChatBubble.svelte';
|
||||||
import FaceSmile from '$lib/components/icons/FaceSmile.svelte';
|
import FaceSmile from '$lib/components/icons/FaceSmile.svelte';
|
||||||
import ReactionPicker from './Message/ReactionPicker.svelte';
|
import EmojiPicker from '$lib/components/common/EmojiPicker.svelte';
|
||||||
import ChevronRight from '$lib/components/icons/ChevronRight.svelte';
|
import ChevronRight from '$lib/components/icons/ChevronRight.svelte';
|
||||||
import { formatDate } from '$lib/utils';
|
import { formatDate } from '$lib/utils';
|
||||||
|
import Emoji from '$lib/components/common/Emoji.svelte';
|
||||||
|
|
||||||
export let message;
|
export let message;
|
||||||
export let showUserProfile = true;
|
export let showUserProfile = true;
|
||||||
|
|
@ -74,7 +75,7 @@
|
||||||
<div
|
<div
|
||||||
class="flex gap-1 rounded-lg bg-white dark:bg-gray-850 shadow-md p-0.5 border border-gray-100 dark:border-gray-850"
|
class="flex gap-1 rounded-lg bg-white dark:bg-gray-850 shadow-md p-0.5 border border-gray-100 dark:border-gray-850"
|
||||||
>
|
>
|
||||||
<ReactionPicker
|
<EmojiPicker
|
||||||
onClose={() => (showButtons = false)}
|
onClose={() => (showButtons = false)}
|
||||||
onSubmit={(name) => {
|
onSubmit={(name) => {
|
||||||
showButtons = false;
|
showButtons = false;
|
||||||
|
|
@ -91,7 +92,7 @@
|
||||||
<FaceSmile />
|
<FaceSmile />
|
||||||
</button>
|
</button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</ReactionPicker>
|
</EmojiPicker>
|
||||||
|
|
||||||
{#if !thread}
|
{#if !thread}
|
||||||
<Tooltip content={$i18n.t('Reply in Thread')}>
|
<Tooltip content={$i18n.t('Reply in Thread')}>
|
||||||
|
|
@ -275,20 +276,7 @@
|
||||||
onReaction(reaction.name);
|
onReaction(reaction.name);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{#if $shortCodesToEmojis[reaction.name]}
|
<Emoji shortCode={reaction.name} />
|
||||||
<img
|
|
||||||
src="{WEBUI_BASE_URL}/assets/emojis/{$shortCodesToEmojis[
|
|
||||||
reaction.name
|
|
||||||
].toLowerCase()}.svg"
|
|
||||||
alt={reaction.name}
|
|
||||||
class=" size-4"
|
|
||||||
loading="lazy"
|
|
||||||
/>
|
|
||||||
{:else}
|
|
||||||
<div>
|
|
||||||
{reaction.name}
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
{#if reaction.user_ids.length > 0}
|
{#if reaction.user_ids.length > 0}
|
||||||
<div class="text-xs font-medium text-gray-500 dark:text-gray-400">
|
<div class="text-xs font-medium text-gray-500 dark:text-gray-400">
|
||||||
|
|
@ -299,7 +287,7 @@
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
{/each}
|
{/each}
|
||||||
|
|
||||||
<ReactionPicker
|
<EmojiPicker
|
||||||
onSubmit={(name) => {
|
onSubmit={(name) => {
|
||||||
onReaction(name);
|
onReaction(name);
|
||||||
}}
|
}}
|
||||||
|
|
@ -311,7 +299,7 @@
|
||||||
<FaceSmile />
|
<FaceSmile />
|
||||||
</div>
|
</div>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</ReactionPicker>
|
</EmojiPicker>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
||||||
|
|
@ -50,7 +50,6 @@
|
||||||
removeAllDetails
|
removeAllDetails
|
||||||
} from '$lib/utils';
|
} from '$lib/utils';
|
||||||
|
|
||||||
import { generateChatCompletion } from '$lib/apis/ollama';
|
|
||||||
import {
|
import {
|
||||||
createNewChat,
|
createNewChat,
|
||||||
getAllTags,
|
getAllTags,
|
||||||
|
|
@ -63,8 +62,6 @@
|
||||||
} from '$lib/apis/chats';
|
} from '$lib/apis/chats';
|
||||||
import { generateOpenAIChatCompletion } from '$lib/apis/openai';
|
import { generateOpenAIChatCompletion } from '$lib/apis/openai';
|
||||||
import { processWeb, processWebSearch, processYoutubeVideo } from '$lib/apis/retrieval';
|
import { processWeb, processWebSearch, processYoutubeVideo } from '$lib/apis/retrieval';
|
||||||
import { createOpenAITextStream } from '$lib/apis/streaming';
|
|
||||||
import { queryMemory } from '$lib/apis/memories';
|
|
||||||
import { getAndUpdateUserLocation, getUserSettings } from '$lib/apis/users';
|
import { getAndUpdateUserLocation, getUserSettings } from '$lib/apis/users';
|
||||||
import {
|
import {
|
||||||
chatCompleted,
|
chatCompleted,
|
||||||
|
|
@ -75,6 +72,10 @@
|
||||||
getTaskIdsByChatId
|
getTaskIdsByChatId
|
||||||
} from '$lib/apis';
|
} from '$lib/apis';
|
||||||
import { getTools } from '$lib/apis/tools';
|
import { getTools } from '$lib/apis/tools';
|
||||||
|
import { uploadFile } from '$lib/apis/files';
|
||||||
|
import { createOpenAITextStream } from '$lib/apis/streaming';
|
||||||
|
|
||||||
|
import { fade } from 'svelte/transition';
|
||||||
|
|
||||||
import Banner from '../common/Banner.svelte';
|
import Banner from '../common/Banner.svelte';
|
||||||
import MessageInput from '$lib/components/chat/MessageInput.svelte';
|
import MessageInput from '$lib/components/chat/MessageInput.svelte';
|
||||||
|
|
@ -85,10 +86,8 @@
|
||||||
import Placeholder from './Placeholder.svelte';
|
import Placeholder from './Placeholder.svelte';
|
||||||
import NotificationToast from '../NotificationToast.svelte';
|
import NotificationToast from '../NotificationToast.svelte';
|
||||||
import Spinner from '../common/Spinner.svelte';
|
import Spinner from '../common/Spinner.svelte';
|
||||||
import { fade } from 'svelte/transition';
|
|
||||||
import Tooltip from '../common/Tooltip.svelte';
|
import Tooltip from '../common/Tooltip.svelte';
|
||||||
import Sidebar from '../icons/Sidebar.svelte';
|
import Sidebar from '../icons/Sidebar.svelte';
|
||||||
import { uploadFile } from '$lib/apis/files';
|
|
||||||
|
|
||||||
export let chatIdProp = '';
|
export let chatIdProp = '';
|
||||||
|
|
||||||
|
|
@ -203,7 +202,12 @@
|
||||||
|
|
||||||
if (type === 'prompt') {
|
if (type === 'prompt') {
|
||||||
// Handle prompt selection
|
// Handle prompt selection
|
||||||
messageInput?.setText(data);
|
messageInput?.setText(data, async () => {
|
||||||
|
if (!($settings?.insertSuggestionPrompt ?? false)) {
|
||||||
|
await tick();
|
||||||
|
submitPrompt(prompt);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -318,12 +322,21 @@
|
||||||
}
|
}
|
||||||
} else if (type === 'chat:completion') {
|
} else if (type === 'chat:completion') {
|
||||||
chatCompletionEventHandler(data, message, event.chat_id);
|
chatCompletionEventHandler(data, message, event.chat_id);
|
||||||
|
} else if (type === 'chat:tasks:cancel') {
|
||||||
|
taskIds = null;
|
||||||
|
const responseMessage = history.messages[history.currentId];
|
||||||
|
// Set all response messages to done
|
||||||
|
for (const messageId of history.messages[responseMessage.parentId].childrenIds) {
|
||||||
|
history.messages[messageId].done = true;
|
||||||
|
}
|
||||||
} else if (type === 'chat:message:delta' || type === 'message') {
|
} else if (type === 'chat:message:delta' || type === 'message') {
|
||||||
message.content += data.content;
|
message.content += data.content;
|
||||||
} else if (type === 'chat:message' || type === 'replace') {
|
} else if (type === 'chat:message' || type === 'replace') {
|
||||||
message.content = data.content;
|
message.content = data.content;
|
||||||
} else if (type === 'chat:message:files' || type === 'files') {
|
} else if (type === 'chat:message:files' || type === 'files') {
|
||||||
message.files = data.files;
|
message.files = data.files;
|
||||||
|
} else if (type === 'chat:message:error') {
|
||||||
|
message.error = data.error;
|
||||||
} else if (type === 'chat:message:follow_ups') {
|
} else if (type === 'chat:message:follow_ups') {
|
||||||
message.followUps = data.follow_ups;
|
message.followUps = data.follow_ups;
|
||||||
|
|
||||||
|
|
@ -669,7 +682,7 @@
|
||||||
console.log(url);
|
console.log(url);
|
||||||
|
|
||||||
const fileItem = {
|
const fileItem = {
|
||||||
type: 'doc',
|
type: 'text',
|
||||||
name: url,
|
name: url,
|
||||||
collection_name: '',
|
collection_name: '',
|
||||||
status: 'uploading',
|
status: 'uploading',
|
||||||
|
|
@ -702,7 +715,7 @@
|
||||||
console.log(url);
|
console.log(url);
|
||||||
|
|
||||||
const fileItem = {
|
const fileItem = {
|
||||||
type: 'doc',
|
type: 'text',
|
||||||
name: url,
|
name: url,
|
||||||
collection_name: '',
|
collection_name: '',
|
||||||
status: 'uploading',
|
status: 'uploading',
|
||||||
|
|
@ -1394,10 +1407,10 @@
|
||||||
const submitPrompt = async (userPrompt, { _raw = false } = {}) => {
|
const submitPrompt = async (userPrompt, { _raw = false } = {}) => {
|
||||||
console.log('submitPrompt', userPrompt, $chatId);
|
console.log('submitPrompt', userPrompt, $chatId);
|
||||||
|
|
||||||
const messages = createMessagesList(history, history.currentId);
|
|
||||||
const _selectedModels = selectedModels.map((modelId) =>
|
const _selectedModels = selectedModels.map((modelId) =>
|
||||||
$models.map((m) => m.id).includes(modelId) ? modelId : ''
|
$models.map((m) => m.id).includes(modelId) ? modelId : ''
|
||||||
);
|
);
|
||||||
|
|
||||||
if (JSON.stringify(selectedModels) !== JSON.stringify(_selectedModels)) {
|
if (JSON.stringify(selectedModels) !== JSON.stringify(_selectedModels)) {
|
||||||
selectedModels = _selectedModels;
|
selectedModels = _selectedModels;
|
||||||
}
|
}
|
||||||
|
|
@ -1411,15 +1424,6 @@
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (messages.length != 0 && messages.at(-1).done != true) {
|
|
||||||
// Response not done
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (messages.length != 0 && messages.at(-1).error && !messages.at(-1).content) {
|
|
||||||
// Error in response
|
|
||||||
toast.error($i18n.t(`Oops! There was an error in the previous response.`));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (
|
if (
|
||||||
files.length > 0 &&
|
files.length > 0 &&
|
||||||
files.filter((file) => file.type !== 'image' && file.status === 'uploading').length > 0
|
files.filter((file) => file.type !== 'image' && file.status === 'uploading').length > 0
|
||||||
|
|
@ -1429,6 +1433,7 @@
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
($config?.file?.max_count ?? null) !== null &&
|
($config?.file?.max_count ?? null) !== null &&
|
||||||
files.length + chatFiles.length > $config?.file?.max_count
|
files.length + chatFiles.length > $config?.file?.max_count
|
||||||
|
|
@ -1441,9 +1446,25 @@
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (history?.currentId) {
|
||||||
|
const lastMessage = history.messages[history.currentId];
|
||||||
|
if (lastMessage.done != true) {
|
||||||
|
// Response not done
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lastMessage.error && !lastMessage.content) {
|
||||||
|
// Error in response
|
||||||
|
toast.error($i18n.t(`Oops! There was an error in the previous response.`));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
messageInput?.setText('');
|
messageInput?.setText('');
|
||||||
prompt = '';
|
prompt = '';
|
||||||
|
|
||||||
|
const messages = createMessagesList(history, history.currentId);
|
||||||
|
|
||||||
// Reset chat input textarea
|
// Reset chat input textarea
|
||||||
if (!($settings?.richTextInput ?? true)) {
|
if (!($settings?.richTextInput ?? true)) {
|
||||||
const chatInputElement = document.getElementById('chat-input');
|
const chatInputElement = document.getElementById('chat-input');
|
||||||
|
|
@ -1618,6 +1639,46 @@
|
||||||
chats.set(await getChatList(localStorage.token, $currentChatPage));
|
chats.set(await getChatList(localStorage.token, $currentChatPage));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getFeatures = () => {
|
||||||
|
let features = {};
|
||||||
|
|
||||||
|
if ($config?.features)
|
||||||
|
features = {
|
||||||
|
image_generation:
|
||||||
|
$config?.features?.enable_image_generation &&
|
||||||
|
($user?.role === 'admin' || $user?.permissions?.features?.image_generation)
|
||||||
|
? imageGenerationEnabled
|
||||||
|
: false,
|
||||||
|
code_interpreter:
|
||||||
|
$config?.features?.enable_code_interpreter &&
|
||||||
|
($user?.role === 'admin' || $user?.permissions?.features?.code_interpreter)
|
||||||
|
? codeInterpreterEnabled
|
||||||
|
: false,
|
||||||
|
web_search:
|
||||||
|
$config?.features?.enable_web_search &&
|
||||||
|
($user?.role === 'admin' || $user?.permissions?.features?.web_search)
|
||||||
|
? webSearchEnabled
|
||||||
|
: false
|
||||||
|
};
|
||||||
|
|
||||||
|
const currentModels = atSelectedModel?.id ? [atSelectedModel.id] : selectedModels;
|
||||||
|
if (
|
||||||
|
currentModels.filter(
|
||||||
|
(model) => $models.find((m) => m.id === model)?.info?.meta?.capabilities?.web_search ?? true
|
||||||
|
).length === currentModels.length
|
||||||
|
) {
|
||||||
|
if (($settings?.webSearch ?? false) === 'always') {
|
||||||
|
features = { ...features, web_search: true };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($settings?.memory ?? false) {
|
||||||
|
features = { ...features, memory: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
return features;
|
||||||
|
};
|
||||||
|
|
||||||
const sendMessageSocket = async (model, _messages, _history, responseMessageId, _chatId) => {
|
const sendMessageSocket = async (model, _messages, _history, responseMessageId, _chatId) => {
|
||||||
const responseMessage = _history.messages[responseMessageId];
|
const responseMessage = _history.messages[responseMessageId];
|
||||||
const userMessage = _history.messages[responseMessage.parentId];
|
const userMessage = _history.messages[responseMessage.parentId];
|
||||||
|
|
@ -1730,25 +1791,7 @@
|
||||||
filter_ids: selectedFilterIds.length > 0 ? selectedFilterIds : undefined,
|
filter_ids: selectedFilterIds.length > 0 ? selectedFilterIds : undefined,
|
||||||
tool_ids: selectedToolIds.length > 0 ? selectedToolIds : undefined,
|
tool_ids: selectedToolIds.length > 0 ? selectedToolIds : undefined,
|
||||||
tool_servers: $toolServers,
|
tool_servers: $toolServers,
|
||||||
|
features: getFeatures(),
|
||||||
features: {
|
|
||||||
image_generation:
|
|
||||||
$config?.features?.enable_image_generation &&
|
|
||||||
($user?.role === 'admin' || $user?.permissions?.features?.image_generation)
|
|
||||||
? imageGenerationEnabled
|
|
||||||
: false,
|
|
||||||
code_interpreter:
|
|
||||||
$config?.features?.enable_code_interpreter &&
|
|
||||||
($user?.role === 'admin' || $user?.permissions?.features?.code_interpreter)
|
|
||||||
? codeInterpreterEnabled
|
|
||||||
: false,
|
|
||||||
web_search:
|
|
||||||
$config?.features?.enable_web_search &&
|
|
||||||
($user?.role === 'admin' || $user?.permissions?.features?.web_search)
|
|
||||||
? webSearchEnabled || ($settings?.webSearch ?? false) === 'always'
|
|
||||||
: false,
|
|
||||||
memory: $settings?.memory ?? false
|
|
||||||
},
|
|
||||||
variables: {
|
variables: {
|
||||||
...getPromptVariables($user?.name, $settings?.userLocation ? userLocation : undefined)
|
...getPromptVariables($user?.name, $settings?.userLocation ? userLocation : undefined)
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -39,7 +39,11 @@
|
||||||
|
|
||||||
export const openPane = () => {
|
export const openPane = () => {
|
||||||
if (parseInt(localStorage?.chatControlsSize)) {
|
if (parseInt(localStorage?.chatControlsSize)) {
|
||||||
pane.resize(parseInt(localStorage?.chatControlsSize));
|
const container = document.getElementById('chat-container');
|
||||||
|
let size = Math.floor(
|
||||||
|
(parseInt(localStorage?.chatControlsSize) / container.clientWidth) * 100
|
||||||
|
);
|
||||||
|
pane.resize(size);
|
||||||
} else {
|
} else {
|
||||||
pane.resize(minSize);
|
pane.resize(minSize);
|
||||||
}
|
}
|
||||||
|
|
@ -91,7 +95,7 @@
|
||||||
const resizeObserver = new ResizeObserver((entries) => {
|
const resizeObserver = new ResizeObserver((entries) => {
|
||||||
for (let entry of entries) {
|
for (let entry of entries) {
|
||||||
const width = entry.contentRect.width;
|
const width = entry.contentRect.width;
|
||||||
// calculate the percentage of 200px
|
// calculate the percentage of 350px
|
||||||
const percentage = (350 / width) * 100;
|
const percentage = (350 / width) * 100;
|
||||||
// set the minSize to the percentage, must be an integer
|
// set the minSize to the percentage, must be an integer
|
||||||
minSize = Math.floor(percentage);
|
minSize = Math.floor(percentage);
|
||||||
|
|
@ -99,6 +103,13 @@
|
||||||
if ($showControls) {
|
if ($showControls) {
|
||||||
if (pane && pane.isExpanded() && pane.getSize() < minSize) {
|
if (pane && pane.isExpanded() && pane.getSize() < minSize) {
|
||||||
pane.resize(minSize);
|
pane.resize(minSize);
|
||||||
|
} else {
|
||||||
|
let size = Math.floor(
|
||||||
|
(parseInt(localStorage?.chatControlsSize) / container.clientWidth) * 100
|
||||||
|
);
|
||||||
|
if (size < minSize) {
|
||||||
|
pane.resize(minSize);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -207,8 +218,6 @@
|
||||||
bind:pane
|
bind:pane
|
||||||
defaultSize={0}
|
defaultSize={0}
|
||||||
onResize={(size) => {
|
onResize={(size) => {
|
||||||
console.log('size', size, minSize);
|
|
||||||
|
|
||||||
if ($showControls && pane.isExpanded()) {
|
if ($showControls && pane.isExpanded()) {
|
||||||
if (size < minSize) {
|
if (size < minSize) {
|
||||||
pane.resize(minSize);
|
pane.resize(minSize);
|
||||||
|
|
@ -217,7 +226,9 @@
|
||||||
if (size < minSize) {
|
if (size < minSize) {
|
||||||
localStorage.chatControlsSize = 0;
|
localStorage.chatControlsSize = 0;
|
||||||
} else {
|
} else {
|
||||||
localStorage.chatControlsSize = size;
|
// save the size in pixels to localStorage
|
||||||
|
const container = document.getElementById('chat-container');
|
||||||
|
localStorage.chatControlsSize = Math.floor((size / 100) * container.clientWidth);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,6 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import * as pdfjs from 'pdfjs-dist';
|
|
||||||
import * as pdfWorker from 'pdfjs-dist/build/pdf.worker.mjs';
|
|
||||||
pdfjs.GlobalWorkerOptions.workerSrc = import.meta.url + 'pdfjs-dist/build/pdf.worker.mjs';
|
|
||||||
|
|
||||||
import DOMPurify from 'dompurify';
|
import DOMPurify from 'dompurify';
|
||||||
import { marked } from 'marked';
|
import { marked } from 'marked';
|
||||||
import heic2any from 'heic2any';
|
|
||||||
|
|
||||||
import { toast } from 'svelte-sonner';
|
import { toast } from 'svelte-sonner';
|
||||||
|
|
||||||
|
|
@ -32,7 +27,7 @@
|
||||||
} from '$lib/stores';
|
} from '$lib/stores';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
blobToFile,
|
convertHeicToJpeg,
|
||||||
compressImage,
|
compressImage,
|
||||||
createMessagesList,
|
createMessagesList,
|
||||||
extractContentFromFile,
|
extractContentFromFile,
|
||||||
|
|
@ -106,6 +101,7 @@
|
||||||
export let codeInterpreterEnabled = false;
|
export let codeInterpreterEnabled = false;
|
||||||
|
|
||||||
let showInputVariablesModal = false;
|
let showInputVariablesModal = false;
|
||||||
|
let inputVariablesModalCallback = (variableValues) => {};
|
||||||
let inputVariables = {};
|
let inputVariables = {};
|
||||||
let inputVariableValues = {};
|
let inputVariableValues = {};
|
||||||
|
|
||||||
|
|
@ -127,11 +123,24 @@
|
||||||
codeInterpreterEnabled
|
codeInterpreterEnabled
|
||||||
});
|
});
|
||||||
|
|
||||||
const inputVariableHandler = async (text: string) => {
|
const inputVariableHandler = async (text: string): Promise<string> => {
|
||||||
inputVariables = extractInputVariables(text);
|
inputVariables = extractInputVariables(text);
|
||||||
if (Object.keys(inputVariables).length > 0) {
|
|
||||||
showInputVariablesModal = true;
|
// No variables? return the original text immediately.
|
||||||
|
if (Object.keys(inputVariables).length === 0) {
|
||||||
|
return text;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Show modal and wait for the user's input.
|
||||||
|
showInputVariablesModal = true;
|
||||||
|
return await new Promise<string>((resolve) => {
|
||||||
|
inputVariablesModalCallback = (variableValues) => {
|
||||||
|
inputVariableValues = { ...inputVariableValues, ...variableValues };
|
||||||
|
replaceVariables(inputVariableValues);
|
||||||
|
showInputVariablesModal = false;
|
||||||
|
resolve(text);
|
||||||
|
};
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const textVariableHandler = async (text: string) => {
|
const textVariableHandler = async (text: string) => {
|
||||||
|
|
@ -249,7 +258,6 @@
|
||||||
text = text.replaceAll('{{CURRENT_WEEKDAY}}', weekday);
|
text = text.replaceAll('{{CURRENT_WEEKDAY}}', weekday);
|
||||||
}
|
}
|
||||||
|
|
||||||
inputVariableHandler(text);
|
|
||||||
return text;
|
return text;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -285,7 +293,7 @@
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const setText = async (text?: string) => {
|
export const setText = async (text?: string, cb?: (text: string) => void) => {
|
||||||
const chatInput = document.getElementById('chat-input');
|
const chatInput = document.getElementById('chat-input');
|
||||||
|
|
||||||
if (chatInput) {
|
if (chatInput) {
|
||||||
|
|
@ -301,6 +309,10 @@
|
||||||
chatInput.focus();
|
chatInput.focus();
|
||||||
chatInput.dispatchEvent(new Event('input'));
|
chatInput.dispatchEvent(new Event('input'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
text = await inputVariableHandler(text);
|
||||||
|
await tick();
|
||||||
|
if (cb) await cb(text);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -417,6 +429,30 @@
|
||||||
let recording = false;
|
let recording = false;
|
||||||
|
|
||||||
let isComposing = false;
|
let isComposing = false;
|
||||||
|
// Safari has a bug where compositionend is not triggered correctly #16615
|
||||||
|
// when using the virtual keyboard on iOS.
|
||||||
|
let compositionEndedAt = -2e8;
|
||||||
|
const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
|
||||||
|
function inOrNearComposition(event: Event) {
|
||||||
|
if (isComposing) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// See https://www.stum.de/2016/06/24/handling-ime-events-in-javascript/.
|
||||||
|
// On Japanese input method editors (IMEs), the Enter key is used to confirm character
|
||||||
|
// selection. On Safari, when Enter is pressed, compositionend and keydown events are
|
||||||
|
// emitted. The keydown event triggers newline insertion, which we don't want.
|
||||||
|
// This method returns true if the keydown event should be ignored.
|
||||||
|
// We only ignore it once, as pressing Enter a second time *should* insert a newline.
|
||||||
|
// Furthermore, the keydown event timestamp must be close to the compositionEndedAt timestamp.
|
||||||
|
// This guards against the case where compositionend is triggered without the keyboard
|
||||||
|
// (e.g. character confirmation may be done with the mouse), and keydown is triggered
|
||||||
|
// afterwards- we wouldn't want to ignore the keydown event in this case.
|
||||||
|
if (isSafari && Math.abs(event.timeStamp - compositionEndedAt) < 500) {
|
||||||
|
compositionEndedAt = -2e8;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
let chatInputContainerElement;
|
let chatInputContainerElement;
|
||||||
let chatInputElement;
|
let chatInputElement;
|
||||||
|
|
@ -616,7 +652,7 @@
|
||||||
} else {
|
} else {
|
||||||
// If temporary chat is enabled, we just add the file to the list without uploading it.
|
// If temporary chat is enabled, we just add the file to the list without uploading it.
|
||||||
|
|
||||||
const content = await extractContentFromFile(file, pdfjsLib).catch((error) => {
|
const content = await extractContentFromFile(file).catch((error) => {
|
||||||
toast.error(
|
toast.error(
|
||||||
$i18n.t('Failed to extract content from the file: {{error}}', { error: error })
|
$i18n.t('Failed to extract content from the file: {{error}}', { error: error })
|
||||||
);
|
);
|
||||||
|
|
@ -739,11 +775,7 @@
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
reader.readAsDataURL(
|
reader.readAsDataURL(file['type'] === 'image/heic' ? await convertHeicToJpeg(file) : file);
|
||||||
file['type'] === 'image/heic'
|
|
||||||
? await heic2any({ blob: file, toType: 'image/jpeg' })
|
|
||||||
: file
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
uploadFileHandler(file);
|
uploadFileHandler(file);
|
||||||
}
|
}
|
||||||
|
|
@ -849,10 +881,7 @@
|
||||||
<InputVariablesModal
|
<InputVariablesModal
|
||||||
bind:show={showInputVariablesModal}
|
bind:show={showInputVariablesModal}
|
||||||
variables={inputVariables}
|
variables={inputVariables}
|
||||||
onSave={(variableValues) => {
|
onSave={inputVariablesModalCallback}
|
||||||
inputVariableValues = { ...inputVariableValues, ...variableValues };
|
|
||||||
replaceVariables(inputVariableValues);
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{#if loaded}
|
{#if loaded}
|
||||||
|
|
@ -1169,19 +1198,9 @@
|
||||||
return res;
|
return res;
|
||||||
}}
|
}}
|
||||||
oncompositionstart={() => (isComposing = true)}
|
oncompositionstart={() => (isComposing = true)}
|
||||||
oncompositionend={() => {
|
oncompositionend={(e) => {
|
||||||
const isSafari = /^((?!chrome|android).)*safari/i.test(
|
compositionEndedAt = e.timeStamp;
|
||||||
navigator.userAgent
|
|
||||||
);
|
|
||||||
|
|
||||||
if (isSafari) {
|
|
||||||
// Safari has a bug where compositionend is not triggered correctly #16615
|
|
||||||
// when using the virtual keyboard on iOS.
|
|
||||||
// We use a timeout to ensure that the composition is ended after a short delay.
|
|
||||||
setTimeout(() => (isComposing = false));
|
|
||||||
} else {
|
|
||||||
isComposing = false;
|
isComposing = false;
|
||||||
}
|
|
||||||
}}
|
}}
|
||||||
on:keydown={async (e) => {
|
on:keydown={async (e) => {
|
||||||
e = e.detail.event;
|
e = e.detail.event;
|
||||||
|
|
@ -1290,7 +1309,7 @@
|
||||||
navigator.msMaxTouchPoints > 0
|
navigator.msMaxTouchPoints > 0
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
if (isComposing) {
|
if (inOrNearComposition(e)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1393,17 +1412,9 @@
|
||||||
command = getCommand();
|
command = getCommand();
|
||||||
}}
|
}}
|
||||||
on:compositionstart={() => (isComposing = true)}
|
on:compositionstart={() => (isComposing = true)}
|
||||||
on:compositionend={() => {
|
on:compositionend={(e) => {
|
||||||
const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
|
compositionEndedAt = e.timeStamp;
|
||||||
|
|
||||||
if (isSafari) {
|
|
||||||
// Safari has a bug where compositionend is not triggered correctly #16615
|
|
||||||
// when using the virtual keyboard on iOS.
|
|
||||||
// We use a timeout to ensure that the composition is ended after a short delay.
|
|
||||||
setTimeout(() => (isComposing = false));
|
|
||||||
} else {
|
|
||||||
isComposing = false;
|
isComposing = false;
|
||||||
}
|
|
||||||
}}
|
}}
|
||||||
on:keydown={async (e) => {
|
on:keydown={async (e) => {
|
||||||
const isCtrlPressed = e.ctrlKey || e.metaKey; // metaKey is for Cmd key on Mac
|
const isCtrlPressed = e.ctrlKey || e.metaKey; // metaKey is for Cmd key on Mac
|
||||||
|
|
@ -1523,7 +1534,7 @@
|
||||||
navigator.msMaxTouchPoints > 0
|
navigator.msMaxTouchPoints > 0
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
if (isComposing) {
|
if (inOrNearComposition(e)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1772,7 +1783,7 @@
|
||||||
<Sparkles className="size-4" strokeWidth="1.75" />
|
<Sparkles className="size-4" strokeWidth="1.75" />
|
||||||
{/if}
|
{/if}
|
||||||
<span
|
<span
|
||||||
class="hidden @xl:block whitespace-nowrap overflow-hidden text-ellipsis leading-none pr-0.5"
|
class="hidden @xl:block whitespace-nowrap text-ellipsis leading-none normal-case pr-0.5"
|
||||||
>{filter?.name}</span
|
>{filter?.name}</span
|
||||||
>
|
>
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -1791,7 +1802,7 @@
|
||||||
>
|
>
|
||||||
<GlobeAlt className="size-4" strokeWidth="1.75" />
|
<GlobeAlt className="size-4" strokeWidth="1.75" />
|
||||||
<span
|
<span
|
||||||
class="hidden @xl:block whitespace-nowrap overflow-hidden text-ellipsis leading-none pr-0.5"
|
class="hidden @xl:block whitespace-nowrap text-ellipsis leading-none normal-case pr-0.5"
|
||||||
>{$i18n.t('Web Search')}</span
|
>{$i18n.t('Web Search')}</span
|
||||||
>
|
>
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -1810,7 +1821,7 @@
|
||||||
>
|
>
|
||||||
<Photo className="size-4" strokeWidth="1.75" />
|
<Photo className="size-4" strokeWidth="1.75" />
|
||||||
<span
|
<span
|
||||||
class="hidden @xl:block whitespace-nowrap overflow-hidden text-ellipsis leading-none pr-0.5"
|
class="hidden @xl:block whitespace-nowrap text-ellipsis leading-none normal-case pr-0.5"
|
||||||
>{$i18n.t('Image')}</span
|
>{$i18n.t('Image')}</span
|
||||||
>
|
>
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -1836,7 +1847,7 @@
|
||||||
>
|
>
|
||||||
<CommandLine className="size-4" strokeWidth="1.75" />
|
<CommandLine className="size-4" strokeWidth="1.75" />
|
||||||
<span
|
<span
|
||||||
class="hidden @xl:block whitespace-nowrap overflow-hidden text-ellipsis leading-none pr-0.5"
|
class="hidden @xl:block whitespace-nowrap text-ellipsis leading-none normal-case pr-0.5"
|
||||||
>{$i18n.t('Code Interpreter')}</span
|
>{$i18n.t('Code Interpreter')}</span
|
||||||
>
|
>
|
||||||
</button>
|
</button>
|
||||||
|
|
|
||||||
|
|
@ -49,6 +49,7 @@
|
||||||
export let addMessages: Function = () => {};
|
export let addMessages: Function = () => {};
|
||||||
|
|
||||||
export let readOnly = false;
|
export let readOnly = false;
|
||||||
|
export let editCodeBlock = true;
|
||||||
|
|
||||||
export let topPadding = false;
|
export let topPadding = false;
|
||||||
export let bottomPadding = false;
|
export let bottomPadding = false;
|
||||||
|
|
@ -56,7 +57,7 @@
|
||||||
|
|
||||||
export let onSelect = (e) => {};
|
export let onSelect = (e) => {};
|
||||||
|
|
||||||
let messagesCount = 20;
|
export let messagesCount: number | null = 20;
|
||||||
let messagesLoading = false;
|
let messagesLoading = false;
|
||||||
|
|
||||||
const loadMoreMessages = async () => {
|
const loadMoreMessages = async () => {
|
||||||
|
|
@ -76,7 +77,7 @@
|
||||||
let _messages = [];
|
let _messages = [];
|
||||||
|
|
||||||
let message = history.messages[history.currentId];
|
let message = history.messages[history.currentId];
|
||||||
while (message && _messages.length <= messagesCount) {
|
while (message && (messagesCount !== null ? _messages.length <= messagesCount : true)) {
|
||||||
_messages.unshift({ ...message });
|
_messages.unshift({ ...message });
|
||||||
message = message.parentId !== null ? history.messages[message.parentId] : null;
|
message = message.parentId !== null ? history.messages[message.parentId] : null;
|
||||||
}
|
}
|
||||||
|
|
@ -447,6 +448,7 @@
|
||||||
{addMessages}
|
{addMessages}
|
||||||
{triggerScroll}
|
{triggerScroll}
|
||||||
{readOnly}
|
{readOnly}
|
||||||
|
{editCodeBlock}
|
||||||
{topPadding}
|
{topPadding}
|
||||||
/>
|
/>
|
||||||
{/each}
|
{/each}
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,6 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { getContext } from 'svelte';
|
import { getContext } from 'svelte';
|
||||||
import CitationsModal from './CitationsModal.svelte';
|
import CitationsModal from '$lib/components/chat/Messages/Citations/CitationsModal.svelte';
|
||||||
import Collapsible from '$lib/components/common/Collapsible.svelte';
|
|
||||||
import ChevronDown from '$lib/components/icons/ChevronDown.svelte';
|
|
||||||
import ChevronUp from '$lib/components/icons/ChevronUp.svelte';
|
|
||||||
|
|
||||||
const i18n = getContext('i18n');
|
const i18n = getContext('i18n');
|
||||||
|
|
||||||
|
|
@ -14,14 +11,16 @@
|
||||||
let showPercentage = false;
|
let showPercentage = false;
|
||||||
let showRelevance = true;
|
let showRelevance = true;
|
||||||
|
|
||||||
|
let citationModal = null;
|
||||||
let showCitationModal = false;
|
let showCitationModal = false;
|
||||||
let selectedCitation: any = null;
|
let selectedCitation: any = null;
|
||||||
let isCollapsibleOpen = false;
|
let isCollapsibleOpen = false;
|
||||||
|
|
||||||
export const showSourceModal = (sourceIdx) => {
|
export const showSourceModal = (sourceIdx) => {
|
||||||
if (citations[sourceIdx]) {
|
if (citations[sourceIdx]) {
|
||||||
selectedCitation = citations[sourceIdx];
|
console.log('Showing citation modal for:', citations[sourceIdx]);
|
||||||
showCitationModal = true;
|
citationModal?.showCitation(citations[sourceIdx]);
|
||||||
|
// showCitationModal = true;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -55,9 +54,9 @@
|
||||||
return acc;
|
return acc;
|
||||||
}
|
}
|
||||||
|
|
||||||
source.document.forEach((document, index) => {
|
source?.document?.forEach((document, index) => {
|
||||||
const metadata = source.metadata?.[index];
|
const metadata = source?.metadata?.[index];
|
||||||
const distance = source.distances?.[index];
|
const distance = source?.distances?.[index];
|
||||||
|
|
||||||
// Within the same citation there could be multiple documents
|
// Within the same citation there could be multiple documents
|
||||||
const id = metadata?.source ?? source?.source?.id ?? 'N/A';
|
const id = metadata?.source ?? source?.source?.id ?? 'N/A';
|
||||||
|
|
@ -87,6 +86,7 @@
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return acc;
|
return acc;
|
||||||
}, []);
|
}, []);
|
||||||
console.log('citations', citations);
|
console.log('citations', citations);
|
||||||
|
|
@ -94,127 +94,46 @@
|
||||||
showRelevance = calculateShowRelevance(citations);
|
showRelevance = calculateShowRelevance(citations);
|
||||||
showPercentage = shouldShowPercentage(citations);
|
showPercentage = shouldShowPercentage(citations);
|
||||||
}
|
}
|
||||||
|
|
||||||
const decodeString = (str: string) => {
|
|
||||||
try {
|
|
||||||
return decodeURIComponent(str);
|
|
||||||
} catch (e) {
|
|
||||||
return str;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<CitationsModal
|
<CitationsModal
|
||||||
|
bind:this={citationModal}
|
||||||
bind:show={showCitationModal}
|
bind:show={showCitationModal}
|
||||||
citation={selectedCitation}
|
{id}
|
||||||
|
{citations}
|
||||||
{showPercentage}
|
{showPercentage}
|
||||||
{showRelevance}
|
{showRelevance}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{#if citations.length > 0}
|
{#if citations.length > 0}
|
||||||
<div class=" py-0.5 -mx-0.5 w-full flex gap-1 items-center flex-wrap">
|
{@const urlCitations = citations.filter((c) => c?.source?.name?.startsWith('http'))}
|
||||||
{#if citations.length <= 3}
|
<div class=" py-1 -mx-0.5 w-full flex gap-1 items-center flex-wrap">
|
||||||
<div class="flex text-xs font-medium flex-wrap">
|
|
||||||
{#each citations as citation, idx}
|
|
||||||
<button
|
<button
|
||||||
id={`source-${id}-${idx + 1}`}
|
class="text-xs font-medium text-gray-600 dark:text-gray-300 px-3.5 h-8 rounded-full hover:bg-gray-100 dark:hover:bg-gray-800 transition flex items-center gap-1 border border-gray-50 dark:border-gray-850"
|
||||||
class="no-toggle outline-hidden flex dark:text-gray-300 p-1 bg-white dark:bg-gray-900 rounded-xl max-w-96"
|
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
showCitationModal = true;
|
showCitationModal = true;
|
||||||
selectedCitation = citation;
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{#if citations.every((c) => c.distances !== undefined)}
|
{#if urlCitations.length > 0}
|
||||||
<div class="bg-gray-50 dark:bg-gray-800 rounded-full size-4">
|
<div class="flex -space-x-1 items-center">
|
||||||
{idx + 1}
|
{#each urlCitations.slice(0, 3) as citation, idx}
|
||||||
</div>
|
<img
|
||||||
{/if}
|
src="https://www.google.com/s2/favicons?sz=32&domain={citation.source.name}"
|
||||||
<div
|
alt="favicon"
|
||||||
class="flex-1 mx-1 truncate text-black/60 hover:text-black dark:text-white/60 dark:hover:text-white transition"
|
class="size-4 rounded-full shrink-0 border border-white dark:border-gray-850 bg-white dark:bg-gray-900"
|
||||||
>
|
/>
|
||||||
{decodeString(citation.source.name)}
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
|
{/if}
|
||||||
|
<div>
|
||||||
|
{#if citations.length === 1}
|
||||||
|
{$i18n.t('1 Source')}
|
||||||
{:else}
|
{:else}
|
||||||
<Collapsible
|
{$i18n.t('{{COUNT}} Sources', {
|
||||||
id={`collapsible-${id}`}
|
COUNT: citations.length
|
||||||
bind:open={isCollapsibleOpen}
|
})}
|
||||||
className="w-full max-w-full "
|
|
||||||
buttonClassName="w-fit max-w-full"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="flex w-full overflow-auto items-center gap-2 text-gray-500 hover:text-gray-600 dark:hover:text-gray-400 transition cursor-pointer"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="flex-1 flex items-center gap-1 overflow-auto scrollbar-none w-full max-w-full"
|
|
||||||
>
|
|
||||||
<span class="whitespace-nowrap hidden sm:inline shrink-0"
|
|
||||||
>{$i18n.t('References from')}</span
|
|
||||||
>
|
|
||||||
<div class="flex items-center overflow-auto scrollbar-none w-full max-w-full flex-1">
|
|
||||||
<div class="flex text-xs font-medium items-center">
|
|
||||||
{#each citations.slice(0, 2) as citation, idx}
|
|
||||||
<button
|
|
||||||
class="no-toggle outline-hidden flex dark:text-gray-300 p-1 bg-gray-50 hover:bg-gray-100 dark:bg-gray-900 dark:hover:bg-gray-850 transition rounded-xl max-w-96"
|
|
||||||
on:click={() => {
|
|
||||||
showCitationModal = true;
|
|
||||||
selectedCitation = citation;
|
|
||||||
}}
|
|
||||||
on:pointerup={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{#if citations.every((c) => c.distances !== undefined)}
|
|
||||||
<div class="bg-gray-50 dark:bg-gray-800 rounded-full size-4">
|
|
||||||
{idx + 1}
|
|
||||||
</div>
|
|
||||||
{/if}
|
{/if}
|
||||||
<div class="flex-1 mx-1 truncate">
|
|
||||||
{decodeString(citation.source.name)}
|
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center gap-1 whitespace-nowrap shrink-0">
|
|
||||||
<span class="hidden sm:inline">{$i18n.t('and')}</span>
|
|
||||||
{citations.length - 2}
|
|
||||||
<span>{$i18n.t('more')}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="shrink-0">
|
|
||||||
{#if isCollapsibleOpen}
|
|
||||||
<ChevronUp strokeWidth="3.5" className="size-3.5" />
|
|
||||||
{:else}
|
|
||||||
<ChevronDown strokeWidth="3.5" className="size-3.5" />
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div slot="content">
|
|
||||||
<div class="flex text-xs font-medium flex-wrap">
|
|
||||||
{#each citations.slice(2) as citation, idx}
|
|
||||||
<button
|
|
||||||
class="no-toggle outline-hidden flex dark:text-gray-300 p-1 bg-gray-50 hover:bg-gray-100 dark:bg-gray-900 dark:hover:bg-gray-850 transition rounded-xl max-w-96"
|
|
||||||
on:click={() => {
|
|
||||||
showCitationModal = true;
|
|
||||||
selectedCitation = citation;
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{#if citations.every((c) => c.distances !== undefined)}
|
|
||||||
<div class="bg-gray-50 dark:bg-gray-800 rounded-full size-4">
|
|
||||||
{idx + 3}
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
<div class="flex-1 mx-1 truncate">
|
|
||||||
{decodeString(citation.source.name)}
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Collapsible>
|
|
||||||
{/if}
|
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
||||||
|
|
@ -61,8 +61,34 @@
|
||||||
<Modal size="lg" bind:show>
|
<Modal size="lg" bind:show>
|
||||||
<div>
|
<div>
|
||||||
<div class=" flex justify-between dark:text-gray-300 px-5 pt-4 pb-2">
|
<div class=" flex justify-between dark:text-gray-300 px-5 pt-4 pb-2">
|
||||||
<div class=" text-lg font-medium self-center capitalize">
|
<div class=" text-lg font-medium self-center">
|
||||||
|
{#if citation?.source?.name}
|
||||||
|
{@const document = mergedDocuments?.[0]}
|
||||||
|
{#if document?.metadata?.file_id || document.source?.url?.includes('http')}
|
||||||
|
<Tooltip
|
||||||
|
className="w-fit"
|
||||||
|
content={$i18n.t('Open file')}
|
||||||
|
placement="top-start"
|
||||||
|
tippyOptions={{ duration: [500, 0] }}
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
class="hover:text-gray-500 dark:hover:text-gray-100 underline grow"
|
||||||
|
href={document?.metadata?.file_id
|
||||||
|
? `${WEBUI_API_BASE_URL}/files/${document?.metadata?.file_id}/content${document?.metadata?.page !== undefined ? `#page=${document.metadata.page + 1}` : ''}`
|
||||||
|
: document.source?.url?.includes('http')
|
||||||
|
? document.source.url
|
||||||
|
: `#`}
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
{decodeString(citation?.source?.name)}
|
||||||
|
</a>
|
||||||
|
</Tooltip>
|
||||||
|
{:else}
|
||||||
|
{decodeString(citation?.source?.name)}
|
||||||
|
{/if}
|
||||||
|
{:else}
|
||||||
{$i18n.t('Citation')}
|
{$i18n.t('Citation')}
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
class="self-center"
|
class="self-center"
|
||||||
|
|
@ -76,57 +102,31 @@
|
||||||
|
|
||||||
<div class="flex flex-col md:flex-row w-full px-6 pb-5 md:space-x-4">
|
<div class="flex flex-col md:flex-row w-full px-6 pb-5 md:space-x-4">
|
||||||
<div
|
<div
|
||||||
class="flex flex-col w-full dark:text-gray-200 overflow-y-scroll max-h-[22rem] scrollbar-hidden"
|
class="flex flex-col w-full dark:text-gray-200 overflow-y-scroll max-h-[22rem] scrollbar-hidden gap-1"
|
||||||
>
|
>
|
||||||
{#each mergedDocuments as document, documentIdx}
|
{#each mergedDocuments as document, documentIdx}
|
||||||
<div class="flex flex-col w-full">
|
<div class="flex flex-col w-full gap-2">
|
||||||
<div class="text-sm font-medium dark:text-gray-300">
|
|
||||||
{$i18n.t('Source')}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{#if document.source?.name}
|
|
||||||
<Tooltip
|
|
||||||
className="w-fit"
|
|
||||||
content={$i18n.t('Open file')}
|
|
||||||
placement="top-start"
|
|
||||||
tippyOptions={{ duration: [500, 0] }}
|
|
||||||
>
|
|
||||||
<div class="text-sm dark:text-gray-400 flex items-center gap-2 w-fit">
|
|
||||||
<a
|
|
||||||
class="hover:text-gray-500 dark:hover:text-gray-100 underline grow"
|
|
||||||
href={document?.metadata?.file_id
|
|
||||||
? `${WEBUI_API_BASE_URL}/files/${document?.metadata?.file_id}/content${document?.metadata?.page !== undefined ? `#page=${document.metadata.page + 1}` : ''}`
|
|
||||||
: document.source?.url?.includes('http')
|
|
||||||
? document.source.url
|
|
||||||
: `#`}
|
|
||||||
target="_blank"
|
|
||||||
>
|
|
||||||
{decodeString(document?.metadata?.name ?? document.source.name)}
|
|
||||||
</a>
|
|
||||||
{#if Number.isInteger(document?.metadata?.page)}
|
|
||||||
<span class="text-xs text-gray-500 dark:text-gray-400">
|
|
||||||
({$i18n.t('page')}
|
|
||||||
{document.metadata.page + 1})
|
|
||||||
</span>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
</Tooltip>
|
|
||||||
{#if document.metadata?.parameters}
|
{#if document.metadata?.parameters}
|
||||||
<div class="text-sm font-medium dark:text-gray-300 mt-2 mb-0.5">
|
<div>
|
||||||
|
<div class="text-sm font-medium dark:text-gray-300 mb-1">
|
||||||
{$i18n.t('Parameters')}
|
{$i18n.t('Parameters')}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Textarea readonly value={JSON.stringify(document.metadata.parameters, null, 2)}
|
<Textarea readonly value={JSON.stringify(document.metadata.parameters, null, 2)}
|
||||||
></Textarea>
|
></Textarea>
|
||||||
{/if}
|
|
||||||
{#if showRelevance}
|
|
||||||
<div class="text-sm font-medium dark:text-gray-300 mt-2">
|
|
||||||
{$i18n.t('Relevance')}
|
|
||||||
</div>
|
</div>
|
||||||
{#if document.distance !== undefined}
|
{/if}
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
class=" text-sm font-medium dark:text-gray-300 flex items-center gap-2 w-fit mb-1"
|
||||||
|
>
|
||||||
|
{$i18n.t('Content')}
|
||||||
|
|
||||||
|
{#if showRelevance && document.distance !== undefined}
|
||||||
<Tooltip
|
<Tooltip
|
||||||
className="w-fit"
|
className="w-fit"
|
||||||
content={$i18n.t('Semantic distance to query')}
|
content={$i18n.t('Relevance')}
|
||||||
placement="top-start"
|
placement="top-start"
|
||||||
tippyOptions={{ duration: [500, 0] }}
|
tippyOptions={{ duration: [500, 0] }}
|
||||||
>
|
>
|
||||||
|
|
@ -141,12 +141,6 @@
|
||||||
{percentage.toFixed(2)}%
|
{percentage.toFixed(2)}%
|
||||||
</span>
|
</span>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if typeof document?.distance === 'number'}
|
|
||||||
<span class="text-gray-500 dark:text-gray-500">
|
|
||||||
({(document?.distance ?? 0).toFixed(4)})
|
|
||||||
</span>
|
|
||||||
{/if}
|
|
||||||
{:else if typeof document?.distance === 'number'}
|
{:else if typeof document?.distance === 'number'}
|
||||||
<span class="text-gray-500 dark:text-gray-500">
|
<span class="text-gray-500 dark:text-gray-500">
|
||||||
({(document?.distance ?? 0).toFixed(4)})
|
({(document?.distance ?? 0).toFixed(4)})
|
||||||
|
|
@ -154,22 +148,16 @@
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
{:else}
|
|
||||||
<div class="text-sm dark:text-gray-400">
|
|
||||||
{$i18n.t('No distance available')}
|
|
||||||
</div>
|
|
||||||
{/if}
|
{/if}
|
||||||
{/if}
|
|
||||||
{:else}
|
{#if Number.isInteger(document?.metadata?.page)}
|
||||||
<div class="text-sm dark:text-gray-400">
|
<span class="text-sm text-gray-500 dark:text-gray-400">
|
||||||
{$i18n.t('No source available')}
|
({$i18n.t('page')}
|
||||||
</div>
|
{document.metadata.page + 1})
|
||||||
|
</span>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col w-full">
|
|
||||||
<div class=" text-sm font-medium dark:text-gray-300 mt-2">
|
|
||||||
{$i18n.t('Content')}
|
|
||||||
</div>
|
|
||||||
{#if document.metadata?.html}
|
{#if document.metadata?.html}
|
||||||
<iframe
|
<iframe
|
||||||
class="w-full border-0 h-auto rounded-none"
|
class="w-full border-0 h-auto rounded-none"
|
||||||
|
|
@ -183,10 +171,7 @@
|
||||||
</pre>
|
</pre>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
{#if documentIdx !== mergedDocuments.length - 1}
|
|
||||||
<hr class="border-gray-100 dark:border-gray-850 my-3" />
|
|
||||||
{/if}
|
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -0,0 +1,82 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { getContext, onMount, tick } from 'svelte';
|
||||||
|
|
||||||
|
const i18n = getContext('i18n');
|
||||||
|
|
||||||
|
import Modal from '$lib/components/common/Modal.svelte';
|
||||||
|
import XMark from '$lib/components/icons/XMark.svelte';
|
||||||
|
import CitationModal from './CitationModal.svelte';
|
||||||
|
|
||||||
|
export let id = '';
|
||||||
|
export let show = false;
|
||||||
|
export let citations = [];
|
||||||
|
export let showPercentage = false;
|
||||||
|
export let showRelevance = true;
|
||||||
|
|
||||||
|
let showCitationModal = false;
|
||||||
|
let selectedCitation: any = null;
|
||||||
|
|
||||||
|
export const showCitation = (citation) => {
|
||||||
|
selectedCitation = citation;
|
||||||
|
showCitationModal = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const decodeString = (str: string) => {
|
||||||
|
try {
|
||||||
|
return decodeURIComponent(str);
|
||||||
|
} catch (e) {
|
||||||
|
return str;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<CitationModal
|
||||||
|
bind:show={showCitationModal}
|
||||||
|
citation={selectedCitation}
|
||||||
|
{showPercentage}
|
||||||
|
{showRelevance}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Modal size="lg" bind:show>
|
||||||
|
<div>
|
||||||
|
<div class=" flex justify-between dark:text-gray-300 px-5 pt-4 pb-2">
|
||||||
|
<div class=" text-lg font-medium self-center capitalize">
|
||||||
|
{$i18n.t('Citations')}
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
class="self-center"
|
||||||
|
on:click={() => {
|
||||||
|
show = false;
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<XMark className={'size-5'} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col md:flex-row w-full px-6 pb-5 md:space-x-4">
|
||||||
|
<div
|
||||||
|
class="flex flex-col w-full dark:text-gray-200 overflow-y-scroll max-h-[22rem] scrollbar-hidden text-left text-sm gap-2"
|
||||||
|
>
|
||||||
|
{#each citations as citation, idx}
|
||||||
|
<button
|
||||||
|
id={`source-${id}-${idx + 1}`}
|
||||||
|
class="no-toggle outline-hidden flex dark:text-gray-300 bg-white dark:bg-gray-900 rounded-xl gap-1.5 items-center"
|
||||||
|
on:click={() => {
|
||||||
|
showCitationModal = true;
|
||||||
|
selectedCitation = citation;
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div class=" font-medium">
|
||||||
|
{idx + 1}.
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="flex-1 truncate text-black/60 hover:text-black dark:text-white/60 dark:hover:text-white transition text-left"
|
||||||
|
>
|
||||||
|
{decodeString(citation.source.name)}
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
|
@ -1,4 +1,6 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import hljs from 'highlight.js';
|
||||||
|
|
||||||
import mermaid from 'mermaid';
|
import mermaid from 'mermaid';
|
||||||
|
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
|
|
@ -22,6 +24,7 @@
|
||||||
const i18n = getContext('i18n');
|
const i18n = getContext('i18n');
|
||||||
|
|
||||||
export let id = '';
|
export let id = '';
|
||||||
|
export let edit = true;
|
||||||
|
|
||||||
export let onSave = (e) => {};
|
export let onSave = (e) => {};
|
||||||
export let onUpdate = (e) => {};
|
export let onUpdate = (e) => {};
|
||||||
|
|
@ -84,7 +87,7 @@
|
||||||
|
|
||||||
const copyCode = async () => {
|
const copyCode = async () => {
|
||||||
copied = true;
|
copied = true;
|
||||||
await copyToClipboard(code);
|
await copyToClipboard(_code);
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
copied = false;
|
copied = false;
|
||||||
|
|
@ -213,19 +216,19 @@
|
||||||
|
|
||||||
const executePythonAsWorker = async (code) => {
|
const executePythonAsWorker = async (code) => {
|
||||||
let packages = [
|
let packages = [
|
||||||
code.includes('requests') ? 'requests' : null,
|
/\bimport\s+requests\b|\bfrom\s+requests\b/.test(code) ? 'requests' : null,
|
||||||
code.includes('bs4') ? 'beautifulsoup4' : null,
|
/\bimport\s+bs4\b|\bfrom\s+bs4\b/.test(code) ? 'beautifulsoup4' : null,
|
||||||
code.includes('numpy') ? 'numpy' : null,
|
/\bimport\s+numpy\b|\bfrom\s+numpy\b/.test(code) ? 'numpy' : null,
|
||||||
code.includes('pandas') ? 'pandas' : null,
|
/\bimport\s+pandas\b|\bfrom\s+pandas\b/.test(code) ? 'pandas' : null,
|
||||||
code.includes('sklearn') ? 'scikit-learn' : null,
|
/\bimport\s+matplotlib\b|\bfrom\s+matplotlib\b/.test(code) ? 'matplotlib' : null,
|
||||||
code.includes('scipy') ? 'scipy' : null,
|
/\bimport\s+seaborn\b|\bfrom\s+seaborn\b/.test(code) ? 'seaborn' : null,
|
||||||
code.includes('re') ? 'regex' : null,
|
/\bimport\s+sklearn\b|\bfrom\s+sklearn\b/.test(code) ? 'scikit-learn' : null,
|
||||||
code.includes('seaborn') ? 'seaborn' : null,
|
/\bimport\s+scipy\b|\bfrom\s+scipy\b/.test(code) ? 'scipy' : null,
|
||||||
code.includes('sympy') ? 'sympy' : null,
|
/\bimport\s+re\b|\bfrom\s+re\b/.test(code) ? 'regex' : null,
|
||||||
code.includes('tiktoken') ? 'tiktoken' : null,
|
/\bimport\s+seaborn\b|\bfrom\s+seaborn\b/.test(code) ? 'seaborn' : null,
|
||||||
code.includes('matplotlib') ? 'matplotlib' : null,
|
/\bimport\s+sympy\b|\bfrom\s+sympy\b/.test(code) ? 'sympy' : null,
|
||||||
code.includes('pytz') ? 'pytz' : null,
|
/\bimport\s+tiktoken\b|\bfrom\s+tiktoken\b/.test(code) ? 'tiktoken' : null,
|
||||||
code.includes('openai') ? 'openai' : null
|
/\bimport\s+pytz\b|\bfrom\s+pytz\b/.test(code) ? 'pytz' : null
|
||||||
].filter(Boolean);
|
].filter(Boolean);
|
||||||
|
|
||||||
console.log(packages);
|
console.log(packages);
|
||||||
|
|
@ -413,11 +416,11 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<div class="relative {className} flex flex-col rounded-lg" dir="ltr">
|
<div class="relative {className} flex flex-col rounded-xl pt-2" dir="ltr">
|
||||||
{#if lang === 'mermaid'}
|
{#if lang === 'mermaid'}
|
||||||
{#if mermaidHtml}
|
{#if mermaidHtml}
|
||||||
<SvgPanZoom
|
<SvgPanZoom
|
||||||
className=" border border-gray-100 dark:border-gray-850 rounded-lg max-h-fit overflow-hidden"
|
className=" border border-gray-100 dark:border-gray-850 rounded-xl max-h-fit overflow-hidden"
|
||||||
svg={mermaidHtml}
|
svg={mermaidHtml}
|
||||||
content={_token.text}
|
content={_token.text}
|
||||||
/>
|
/>
|
||||||
|
|
@ -425,16 +428,16 @@
|
||||||
<pre class="mermaid">{code}</pre>
|
<pre class="mermaid">{code}</pre>
|
||||||
{/if}
|
{/if}
|
||||||
{:else}
|
{:else}
|
||||||
<div class="text-text-300 absolute pl-4 py-1.5 text-xs font-medium dark:text-white">
|
<div class="text-text-300 absolute pl-4 text-xs font-medium dark:text-white">
|
||||||
{lang}
|
{lang}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="sticky {stickyButtonsClassName} mb-1 py-1 pr-2.5 flex items-center justify-end z-10 text-xs text-black dark:text-white"
|
class="sticky {stickyButtonsClassName} mb-1 pr-2.5 flex items-center justify-end z-10 text-xs text-black dark:text-white"
|
||||||
>
|
>
|
||||||
<div class="flex items-center gap-0.5 translate-y-[1px]">
|
<div class="flex items-center gap-0.5">
|
||||||
<button
|
<button
|
||||||
class="flex gap-1 items-center bg-none border-none bg-gray-50 hover:bg-gray-100 dark:bg-gray-850 dark:hover:bg-gray-800 transition rounded-md px-1.5 py-0.5"
|
class="flex gap-1 items-center bg-none border-none bg-gray-50 dark:bg-black transition rounded-md px-1.5 py-0.5"
|
||||||
on:click={collapseCodeBlock}
|
on:click={collapseCodeBlock}
|
||||||
>
|
>
|
||||||
<div class=" -translate-y-[0.5px]">
|
<div class=" -translate-y-[0.5px]">
|
||||||
|
|
@ -448,7 +451,7 @@
|
||||||
|
|
||||||
{#if preview && ['html', 'svg'].includes(lang)}
|
{#if preview && ['html', 'svg'].includes(lang)}
|
||||||
<button
|
<button
|
||||||
class="flex gap-1 items-center run-code-button bg-none border-none bg-gray-50 hover:bg-gray-100 dark:bg-gray-850 dark:hover:bg-gray-800 transition rounded-md px-1.5 py-0.5"
|
class="flex gap-1 items-center run-code-button bg-none border-none bg-gray-50 dark:bg-black transition rounded-md px-1.5 py-0.5"
|
||||||
on:click={previewCode}
|
on:click={previewCode}
|
||||||
>
|
>
|
||||||
<div class=" -translate-y-[0.5px]">
|
<div class=" -translate-y-[0.5px]">
|
||||||
|
|
@ -468,7 +471,7 @@
|
||||||
</div>
|
</div>
|
||||||
{:else if run}
|
{:else if run}
|
||||||
<button
|
<button
|
||||||
class="flex gap-1 items-center run-code-button bg-none border-none bg-gray-50 hover:bg-gray-100 dark:bg-gray-850 dark:hover:bg-gray-800 transition rounded-md px-1.5 py-0.5"
|
class="flex gap-1 items-center run-code-button bg-none border-none bg-gray-50 dark:bg-black transition rounded-md px-1.5 py-0.5"
|
||||||
on:click={async () => {
|
on:click={async () => {
|
||||||
code = _code;
|
code = _code;
|
||||||
await tick();
|
await tick();
|
||||||
|
|
@ -488,7 +491,7 @@
|
||||||
|
|
||||||
{#if save}
|
{#if save}
|
||||||
<button
|
<button
|
||||||
class="save-code-button bg-none border-none bg-gray-50 hover:bg-gray-100 dark:bg-gray-850 dark:hover:bg-gray-800 transition rounded-md px-1.5 py-0.5"
|
class="save-code-button bg-none border-none bg-gray-50 dark:bg-black transition rounded-md px-1.5 py-0.5"
|
||||||
on:click={saveCode}
|
on:click={saveCode}
|
||||||
>
|
>
|
||||||
{saved ? $i18n.t('Saved') : $i18n.t('Save')}
|
{saved ? $i18n.t('Saved') : $i18n.t('Save')}
|
||||||
|
|
@ -496,22 +499,23 @@
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<button
|
<button
|
||||||
class="copy-code-button bg-none border-none bg-gray-50 hover:bg-gray-100 dark:bg-gray-850 dark:hover:bg-gray-800 transition rounded-md px-1.5 py-0.5"
|
class="copy-code-button bg-none border-none bg-gray-50 dark:bg-black transition rounded-md px-1.5 py-0.5"
|
||||||
on:click={copyCode}>{copied ? $i18n.t('Copied') : $i18n.t('Copy')}</button
|
on:click={copyCode}>{copied ? $i18n.t('Copied') : $i18n.t('Copy')}</button
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="language-{lang} rounded-t-lg -mt-8 {editorClassName
|
class="language-{lang} rounded-t-xl -mt-8 {editorClassName
|
||||||
? editorClassName
|
? editorClassName
|
||||||
: executing || stdout || stderr || result
|
: executing || stdout || stderr || result
|
||||||
? ''
|
? ''
|
||||||
: 'rounded-b-lg'} overflow-hidden"
|
: 'rounded-b-xl'} overflow-hidden"
|
||||||
>
|
>
|
||||||
<div class=" pt-7 bg-gray-50 dark:bg-gray-850"></div>
|
<div class=" pt-8 bg-gray-50 dark:bg-black"></div>
|
||||||
|
|
||||||
{#if !collapsed}
|
{#if !collapsed}
|
||||||
|
{#if edit}
|
||||||
<CodeEditor
|
<CodeEditor
|
||||||
value={code}
|
value={code}
|
||||||
{id}
|
{id}
|
||||||
|
|
@ -523,9 +527,22 @@
|
||||||
_code = value;
|
_code = value;
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
{:else}
|
||||||
|
<pre
|
||||||
|
class=" hljs p-4 px-5 overflow-x-auto"
|
||||||
|
style="border-top-left-radius: 0px; border-top-right-radius: 0px; {(executing ||
|
||||||
|
stdout ||
|
||||||
|
stderr ||
|
||||||
|
result) &&
|
||||||
|
'border-bottom-left-radius: 0px; border-bottom-right-radius: 0px;'}"><code
|
||||||
|
class="language-{lang} rounded-t-none whitespace-pre text-sm"
|
||||||
|
>{@html hljs.highlightAuto(code, hljs.getLanguage(lang)?.aliases).value ||
|
||||||
|
code}</code
|
||||||
|
></pre>
|
||||||
|
{/if}
|
||||||
{:else}
|
{:else}
|
||||||
<div
|
<div
|
||||||
class="bg-gray-50 dark:bg-black dark:text-white rounded-b-lg! pt-2 pb-2 px-4 flex flex-col gap-2 text-xs"
|
class="bg-gray-50 dark:bg-black dark:text-white rounded-b-xl! pt-2 pb-2 px-4 flex flex-col gap-2 text-xs"
|
||||||
>
|
>
|
||||||
<span class="text-gray-500 italic">
|
<span class="text-gray-500 italic">
|
||||||
{$i18n.t('{{COUNT}} hidden lines', {
|
{$i18n.t('{{COUNT}} hidden lines', {
|
||||||
|
|
@ -539,12 +556,12 @@
|
||||||
{#if !collapsed}
|
{#if !collapsed}
|
||||||
<div
|
<div
|
||||||
id="plt-canvas-{id}"
|
id="plt-canvas-{id}"
|
||||||
class="bg-gray-50 dark:bg-[#202123] dark:text-white max-w-full overflow-x-auto scrollbar-hidden"
|
class="bg-gray-50 dark:bg-black dark:text-white max-w-full overflow-x-auto scrollbar-hidden"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{#if executing || stdout || stderr || result || files}
|
{#if executing || stdout || stderr || result || files}
|
||||||
<div
|
<div
|
||||||
class="bg-gray-50 dark:bg-[#202123] dark:text-white rounded-b-lg! py-4 px-4 flex flex-col gap-2"
|
class="bg-gray-50 dark:bg-black dark:text-white rounded-b-xl! py-4 px-4 flex flex-col gap-2"
|
||||||
>
|
>
|
||||||
{#if executing}
|
{#if executing}
|
||||||
<div class=" ">
|
<div class=" ">
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,8 @@
|
||||||
export let save = false;
|
export let save = false;
|
||||||
export let preview = false;
|
export let preview = false;
|
||||||
export let floatingButtons = true;
|
export let floatingButtons = true;
|
||||||
|
|
||||||
|
export let editCodeBlock = true;
|
||||||
export let topPadding = false;
|
export let topPadding = false;
|
||||||
|
|
||||||
export let onSave = (e) => {};
|
export let onSave = (e) => {};
|
||||||
|
|
@ -138,6 +140,7 @@
|
||||||
{save}
|
{save}
|
||||||
{preview}
|
{preview}
|
||||||
{done}
|
{done}
|
||||||
|
{editCodeBlock}
|
||||||
{topPadding}
|
{topPadding}
|
||||||
sourceIds={(sources ?? []).reduce((acc, source) => {
|
sourceIds={(sources ?? []).reduce((acc, source) => {
|
||||||
let ids = [];
|
let ids = [];
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,8 @@
|
||||||
export let model = null;
|
export let model = null;
|
||||||
export let save = false;
|
export let save = false;
|
||||||
export let preview = false;
|
export let preview = false;
|
||||||
|
|
||||||
|
export let editCodeBlock = true;
|
||||||
export let topPadding = false;
|
export let topPadding = false;
|
||||||
|
|
||||||
export let sourceIds = [];
|
export let sourceIds = [];
|
||||||
|
|
@ -52,6 +54,7 @@
|
||||||
{done}
|
{done}
|
||||||
{save}
|
{save}
|
||||||
{preview}
|
{preview}
|
||||||
|
{editCodeBlock}
|
||||||
{topPadding}
|
{topPadding}
|
||||||
{onTaskClick}
|
{onTaskClick}
|
||||||
{onSourceClick}
|
{onSourceClick}
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,8 @@
|
||||||
|
|
||||||
export let save = false;
|
export let save = false;
|
||||||
export let preview = false;
|
export let preview = false;
|
||||||
|
|
||||||
|
export let editCodeBlock = true;
|
||||||
export let topPadding = false;
|
export let topPadding = false;
|
||||||
|
|
||||||
export let onSave: Function = () => {};
|
export let onSave: Function = () => {};
|
||||||
|
|
@ -106,6 +108,7 @@
|
||||||
{attributes}
|
{attributes}
|
||||||
{save}
|
{save}
|
||||||
{preview}
|
{preview}
|
||||||
|
edit={editCodeBlock}
|
||||||
stickyButtonsClassName={topPadding ? 'top-8' : 'top-0'}
|
stickyButtonsClassName={topPadding ? 'top-8' : 'top-0'}
|
||||||
onSave={(value) => {
|
onSave={(value) => {
|
||||||
onSave({
|
onSave({
|
||||||
|
|
@ -198,6 +201,7 @@
|
||||||
id={`${id}-${tokenIdx}`}
|
id={`${id}-${tokenIdx}`}
|
||||||
tokens={token.tokens}
|
tokens={token.tokens}
|
||||||
{done}
|
{done}
|
||||||
|
{editCodeBlock}
|
||||||
{onTaskClick}
|
{onTaskClick}
|
||||||
{onSourceClick}
|
{onSourceClick}
|
||||||
/>
|
/>
|
||||||
|
|
@ -231,6 +235,7 @@
|
||||||
tokens={item.tokens}
|
tokens={item.tokens}
|
||||||
top={token.loose}
|
top={token.loose}
|
||||||
{done}
|
{done}
|
||||||
|
{editCodeBlock}
|
||||||
{onTaskClick}
|
{onTaskClick}
|
||||||
{onSourceClick}
|
{onSourceClick}
|
||||||
/>
|
/>
|
||||||
|
|
@ -264,6 +269,7 @@
|
||||||
tokens={item.tokens}
|
tokens={item.tokens}
|
||||||
top={token.loose}
|
top={token.loose}
|
||||||
{done}
|
{done}
|
||||||
|
{editCodeBlock}
|
||||||
{onTaskClick}
|
{onTaskClick}
|
||||||
{onSourceClick}
|
{onSourceClick}
|
||||||
/>
|
/>
|
||||||
|
|
@ -274,6 +280,7 @@
|
||||||
tokens={item.tokens}
|
tokens={item.tokens}
|
||||||
top={token.loose}
|
top={token.loose}
|
||||||
{done}
|
{done}
|
||||||
|
{editCodeBlock}
|
||||||
{onTaskClick}
|
{onTaskClick}
|
||||||
{onSourceClick}
|
{onSourceClick}
|
||||||
/>
|
/>
|
||||||
|
|
@ -296,6 +303,7 @@
|
||||||
tokens={marked.lexer(token.text)}
|
tokens={marked.lexer(token.text)}
|
||||||
attributes={token?.attributes}
|
attributes={token?.attributes}
|
||||||
{done}
|
{done}
|
||||||
|
{editCodeBlock}
|
||||||
{onTaskClick}
|
{onTaskClick}
|
||||||
{onSourceClick}
|
{onSourceClick}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,10 @@
|
||||||
// Helper function to return only the domain from a URL
|
// Helper function to return only the domain from a URL
|
||||||
function getDomain(url: string): string {
|
function getDomain(url: string): string {
|
||||||
const domain = url.replace('http://', '').replace('https://', '').split(/[/?#]/)[0];
|
const domain = url.replace('http://', '').replace('https://', '').split(/[/?#]/)[0];
|
||||||
|
|
||||||
|
if (domain.startsWith('www.')) {
|
||||||
|
return domain.slice(4);
|
||||||
|
}
|
||||||
return domain;
|
return domain;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -33,6 +37,14 @@
|
||||||
return title;
|
return title;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getDisplayTitle = (title: string) => {
|
||||||
|
if (!title) return 'N/A';
|
||||||
|
if (title.length > 30) {
|
||||||
|
return title.slice(0, 15) + '...' + title.slice(-10);
|
||||||
|
}
|
||||||
|
return title;
|
||||||
|
};
|
||||||
|
|
||||||
$: attributes = extractAttributes(token.text);
|
$: attributes = extractAttributes(token.text);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
@ -44,7 +56,11 @@
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span class="line-clamp-1">
|
<span class="line-clamp-1">
|
||||||
{attributes.title ? formattedTitle(attributes.title) : ''}
|
{getDisplayTitle(
|
||||||
|
decodeURIComponent(attributes.title)
|
||||||
|
? formattedTitle(decodeURIComponent(attributes.title))
|
||||||
|
: ''
|
||||||
|
)}
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
||||||
|
|
@ -41,10 +41,11 @@
|
||||||
export let addMessages;
|
export let addMessages;
|
||||||
export let triggerScroll;
|
export let triggerScroll;
|
||||||
export let readOnly = false;
|
export let readOnly = false;
|
||||||
|
export let editCodeBlock = true;
|
||||||
export let topPadding = false;
|
export let topPadding = false;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<li
|
<div
|
||||||
class="flex flex-col justify-between px-5 mb-3 w-full {($settings?.widescreenMode ?? null)
|
class="flex flex-col justify-between px-5 mb-3 w-full {($settings?.widescreenMode ?? null)
|
||||||
? 'max-w-full'
|
? 'max-w-full'
|
||||||
: 'max-w-5xl'} mx-auto rounded-lg group"
|
: 'max-w-5xl'} mx-auto rounded-lg group"
|
||||||
|
|
@ -68,6 +69,7 @@
|
||||||
{editMessage}
|
{editMessage}
|
||||||
{deleteMessage}
|
{deleteMessage}
|
||||||
{readOnly}
|
{readOnly}
|
||||||
|
{editCodeBlock}
|
||||||
{topPadding}
|
{topPadding}
|
||||||
/>
|
/>
|
||||||
{:else if (history.messages[history.messages[messageId].parentId]?.models?.length ?? 1) === 1}
|
{:else if (history.messages[history.messages[messageId].parentId]?.models?.length ?? 1) === 1}
|
||||||
|
|
@ -93,6 +95,7 @@
|
||||||
{regenerateResponse}
|
{regenerateResponse}
|
||||||
{addMessages}
|
{addMessages}
|
||||||
{readOnly}
|
{readOnly}
|
||||||
|
{editCodeBlock}
|
||||||
{topPadding}
|
{topPadding}
|
||||||
/>
|
/>
|
||||||
{:else}
|
{:else}
|
||||||
|
|
@ -116,8 +119,9 @@
|
||||||
{triggerScroll}
|
{triggerScroll}
|
||||||
{addMessages}
|
{addMessages}
|
||||||
{readOnly}
|
{readOnly}
|
||||||
|
{editCodeBlock}
|
||||||
{topPadding}
|
{topPadding}
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
</li>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,7 @@
|
||||||
|
|
||||||
export let isLastMessage;
|
export let isLastMessage;
|
||||||
export let readOnly = false;
|
export let readOnly = false;
|
||||||
|
export let editCodeBlock = true;
|
||||||
|
|
||||||
export let setInputText: Function = () => {};
|
export let setInputText: Function = () => {};
|
||||||
export let updateChat: Function;
|
export let updateChat: Function;
|
||||||
|
|
@ -379,6 +380,7 @@
|
||||||
}}
|
}}
|
||||||
{addMessages}
|
{addMessages}
|
||||||
{readOnly}
|
{readOnly}
|
||||||
|
{editCodeBlock}
|
||||||
{topPadding}
|
{topPadding}
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
||||||
|
|
@ -52,6 +52,7 @@
|
||||||
import { fade } from 'svelte/transition';
|
import { fade } from 'svelte/transition';
|
||||||
import { flyAndScale } from '$lib/utils/transitions';
|
import { flyAndScale } from '$lib/utils/transitions';
|
||||||
import RegenerateMenu from './ResponseMessage/RegenerateMenu.svelte';
|
import RegenerateMenu from './ResponseMessage/RegenerateMenu.svelte';
|
||||||
|
import StatusHistory from './ResponseMessage/StatusHistory.svelte';
|
||||||
|
|
||||||
interface MessageType {
|
interface MessageType {
|
||||||
id: string;
|
id: string;
|
||||||
|
|
@ -138,6 +139,7 @@
|
||||||
|
|
||||||
export let isLastMessage = true;
|
export let isLastMessage = true;
|
||||||
export let readOnly = false;
|
export let readOnly = false;
|
||||||
|
export let editCodeBlock = true;
|
||||||
export let topPadding = false;
|
export let topPadding = false;
|
||||||
|
|
||||||
let citationsElement: HTMLDivElement;
|
let citationsElement: HTMLDivElement;
|
||||||
|
|
@ -641,76 +643,11 @@
|
||||||
<div>
|
<div>
|
||||||
<div class="chat-{message.role} w-full min-w-full markdown-prose">
|
<div class="chat-{message.role} w-full min-w-full markdown-prose">
|
||||||
<div>
|
<div>
|
||||||
{#if (message?.statusHistory ?? [...(message?.status ? [message?.status] : [])]).length > 0}
|
{#if model?.info?.meta?.capabilities?.status_updates ?? true}
|
||||||
{@const status = (
|
<StatusHistory
|
||||||
message?.statusHistory ?? [...(message?.status ? [message?.status] : [])]
|
statusHistory={message?.statusHistory}
|
||||||
).at(-1)}
|
expand={message?.content === ''}
|
||||||
{#if !status?.hidden}
|
/>
|
||||||
<div class="status-description flex items-center gap-2 py-0.5">
|
|
||||||
{#if status?.action === 'web_search' && status?.urls}
|
|
||||||
<WebSearchResults {status}>
|
|
||||||
<div class="flex flex-col justify-center -space-y-0.5">
|
|
||||||
<div
|
|
||||||
class="{status?.done === false
|
|
||||||
? 'shimmer'
|
|
||||||
: ''} text-base line-clamp-1 text-wrap"
|
|
||||||
>
|
|
||||||
<!-- $i18n.t("Generating search query") -->
|
|
||||||
<!-- $i18n.t("No search query generated") -->
|
|
||||||
|
|
||||||
<!-- $i18n.t('Searched {{count}} sites') -->
|
|
||||||
{#if status?.description.includes('{{count}}')}
|
|
||||||
{$i18n.t(status?.description, {
|
|
||||||
count: status?.urls.length
|
|
||||||
})}
|
|
||||||
{:else if status?.description === 'No search query generated'}
|
|
||||||
{$i18n.t('No search query generated')}
|
|
||||||
{:else if status?.description === 'Generating search query'}
|
|
||||||
{$i18n.t('Generating search query')}
|
|
||||||
{:else}
|
|
||||||
{status?.description}
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</WebSearchResults>
|
|
||||||
{:else if status?.action === 'knowledge_search'}
|
|
||||||
<div class="flex flex-col justify-center -space-y-0.5">
|
|
||||||
<div
|
|
||||||
class="{status?.done === false
|
|
||||||
? 'shimmer'
|
|
||||||
: ''} text-gray-500 dark:text-gray-500 text-base line-clamp-1 text-wrap"
|
|
||||||
>
|
|
||||||
{$i18n.t(`Searching Knowledge for "{{searchQuery}}"`, {
|
|
||||||
searchQuery: status.query
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{:else}
|
|
||||||
<div class="flex flex-col justify-center -space-y-0.5">
|
|
||||||
<div
|
|
||||||
class="{status?.done === false
|
|
||||||
? 'shimmer'
|
|
||||||
: ''} text-gray-500 dark:text-gray-500 text-base line-clamp-1 text-wrap"
|
|
||||||
>
|
|
||||||
<!-- $i18n.t(`Searching "{{searchQuery}}"`) -->
|
|
||||||
{#if status?.description.includes('{{searchQuery}}')}
|
|
||||||
{$i18n.t(status?.description, {
|
|
||||||
searchQuery: status?.query
|
|
||||||
})}
|
|
||||||
{:else if status?.description === 'No search query generated'}
|
|
||||||
{$i18n.t('No search query generated')}
|
|
||||||
{:else if status?.description === 'Generating search query'}
|
|
||||||
{$i18n.t('Generating search query')}
|
|
||||||
{:else if status?.description === 'Searching the web'}
|
|
||||||
{$i18n.t('Searching the web...')}
|
|
||||||
{:else}
|
|
||||||
{status?.description}
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if message?.files && message.files?.filter((f) => f.type === 'image').length > 0}
|
{#if message?.files && message.files?.filter((f) => f.type === 'image').length > 0}
|
||||||
|
|
@ -797,7 +734,7 @@
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<div class="w-full flex flex-col relative" id="response-content-container">
|
<div class="w-full flex flex-col relative" id="response-content-container">
|
||||||
{#if message.content === '' && !message.error && (message?.statusHistory ?? [...(message?.status ? [message?.status] : [])]).length === 0}
|
{#if message.content === '' && !message.error && ((model?.info?.meta?.capabilities?.status_updates ?? true) ? (message?.statusHistory ?? [...(message?.status ? [message?.status] : [])]).length === 0 || (message?.statusHistory?.at(-1)?.hidden ?? false) : true)}
|
||||||
<Skeleton />
|
<Skeleton />
|
||||||
{:else if message.content && message.error !== true}
|
{:else if message.content && message.error !== true}
|
||||||
<!-- always show message contents even if there's an error -->
|
<!-- always show message contents even if there's an error -->
|
||||||
|
|
@ -814,6 +751,7 @@
|
||||||
($settings?.showFloatingActionButtons ?? true)}
|
($settings?.showFloatingActionButtons ?? true)}
|
||||||
save={!readOnly}
|
save={!readOnly}
|
||||||
preview={!readOnly}
|
preview={!readOnly}
|
||||||
|
{editCodeBlock}
|
||||||
{topPadding}
|
{topPadding}
|
||||||
done={($settings?.chatFadeStreamingText ?? true)
|
done={($settings?.chatFadeStreamingText ?? true)
|
||||||
? (message?.done ?? false)
|
? (message?.done ?? false)
|
||||||
|
|
@ -1222,7 +1160,7 @@
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if !readOnly}
|
{#if !readOnly}
|
||||||
{#if !$temporaryChatEnabled && ($config?.features.enable_message_rating ?? true)}
|
{#if !$temporaryChatEnabled && ($config?.features.enable_message_rating ?? true) && ($user?.role === 'admin' || ($user?.permissions?.chat?.rate_response ?? true))}
|
||||||
<Tooltip content={$i18n.t('Good Response')} placement="bottom">
|
<Tooltip content={$i18n.t('Good Response')} placement="bottom">
|
||||||
<button
|
<button
|
||||||
aria-label={$i18n.t('Good Response')}
|
aria-label={$i18n.t('Good Response')}
|
||||||
|
|
@ -1300,7 +1238,7 @@
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if isLastMessage}
|
{#if isLastMessage && ($user?.role === 'admin' || ($user?.permissions?.chat?.continue_response ?? true))}
|
||||||
<Tooltip content={$i18n.t('Continue Response')} placement="bottom">
|
<Tooltip content={$i18n.t('Continue Response')} placement="bottom">
|
||||||
<button
|
<button
|
||||||
aria-label={$i18n.t('Continue Response')}
|
aria-label={$i18n.t('Continue Response')}
|
||||||
|
|
@ -1337,6 +1275,7 @@
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
{#if $user?.role === 'admin' || ($user?.permissions?.chat?.regenerate_response ?? true)}
|
||||||
{#if $settings?.regenerateMenu ?? true}
|
{#if $settings?.regenerateMenu ?? true}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
|
@ -1445,7 +1384,9 @@
|
||||||
</button>
|
</button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
{/if}
|
{/if}
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if $user?.role === 'admin' || ($user?.permissions?.chat?.delete_message ?? true)}
|
||||||
{#if siblings.length > 1}
|
{#if siblings.length > 1}
|
||||||
<Tooltip content={$i18n.t('Delete')} placement="bottom">
|
<Tooltip content={$i18n.t('Delete')} placement="bottom">
|
||||||
<button
|
<button
|
||||||
|
|
@ -1477,6 +1418,7 @@
|
||||||
</button>
|
</button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
{/if}
|
{/if}
|
||||||
|
{/if}
|
||||||
|
|
||||||
{#if isLastMessage}
|
{#if isLastMessage}
|
||||||
{#each model?.actions ?? [] as action}
|
{#each model?.actions ?? [] as action}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
||||||
import ArrowTurnDownRight from '$lib/components/icons/ArrowTurnDownRight.svelte';
|
|
||||||
import { onMount, tick, getContext } from 'svelte';
|
import { onMount, tick, getContext } from 'svelte';
|
||||||
|
|
||||||
const i18n = getContext('i18n');
|
const i18n = getContext('i18n');
|
||||||
|
|
@ -20,13 +19,10 @@
|
||||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||||
<Tooltip content={followUp} placement="top-start" className="line-clamp-1">
|
<Tooltip content={followUp} placement="top-start" className="line-clamp-1">
|
||||||
<div
|
<div
|
||||||
class=" mr-2 py-1.5 bg-transparent text-left text-sm flex items-center gap-2 px-1.5 text-gray-500 dark:text-gray-400 hover:text-black dark:hover:text-white transition cursor-pointer"
|
class=" py-1.5 bg-transparent text-left text-sm flex items-center gap-2 text-gray-500 dark:text-gray-400 hover:text-black dark:hover:text-white transition cursor-pointer"
|
||||||
on:click={() => onClick(followUp)}
|
on:click={() => onClick(followUp)}
|
||||||
title={followUp}
|
|
||||||
aria-label={followUp}
|
aria-label={followUp}
|
||||||
>
|
>
|
||||||
<ArrowTurnDownRight className="size-3.5" />
|
|
||||||
|
|
||||||
<div class="line-clamp-1">
|
<div class="line-clamp-1">
|
||||||
{followUp}
|
{followUp}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -34,7 +30,7 @@
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
{#if idx < followUps.length - 1}
|
{#if idx < followUps.length - 1}
|
||||||
<hr class="border-gray-100 dark:border-gray-850" />
|
<hr class="border-gray-50 dark:border-gray-850" />
|
||||||
{/if}
|
{/if}
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,80 @@
|
||||||
|
<script>
|
||||||
|
import { getContext } from 'svelte';
|
||||||
|
const i18n = getContext('i18n');
|
||||||
|
|
||||||
|
import StatusItem from './StatusHistory/StatusItem.svelte';
|
||||||
|
export let statusHistory = [];
|
||||||
|
export let expand = false;
|
||||||
|
|
||||||
|
let showHistory = true;
|
||||||
|
|
||||||
|
$: if (expand) {
|
||||||
|
showHistory = true;
|
||||||
|
} else {
|
||||||
|
showHistory = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
let history = [];
|
||||||
|
let status = null;
|
||||||
|
|
||||||
|
$: if (history && history.length > 0) {
|
||||||
|
status = history.at(-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
$: if (JSON.stringify(statusHistory) !== JSON.stringify(history)) {
|
||||||
|
history = statusHistory;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if history && history.length > 0}
|
||||||
|
{#if status?.hidden !== true}
|
||||||
|
<div class="text-sm flex flex-col w-full">
|
||||||
|
{#if showHistory}
|
||||||
|
<div class="flex flex-row">
|
||||||
|
{#if history.length > 1}
|
||||||
|
<div class="w-1 border-r border-gray-50 dark:border-gray-800 mt-3 -mb-2.5" />
|
||||||
|
|
||||||
|
<div class="w-full -translate-x-[7.5px]">
|
||||||
|
{#each history as status, idx}
|
||||||
|
{#if idx !== history.length - 1}
|
||||||
|
<div class="flex items-start gap-2 mb-1">
|
||||||
|
<div class="pt-3 px-1">
|
||||||
|
<span class="relative flex size-2">
|
||||||
|
<span
|
||||||
|
class="relative inline-flex size-1.5 rounded-full bg-gray-200 dark:bg-gray-700"
|
||||||
|
></span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<StatusItem {status} done={true} />
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<button
|
||||||
|
class="w-full -translate-x-[3.5px]"
|
||||||
|
on:click={() => {
|
||||||
|
showHistory = !showHistory;
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div class="flex items-start gap-2">
|
||||||
|
<div class="pt-3 px-1">
|
||||||
|
<span class="relative flex size-2">
|
||||||
|
{#if status?.done === false}
|
||||||
|
<span
|
||||||
|
class="absolute inline-flex h-full w-full animate-ping rounded-full bg-gray-400 dark:bg-gray-700 opacity-75"
|
||||||
|
></span>
|
||||||
|
{/if}
|
||||||
|
<span class="relative inline-flex size-1.5 rounded-full bg-gray-200 dark:bg-gray-700"
|
||||||
|
></span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<StatusItem {status} />
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{/if}
|
||||||
|
|
@ -0,0 +1,150 @@
|
||||||
|
<script>
|
||||||
|
import { getContext } from 'svelte';
|
||||||
|
const i18n = getContext('i18n');
|
||||||
|
import WebSearchResults from '../WebSearchResults.svelte';
|
||||||
|
import Search from '$lib/components/icons/Search.svelte';
|
||||||
|
import { t } from 'i18next';
|
||||||
|
|
||||||
|
export let status = null;
|
||||||
|
export let done = false;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if !status?.hidden}
|
||||||
|
<div class="status-description flex items-center gap-2 py-0.5 w-full text-left">
|
||||||
|
{#if status?.action === 'web_search' && (status?.urls || status?.items)}
|
||||||
|
<WebSearchResults {status}>
|
||||||
|
<div class="flex flex-col justify-center -space-y-0.5">
|
||||||
|
<div
|
||||||
|
class="{(done || status?.done) === false
|
||||||
|
? 'shimmer'
|
||||||
|
: ''} text-base line-clamp-1 text-wrap"
|
||||||
|
>
|
||||||
|
<!-- $i18n.t("Generating search query") -->
|
||||||
|
<!-- $i18n.t("No search query generated") -->
|
||||||
|
<!-- $i18n.t('Searched {{count}} sites') -->
|
||||||
|
{#if status?.description.includes('{{count}}')}
|
||||||
|
{$i18n.t(status?.description, {
|
||||||
|
count: (status?.urls || status?.items).length
|
||||||
|
})}
|
||||||
|
{:else if status?.description === 'No search query generated'}
|
||||||
|
{$i18n.t('No search query generated')}
|
||||||
|
{:else if status?.description === 'Generating search query'}
|
||||||
|
{$i18n.t('Generating search query')}
|
||||||
|
{:else}
|
||||||
|
{status?.description}
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</WebSearchResults>
|
||||||
|
{:else if status?.action === 'knowledge_search'}
|
||||||
|
<div class="flex flex-col justify-center -space-y-0.5">
|
||||||
|
<div
|
||||||
|
class="{(done || status?.done) === false
|
||||||
|
? 'shimmer'
|
||||||
|
: ''} text-gray-500 dark:text-gray-500 text-base line-clamp-1 text-wrap"
|
||||||
|
>
|
||||||
|
{$i18n.t(`Searching Knowledge for "{{searchQuery}}"`, {
|
||||||
|
searchQuery: status.query
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{:else if status?.action === 'web_search_queries_generated' && status?.queries}
|
||||||
|
<div class="flex flex-col justify-center -space-y-0.5">
|
||||||
|
<div
|
||||||
|
class="{(done || status?.done) === false
|
||||||
|
? 'shimmer'
|
||||||
|
: ''} text-gray-500 dark:text-gray-500 text-base line-clamp-1 text-wrap"
|
||||||
|
>
|
||||||
|
{$i18n.t(`Searching`)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class=" flex gap-1 flex-wrap mt-2">
|
||||||
|
{#each status.queries as query, idx (query)}
|
||||||
|
<div
|
||||||
|
class="bg-gray-50 dark:bg-gray-850 flex rounded-lg py-1 px-2 items-center gap-1 text-xs"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<Search className="size-3" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<span class="line-clamp-1">
|
||||||
|
{query}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{:else if status?.action === 'queries_generated' && status?.queries}
|
||||||
|
<div class="flex flex-col justify-center -space-y-0.5">
|
||||||
|
<div
|
||||||
|
class="{(done || status?.done) === false
|
||||||
|
? 'shimmer'
|
||||||
|
: ''} text-gray-500 dark:text-gray-500 text-base line-clamp-1 text-wrap"
|
||||||
|
>
|
||||||
|
{$i18n.t(`Querying`)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class=" flex gap-1 flex-wrap mt-2">
|
||||||
|
{#each status.queries as query, idx (query)}
|
||||||
|
<div
|
||||||
|
class="bg-gray-50 dark:bg-gray-850 flex rounded-lg py-1 px-2 items-center gap-1 text-xs"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<Search className="size-3" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<span class="line-clamp-1">
|
||||||
|
{query}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{:else if status?.action === 'sources_retrieved' && status?.count !== undefined}
|
||||||
|
<div class="flex flex-col justify-center -space-y-0.5">
|
||||||
|
<div
|
||||||
|
class="{(done || status?.done) === false
|
||||||
|
? 'shimmer'
|
||||||
|
: ''} text-gray-500 dark:text-gray-500 text-base line-clamp-1 text-wrap"
|
||||||
|
>
|
||||||
|
{#if status.count === 0}
|
||||||
|
{$i18n.t('No sources found')}
|
||||||
|
{:else if status.count === 1}
|
||||||
|
{$i18n.t('Retrieved 1 source')}
|
||||||
|
{:else}
|
||||||
|
<!-- {$i18n.t('Source')} -->
|
||||||
|
<!-- {$i18n.t('No source available')} -->
|
||||||
|
<!-- {$i18n.t('No distance available')} -->
|
||||||
|
<!-- {$i18n.t('Retrieved {{count}} sources')} -->
|
||||||
|
{$i18n.t('Retrieved {{count}} sources', {
|
||||||
|
count: status.count
|
||||||
|
})}
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<div class="flex flex-col justify-center -space-y-0.5">
|
||||||
|
<div
|
||||||
|
class="{(done || status?.done) === false
|
||||||
|
? 'shimmer'
|
||||||
|
: ''} text-gray-500 dark:text-gray-500 text-base line-clamp-1 text-wrap"
|
||||||
|
>
|
||||||
|
<!-- $i18n.t(`Searching "{{searchQuery}}"`) -->
|
||||||
|
{#if status?.description?.includes('{{searchQuery}}')}
|
||||||
|
{$i18n.t(status?.description, {
|
||||||
|
searchQuery: status?.query
|
||||||
|
})}
|
||||||
|
{:else if status?.description === 'No search query generated'}
|
||||||
|
{$i18n.t('No search query generated')}
|
||||||
|
{:else if status?.description === 'Generating search query'}
|
||||||
|
{$i18n.t('Generating search query')}
|
||||||
|
{:else if status?.description === 'Searching the web'}
|
||||||
|
{$i18n.t('Searching the web')}
|
||||||
|
{:else}
|
||||||
|
{status?.description}
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
@ -8,27 +8,25 @@
|
||||||
let state = false;
|
let state = false;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Collapsible bind:open={state} className="w-full space-y-1">
|
<Collapsible grow={true} className="w-full" buttonClassName="w-full" bind:open={state}>
|
||||||
<div
|
<div class="flex items-center gap-2 text-gray-500 transition">
|
||||||
class="flex items-center gap-2 text-gray-500 hover:text-gray-700 dark:hover:text-gray-300 transition"
|
|
||||||
>
|
|
||||||
<slot />
|
<slot />
|
||||||
|
|
||||||
{#if state}
|
{#if state}
|
||||||
<ChevronUp strokeWidth="3.5" className="size-3.5 " />
|
<ChevronUp strokeWidth="2.5" className="size-3.5 " />
|
||||||
{:else}
|
{:else}
|
||||||
<ChevronDown strokeWidth="3.5" className="size-3.5 " />
|
<ChevronDown strokeWidth="2.5" className="size-3.5 " />
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="text-sm border border-gray-300/30 dark:border-gray-700/50 rounded-xl mb-1.5"
|
class="text-sm border border-gray-50 dark:border-gray-850 rounded-xl my-1.5 p-2 w-full"
|
||||||
slot="content"
|
slot="content"
|
||||||
>
|
>
|
||||||
{#if status?.query}
|
{#if status?.query}
|
||||||
<a
|
<a
|
||||||
href="https://www.google.com/search?q={status.query}"
|
href="https://www.google.com/search?q={status.query}"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
class="flex w-full items-center p-3 px-4 border-b border-gray-300/30 dark:border-gray-700/50 group/item justify-between font-normal text-gray-800 dark:text-gray-300 no-underline"
|
class="flex w-full items-center p-1 px-3 group/item justify-between text-gray-800 dark:text-gray-300 font-normal! no-underline!"
|
||||||
>
|
>
|
||||||
<div class="flex gap-2 items-center">
|
<div class="flex gap-2 items-center">
|
||||||
<Search />
|
<Search />
|
||||||
|
|
@ -58,16 +56,25 @@
|
||||||
</a>
|
</a>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#each status.urls as url, urlIdx}
|
{#if status?.items}
|
||||||
|
{#each status.items as item, itemIdx}
|
||||||
<a
|
<a
|
||||||
href={url}
|
href={item.link}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
class="flex w-full items-center p-3 px-4 {urlIdx === status.urls.length - 1
|
class="flex w-full items-center p-1 px-3 group/item justify-between text-gray-800 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-850 rounded-lg font-normal! no-underline! mb-1"
|
||||||
? ''
|
|
||||||
: 'border-b border-gray-300/30 dark:border-gray-700/50'} group/item justify-between font-normal text-gray-800 dark:text-gray-300"
|
|
||||||
>
|
>
|
||||||
<div class=" line-clamp-1">
|
<div class=" flex justify-center items-center gap-3">
|
||||||
{url}
|
<div class="w-fit">
|
||||||
|
<img
|
||||||
|
src="https://www.google.com/s2/favicons?sz=32&domain={item.link}"
|
||||||
|
alt="favicon"
|
||||||
|
class="size-3.5"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="w-full text-sm line-clamp-1">
|
||||||
|
{item?.title ?? item.link}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
|
|
@ -89,5 +96,46 @@
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
{/each}
|
{/each}
|
||||||
|
{:else if status?.urls}
|
||||||
|
{#each status.urls as url, urlIdx}
|
||||||
|
<a
|
||||||
|
href={url}
|
||||||
|
target="_blank"
|
||||||
|
class="flex w-full items-center p-1 px-3 group/item justify-between text-gray-800 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-850 rounded-lg no-underline mb-1"
|
||||||
|
>
|
||||||
|
<div class=" flex justify-center items-center gap-3">
|
||||||
|
<div class="w-fit">
|
||||||
|
<img
|
||||||
|
src="https://www.google.com/s2/favicons?sz=32&domain={url}"
|
||||||
|
alt="favicon"
|
||||||
|
class="size-3.5"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="w-full text-sm line-clamp-1">
|
||||||
|
{url}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class=" ml-1 text-white dark:text-gray-900 group-hover/item:text-gray-600 dark:group-hover/item:text-white transition"
|
||||||
|
>
|
||||||
|
<!-- -->
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 16 16"
|
||||||
|
fill="currentColor"
|
||||||
|
class="size-4"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fill-rule="evenodd"
|
||||||
|
d="M4.22 11.78a.75.75 0 0 1 0-1.06L9.44 5.5H5.75a.75.75 0 0 1 0-1.5h5.5a.75.75 0 0 1 .75.75v5.5a.75.75 0 0 1-1.5 0V6.56l-5.22 5.22a.75.75 0 0 1-1.06 0Z"
|
||||||
|
clip-rule="evenodd"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
{/each}
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</Collapsible>
|
</Collapsible>
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,7 @@
|
||||||
|
|
||||||
export let isFirstMessage: boolean;
|
export let isFirstMessage: boolean;
|
||||||
export let readOnly: boolean;
|
export let readOnly: boolean;
|
||||||
|
export let editCodeBlock = true;
|
||||||
export let topPadding = false;
|
export let topPadding = false;
|
||||||
|
|
||||||
let showDeleteConfirm = false;
|
let showDeleteConfirm = false;
|
||||||
|
|
@ -326,13 +327,18 @@
|
||||||
<div class="flex {($settings?.chatBubble ?? true) ? 'justify-end pb-1' : 'w-full'}">
|
<div class="flex {($settings?.chatBubble ?? true) ? 'justify-end pb-1' : 'w-full'}">
|
||||||
<div
|
<div
|
||||||
class="rounded-3xl {($settings?.chatBubble ?? true)
|
class="rounded-3xl {($settings?.chatBubble ?? true)
|
||||||
? `max-w-[90%] px-5 py-2 bg-gray-50 dark:bg-gray-850 ${
|
? `max-w-[90%] px-4 py-1.5 bg-gray-50 dark:bg-gray-850 ${
|
||||||
message.files ? 'rounded-tr-lg' : ''
|
message.files ? 'rounded-tr-lg' : ''
|
||||||
}`
|
}`
|
||||||
: ' w-full'}"
|
: ' w-full'}"
|
||||||
>
|
>
|
||||||
{#if message.content}
|
{#if message.content}
|
||||||
<Markdown id={`${chatId}-${message.id}`} content={message.content} {topPadding} />
|
<Markdown
|
||||||
|
id={`${chatId}-${message.id}`}
|
||||||
|
content={message.content}
|
||||||
|
{editCodeBlock}
|
||||||
|
{topPadding}
|
||||||
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -495,6 +501,7 @@
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
{#if $_user?.role === 'admin' || ($_user?.permissions?.chat?.delete_message ?? false)}
|
||||||
{#if !readOnly && (!isFirstMessage || siblings.length > 1)}
|
{#if !readOnly && (!isFirstMessage || siblings.length > 1)}
|
||||||
<Tooltip content={$i18n.t('Delete')} placement="bottom">
|
<Tooltip content={$i18n.t('Delete')} placement="bottom">
|
||||||
<button
|
<button
|
||||||
|
|
@ -522,6 +529,7 @@
|
||||||
</button>
|
</button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
{/if}
|
{/if}
|
||||||
|
{/if}
|
||||||
|
|
||||||
{#if $settings?.chatBubble ?? true}
|
{#if $settings?.chatBubble ?? true}
|
||||||
{#if siblings.length > 1}
|
{#if siblings.length > 1}
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,8 @@
|
||||||
import FolderMenu from '$lib/components/layout/Sidebar/Folders/FolderMenu.svelte';
|
import FolderMenu from '$lib/components/layout/Sidebar/Folders/FolderMenu.svelte';
|
||||||
import EllipsisHorizontal from '$lib/components/icons/EllipsisHorizontal.svelte';
|
import EllipsisHorizontal from '$lib/components/icons/EllipsisHorizontal.svelte';
|
||||||
import DeleteConfirmDialog from '$lib/components/common/ConfirmDialog.svelte';
|
import DeleteConfirmDialog from '$lib/components/common/ConfirmDialog.svelte';
|
||||||
|
import Emoji from '$lib/components/common/Emoji.svelte';
|
||||||
|
import EmojiPicker from '$lib/components/common/EmojiPicker.svelte';
|
||||||
|
|
||||||
export let folder = null;
|
export let folder = null;
|
||||||
|
|
||||||
|
|
@ -63,6 +65,25 @@
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const updateIconHandler = async (iconName) => {
|
||||||
|
const res = await updateFolderById(localStorage.token, folder.id, {
|
||||||
|
meta: {
|
||||||
|
icon: iconName
|
||||||
|
}
|
||||||
|
}).catch((error) => {
|
||||||
|
toast.error(`${error}`);
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res) {
|
||||||
|
folder.meta = { ...folder.meta, icon: iconName };
|
||||||
|
|
||||||
|
toast.success($i18n.t('Folder updated successfully'));
|
||||||
|
selectedFolder.set(folder);
|
||||||
|
onUpdate(folder);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const deleteHandler = async () => {
|
const deleteHandler = async () => {
|
||||||
const res = await deleteFolderById(localStorage.token, folder.id).catch((error) => {
|
const res = await deleteFolderById(localStorage.token, folder.id).catch((error) => {
|
||||||
toast.error(`${error}`);
|
toast.error(`${error}`);
|
||||||
|
|
@ -116,9 +137,23 @@
|
||||||
|
|
||||||
<div class="mb-3 px-6 @md:max-w-3xl justify-between w-full flex relative group items-center">
|
<div class="mb-3 px-6 @md:max-w-3xl justify-between w-full flex relative group items-center">
|
||||||
<div class="text-center flex gap-3.5 items-center">
|
<div class="text-center flex gap-3.5 items-center">
|
||||||
<div class=" rounded-full bg-gray-50 dark:bg-gray-800 p-3 w-fit">
|
<EmojiPicker
|
||||||
|
onClose={() => {}}
|
||||||
|
onSubmit={(name) => {
|
||||||
|
console.log(name);
|
||||||
|
updateIconHandler(name);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class=" rounded-full bg-gray-50 dark:bg-gray-800 size-11 flex justify-center items-center"
|
||||||
|
>
|
||||||
|
{#if folder?.meta?.icon}
|
||||||
|
<Emoji className="size-6" shortCode={folder.meta.icon} />
|
||||||
|
{:else}
|
||||||
<Folder className="size-4.5" strokeWidth="2" />
|
<Folder className="size-4.5" strokeWidth="2" />
|
||||||
</div>
|
{/if}
|
||||||
|
</button>
|
||||||
|
</EmojiPicker>
|
||||||
|
|
||||||
<div class="text-3xl">
|
<div class="text-3xl">
|
||||||
{folder.name}
|
{folder.name}
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@
|
||||||
stream_response: null, // Set stream responses for this model individually
|
stream_response: null, // Set stream responses for this model individually
|
||||||
stream_delta_chunk_size: null, // Set the chunk size for streaming responses
|
stream_delta_chunk_size: null, // Set the chunk size for streaming responses
|
||||||
function_calling: null,
|
function_calling: null,
|
||||||
|
reasoning_tags: null,
|
||||||
seed: null,
|
seed: null,
|
||||||
stop: null,
|
stop: null,
|
||||||
temperature: null,
|
temperature: null,
|
||||||
|
|
@ -175,6 +176,71 @@
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class=" py-0.5 w-full justify-between">
|
||||||
|
<Tooltip
|
||||||
|
content={$i18n.t(
|
||||||
|
'Enable, disable, or customize the reasoning tags used by the model. "Enabled" uses default tags, "Disabled" turns off reasoning tags, and "Custom" lets you specify your own start and end tags.'
|
||||||
|
)}
|
||||||
|
placement="top-start"
|
||||||
|
className="inline-tooltip"
|
||||||
|
>
|
||||||
|
<div class="flex w-full justify-between">
|
||||||
|
<div class=" self-center text-xs font-medium">
|
||||||
|
{$i18n.t('Reasoning Tags')}
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
class="p-1 px-3 text-xs flex rounded-sm transition shrink-0 outline-hidden"
|
||||||
|
type="button"
|
||||||
|
on:click={() => {
|
||||||
|
if ((params?.reasoning_tags ?? null) === null) {
|
||||||
|
params.reasoning_tags = ['', ''];
|
||||||
|
} else if ((params?.reasoning_tags ?? []).length === 2) {
|
||||||
|
params.reasoning_tags = true;
|
||||||
|
} else if ((params?.reasoning_tags ?? null) !== false) {
|
||||||
|
params.reasoning_tags = false;
|
||||||
|
} else {
|
||||||
|
params.reasoning_tags = null;
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{#if (params?.reasoning_tags ?? null) === null}
|
||||||
|
<span class="ml-2 self-center"> {$i18n.t('Default')} </span>
|
||||||
|
{:else if (params?.reasoning_tags ?? null) === true}
|
||||||
|
<span class="ml-2 self-center"> {$i18n.t('Enabled')} </span>
|
||||||
|
{:else if (params?.reasoning_tags ?? null) === false}
|
||||||
|
<span class="ml-2 self-center"> {$i18n.t('Disabled')} </span>
|
||||||
|
{:else}
|
||||||
|
<span class="ml-2 self-center"> {$i18n.t('Custom')} </span>
|
||||||
|
{/if}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
|
{#if ![true, false, null].includes(params?.reasoning_tags ?? null) && (params?.reasoning_tags ?? []).length === 2}
|
||||||
|
<div class="flex mt-0.5 space-x-2">
|
||||||
|
<div class=" flex-1">
|
||||||
|
<input
|
||||||
|
class="text-sm w-full bg-transparent outline-hidden outline-none"
|
||||||
|
type="text"
|
||||||
|
placeholder={$i18n.t('Start Tag')}
|
||||||
|
bind:value={params.reasoning_tags[0]}
|
||||||
|
autocomplete="off"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class=" flex-1">
|
||||||
|
<input
|
||||||
|
class="text-sm w-full bg-transparent outline-hidden outline-none"
|
||||||
|
type="text"
|
||||||
|
placeholder={$i18n.t('End Tag')}
|
||||||
|
bind:value={params.reasoning_tags[1]}
|
||||||
|
autocomplete="off"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class=" py-0.5 w-full justify-between">
|
<div class=" py-0.5 w-full justify-between">
|
||||||
<Tooltip
|
<Tooltip
|
||||||
content={$i18n.t(
|
content={$i18n.t(
|
||||||
|
|
|
||||||
|
|
@ -72,12 +72,6 @@
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<SensitiveInput
|
|
||||||
inputClassName="bg-transparent w-full"
|
|
||||||
placeholder={$i18n.t('API Key')}
|
|
||||||
bind:value={key}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue