github-actions[bot] commited on
Commit
011d62e
0 Parent(s):

GitHub deploy: 7870749ff31793907abbe2bf74eb3b5ee7640bd2

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .dockerignore +19 -0
  2. .env.example +13 -0
  3. .eslintignore +13 -0
  4. .eslintrc.cjs +31 -0
  5. .gitattributes +2 -0
  6. .github/FUNDING.yml +1 -0
  7. .github/ISSUE_TEMPLATE/bug_report.md +66 -0
  8. .github/ISSUE_TEMPLATE/feature_request.md +19 -0
  9. .github/dependabot.yml +12 -0
  10. .github/pull_request_template.md +72 -0
  11. .github/workflows/build-release.yml +72 -0
  12. .github/workflows/deploy-to-hf-spaces.yml +61 -0
  13. .github/workflows/docker-build.yaml +477 -0
  14. .github/workflows/format-backend.yaml +39 -0
  15. .github/workflows/format-build-frontend.yaml +57 -0
  16. .github/workflows/integration-test.yml +250 -0
  17. .github/workflows/lint-backend.disabled +27 -0
  18. .github/workflows/lint-frontend.disabled +21 -0
  19. .github/workflows/release-pypi.yml +32 -0
  20. .gitignore +309 -0
  21. .npmrc +1 -0
  22. .prettierignore +316 -0
  23. .prettierrc +9 -0
  24. CHANGELOG.md +1085 -0
  25. CODE_OF_CONDUCT.md +77 -0
  26. Caddyfile.localhost +64 -0
  27. Dockerfile +166 -0
  28. INSTALLATION.md +35 -0
  29. LICENSE +21 -0
  30. Makefile +33 -0
  31. README.md +230 -0
  32. TROUBLESHOOTING.md +36 -0
  33. backend/.dockerignore +14 -0
  34. backend/.gitignore +12 -0
  35. backend/dev.sh +2 -0
  36. backend/open_webui/__init__.py +77 -0
  37. backend/open_webui/alembic.ini +114 -0
  38. backend/open_webui/apps/audio/main.py +583 -0
  39. backend/open_webui/apps/images/main.py +597 -0
  40. backend/open_webui/apps/images/utils/comfyui.py +174 -0
  41. backend/open_webui/apps/ollama/main.py +1135 -0
  42. backend/open_webui/apps/openai/main.py +546 -0
  43. backend/open_webui/apps/rag/main.py +1577 -0
  44. backend/open_webui/apps/rag/search/brave.py +42 -0
  45. backend/open_webui/apps/rag/search/duckduckgo.py +50 -0
  46. backend/open_webui/apps/rag/search/google_pse.py +50 -0
  47. backend/open_webui/apps/rag/search/jina_search.py +41 -0
  48. backend/open_webui/apps/rag/search/main.py +22 -0
  49. backend/open_webui/apps/rag/search/searchapi.py +48 -0
  50. backend/open_webui/apps/rag/search/searxng.py +91 -0
.dockerignore ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .github
2
+ .DS_Store
3
+ docs
4
+ kubernetes
5
+ node_modules
6
+ /.svelte-kit
7
+ /package
8
+ .env
9
+ .env.*
10
+ vite.config.js.timestamp-*
11
+ vite.config.ts.timestamp-*
12
+ __pycache__
13
+ .idea
14
+ venv
15
+ _old
16
+ uploads
17
+ .ipynb_checkpoints
18
+ **/*.db
19
+ _test
.env.example ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Ollama URL for the backend to connect
2
+ # The path '/ollama' will be redirected to the specified backend URL
3
+ OLLAMA_BASE_URL='http://localhost:11434'
4
+
5
+ OPENAI_API_BASE_URL=''
6
+ OPENAI_API_KEY=''
7
+
8
+ # AUTOMATIC1111_BASE_URL="http://localhost:7860"
9
+
10
+ # DO NOT TRACK
11
+ SCARF_NO_ANALYTICS=true
12
+ DO_NOT_TRACK=true
13
+ ANONYMIZED_TELEMETRY=false
.eslintignore ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .DS_Store
2
+ node_modules
3
+ /build
4
+ /.svelte-kit
5
+ /package
6
+ .env
7
+ .env.*
8
+ !.env.example
9
+
10
+ # Ignore files for PNPM, NPM and YARN
11
+ pnpm-lock.yaml
12
+ package-lock.json
13
+ yarn.lock
.eslintrc.cjs ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ module.exports = {
2
+ root: true,
3
+ extends: [
4
+ 'eslint:recommended',
5
+ 'plugin:@typescript-eslint/recommended',
6
+ 'plugin:svelte/recommended',
7
+ 'plugin:cypress/recommended',
8
+ 'prettier'
9
+ ],
10
+ parser: '@typescript-eslint/parser',
11
+ plugins: ['@typescript-eslint'],
12
+ parserOptions: {
13
+ sourceType: 'module',
14
+ ecmaVersion: 2020,
15
+ extraFileExtensions: ['.svelte']
16
+ },
17
+ env: {
18
+ browser: true,
19
+ es2017: true,
20
+ node: true
21
+ },
22
+ overrides: [
23
+ {
24
+ files: ['*.svelte'],
25
+ parser: 'svelte-eslint-parser',
26
+ parserOptions: {
27
+ parser: '@typescript-eslint/parser'
28
+ }
29
+ }
30
+ ]
31
+ };
.gitattributes ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ *.sh text eol=lf
2
+ *.ttf filter=lfs diff=lfs merge=lfs -text
.github/FUNDING.yml ADDED
@@ -0,0 +1 @@
 
 
1
+ github: tjbck
.github/ISSUE_TEMPLATE/bug_report.md ADDED
@@ -0,0 +1,66 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ name: Bug report
3
+ about: Create a report to help us improve
4
+ title: ''
5
+ labels: ''
6
+ assignees: ''
7
+ ---
8
+
9
+ # Bug Report
10
+
11
+ ## Installation Method
12
+
13
+ [Describe the method you used to install the project, e.g., git clone, Docker, pip, etc.]
14
+
15
+ ## Environment
16
+
17
+ - **Open WebUI Version:** [e.g., v0.3.11]
18
+ - **Ollama (if applicable):** [e.g., v0.2.0, v0.1.32-rc1]
19
+
20
+ - **Operating System:** [e.g., Windows 10, macOS Big Sur, Ubuntu 20.04]
21
+ - **Browser (if applicable):** [e.g., Chrome 100.0, Firefox 98.0]
22
+
23
+ **Confirmation:**
24
+
25
+ - [ ] I have read and followed all the instructions provided in the README.md.
26
+ - [ ] I am on the latest version of both Open WebUI and Ollama.
27
+ - [ ] I have included the browser console logs.
28
+ - [ ] I have included the Docker container logs.
29
+ - [ ] I have provided the exact steps to reproduce the bug in the "Steps to Reproduce" section below.
30
+
31
+ ## Expected Behavior:
32
+
33
+ [Describe what you expected to happen.]
34
+
35
+ ## Actual Behavior:
36
+
37
+ [Describe what actually happened.]
38
+
39
+ ## Description
40
+
41
+ **Bug Summary:**
42
+ [Provide a brief but clear summary of the bug]
43
+
44
+ ## Reproduction Details
45
+
46
+ **Steps to Reproduce:**
47
+ [Outline the steps to reproduce the bug. Be as detailed as possible.]
48
+
49
+ ## Logs and Screenshots
50
+
51
+ **Browser Console Logs:**
52
+ [Include relevant browser console logs, if applicable]
53
+
54
+ **Docker Container Logs:**
55
+ [Include relevant Docker container logs, if applicable]
56
+
57
+ **Screenshots/Screen Recordings (if applicable):**
58
+ [Attach any relevant screenshots to help illustrate the issue]
59
+
60
+ ## Additional Information
61
+
62
+ [Include any additional details that may help in understanding and reproducing the issue. This could include specific configurations, error messages, or anything else relevant to the bug.]
63
+
64
+ ## Note
65
+
66
+ If the bug report is incomplete or does not follow the provided instructions, it may not be addressed. Please ensure that you have followed the steps outlined in the README.md and troubleshooting.md documents, and provide all necessary information for us to reproduce and address the issue. Thank you!
.github/ISSUE_TEMPLATE/feature_request.md ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ name: Feature request
3
+ about: Suggest an idea for this project
4
+ title: ''
5
+ labels: ''
6
+ assignees: ''
7
+ ---
8
+
9
+ **Is your feature request related to a problem? Please describe.**
10
+ A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
11
+
12
+ **Describe the solution you'd like**
13
+ A clear and concise description of what you want to happen.
14
+
15
+ **Describe alternatives you've considered**
16
+ A clear and concise description of any alternative solutions or features you've considered.
17
+
18
+ **Additional context**
19
+ Add any other context or screenshots about the feature request here.
.github/dependabot.yml ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ version: 2
2
+ updates:
3
+ - package-ecosystem: pip
4
+ directory: '/backend'
5
+ schedule:
6
+ interval: monthly
7
+ target-branch: 'dev'
8
+ - package-ecosystem: 'github-actions'
9
+ directory: '/'
10
+ schedule:
11
+ # Check for updates to GitHub Actions every week
12
+ interval: monthly
.github/pull_request_template.md ADDED
@@ -0,0 +1,72 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Pull Request Checklist
2
+
3
+ ### Note to first-time contributors: Please open a discussion post in [Discussions](https://github.com/open-webui/open-webui/discussions) and describe your changes before submitting a pull request.
4
+
5
+ **Before submitting, make sure you've checked the following:**
6
+
7
+ - [ ] **Target branch:** Please verify that the pull request targets the `dev` branch.
8
+ - [ ] **Description:** Provide a concise description of the changes made in this pull request.
9
+ - [ ] **Changelog:** Ensure a changelog entry following the format of [Keep a Changelog](https://keepachangelog.com/) is added at the bottom of the PR description.
10
+ - [ ] **Documentation:** Have you updated relevant documentation [Open WebUI Docs](https://github.com/open-webui/docs), or other documentation sources?
11
+ - [ ] **Dependencies:** Are there any new dependencies? Have you updated the dependency versions in the documentation?
12
+ - [ ] **Testing:** Have you written and run sufficient tests for validating the changes?
13
+ - [ ] **Code review:** Have you performed a self-review of your code, addressing any coding standard issues and ensuring adherence to the project's coding standards?
14
+ - [ ] **Prefix:** To cleary categorize this pull request, prefix the pull request title, using one of the following:
15
+ - **BREAKING CHANGE**: Significant changes that may affect compatibility
16
+ - **build**: Changes that affect the build system or external dependencies
17
+ - **ci**: Changes to our continuous integration processes or workflows
18
+ - **chore**: Refactor, cleanup, or other non-functional code changes
19
+ - **docs**: Documentation update or addition
20
+ - **feat**: Introduces a new feature or enhancement to the codebase
21
+ - **fix**: Bug fix or error correction
22
+ - **i18n**: Internationalization or localization changes
23
+ - **perf**: Performance improvement
24
+ - **refactor**: Code restructuring for better maintainability, readability, or scalability
25
+ - **style**: Changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc.)
26
+ - **test**: Adding missing tests or correcting existing tests
27
+ - **WIP**: Work in progress, a temporary label for incomplete or ongoing work
28
+
29
+ # Changelog Entry
30
+
31
+ ### Description
32
+
33
+ - [Concisely describe the changes made in this pull request, including any relevant motivation and impact (e.g., fixing a bug, adding a feature, or improving performance)]
34
+
35
+ ### Added
36
+
37
+ - [List any new features, functionalities, or additions]
38
+
39
+ ### Changed
40
+
41
+ - [List any changes, updates, refactorings, or optimizations]
42
+
43
+ ### Deprecated
44
+
45
+ - [List any deprecated functionality or features that have been removed]
46
+
47
+ ### Removed
48
+
49
+ - [List any removed features, files, or functionalities]
50
+
51
+ ### Fixed
52
+
53
+ - [List any fixes, corrections, or bug fixes]
54
+
55
+ ### Security
56
+
57
+ - [List any new or updated security-related changes, including vulnerability fixes]
58
+
59
+ ### Breaking Changes
60
+
61
+ - **BREAKING CHANGE**: [List any breaking changes affecting compatibility or functionality]
62
+
63
+ ---
64
+
65
+ ### Additional Information
66
+
67
+ - [Insert any additional context, notes, or explanations for the changes]
68
+ - [Reference any related issues, commits, or other relevant information]
69
+
70
+ ### Screenshots or Videos
71
+
72
+ - [Attach any relevant screenshots or videos demonstrating the changes]
.github/workflows/build-release.yml ADDED
@@ -0,0 +1,72 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: Release
2
+
3
+ on:
4
+ push:
5
+ branches:
6
+ - main # or whatever branch you want to use
7
+
8
+ jobs:
9
+ release:
10
+ runs-on: ubuntu-latest
11
+
12
+ steps:
13
+ - name: Checkout repository
14
+ uses: actions/checkout@v4
15
+
16
+ - name: Check for changes in package.json
17
+ run: |
18
+ git diff --cached --diff-filter=d package.json || {
19
+ echo "No changes to package.json"
20
+ exit 1
21
+ }
22
+
23
+ - name: Get version number from package.json
24
+ id: get_version
25
+ run: |
26
+ VERSION=$(jq -r '.version' package.json)
27
+ echo "::set-output name=version::$VERSION"
28
+
29
+ - name: Extract latest CHANGELOG entry
30
+ id: changelog
31
+ run: |
32
+ CHANGELOG_CONTENT=$(awk 'BEGIN {print_section=0;} /^## \[/ {if (print_section == 0) {print_section=1;} else {exit;}} print_section {print;}' CHANGELOG.md)
33
+ CHANGELOG_ESCAPED=$(echo "$CHANGELOG_CONTENT" | sed ':a;N;$!ba;s/\n/%0A/g')
34
+ echo "Extracted latest release notes from CHANGELOG.md:"
35
+ echo -e "$CHANGELOG_CONTENT"
36
+ echo "::set-output name=content::$CHANGELOG_ESCAPED"
37
+
38
+ - name: Create GitHub release
39
+ uses: actions/github-script@v7
40
+ with:
41
+ github-token: ${{ secrets.GITHUB_TOKEN }}
42
+ script: |
43
+ const changelog = `${{ steps.changelog.outputs.content }}`;
44
+ const release = await github.rest.repos.createRelease({
45
+ owner: context.repo.owner,
46
+ repo: context.repo.repo,
47
+ tag_name: `v${{ steps.get_version.outputs.version }}`,
48
+ name: `v${{ steps.get_version.outputs.version }}`,
49
+ body: changelog,
50
+ })
51
+ console.log(`Created release ${release.data.html_url}`)
52
+
53
+ - name: Upload package to GitHub release
54
+ uses: actions/upload-artifact@v4
55
+ with:
56
+ name: package
57
+ path: |
58
+ .
59
+ !.git
60
+ env:
61
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
62
+
63
+ - name: Trigger Docker build workflow
64
+ uses: actions/github-script@v7
65
+ with:
66
+ script: |
67
+ github.rest.actions.createWorkflowDispatch({
68
+ owner: context.repo.owner,
69
+ repo: context.repo.repo,
70
+ workflow_id: 'docker-build.yaml',
71
+ ref: 'v${{ steps.get_version.outputs.version }}',
72
+ })
.github/workflows/deploy-to-hf-spaces.yml ADDED
@@ -0,0 +1,61 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: Deploy to HuggingFace Spaces
2
+
3
+ on:
4
+ push:
5
+ branches:
6
+ - dev
7
+ - main
8
+ workflow_dispatch:
9
+ schedule:
10
+ - cron: '0 16 * * *' # 每天北京时间0点自动执行
11
+
12
+ jobs:
13
+ check-secret:
14
+ runs-on: ubuntu-latest
15
+ outputs:
16
+ token-set: ${{ steps.check-key.outputs.defined }}
17
+ steps:
18
+ - id: check-key
19
+ env:
20
+ HF_TOKEN: ${{ secrets.HF_TOKEN }}
21
+ if: "${{ env.HF_TOKEN != '' }}"
22
+ run: echo "defined=true" >> $GITHUB_OUTPUT
23
+
24
+ deploy:
25
+ runs-on: ubuntu-latest
26
+ needs: [check-secret]
27
+ if: needs.check-secret.outputs.token-set == 'true'
28
+ env:
29
+ HF_TOKEN: ${{ secrets.HF_TOKEN }}
30
+ steps:
31
+ - name: Checkout repository
32
+ uses: actions/checkout@v4
33
+
34
+ - name: Remove git history
35
+ run: rm -rf .git
36
+
37
+ - name: Prepend YAML front matter to README.md
38
+ run: |
39
+ echo "---" > temp_readme.md
40
+ echo "title: AI Station " >> temp_readme.md
41
+ echo "emoji: 🐳" >> temp_readme.md
42
+ echo "colorFrom: purple" >> temp_readme.md
43
+ echo "colorTo: gray" >> temp_readme.md
44
+ echo "sdk: docker" >> temp_readme.md
45
+ echo "app_port: 8080" >> temp_readme.md
46
+ echo "---" >> temp_readme.md
47
+ cat README.md >> temp_readme.md
48
+ mv temp_readme.md README.md
49
+
50
+ - name: Configure git
51
+ run: |
52
+ git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com"
53
+ git config --global user.name "github-actions[bot]"
54
+ - name: Set up Git and push to Space
55
+ run: |
56
+ git init --initial-branch=main
57
+ git lfs track "*.ttf"
58
+ rm demo.gif
59
+ git add .
60
+ git commit -m "GitHub deploy: ${{ github.sha }}"
61
+ git push --force https://open-webui:${HF_TOKEN}@huggingface.co/spaces/tokenfactory/ai-station main
.github/workflows/docker-build.yaml ADDED
@@ -0,0 +1,477 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: Create and publish Docker images with specific build args
2
+
3
+ on:
4
+ workflow_dispatch:
5
+ push:
6
+ branches:
7
+ - main
8
+ - dev
9
+ tags:
10
+ - v*
11
+
12
+ env:
13
+ REGISTRY: ghcr.io
14
+
15
+ jobs:
16
+ build-main-image:
17
+ runs-on: ubuntu-latest
18
+ permissions:
19
+ contents: read
20
+ packages: write
21
+ strategy:
22
+ fail-fast: false
23
+ matrix:
24
+ platform:
25
+ - linux/amd64
26
+ - linux/arm64
27
+
28
+ steps:
29
+ # GitHub Packages requires the entire repository name to be in lowercase
30
+ # although the repository owner has a lowercase username, this prevents some people from running actions after forking
31
+ - name: Set repository and image name to lowercase
32
+ run: |
33
+ echo "IMAGE_NAME=${IMAGE_NAME,,}" >>${GITHUB_ENV}
34
+ echo "FULL_IMAGE_NAME=ghcr.io/${IMAGE_NAME,,}" >>${GITHUB_ENV}
35
+ env:
36
+ IMAGE_NAME: '${{ github.repository }}'
37
+
38
+ - name: Prepare
39
+ run: |
40
+ platform=${{ matrix.platform }}
41
+ echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV
42
+
43
+ - name: Checkout repository
44
+ uses: actions/checkout@v4
45
+
46
+ - name: Set up QEMU
47
+ uses: docker/setup-qemu-action@v3
48
+
49
+ - name: Set up Docker Buildx
50
+ uses: docker/setup-buildx-action@v3
51
+
52
+ - name: Log in to the Container registry
53
+ uses: docker/login-action@v3
54
+ with:
55
+ registry: ${{ env.REGISTRY }}
56
+ username: ${{ github.actor }}
57
+ password: ${{ secrets.GITHUB_TOKEN }}
58
+
59
+ - name: Extract metadata for Docker images (default latest tag)
60
+ id: meta
61
+ uses: docker/metadata-action@v5
62
+ with:
63
+ images: ${{ env.FULL_IMAGE_NAME }}
64
+ tags: |
65
+ type=ref,event=branch
66
+ type=ref,event=tag
67
+ type=sha,prefix=git-
68
+ type=semver,pattern={{version}}
69
+ type=semver,pattern={{major}}.{{minor}}
70
+ flavor: |
71
+ latest=${{ github.ref == 'refs/heads/main' }}
72
+
73
+ - name: Extract metadata for Docker cache
74
+ id: cache-meta
75
+ uses: docker/metadata-action@v5
76
+ with:
77
+ images: ${{ env.FULL_IMAGE_NAME }}
78
+ tags: |
79
+ type=ref,event=branch
80
+ ${{ github.ref_type == 'tag' && 'type=raw,value=main' || '' }}
81
+ flavor: |
82
+ prefix=cache-${{ matrix.platform }}-
83
+ latest=false
84
+
85
+ - name: Build Docker image (latest)
86
+ uses: docker/build-push-action@v5
87
+ id: build
88
+ with:
89
+ context: .
90
+ push: true
91
+ platforms: ${{ matrix.platform }}
92
+ labels: ${{ steps.meta.outputs.labels }}
93
+ outputs: type=image,name=${{ env.FULL_IMAGE_NAME }},push-by-digest=true,name-canonical=true,push=true
94
+ cache-from: type=registry,ref=${{ steps.cache-meta.outputs.tags }}
95
+ cache-to: type=registry,ref=${{ steps.cache-meta.outputs.tags }},mode=max
96
+ build-args: |
97
+ BUILD_HASH=${{ github.sha }}
98
+
99
+ - name: Export digest
100
+ run: |
101
+ mkdir -p /tmp/digests
102
+ digest="${{ steps.build.outputs.digest }}"
103
+ touch "/tmp/digests/${digest#sha256:}"
104
+
105
+ - name: Upload digest
106
+ uses: actions/upload-artifact@v4
107
+ with:
108
+ name: digests-main-${{ env.PLATFORM_PAIR }}
109
+ path: /tmp/digests/*
110
+ if-no-files-found: error
111
+ retention-days: 1
112
+
113
+ build-cuda-image:
114
+ runs-on: ubuntu-latest
115
+ permissions:
116
+ contents: read
117
+ packages: write
118
+ strategy:
119
+ fail-fast: false
120
+ matrix:
121
+ platform:
122
+ - linux/amd64
123
+ - linux/arm64
124
+
125
+ steps:
126
+ # GitHub Packages requires the entire repository name to be in lowercase
127
+ # although the repository owner has a lowercase username, this prevents some people from running actions after forking
128
+ - name: Set repository and image name to lowercase
129
+ run: |
130
+ echo "IMAGE_NAME=${IMAGE_NAME,,}" >>${GITHUB_ENV}
131
+ echo "FULL_IMAGE_NAME=ghcr.io/${IMAGE_NAME,,}" >>${GITHUB_ENV}
132
+ env:
133
+ IMAGE_NAME: '${{ github.repository }}'
134
+
135
+ - name: Prepare
136
+ run: |
137
+ platform=${{ matrix.platform }}
138
+ echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV
139
+
140
+ - name: Checkout repository
141
+ uses: actions/checkout@v4
142
+
143
+ - name: Set up QEMU
144
+ uses: docker/setup-qemu-action@v3
145
+
146
+ - name: Set up Docker Buildx
147
+ uses: docker/setup-buildx-action@v3
148
+
149
+ - name: Log in to the Container registry
150
+ uses: docker/login-action@v3
151
+ with:
152
+ registry: ${{ env.REGISTRY }}
153
+ username: ${{ github.actor }}
154
+ password: ${{ secrets.GITHUB_TOKEN }}
155
+
156
+ - name: Extract metadata for Docker images (cuda tag)
157
+ id: meta
158
+ uses: docker/metadata-action@v5
159
+ with:
160
+ images: ${{ env.FULL_IMAGE_NAME }}
161
+ tags: |
162
+ type=ref,event=branch
163
+ type=ref,event=tag
164
+ type=sha,prefix=git-
165
+ type=semver,pattern={{version}}
166
+ type=semver,pattern={{major}}.{{minor}}
167
+ type=raw,enable=${{ github.ref == 'refs/heads/main' }},prefix=,suffix=,value=cuda
168
+ flavor: |
169
+ latest=${{ github.ref == 'refs/heads/main' }}
170
+ suffix=-cuda,onlatest=true
171
+
172
+ - name: Extract metadata for Docker cache
173
+ id: cache-meta
174
+ uses: docker/metadata-action@v5
175
+ with:
176
+ images: ${{ env.FULL_IMAGE_NAME }}
177
+ tags: |
178
+ type=ref,event=branch
179
+ ${{ github.ref_type == 'tag' && 'type=raw,value=main' || '' }}
180
+ flavor: |
181
+ prefix=cache-cuda-${{ matrix.platform }}-
182
+ latest=false
183
+
184
+ - name: Build Docker image (cuda)
185
+ uses: docker/build-push-action@v5
186
+ id: build
187
+ with:
188
+ context: .
189
+ push: true
190
+ platforms: ${{ matrix.platform }}
191
+ labels: ${{ steps.meta.outputs.labels }}
192
+ outputs: type=image,name=${{ env.FULL_IMAGE_NAME }},push-by-digest=true,name-canonical=true,push=true
193
+ cache-from: type=registry,ref=${{ steps.cache-meta.outputs.tags }}
194
+ cache-to: type=registry,ref=${{ steps.cache-meta.outputs.tags }},mode=max
195
+ build-args: |
196
+ BUILD_HASH=${{ github.sha }}
197
+ USE_CUDA=true
198
+
199
+ - name: Export digest
200
+ run: |
201
+ mkdir -p /tmp/digests
202
+ digest="${{ steps.build.outputs.digest }}"
203
+ touch "/tmp/digests/${digest#sha256:}"
204
+
205
+ - name: Upload digest
206
+ uses: actions/upload-artifact@v4
207
+ with:
208
+ name: digests-cuda-${{ env.PLATFORM_PAIR }}
209
+ path: /tmp/digests/*
210
+ if-no-files-found: error
211
+ retention-days: 1
212
+
213
+ build-ollama-image:
214
+ runs-on: ubuntu-latest
215
+ permissions:
216
+ contents: read
217
+ packages: write
218
+ strategy:
219
+ fail-fast: false
220
+ matrix:
221
+ platform:
222
+ - linux/amd64
223
+ - linux/arm64
224
+
225
+ steps:
226
+ # GitHub Packages requires the entire repository name to be in lowercase
227
+ # although the repository owner has a lowercase username, this prevents some people from running actions after forking
228
+ - name: Set repository and image name to lowercase
229
+ run: |
230
+ echo "IMAGE_NAME=${IMAGE_NAME,,}" >>${GITHUB_ENV}
231
+ echo "FULL_IMAGE_NAME=ghcr.io/${IMAGE_NAME,,}" >>${GITHUB_ENV}
232
+ env:
233
+ IMAGE_NAME: '${{ github.repository }}'
234
+
235
+ - name: Prepare
236
+ run: |
237
+ platform=${{ matrix.platform }}
238
+ echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV
239
+
240
+ - name: Checkout repository
241
+ uses: actions/checkout@v4
242
+
243
+ - name: Set up QEMU
244
+ uses: docker/setup-qemu-action@v3
245
+
246
+ - name: Set up Docker Buildx
247
+ uses: docker/setup-buildx-action@v3
248
+
249
+ - name: Log in to the Container registry
250
+ uses: docker/login-action@v3
251
+ with:
252
+ registry: ${{ env.REGISTRY }}
253
+ username: ${{ github.actor }}
254
+ password: ${{ secrets.GITHUB_TOKEN }}
255
+
256
+ - name: Extract metadata for Docker images (ollama tag)
257
+ id: meta
258
+ uses: docker/metadata-action@v5
259
+ with:
260
+ images: ${{ env.FULL_IMAGE_NAME }}
261
+ tags: |
262
+ type=ref,event=branch
263
+ type=ref,event=tag
264
+ type=sha,prefix=git-
265
+ type=semver,pattern={{version}}
266
+ type=semver,pattern={{major}}.{{minor}}
267
+ type=raw,enable=${{ github.ref == 'refs/heads/main' }},prefix=,suffix=,value=ollama
268
+ flavor: |
269
+ latest=${{ github.ref == 'refs/heads/main' }}
270
+ suffix=-ollama,onlatest=true
271
+
272
+ - name: Extract metadata for Docker cache
273
+ id: cache-meta
274
+ uses: docker/metadata-action@v5
275
+ with:
276
+ images: ${{ env.FULL_IMAGE_NAME }}
277
+ tags: |
278
+ type=ref,event=branch
279
+ ${{ github.ref_type == 'tag' && 'type=raw,value=main' || '' }}
280
+ flavor: |
281
+ prefix=cache-ollama-${{ matrix.platform }}-
282
+ latest=false
283
+
284
+ - name: Build Docker image (ollama)
285
+ uses: docker/build-push-action@v5
286
+ id: build
287
+ with:
288
+ context: .
289
+ push: true
290
+ platforms: ${{ matrix.platform }}
291
+ labels: ${{ steps.meta.outputs.labels }}
292
+ outputs: type=image,name=${{ env.FULL_IMAGE_NAME }},push-by-digest=true,name-canonical=true,push=true
293
+ cache-from: type=registry,ref=${{ steps.cache-meta.outputs.tags }}
294
+ cache-to: type=registry,ref=${{ steps.cache-meta.outputs.tags }},mode=max
295
+ build-args: |
296
+ BUILD_HASH=${{ github.sha }}
297
+ USE_OLLAMA=true
298
+
299
+ - name: Export digest
300
+ run: |
301
+ mkdir -p /tmp/digests
302
+ digest="${{ steps.build.outputs.digest }}"
303
+ touch "/tmp/digests/${digest#sha256:}"
304
+
305
+ - name: Upload digest
306
+ uses: actions/upload-artifact@v4
307
+ with:
308
+ name: digests-ollama-${{ env.PLATFORM_PAIR }}
309
+ path: /tmp/digests/*
310
+ if-no-files-found: error
311
+ retention-days: 1
312
+
313
+ merge-main-images:
314
+ runs-on: ubuntu-latest
315
+ needs: [build-main-image]
316
+ steps:
317
+ # GitHub Packages requires the entire repository name to be in lowercase
318
+ # although the repository owner has a lowercase username, this prevents some people from running actions after forking
319
+ - name: Set repository and image name to lowercase
320
+ run: |
321
+ echo "IMAGE_NAME=${IMAGE_NAME,,}" >>${GITHUB_ENV}
322
+ echo "FULL_IMAGE_NAME=ghcr.io/${IMAGE_NAME,,}" >>${GITHUB_ENV}
323
+ env:
324
+ IMAGE_NAME: '${{ github.repository }}'
325
+
326
+ - name: Download digests
327
+ uses: actions/download-artifact@v4
328
+ with:
329
+ pattern: digests-main-*
330
+ path: /tmp/digests
331
+ merge-multiple: true
332
+
333
+ - name: Set up Docker Buildx
334
+ uses: docker/setup-buildx-action@v3
335
+
336
+ - name: Log in to the Container registry
337
+ uses: docker/login-action@v3
338
+ with:
339
+ registry: ${{ env.REGISTRY }}
340
+ username: ${{ github.actor }}
341
+ password: ${{ secrets.GITHUB_TOKEN }}
342
+
343
+ - name: Extract metadata for Docker images (default latest tag)
344
+ id: meta
345
+ uses: docker/metadata-action@v5
346
+ with:
347
+ images: ${{ env.FULL_IMAGE_NAME }}
348
+ tags: |
349
+ type=ref,event=branch
350
+ type=ref,event=tag
351
+ type=sha,prefix=git-
352
+ type=semver,pattern={{version}}
353
+ type=semver,pattern={{major}}.{{minor}}
354
+ flavor: |
355
+ latest=${{ github.ref == 'refs/heads/main' }}
356
+
357
+ - name: Create manifest list and push
358
+ working-directory: /tmp/digests
359
+ run: |
360
+ docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
361
+ $(printf '${{ env.FULL_IMAGE_NAME }}@sha256:%s ' *)
362
+
363
+ - name: Inspect image
364
+ run: |
365
+ docker buildx imagetools inspect ${{ env.FULL_IMAGE_NAME }}:${{ steps.meta.outputs.version }}
366
+
367
+ merge-cuda-images:
368
+ runs-on: ubuntu-latest
369
+ needs: [build-cuda-image]
370
+ steps:
371
+ # GitHub Packages requires the entire repository name to be in lowercase
372
+ # although the repository owner has a lowercase username, this prevents some people from running actions after forking
373
+ - name: Set repository and image name to lowercase
374
+ run: |
375
+ echo "IMAGE_NAME=${IMAGE_NAME,,}" >>${GITHUB_ENV}
376
+ echo "FULL_IMAGE_NAME=ghcr.io/${IMAGE_NAME,,}" >>${GITHUB_ENV}
377
+ env:
378
+ IMAGE_NAME: '${{ github.repository }}'
379
+
380
+ - name: Download digests
381
+ uses: actions/download-artifact@v4
382
+ with:
383
+ pattern: digests-cuda-*
384
+ path: /tmp/digests
385
+ merge-multiple: true
386
+
387
+ - name: Set up Docker Buildx
388
+ uses: docker/setup-buildx-action@v3
389
+
390
+ - name: Log in to the Container registry
391
+ uses: docker/login-action@v3
392
+ with:
393
+ registry: ${{ env.REGISTRY }}
394
+ username: ${{ github.actor }}
395
+ password: ${{ secrets.GITHUB_TOKEN }}
396
+
397
+ - name: Extract metadata for Docker images (default latest tag)
398
+ id: meta
399
+ uses: docker/metadata-action@v5
400
+ with:
401
+ images: ${{ env.FULL_IMAGE_NAME }}
402
+ tags: |
403
+ type=ref,event=branch
404
+ type=ref,event=tag
405
+ type=sha,prefix=git-
406
+ type=semver,pattern={{version}}
407
+ type=semver,pattern={{major}}.{{minor}}
408
+ type=raw,enable=${{ github.ref == 'refs/heads/main' }},prefix=,suffix=,value=cuda
409
+ flavor: |
410
+ latest=${{ github.ref == 'refs/heads/main' }}
411
+ suffix=-cuda,onlatest=true
412
+
413
+ - name: Create manifest list and push
414
+ working-directory: /tmp/digests
415
+ run: |
416
+ docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
417
+ $(printf '${{ env.FULL_IMAGE_NAME }}@sha256:%s ' *)
418
+
419
+ - name: Inspect image
420
+ run: |
421
+ docker buildx imagetools inspect ${{ env.FULL_IMAGE_NAME }}:${{ steps.meta.outputs.version }}
422
+
423
+ merge-ollama-images:
424
+ runs-on: ubuntu-latest
425
+ needs: [build-ollama-image]
426
+ steps:
427
+ # GitHub Packages requires the entire repository name to be in lowercase
428
+ # although the repository owner has a lowercase username, this prevents some people from running actions after forking
429
+ - name: Set repository and image name to lowercase
430
+ run: |
431
+ echo "IMAGE_NAME=${IMAGE_NAME,,}" >>${GITHUB_ENV}
432
+ echo "FULL_IMAGE_NAME=ghcr.io/${IMAGE_NAME,,}" >>${GITHUB_ENV}
433
+ env:
434
+ IMAGE_NAME: '${{ github.repository }}'
435
+
436
+ - name: Download digests
437
+ uses: actions/download-artifact@v4
438
+ with:
439
+ pattern: digests-ollama-*
440
+ path: /tmp/digests
441
+ merge-multiple: true
442
+
443
+ - name: Set up Docker Buildx
444
+ uses: docker/setup-buildx-action@v3
445
+
446
+ - name: Log in to the Container registry
447
+ uses: docker/login-action@v3
448
+ with:
449
+ registry: ${{ env.REGISTRY }}
450
+ username: ${{ github.actor }}
451
+ password: ${{ secrets.GITHUB_TOKEN }}
452
+
453
+ - name: Extract metadata for Docker images (default ollama tag)
454
+ id: meta
455
+ uses: docker/metadata-action@v5
456
+ with:
457
+ images: ${{ env.FULL_IMAGE_NAME }}
458
+ tags: |
459
+ type=ref,event=branch
460
+ type=ref,event=tag
461
+ type=sha,prefix=git-
462
+ type=semver,pattern={{version}}
463
+ type=semver,pattern={{major}}.{{minor}}
464
+ type=raw,enable=${{ github.ref == 'refs/heads/main' }},prefix=,suffix=,value=ollama
465
+ flavor: |
466
+ latest=${{ github.ref == 'refs/heads/main' }}
467
+ suffix=-ollama,onlatest=true
468
+
469
+ - name: Create manifest list and push
470
+ working-directory: /tmp/digests
471
+ run: |
472
+ docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
473
+ $(printf '${{ env.FULL_IMAGE_NAME }}@sha256:%s ' *)
474
+
475
+ - name: Inspect image
476
+ run: |
477
+ docker buildx imagetools inspect ${{ env.FULL_IMAGE_NAME }}:${{ steps.meta.outputs.version }}
.github/workflows/format-backend.yaml ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: Python CI
2
+
3
+ on:
4
+ push:
5
+ branches:
6
+ - main
7
+ - dev
8
+ pull_request:
9
+ branches:
10
+ - main
11
+ - dev
12
+
13
+ jobs:
14
+ build:
15
+ name: 'Format Backend'
16
+ runs-on: ubuntu-latest
17
+
18
+ strategy:
19
+ matrix:
20
+ python-version: [3.11]
21
+
22
+ steps:
23
+ - uses: actions/checkout@v4
24
+
25
+ - name: Set up Python
26
+ uses: actions/setup-python@v4
27
+ with:
28
+ python-version: ${{ matrix.python-version }}
29
+
30
+ - name: Install dependencies
31
+ run: |
32
+ python -m pip install --upgrade pip
33
+ pip install black
34
+
35
+ - name: Format backend
36
+ run: npm run format:backend
37
+
38
+ - name: Check for changes after format
39
+ run: git diff --exit-code
.github/workflows/format-build-frontend.yaml ADDED
@@ -0,0 +1,57 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: Frontend Build
2
+
3
+ on:
4
+ push:
5
+ branches:
6
+ - main
7
+ - dev
8
+ pull_request:
9
+ branches:
10
+ - main
11
+ - dev
12
+
13
+ jobs:
14
+ build:
15
+ name: 'Format & Build Frontend'
16
+ runs-on: ubuntu-latest
17
+ steps:
18
+ - name: Checkout Repository
19
+ uses: actions/checkout@v4
20
+
21
+ - name: Setup Node.js
22
+ uses: actions/setup-node@v4
23
+ with:
24
+ node-version: '20' # Or specify any other version you want to use
25
+
26
+ - name: Install Dependencies
27
+ run: npm install
28
+
29
+ - name: Format Frontend
30
+ run: npm run format
31
+
32
+ - name: Run i18next
33
+ run: npm run i18n:parse
34
+
35
+ - name: Check for Changes After Format
36
+ run: git diff --exit-code
37
+
38
+ - name: Build Frontend
39
+ run: npm run build
40
+
41
+ test-frontend:
42
+ name: 'Frontend Unit Tests'
43
+ runs-on: ubuntu-latest
44
+ steps:
45
+ - name: Checkout Repository
46
+ uses: actions/checkout@v4
47
+
48
+ - name: Setup Node.js
49
+ uses: actions/setup-node@v4
50
+ with:
51
+ node-version: '20'
52
+
53
+ - name: Install Dependencies
54
+ run: npm ci
55
+
56
+ - name: Run vitest
57
+ run: npm run test:frontend
.github/workflows/integration-test.yml ADDED
@@ -0,0 +1,250 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: Integration Test
2
+
3
+ on:
4
+ push:
5
+ branches:
6
+ - main
7
+ - dev
8
+ pull_request:
9
+ branches:
10
+ - main
11
+ - dev
12
+
13
+ jobs:
14
+ cypress-run:
15
+ name: Run Cypress Integration Tests
16
+ runs-on: ubuntu-latest
17
+ steps:
18
+ - name: Maximize build space
19
+ uses: AdityaGarg8/[email protected]
20
+ with:
21
+ remove-android: 'true'
22
+ remove-haskell: 'true'
23
+ remove-codeql: 'true'
24
+
25
+ - name: Checkout Repository
26
+ uses: actions/checkout@v4
27
+
28
+ - name: Build and run Compose Stack
29
+ run: |
30
+ docker compose \
31
+ --file docker-compose.yaml \
32
+ --file docker-compose.api.yaml \
33
+ --file docker-compose.a1111-test.yaml \
34
+ up --detach --build
35
+
36
+ - name: Delete Docker build cache
37
+ run: |
38
+ docker builder prune --all --force
39
+
40
+ - name: Wait for Ollama to be up
41
+ timeout-minutes: 5
42
+ run: |
43
+ until curl --output /dev/null --silent --fail http://localhost:11434; do
44
+ printf '.'
45
+ sleep 1
46
+ done
47
+ echo "Service is up!"
48
+
49
+ - name: Preload Ollama model
50
+ run: |
51
+ docker exec ollama ollama pull qwen:0.5b-chat-v1.5-q2_K
52
+
53
+ - name: Cypress run
54
+ uses: cypress-io/github-action@v6
55
+ with:
56
+ browser: chrome
57
+ wait-on: 'http://localhost:3000'
58
+ config: baseUrl=http://localhost:3000
59
+
60
+ - uses: actions/upload-artifact@v4
61
+ if: always()
62
+ name: Upload Cypress videos
63
+ with:
64
+ name: cypress-videos
65
+ path: cypress/videos
66
+ if-no-files-found: ignore
67
+
68
+ - name: Extract Compose logs
69
+ if: always()
70
+ run: |
71
+ docker compose logs > compose-logs.txt
72
+
73
+ - uses: actions/upload-artifact@v4
74
+ if: always()
75
+ name: Upload Compose logs
76
+ with:
77
+ name: compose-logs
78
+ path: compose-logs.txt
79
+ if-no-files-found: ignore
80
+
81
+ # pytest:
82
+ # name: Run Backend Tests
83
+ # runs-on: ubuntu-latest
84
+ # steps:
85
+ # - uses: actions/checkout@v4
86
+
87
+ # - name: Set up Python
88
+ # uses: actions/setup-python@v4
89
+ # with:
90
+ # python-version: ${{ matrix.python-version }}
91
+
92
+ # - name: Install dependencies
93
+ # run: |
94
+ # python -m pip install --upgrade pip
95
+ # pip install -r backend/requirements.txt
96
+
97
+ # - name: pytest run
98
+ # run: |
99
+ # ls -al
100
+ # cd backend
101
+ # PYTHONPATH=. pytest . -o log_cli=true -o log_cli_level=INFO
102
+
103
+ migration_test:
104
+ name: Run Migration Tests
105
+ runs-on: ubuntu-latest
106
+ services:
107
+ postgres:
108
+ image: postgres
109
+ env:
110
+ POSTGRES_PASSWORD: postgres
111
+ options: >-
112
+ --health-cmd pg_isready
113
+ --health-interval 10s
114
+ --health-timeout 5s
115
+ --health-retries 5
116
+ ports:
117
+ - 5432:5432
118
+ # mysql:
119
+ # image: mysql
120
+ # env:
121
+ # MYSQL_ROOT_PASSWORD: mysql
122
+ # MYSQL_DATABASE: mysql
123
+ # options: >-
124
+ # --health-cmd "mysqladmin ping -h localhost"
125
+ # --health-interval 10s
126
+ # --health-timeout 5s
127
+ # --health-retries 5
128
+ # ports:
129
+ # - 3306:3306
130
+ steps:
131
+ - name: Checkout Repository
132
+ uses: actions/checkout@v4
133
+
134
+ - name: Set up Python
135
+ uses: actions/setup-python@v5
136
+ with:
137
+ python-version: ${{ matrix.python-version }}
138
+
139
+ - name: Set up uv
140
+ uses: yezz123/setup-uv@v4
141
+ with:
142
+ uv-venv: venv
143
+
144
+ - name: Activate virtualenv
145
+ run: |
146
+ . venv/bin/activate
147
+ echo PATH=$PATH >> $GITHUB_ENV
148
+
149
+ - name: Install dependencies
150
+ run: |
151
+ uv pip install -r backend/requirements.txt
152
+
153
+ - name: Test backend with SQLite
154
+ id: sqlite
155
+ env:
156
+ WEBUI_SECRET_KEY: secret-key
157
+ GLOBAL_LOG_LEVEL: debug
158
+ run: |
159
+ cd backend
160
+ uvicorn open_webui.main:app --port "8080" --forwarded-allow-ips '*' &
161
+ UVICORN_PID=$!
162
+ # Wait up to 40 seconds for the server to start
163
+ for i in {1..40}; do
164
+ curl -s http://localhost:8080/api/config > /dev/null && break
165
+ sleep 1
166
+ if [ $i -eq 40 ]; then
167
+ echo "Server failed to start"
168
+ kill -9 $UVICORN_PID
169
+ exit 1
170
+ fi
171
+ done
172
+ # Check that the server is still running after 5 seconds
173
+ sleep 5
174
+ if ! kill -0 $UVICORN_PID; then
175
+ echo "Server has stopped"
176
+ exit 1
177
+ fi
178
+
179
+ - name: Test backend with Postgres
180
+ if: success() || steps.sqlite.conclusion == 'failure'
181
+ env:
182
+ WEBUI_SECRET_KEY: secret-key
183
+ GLOBAL_LOG_LEVEL: debug
184
+ DATABASE_URL: postgresql://postgres:postgres@localhost:5432/postgres
185
+ run: |
186
+ cd backend
187
+ uvicorn open_webui.main:app --port "8081" --forwarded-allow-ips '*' &
188
+ UVICORN_PID=$!
189
+ # Wait up to 20 seconds for the server to start
190
+ for i in {1..20}; do
191
+ curl -s http://localhost:8081/api/config > /dev/null && break
192
+ sleep 1
193
+ if [ $i -eq 20 ]; then
194
+ echo "Server failed to start"
195
+ kill -9 $UVICORN_PID
196
+ exit 1
197
+ fi
198
+ done
199
+ # Check that the server is still running after 5 seconds
200
+ sleep 5
201
+ if ! kill -0 $UVICORN_PID; then
202
+ echo "Server has stopped"
203
+ exit 1
204
+ fi
205
+
206
+ # Check that service will reconnect to postgres when connection will be closed
207
+ status_code=$(curl --write-out %{http_code} -s --output /dev/null http://localhost:8081/health/db)
208
+ if [[ "$status_code" -ne 200 ]] ; then
209
+ echo "Server has failed before postgres reconnect check"
210
+ exit 1
211
+ fi
212
+
213
+ echo "Terminating all connections to postgres..."
214
+ python -c "import os, psycopg2 as pg2; \
215
+ conn = pg2.connect(dsn=os.environ['DATABASE_URL'].replace('+pool', '')); \
216
+ cur = conn.cursor(); \
217
+ cur.execute('SELECT pg_terminate_backend(psa.pid) FROM pg_stat_activity psa WHERE datname = current_database() AND pid <> pg_backend_pid();')"
218
+
219
+ status_code=$(curl --write-out %{http_code} -s --output /dev/null http://localhost:8081/health/db)
220
+ if [[ "$status_code" -ne 200 ]] ; then
221
+ echo "Server has not reconnected to postgres after connection was closed: returned status $status_code"
222
+ exit 1
223
+ fi
224
+
225
+ # - name: Test backend with MySQL
226
+ # if: success() || steps.sqlite.conclusion == 'failure' || steps.postgres.conclusion == 'failure'
227
+ # env:
228
+ # WEBUI_SECRET_KEY: secret-key
229
+ # GLOBAL_LOG_LEVEL: debug
230
+ # DATABASE_URL: mysql://root:mysql@localhost:3306/mysql
231
+ # run: |
232
+ # cd backend
233
+ # uvicorn open_webui.main:app --port "8083" --forwarded-allow-ips '*' &
234
+ # UVICORN_PID=$!
235
+ # # Wait up to 20 seconds for the server to start
236
+ # for i in {1..20}; do
237
+ # curl -s http://localhost:8083/api/config > /dev/null && break
238
+ # sleep 1
239
+ # if [ $i -eq 20 ]; then
240
+ # echo "Server failed to start"
241
+ # kill -9 $UVICORN_PID
242
+ # exit 1
243
+ # fi
244
+ # done
245
+ # # Check that the server is still running after 5 seconds
246
+ # sleep 5
247
+ # if ! kill -0 $UVICORN_PID; then
248
+ # echo "Server has stopped"
249
+ # exit 1
250
+ # fi
.github/workflows/lint-backend.disabled ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: Python CI
2
+ on:
3
+ push:
4
+ branches: ['main']
5
+ pull_request:
6
+ jobs:
7
+ build:
8
+ name: 'Lint Backend'
9
+ env:
10
+ PUBLIC_API_BASE_URL: ''
11
+ runs-on: ubuntu-latest
12
+ strategy:
13
+ matrix:
14
+ node-version:
15
+ - latest
16
+ steps:
17
+ - uses: actions/checkout@v4
18
+ - name: Use Python
19
+ uses: actions/setup-python@v4
20
+ - name: Use Bun
21
+ uses: oven-sh/setup-bun@v1
22
+ - name: Install dependencies
23
+ run: |
24
+ python -m pip install --upgrade pip
25
+ pip install pylint
26
+ - name: Lint backend
27
+ run: bun run lint:backend
.github/workflows/lint-frontend.disabled ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: Bun CI
2
+ on:
3
+ push:
4
+ branches: ['main']
5
+ pull_request:
6
+ jobs:
7
+ build:
8
+ name: 'Lint Frontend'
9
+ env:
10
+ PUBLIC_API_BASE_URL: ''
11
+ runs-on: ubuntu-latest
12
+ steps:
13
+ - uses: actions/checkout@v4
14
+ - name: Use Bun
15
+ uses: oven-sh/setup-bun@v1
16
+ - run: bun --version
17
+ - name: Install frontend dependencies
18
+ run: bun install --frozen-lockfile
19
+ - run: bun run lint:frontend
20
+ - run: bun run lint:types
21
+ if: success() || failure()
.github/workflows/release-pypi.yml ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: Release to PyPI
2
+
3
+ on:
4
+ push:
5
+ branches:
6
+ - main # or whatever branch you want to use
7
+ - pypi-release
8
+
9
+ jobs:
10
+ release:
11
+ runs-on: ubuntu-latest
12
+ environment:
13
+ name: pypi
14
+ url: https://pypi.org/p/open-webui
15
+ permissions:
16
+ id-token: write
17
+ steps:
18
+ - name: Checkout repository
19
+ uses: actions/checkout@v4
20
+ - uses: actions/setup-node@v4
21
+ with:
22
+ node-version: 18
23
+ - uses: actions/setup-python@v5
24
+ with:
25
+ python-version: 3.11
26
+ - name: Build
27
+ run: |
28
+ python -m pip install --upgrade pip
29
+ pip install build
30
+ python -m build .
31
+ - name: Publish package distributions to PyPI
32
+ uses: pypa/gh-action-pypi-publish@release/v1
.gitignore ADDED
@@ -0,0 +1,309 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .DS_Store
2
+ node_modules
3
+ /build
4
+ /.svelte-kit
5
+ /package
6
+ .env
7
+ .env.*
8
+ !.env.example
9
+ vite.config.js.timestamp-*
10
+ vite.config.ts.timestamp-*
11
+ # Byte-compiled / optimized / DLL files
12
+ __pycache__/
13
+ *.py[cod]
14
+ *$py.class
15
+
16
+ # C extensions
17
+ *.so
18
+
19
+ # Pyodide distribution
20
+ static/pyodide/*
21
+ !static/pyodide/pyodide-lock.json
22
+
23
+ # Distribution / packaging
24
+ .Python
25
+ build/
26
+ develop-eggs/
27
+ dist/
28
+ downloads/
29
+ eggs/
30
+ .eggs/
31
+ lib64/
32
+ parts/
33
+ sdist/
34
+ var/
35
+ wheels/
36
+ share/python-wheels/
37
+ *.egg-info/
38
+ .installed.cfg
39
+ *.egg
40
+ MANIFEST
41
+
42
+ # PyInstaller
43
+ # Usually these files are written by a python script from a template
44
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
45
+ *.manifest
46
+ *.spec
47
+
48
+ # Installer logs
49
+ pip-log.txt
50
+ pip-delete-this-directory.txt
51
+
52
+ # Unit test / coverage reports
53
+ htmlcov/
54
+ .tox/
55
+ .nox/
56
+ .coverage
57
+ .coverage.*
58
+ .cache
59
+ nosetests.xml
60
+ coverage.xml
61
+ *.cover
62
+ *.py,cover
63
+ .hypothesis/
64
+ .pytest_cache/
65
+ cover/
66
+
67
+ # Translations
68
+ *.mo
69
+ *.pot
70
+
71
+ # Django stuff:
72
+ *.log
73
+ local_settings.py
74
+ db.sqlite3
75
+ db.sqlite3-journal
76
+
77
+ # Flask stuff:
78
+ instance/
79
+ .webassets-cache
80
+
81
+ # Scrapy stuff:
82
+ .scrapy
83
+
84
+ # Sphinx documentation
85
+ docs/_build/
86
+
87
+ # PyBuilder
88
+ .pybuilder/
89
+ target/
90
+
91
+ # Jupyter Notebook
92
+ .ipynb_checkpoints
93
+
94
+ # IPython
95
+ profile_default/
96
+ ipython_config.py
97
+
98
+ # pyenv
99
+ # For a library or package, you might want to ignore these files since the code is
100
+ # intended to run in multiple environments; otherwise, check them in:
101
+ # .python-version
102
+
103
+ # pipenv
104
+ # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
105
+ # However, in case of collaboration, if having platform-specific dependencies or dependencies
106
+ # having no cross-platform support, pipenv may install dependencies that don't work, or not
107
+ # install all needed dependencies.
108
+ #Pipfile.lock
109
+
110
+ # poetry
111
+ # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
112
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
113
+ # commonly ignored for libraries.
114
+ # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
115
+ #poetry.lock
116
+
117
+ # pdm
118
+ # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
119
+ #pdm.lock
120
+ # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
121
+ # in version control.
122
+ # https://pdm.fming.dev/#use-with-ide
123
+ .pdm.toml
124
+
125
+ # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
126
+ __pypackages__/
127
+
128
+ # Celery stuff
129
+ celerybeat-schedule
130
+ celerybeat.pid
131
+
132
+ # SageMath parsed files
133
+ *.sage.py
134
+
135
+ # Environments
136
+ .env
137
+ .venv
138
+ env/
139
+ venv/
140
+ ENV/
141
+ env.bak/
142
+ venv.bak/
143
+
144
+ # Spyder project settings
145
+ .spyderproject
146
+ .spyproject
147
+
148
+ # Rope project settings
149
+ .ropeproject
150
+
151
+ # mkdocs documentation
152
+ /site
153
+
154
+ # mypy
155
+ .mypy_cache/
156
+ .dmypy.json
157
+ dmypy.json
158
+
159
+ # Pyre type checker
160
+ .pyre/
161
+
162
+ # pytype static type analyzer
163
+ .pytype/
164
+
165
+ # Cython debug symbols
166
+ cython_debug/
167
+
168
+ # PyCharm
169
+ # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
170
+ # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
171
+ # and can be added to the global gitignore or merged into this file. For a more nuclear
172
+ # option (not recommended) you can uncomment the following to ignore the entire idea folder.
173
+ .idea/
174
+
175
+ # Logs
176
+ logs
177
+ *.log
178
+ npm-debug.log*
179
+ yarn-debug.log*
180
+ yarn-error.log*
181
+ lerna-debug.log*
182
+ .pnpm-debug.log*
183
+
184
+ # Diagnostic reports (https://nodejs.org/api/report.html)
185
+ report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
186
+
187
+ # Runtime data
188
+ pids
189
+ *.pid
190
+ *.seed
191
+ *.pid.lock
192
+
193
+ # Directory for instrumented libs generated by jscoverage/JSCover
194
+ lib-cov
195
+
196
+ # Coverage directory used by tools like istanbul
197
+ coverage
198
+ *.lcov
199
+
200
+ # nyc test coverage
201
+ .nyc_output
202
+
203
+ # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
204
+ .grunt
205
+
206
+ # Bower dependency directory (https://bower.io/)
207
+ bower_components
208
+
209
+ # node-waf configuration
210
+ .lock-wscript
211
+
212
+ # Compiled binary addons (https://nodejs.org/api/addons.html)
213
+ build/Release
214
+
215
+ # Dependency directories
216
+ node_modules/
217
+ jspm_packages/
218
+
219
+ # Snowpack dependency directory (https://snowpack.dev/)
220
+ web_modules/
221
+
222
+ # TypeScript cache
223
+ *.tsbuildinfo
224
+
225
+ # Optional npm cache directory
226
+ .npm
227
+
228
+ # Optional eslint cache
229
+ .eslintcache
230
+
231
+ # Optional stylelint cache
232
+ .stylelintcache
233
+
234
+ # Microbundle cache
235
+ .rpt2_cache/
236
+ .rts2_cache_cjs/
237
+ .rts2_cache_es/
238
+ .rts2_cache_umd/
239
+
240
+ # Optional REPL history
241
+ .node_repl_history
242
+
243
+ # Output of 'npm pack'
244
+ *.tgz
245
+
246
+ # Yarn Integrity file
247
+ .yarn-integrity
248
+
249
+ # dotenv environment variable files
250
+ .env
251
+ .env.development.local
252
+ .env.test.local
253
+ .env.production.local
254
+ .env.local
255
+
256
+ # parcel-bundler cache (https://parceljs.org/)
257
+ .cache
258
+ .parcel-cache
259
+
260
+ # Next.js build output
261
+ .next
262
+ out
263
+
264
+ # Nuxt.js build / generate output
265
+ .nuxt
266
+ dist
267
+
268
+ # Gatsby files
269
+ .cache/
270
+ # Comment in the public line in if your project uses Gatsby and not Next.js
271
+ # https://nextjs.org/blog/next-9-1#public-directory-support
272
+ # public
273
+
274
+ # vuepress build output
275
+ .vuepress/dist
276
+
277
+ # vuepress v2.x temp and cache directory
278
+ .temp
279
+ .cache
280
+
281
+ # Docusaurus cache and generated files
282
+ .docusaurus
283
+
284
+ # Serverless directories
285
+ .serverless/
286
+
287
+ # FuseBox cache
288
+ .fusebox/
289
+
290
+ # DynamoDB Local files
291
+ .dynamodb/
292
+
293
+ # TernJS port file
294
+ .tern-port
295
+
296
+ # Stores VSCode versions used for testing VSCode extensions
297
+ .vscode-test
298
+
299
+ # yarn v2
300
+ .yarn/cache
301
+ .yarn/unplugged
302
+ .yarn/build-state.yml
303
+ .yarn/install-state.gz
304
+ .pnp.*
305
+
306
+ # cypress artifacts
307
+ cypress/videos
308
+ cypress/screenshots
309
+ .vscode/settings.json
.npmrc ADDED
@@ -0,0 +1 @@
 
 
1
+ engine-strict=true
.prettierignore ADDED
@@ -0,0 +1,316 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Ignore files for PNPM, NPM and YARN
2
+ pnpm-lock.yaml
3
+ package-lock.json
4
+ yarn.lock
5
+
6
+ kubernetes/
7
+
8
+ # Copy of .gitignore
9
+ .DS_Store
10
+ node_modules
11
+ /build
12
+ /.svelte-kit
13
+ /package
14
+ .env
15
+ .env.*
16
+ !.env.example
17
+ vite.config.js.timestamp-*
18
+ vite.config.ts.timestamp-*
19
+ # Byte-compiled / optimized / DLL files
20
+ __pycache__/
21
+ *.py[cod]
22
+ *$py.class
23
+
24
+ # C extensions
25
+ *.so
26
+
27
+ # Distribution / packaging
28
+ .Python
29
+ build/
30
+ develop-eggs/
31
+ dist/
32
+ downloads/
33
+ eggs/
34
+ .eggs/
35
+ lib64/
36
+ parts/
37
+ sdist/
38
+ var/
39
+ wheels/
40
+ share/python-wheels/
41
+ *.egg-info/
42
+ .installed.cfg
43
+ *.egg
44
+ MANIFEST
45
+
46
+ # PyInstaller
47
+ # Usually these files are written by a python script from a template
48
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
49
+ *.manifest
50
+ *.spec
51
+
52
+ # Installer logs
53
+ pip-log.txt
54
+ pip-delete-this-directory.txt
55
+
56
+ # Unit test / coverage reports
57
+ htmlcov/
58
+ .tox/
59
+ .nox/
60
+ .coverage
61
+ .coverage.*
62
+ .cache
63
+ nosetests.xml
64
+ coverage.xml
65
+ *.cover
66
+ *.py,cover
67
+ .hypothesis/
68
+ .pytest_cache/
69
+ cover/
70
+
71
+ # Translations
72
+ *.mo
73
+ *.pot
74
+
75
+ # Django stuff:
76
+ *.log
77
+ local_settings.py
78
+ db.sqlite3
79
+ db.sqlite3-journal
80
+
81
+ # Flask stuff:
82
+ instance/
83
+ .webassets-cache
84
+
85
+ # Scrapy stuff:
86
+ .scrapy
87
+
88
+ # Sphinx documentation
89
+ docs/_build/
90
+
91
+ # PyBuilder
92
+ .pybuilder/
93
+ target/
94
+
95
+ # Jupyter Notebook
96
+ .ipynb_checkpoints
97
+
98
+ # IPython
99
+ profile_default/
100
+ ipython_config.py
101
+
102
+ # pyenv
103
+ # For a library or package, you might want to ignore these files since the code is
104
+ # intended to run in multiple environments; otherwise, check them in:
105
+ # .python-version
106
+
107
+ # pipenv
108
+ # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
109
+ # However, in case of collaboration, if having platform-specific dependencies or dependencies
110
+ # having no cross-platform support, pipenv may install dependencies that don't work, or not
111
+ # install all needed dependencies.
112
+ #Pipfile.lock
113
+
114
+ # poetry
115
+ # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
116
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
117
+ # commonly ignored for libraries.
118
+ # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
119
+ #poetry.lock
120
+
121
+ # pdm
122
+ # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
123
+ #pdm.lock
124
+ # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
125
+ # in version control.
126
+ # https://pdm.fming.dev/#use-with-ide
127
+ .pdm.toml
128
+
129
+ # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
130
+ __pypackages__/
131
+
132
+ # Celery stuff
133
+ celerybeat-schedule
134
+ celerybeat.pid
135
+
136
+ # SageMath parsed files
137
+ *.sage.py
138
+
139
+ # Environments
140
+ .env
141
+ .venv
142
+ env/
143
+ venv/
144
+ ENV/
145
+ env.bak/
146
+ venv.bak/
147
+
148
+ # Spyder project settings
149
+ .spyderproject
150
+ .spyproject
151
+
152
+ # Rope project settings
153
+ .ropeproject
154
+
155
+ # mkdocs documentation
156
+ /site
157
+
158
+ # mypy
159
+ .mypy_cache/
160
+ .dmypy.json
161
+ dmypy.json
162
+
163
+ # Pyre type checker
164
+ .pyre/
165
+
166
+ # pytype static type analyzer
167
+ .pytype/
168
+
169
+ # Cython debug symbols
170
+ cython_debug/
171
+
172
+ # PyCharm
173
+ # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
174
+ # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
175
+ # and can be added to the global gitignore or merged into this file. For a more nuclear
176
+ # option (not recommended) you can uncomment the following to ignore the entire idea folder.
177
+ .idea/
178
+
179
+ # Logs
180
+ logs
181
+ *.log
182
+ npm-debug.log*
183
+ yarn-debug.log*
184
+ yarn-error.log*
185
+ lerna-debug.log*
186
+ .pnpm-debug.log*
187
+
188
+ # Diagnostic reports (https://nodejs.org/api/report.html)
189
+ report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
190
+
191
+ # Runtime data
192
+ pids
193
+ *.pid
194
+ *.seed
195
+ *.pid.lock
196
+
197
+ # Directory for instrumented libs generated by jscoverage/JSCover
198
+ lib-cov
199
+
200
+ # Coverage directory used by tools like istanbul
201
+ coverage
202
+ *.lcov
203
+
204
+ # nyc test coverage
205
+ .nyc_output
206
+
207
+ # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
208
+ .grunt
209
+
210
+ # Bower dependency directory (https://bower.io/)
211
+ bower_components
212
+
213
+ # node-waf configuration
214
+ .lock-wscript
215
+
216
+ # Compiled binary addons (https://nodejs.org/api/addons.html)
217
+ build/Release
218
+
219
+ # Dependency directories
220
+ node_modules/
221
+ jspm_packages/
222
+
223
+ # Snowpack dependency directory (https://snowpack.dev/)
224
+ web_modules/
225
+
226
+ # TypeScript cache
227
+ *.tsbuildinfo
228
+
229
+ # Optional npm cache directory
230
+ .npm
231
+
232
+ # Optional eslint cache
233
+ .eslintcache
234
+
235
+ # Optional stylelint cache
236
+ .stylelintcache
237
+
238
+ # Microbundle cache
239
+ .rpt2_cache/
240
+ .rts2_cache_cjs/
241
+ .rts2_cache_es/
242
+ .rts2_cache_umd/
243
+
244
+ # Optional REPL history
245
+ .node_repl_history
246
+
247
+ # Output of 'npm pack'
248
+ *.tgz
249
+
250
+ # Yarn Integrity file
251
+ .yarn-integrity
252
+
253
+ # dotenv environment variable files
254
+ .env
255
+ .env.development.local
256
+ .env.test.local
257
+ .env.production.local
258
+ .env.local
259
+
260
+ # parcel-bundler cache (https://parceljs.org/)
261
+ .cache
262
+ .parcel-cache
263
+
264
+ # Next.js build output
265
+ .next
266
+ out
267
+
268
+ # Nuxt.js build / generate output
269
+ .nuxt
270
+ dist
271
+
272
+ # Gatsby files
273
+ .cache/
274
+ # Comment in the public line in if your project uses Gatsby and not Next.js
275
+ # https://nextjs.org/blog/next-9-1#public-directory-support
276
+ # public
277
+
278
+ # vuepress build output
279
+ .vuepress/dist
280
+
281
+ # vuepress v2.x temp and cache directory
282
+ .temp
283
+ .cache
284
+
285
+ # Docusaurus cache and generated files
286
+ .docusaurus
287
+
288
+ # Serverless directories
289
+ .serverless/
290
+
291
+ # FuseBox cache
292
+ .fusebox/
293
+
294
+ # DynamoDB Local files
295
+ .dynamodb/
296
+
297
+ # TernJS port file
298
+ .tern-port
299
+
300
+ # Stores VSCode versions used for testing VSCode extensions
301
+ .vscode-test
302
+
303
+ # yarn v2
304
+ .yarn/cache
305
+ .yarn/unplugged
306
+ .yarn/build-state.yml
307
+ .yarn/install-state.gz
308
+ .pnp.*
309
+
310
+ # cypress artifacts
311
+ cypress/videos
312
+ cypress/screenshots
313
+
314
+
315
+
316
+ /static/*
.prettierrc ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "useTabs": true,
3
+ "singleQuote": true,
4
+ "trailingComma": "none",
5
+ "printWidth": 100,
6
+ "plugins": ["prettier-plugin-svelte"],
7
+ "pluginSearchDirs": ["."],
8
+ "overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }]
9
+ }
CHANGELOG.md ADDED
@@ -0,0 +1,1085 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [0.3.28] - 2024-09-24
9
+
10
+ ### Fixed
11
+
12
+ - **🔍 Web Search Functionality**: Corrected an issue where the web search option was not functioning properly.
13
+
14
+ ## [0.3.27] - 2024-09-24
15
+
16
+ ### Fixed
17
+
18
+ - **🔄 Periodic Cleanup Error Resolved**: Fixed a critical RuntimeError related to the 'periodic_usage_pool_cleanup' coroutine, ensuring smooth and efficient performance post-pip install, correcting a persisting issue from version 0.3.26.
19
+ - **📊 Enhanced LaTeX Rendering**: Improved rendering for LaTeX content, enhancing clarity and visual presentation in documents and mathematical models.
20
+
21
+ ## [0.3.26] - 2024-09-24
22
+
23
+ ### Fixed
24
+
25
+ - **🔄 Event Loop Error Resolution**: Addressed a critical error where a missing running event loop caused 'periodic_usage_pool_cleanup' to fail with pip installs. This fix ensures smoother and more reliable updates and installations, enhancing overall system stability.
26
+
27
+ ## [0.3.25] - 2024-09-24
28
+
29
+ ### Fixed
30
+
31
+ - **🖼️ Image Generation Functionality**: Resolved an issue where image generation was not functioning, restoring full capability for visual content creation.
32
+ - **⚖️ Rate Response Corrections**: Addressed a problem where rate responses were not working, ensuring reliable feedback mechanisms are operational.
33
+
34
+ ## [0.3.24] - 2024-09-24
35
+
36
+ ### Added
37
+
38
+ - **🚀 Rendering Optimization**: Significantly improved message rendering performance, enhancing user experience and webui responsiveness.
39
+ - **💖 Favorite Response Feature in Chat Overview**: Users can now mark responses as favorite directly from the chat overview, enhancing ease of retrieval and organization of preferred responses.
40
+ - **💬 Create Message Pairs with Shortcut**: Implemented creation of new message pairs using Cmd/Ctrl+Shift+Enter, making conversation editing faster and more intuitive.
41
+ - **🌍 Expanded User Prompt Variables**: Added weekday, timezone, and language information variables to user prompts to match system prompt variables.
42
+ - **🎵 Enhanced Audio Support**: Now includes support for 'audio/x-m4a' files, broadening compatibility with audio content within the platform.
43
+ - **🔏 Model URL Search Parameter**: Added an ability to select a model directly via URL parameters, streamlining navigation and model access.
44
+ - **📄 Enhanced PDF Citations**: PDF citations now open at the associated page, streamlining reference checks and document handling.
45
+ - **🔧Use of Redis in Sockets**: Enhanced socket implementation to fully support Redis, enabling effective stateless instances suitable for scalable load balancing.
46
+ - **🌍 Stream Individual Model Responses**: Allows specific models to have individualized streaming settings, enhancing performance and customization.
47
+ - **🕒 Display Model Hash and Last Modified Timestamp for Ollama Models**: Provides critical model details directly in the Models workspace for enhanced tracking.
48
+ - **❗ Update Info Notification for Admins**: Ensures administrators receive immediate updates upon login, keeping them informed of the latest changes and system statuses.
49
+
50
+ ### Fixed
51
+
52
+ - **🗑️ Temporary File Handling On Windows**: Fixed an issue causing errors when accessing a temporary file being used by another process, Tools & Functions should now work as intended.
53
+ - **🔓 Authentication Toggle Issue**: Resolved the malfunction where setting 'WEBUI_AUTH=False' did not appropriately disable authentication, ensuring that user experience and system security settings function as configured.
54
+ - **🔧 Save As Copy Issue for Many Model Chats**: Resolved an error preventing users from save messages as copies in many model chats.
55
+ - **🔒 Sidebar Closure on Mobile**: Resolved an issue where the mobile sidebar remained open after menu engagement, improving user interface responsivity and comfort.
56
+ - **🛡️ Tooltip XSS Vulnerability**: Resolved a cross-site scripting (XSS) issue within tooltips, ensuring enhanced security and data integrity during user interactions.
57
+
58
+ ### Changed
59
+
60
+ - **↩️ Deprecated Interface Stream Response Settings**: Moved to advanced parameters to streamline interface settings and enhance user clarity.
61
+ - **⚙️ Renamed 'speedRate' to 'playbackRate'**: Standardizes terminology, improving usability and understanding in media settings.
62
+
63
+ ## [0.3.23] - 2024-09-21
64
+
65
+ ### Added
66
+
67
+ - **🚀 WebSocket Redis Support**: Enhanced load balancing capabilities for multiple instance setups, promoting better performance and reliability in WebUI.
68
+ - **🔧 Adjustable Chat Controls**: Introduced width-adjustable chat controls, enabling a personalized and more comfortable user interface.
69
+ - **🌎 i18n Updates**: Improved and updated the Chinese translations.
70
+
71
+ ### Fixed
72
+
73
+ - **🌐 Task Model Unloading Issue**: Modified task handling to use the Ollama /api/chat endpoint instead of OpenAI compatible endpoint, ensuring models stay loaded and ready with custom parameters, thus minimizing delays in task execution.
74
+ - **📝 Title Generation Fix for OpenAI Compatible APIs**: Resolved an issue preventing the generation of titles, enhancing consistency and reliability when using multiple API providers.
75
+ - **🗃️ RAG Duplicate Collection Issue**: Fixed a bug causing repeated processing of the same uploaded file. Now utilizes indexed files to prevent unnecessary duplications, optimizing resource usage.
76
+ - **🖼️ Image Generation Enhancement**: Refactored OpenAI image generation endpoint to be asynchronous, preventing the WebUI from becoming unresponsive during processing, thus enhancing user experience.
77
+ - **🔓 Downgrade Authlib**: Reverted Authlib to version 1.3.1 to address and resolve issues concerning OAuth functionality.
78
+
79
+ ### Changed
80
+
81
+ - **🔍 Improved Message Interaction**: Enhanced the message node interface to allow for easier focus redirection with a simple click, streamlining user interaction.
82
+ - **✨ Styling Refactor**: Updated WebUI styling for a cleaner, more modern look, enhancing user experience across the platform.
83
+
84
+ ## [0.3.22] - 2024-09-19
85
+
86
+ ### Added
87
+
88
+ - **⭐ Chat Overview**: Introducing a node-based interactive messages diagram for improved visualization of conversation flows.
89
+ - **🔗 Multiple Vector DB Support**: Now supports multiple vector databases, including the newly added Milvus support. Community contributions for additional database support are highly encouraged!
90
+ - **📡 Experimental Non-Stream Chat Completion**: Experimental feature allowing the use of OpenAI o1 models, which do not support streaming, ensuring more versatile model deployment.
91
+ - **🔍 Experimental Colbert-AI Reranker Integration**: Added support for "jinaai/jina-colbert-v2" as a reranker, enhancing search relevance and accuracy. Note: it may not function at all on low-spec computers.
92
+ - **🕸️ ENABLE_WEBSOCKET_SUPPORT**: Added environment variable for instances to ignore websocket upgrades, stabilizing connections on platforms with websocket issues.
93
+ - **🔊 Azure Speech Service Integration**: Added support for Azure Speech services for Text-to-Speech (TTS).
94
+ - **🎚️ Customizable Playback Speed**: Playback speed control is now available in Call mode settings, allowing users to adjust audio playback speed to their preferences.
95
+ - **🧠 Enhanced Error Messaging**: System now displays helpful error messages directly to users during chat completion issues.
96
+ - **📂 Save Model as Transparent PNG**: Model profile images are now saved as PNGs, supporting transparency and improving visual integration.
97
+ - **📱 iPhone Compatibility Adjustments**: Added padding to accommodate the iPhone navigation bar, improving UI display on these devices.
98
+ - **🔗 Secure Response Headers**: Implemented security response headers, bolstering web application security.
99
+ - **🔧 Enhanced AUTOMATIC1111 Settings**: Users can now configure 'CFG Scale', 'Sampler', and 'Scheduler' parameters directly in the admin settings, enhancing workflow flexibility without source code modifications.
100
+ - **🌍 i18n Updates**: Enhanced translations for Chinese, Ukrainian, Russian, and French, fostering a better localized experience.
101
+
102
+ ### Fixed
103
+
104
+ - **🛠️ Chat Message Deletion**: Resolved issues with chat message deletion, ensuring a smoother user interaction and system stability.
105
+ - **🔢 Ordered List Numbering**: Fixed the incorrect ordering in lists.
106
+
107
+ ### Changed
108
+
109
+ - **🎨 Transparent Icon Handling**: Allowed model icons to be displayed on transparent backgrounds, improving UI aesthetics.
110
+ - **📝 Improved RAG Template**: Enhanced Retrieval-Augmented Generation template, optimizing context handling and error checking for more precise operation.
111
+
112
+ ## [0.3.21] - 2024-09-08
113
+
114
+ ### Added
115
+
116
+ - **📊 Document Count Display**: Now displays the total number of documents directly within the dashboard.
117
+ - **🚀 Ollama Embed API Endpoint**: Enabled /api/embed endpoint proxy support.
118
+
119
+ ### Fixed
120
+
121
+ - **🐳 Docker Launch Issue**: Resolved the problem preventing Open-WebUI from launching correctly when using Docker.
122
+
123
+ ### Changed
124
+
125
+ - **🔍 Enhanced Search Prompts**: Improved the search query generation prompts for better accuracy and user interaction, enhancing the overall search experience.
126
+
127
+ ## [0.3.20] - 2024-09-07
128
+
129
+ ### Added
130
+
131
+ - **🌐 Translation Update**: Updated Catalan translations to improve user experience for Catalan speakers.
132
+
133
+ ### Fixed
134
+
135
+ - **📄 PDF Download**: Resolved a configuration issue with fonts directory, ensuring PDFs are now downloaded with the correct formatting.
136
+ - **🛠️ Installation of Tools & Functions Requirements**: Fixed a bug where necessary requirements for tools and functions were not properly installing.
137
+ - **🔗 Inline Image Link Rendering**: Enabled rendering of images directly from links in chat.
138
+ - **📞 Post-Call User Interface Cleanup**: Adjusted UI behavior to automatically close chat controls after a voice call ends, reducing screen clutter.
139
+ - **🎙️ Microphone Deactivation Post-Call**: Addressed an issue where the microphone remained active after calls.
140
+ - **✍️ Markdown Spacing Correction**: Corrected spacing in Markdown rendering, ensuring text appears neatly and as expected.
141
+ - **🔄 Message Re-rendering**: Fixed an issue causing all response messages to re-render with each new message, now improving chat performance.
142
+
143
+ ### Changed
144
+
145
+ - **🌐 Refined Web Search Integration**: Deprecated the Search Query Generation Prompt threshold; introduced a toggle button for "Enable Web Search Query Generation" allowing users to opt-in to using web search more judiciously.
146
+ - **📝 Default Prompt Templates Update**: Emptied environment variable templates for search and title generation now default to the Open WebUI default prompt templates, simplifying configuration efforts.
147
+
148
+ ## [0.3.19] - 2024-09-05
149
+
150
+ ### Added
151
+
152
+ - **🌐 Translation Update**: Improved Chinese translations.
153
+
154
+ ### Fixed
155
+
156
+ - **📂 DATA_DIR Overriding**: Fixed an issue to avoid overriding DATA_DIR, preventing errors when directories are set identically, ensuring smoother operation and data management.
157
+ - **🛠️ Frontmatter Extraction**: Fixed the extraction process for frontmatter in tools and functions.
158
+
159
+ ### Changed
160
+
161
+ - **🎨 UI Styling**: Refined the user interface styling for enhanced visual coherence and user experience.
162
+
163
+ ## [0.3.18] - 2024-09-04
164
+
165
+ ### Added
166
+
167
+ - **🛠️ Direct Database Execution for Tools & Functions**: Enhanced the execution of Python files for tools and functions, now directly loading from the database for a more streamlined backend process.
168
+
169
+ ### Fixed
170
+
171
+ - **🔄 Automatic Rewrite of Import Statements in Tools & Functions**: Tool and function scripts that import 'utils', 'apps', 'main', 'config' will now automatically rename these with 'open_webui.', ensuring compatibility and consistency across different modules.
172
+ - **🎨 Styling Adjustments**: Minor fixes in the visual styling to improve user experience and interface consistency.
173
+
174
+ ## [0.3.17] - 2024-09-04
175
+
176
+ ### Added
177
+
178
+ - **🔄 Import/Export Configuration**: Users can now import and export webui configurations from admin settings > Database, simplifying setup replication across systems.
179
+ - **🌍 Web Search via URL Parameter**: Added support for activating web search directly through URL by setting 'web-search=true'.
180
+ - **🌐 SearchApi Integration**: Added support for SearchApi as an alternative web search provider, enhancing search capabilities within the platform.
181
+ - **🔍 Literal Type Support in Tools**: Tools now support the Literal type.
182
+ - **🌍 Updated Translations**: Improved translations for Chinese, Ukrainian, and Catalan.
183
+
184
+ ### Fixed
185
+
186
+ - **🔧 Pip Install Issue**: Resolved the issue where pip install failed due to missing 'alembic.ini', ensuring smoother installation processes.
187
+ - **🌃 Automatic Theme Update**: Fixed an issue where the color theme did not update dynamically with system changes.
188
+ - **🛠️ User Agent in ComfyUI**: Added default headers in ComfyUI to fix access issues, improving reliability in network communications.
189
+ - **🔄 Missing Chat Completion Response Headers**: Ensured proper return of proxied response headers during chat completion, improving API reliability.
190
+ - **🔗 Websocket Connection Prioritization**: Modified socket.io configuration to prefer websockets and more reliably fallback to polling, enhancing connection stability.
191
+ - **🎭 Accessibility Enhancements**: Added missing ARIA labels for buttons, improving accessibility for visually impaired users.
192
+ - **⚖️ Advanced Parameter**: Fixed an issue ensuring that advanced parameters are correctly applied in all scenarios, ensuring consistent behavior of user-defined settings.
193
+
194
+ ### Changed
195
+
196
+ - **🔁 Namespace Reorganization**: Reorganized all Python files under the 'open_webui' namespace to streamline the project structure and improve maintainability. Tools and functions importing from 'utils' should now use 'open_webui.utils'.
197
+ - **🚧 Dependency Updates**: Updated several backend dependencies like 'aiohttp', 'authlib', 'duckduckgo-search', 'flask-cors', and 'langchain' to their latest versions, enhancing performance and security.
198
+
199
+ ## [0.3.16] - 2024-08-27
200
+
201
+ ### Added
202
+
203
+ - **🚀 Config DB Migration**: Migrated configuration handling from config.json to the database, enabling high-availability setups and load balancing across multiple Open WebUI instances.
204
+ - **🔗 Call Mode Activation via URL**: Added a 'call=true' URL search parameter enabling direct shortcuts to activate call mode, enhancing user interaction on mobile devices.
205
+ - **✨ TTS Content Control**: Added functionality to control how message content is segmented for Text-to-Speech (TTS) generation requests, allowing for more flexible speech output options.
206
+ - **😄 Show Knowledge Search Status**: Enhanced model usage transparency by displaying status when working with knowledge-augmented models, helping users understand the system's state during queries.
207
+ - **👆 Click-to-Copy for Codespan**: Enhanced interactive experience in the WebUI by allowing users to click to copy content from code spans directly.
208
+ - **🚫 API User Blocking via Model Filter**: Introduced the ability to block API users based on customized model filters, enhancing security and control over API access.
209
+ - **🎬 Call Overlay Styling**: Adjusted call overlay styling on large screens to not cover the entire interface, but only the chat control area, for a more unobtrusive interaction experience.
210
+
211
+ ### Fixed
212
+
213
+ - **🔧 LaTeX Rendering Issue**: Addressed an issue that affected the correct rendering of LaTeX.
214
+ - **📁 File Leak Prevention**: Resolved the issue of uploaded files mistakenly being accessible across user chats.
215
+ - **🔧 Pipe Functions with '**files**' Param**: Fixed issues with '**files**' parameter not functioning correctly in pipe functions.
216
+ - **📝 Markdown Processing for RAG**: Fixed issues with processing Markdown in files.
217
+ - **🚫 Duplicate System Prompts**: Fixed bugs causing system prompts to duplicate.
218
+
219
+ ### Changed
220
+
221
+ - **🔋 Wakelock Permission**: Optimized the activation of wakelock to only engage during call mode, conserving device resources and improving battery performance during idle periods.
222
+ - **🔍 Content-Type for Ollama Chats**: Added 'application/x-ndjson' content-type to '/api/chat' endpoint responses to match raw Ollama responses.
223
+ - **✋ Disable Signups Conditionally**: Implemented conditional logic to disable sign-ups when 'ENABLE_LOGIN_FORM' is set to false.
224
+
225
+ ## [0.3.15] - 2024-08-21
226
+
227
+ ### Added
228
+
229
+ - **🔗 Temporary Chat Activation**: Integrated a new URL parameter 'temporary-chat=true' to enable temporary chat sessions directly through the URL.
230
+ - **🌄 ComfyUI Seed Node Support**: Introduced seed node support in ComfyUI for image generation, allowing users to specify node IDs for randomized seed assignment.
231
+
232
+ ### Fixed
233
+
234
+ - **🛠️ Tools and Functions**: Resolved a critical issue where Tools and Functions were not properly functioning, restoring full capability and reliability to these essential features.
235
+ - **🔘 Chat Action Button in Many Model Chat**: Fixed the malfunctioning of chat action buttons in many model chat environments, ensuring a smoother and more responsive user interaction.
236
+ - **⏪ Many Model Chat Compatibility**: Restored backward compatibility for many model chats.
237
+
238
+ ## [0.3.14] - 2024-08-21
239
+
240
+ ### Added
241
+
242
+ - **🛠️ Custom ComfyUI Workflow**: Deprecating several older environment variables, this enhancement introduces a new, customizable workflow for a more tailored user experience.
243
+ - **🔀 Merge Responses in Many Model Chat**: Enhances the dialogue by merging responses from multiple models into a single, coherent reply, improving the interaction quality in many model chats.
244
+ - **✅ Multiple Instances of Same Model in Chats**: Enhanced many model chat to support adding multiple instances of the same model.
245
+ - **🔧 Quick Actions in Model Workspace**: Enhanced Shift key quick actions for hiding/unhiding and deleting models, facilitating a smoother workflow.
246
+ - **🗨️ Markdown Rendering in User Messages**: User messages are now rendered in Markdown, enhancing readability and interaction.
247
+ - **💬 Temporary Chat Feature**: Introduced a temporary chat feature, deprecating the old chat history setting to enhance user interaction flexibility.
248
+ - **🖋️ User Message Editing**: Enhanced the user chat editing feature to allow saving changes without sending, providing more flexibility in message management.
249
+ - **🛡️ Security Enhancements**: Various security improvements implemented across the platform to ensure safer user experiences.
250
+ - **🌍 Updated Translations**: Enhanced translations for Chinese, Ukrainian, and Bahasa Malaysia, improving localization and user comprehension.
251
+
252
+ ### Fixed
253
+
254
+ - **📑 Mermaid Rendering Issue**: Addressed issues with Mermaid chart rendering to ensure clean and clear visual data representation.
255
+ - **🎭 PWA Icon Maskability**: Fixed the Progressive Web App icon to be maskable, ensuring proper display on various device home screens.
256
+ - **🔀 Cloned Model Chat Freezing Issue**: Fixed a bug where cloning many model chats would cause freezing, enhancing stability and responsiveness.
257
+ - **🔍 Generic Error Handling and Refinements**: Various minor fixes and refinements to address previously untracked issues, ensuring smoother operations.
258
+
259
+ ### Changed
260
+
261
+ - **🖼️ Image Generation Refactor**: Overhauled image generation processes for improved efficiency and quality.
262
+ - **🔨 Refactor Tool and Function Calling**: Refactored tool and function calling mechanisms for improved clarity and maintainability.
263
+ - **🌐 Backend Library Updates**: Updated critical backend libraries including SQLAlchemy, uvicorn[standard], faster-whisper, bcrypt, and boto3 for enhanced performance and security.
264
+
265
+ ### Removed
266
+
267
+ - **🚫 Deprecated ComfyUI Environment Variables**: Removed several outdated environment variables related to ComfyUI settings, simplifying configuration management.
268
+
269
+ ## [0.3.13] - 2024-08-14
270
+
271
+ ### Added
272
+
273
+ - **🎨 Enhanced Markdown Rendering**: Significant improvements in rendering markdown, ensuring smooth and reliable display of LaTeX and Mermaid charts, enhancing user experience with more robust visual content.
274
+ - **🔄 Auto-Install Tools & Functions Python Dependencies**: For 'Tools' and 'Functions', Open WebUI now automatically install extra python requirements specified in the frontmatter, streamlining setup processes and customization.
275
+ - **🌀 OAuth Email Claim Customization**: Introduced an 'OAUTH_EMAIL_CLAIM' variable to allow customization of the default "email" claim within OAuth configurations, providing greater flexibility in authentication processes.
276
+ - **📶 Websocket Reconnection**: Enhanced reliability with the capability to automatically reconnect when a websocket is closed, ensuring consistent and stable communication.
277
+ - **🤳 Haptic Feedback on Support Devices**: Android devices now support haptic feedback for an immersive tactile experience during certain interactions.
278
+
279
+ ### Fixed
280
+
281
+ - **🛠️ ComfyUI Performance Improvement**: Addressed an issue causing FastAPI to stall when ComfyUI image generation was active; now runs in a separate thread to prevent UI unresponsiveness.
282
+ - **🔀 Session Handling**: Fixed an issue mandating session_id on client-side to ensure smoother session management and transitions.
283
+ - **🖋️ Minor Bug Fixes and Format Corrections**: Various minor fixes including typo corrections, backend formatting improvements, and test amendments enhancing overall system stability and performance.
284
+
285
+ ### Changed
286
+
287
+ - **🚀 Migration to SvelteKit 2**: Upgraded the underlying framework to SvelteKit version 2, offering enhanced speed, better code structure, and improved deployment capabilities.
288
+ - **🧹 General Cleanup and Refactoring**: Performed broad cleanup and refactoring across the platform, improving code efficiency and maintaining high standards of code health.
289
+ - **🚧 Integration Testing Improvements**: Modified how Cypress integration tests detect chat messages and updated sharing tests for better reliability and accuracy.
290
+ - **📁 Standardized '.safetensors' File Extension**: Renamed the '.sft' file extension to '.safetensors' for ComfyUI workflows, standardizing file formats across the platform.
291
+
292
+ ### Removed
293
+
294
+ - **🗑️ Deprecated Frontend Functions**: Removed frontend functions that were migrated to backend to declutter the codebase and reduce redundancy.
295
+
296
+ ## [0.3.12] - 2024-08-07
297
+
298
+ ### Added
299
+
300
+ - **🔄 Sidebar Infinite Scroll**: Added an infinite scroll feature in the sidebar for more efficient chat navigation, reducing load times and enhancing user experience.
301
+ - **🚀 Enhanced Markdown Rendering**: Support for rendering all code blocks and making images clickable for preview; codespan styling is also enhanced to improve readability and user interaction.
302
+ - **🔒 Admin Shared Chat Visibility**: Admins no longer have default visibility over shared chats when ENABLE_ADMIN_CHAT_ACCESS is set to false, tightening security and privacy settings for users.
303
+ - **🌍 Language Updates**: Added Malay (Bahasa Malaysia) translation and updated Catalan and Traditional Chinese translations to improve accessibility for more users.
304
+
305
+ ### Fixed
306
+
307
+ - **📊 Markdown Rendering Issues**: Resolved issues with markdown rendering to ensure consistent and correct display across components.
308
+ - **🛠️ Styling Issues**: Multiple fixes applied to styling throughout the application, improving the overall visual experience and interface consistency.
309
+ - **🗃️ Modal Handling**: Fixed an issue where modals were not closing correctly in various model chat scenarios, enhancing usability and interface reliability.
310
+ - **📄 Missing OpenAI Usage Information**: Resolved issues where usage statistics for OpenAI services were not being correctly displayed, ensuring users have access to crucial data for managing and monitoring their API consumption.
311
+ - **🔧 Non-Streaming Support for Functions Plugin**: Fixed a functionality issue with the Functions plugin where non-streaming operations were not functioning as intended, restoring full capabilities for async and sync integration within the platform.
312
+ - **🔄 Environment Variable Type Correction (COMFYUI_FLUX_FP8_CLIP)**: Corrected the data type of the 'COMFYUI_FLUX_FP8_CLIP' environment variable from string to boolean, ensuring environment settings apply correctly and enhance configuration management.
313
+
314
+ ### Changed
315
+
316
+ - **🔧 Backend Dependency Updates**: Updated several backend dependencies such as boto3, pypdf, python-pptx, validators, and black, ensuring up-to-date security and performance optimizations.
317
+
318
+ ## [0.3.11] - 2024-08-02
319
+
320
+ ### Added
321
+
322
+ - **📊 Model Information Display**: Added visuals for model selection, including images next to model names for more intuitive navigation.
323
+ - **🗣 ElevenLabs Voice Adaptations**: Voice enhancements including support for ElevenLabs voice ID by name for personalized vocal interactions.
324
+ - **⌨️ Arrow Keys Model Selection**: Users can now use arrow keys for quicker model selection, enhancing accessibility.
325
+ - **🔍 Fuzzy Search in Model Selector**: Enhanced model selector with fuzzy search to locate models swiftly, including descriptions.
326
+ - **🕹️ ComfyUI Flux Image Generation**: Added support for the new Flux image gen model; introduces environment controls like weight precision and CLIP model options in Settings.
327
+ - **💾 Display File Size for Uploads**: Enhanced file interface now displays file size, preparing for upcoming upload restrictions.
328
+ - **🎚️ Advanced Params "Min P"**: Added 'Min P' parameter in the advanced settings for customized model precision control.
329
+ - **🔒 Enhanced OAuth**: Introduced custom redirect URI support for OAuth behind reverse proxies, enabling safer authentication processes.
330
+ - **🖥 Enhanced Latex Rendering**: Adjustments made to latex rendering processes, now accurately detecting and presenting latex inputs from text.
331
+ - **🌐 Internationalization**: Enhanced with new Romanian and updated Vietnamese and Ukrainian translations, helping broaden accessibility for international users.
332
+
333
+ ### Fixed
334
+
335
+ - **🔧 Tags Handling in Document Upload**: Tags are now properly sent to the upload document handler, resolving issues with missing metadata.
336
+ - **🖥️ Sensitive Input Fields**: Corrected browser misinterpretation of secure input fields, preventing misclassification as password fields.
337
+ - **📂 Static Path Resolution in PDF Generation**: Fixed static paths that adjust dynamically to prevent issues across various environments.
338
+
339
+ ### Changed
340
+
341
+ - **🎨 UI/UX Styling Enhancements**: Multiple minor styling updates for a cleaner and more intuitive user interface.
342
+ - **🚧 Refactoring Various Components**: Numerous refactoring changes across styling, file handling, and function simplifications for clarity and performance.
343
+ - **🎛️ User Valves Management**: Moved user valves from settings to direct chat controls for more user-friendly access during interactions.
344
+
345
+ ### Removed
346
+
347
+ - **⚙️ Health Check Logging**: Removed verbose logging from the health checking processes to declutter logs and improve backend performance.
348
+
349
+ ## [0.3.10] - 2024-07-17
350
+
351
+ ### Fixed
352
+
353
+ - **🔄 Improved File Upload**: Addressed the issue where file uploads lacked animation.
354
+ - **💬 Chat Continuity**: Fixed a problem where existing chats were not functioning properly in some instances.
355
+ - **🗂️ Chat File Reset**: Resolved the issue of chat files not resetting for new conversations, now ensuring a clean slate for each chat session.
356
+ - **📁 Document Workspace Uploads**: Corrected the handling of document uploads in the workspace using the Files API.
357
+
358
+ ## [0.3.9] - 2024-07-17
359
+
360
+ ### Added
361
+
362
+ - **📁 Files Chat Controls**: We've reverted to the old file handling behavior where uploaded files are always included. You can now manage files directly within the chat controls section, giving you the ability to remove files as needed.
363
+ - **🔧 "Action" Function Support**: Introducing a new "Action" function to write custom buttons to the message toolbar. This feature enables more interactive messaging, with documentation coming soon.
364
+ - **📜 Citations Handling**: For newly uploaded files in documents workspace, citations will now display the actual filename. Additionally, you can click on these filenames to open the file in a new tab for easier access.
365
+ - **🛠️ Event Emitter and Call Updates**: Enhanced 'event_emitter' to allow message replacement and 'event_call' to support text input for Tools and Functions. Detailed documentation will be provided shortly.
366
+ - **🎨 Styling Refactor**: Various styling updates for a cleaner and more cohesive user interface.
367
+ - **🌐 Enhanced Translations**: Improved translations for Catalan, Ukrainian, and Brazilian Portuguese.
368
+
369
+ ### Fixed
370
+
371
+ - **🔧 Chat Controls Priority**: Resolved an issue where Chat Controls values were being overridden by model information parameters. The priority is now Chat Controls, followed by Global Settings, then Model Settings.
372
+ - **🪲 Debug Logs**: Fixed an issue where debug logs were not being logged properly.
373
+ - **🔑 Automatic1111 Auth Key**: The auth key for Automatic1111 is no longer required.
374
+ - **📝 Title Generation**: Ensured that the title generation runs only once, even when multiple models are in a chat.
375
+ - **✅ Boolean Values in Params**: Added support for boolean values in parameters.
376
+ - **🖼️ Files Overlay Styling**: Fixed the styling issue with the files overlay.
377
+
378
+ ### Changed
379
+
380
+ - **⬆️ Dependency Updates**
381
+ - Upgraded 'pydantic' from version 2.7.1 to 2.8.2.
382
+ - Upgraded 'sqlalchemy' from version 2.0.30 to 2.0.31.
383
+ - Upgraded 'unstructured' from version 0.14.9 to 0.14.10.
384
+ - Upgraded 'chromadb' from version 0.5.3 to 0.5.4.
385
+
386
+ ## [0.3.8] - 2024-07-09
387
+
388
+ ### Added
389
+
390
+ - **💬 Chat Controls**: Easily adjust parameters for each chat session, offering more precise control over your interactions.
391
+ - **📌 Pinned Chats**: Support for pinned chats, allowing you to keep important conversations easily accessible.
392
+ - **📄 Apache Tika Integration**: Added support for using Apache Tika as a document loader, enhancing document processing capabilities.
393
+ - **🛠️ Custom Environment for OpenID Claims**: Allows setting custom claims for OpenID, providing more flexibility in user authentication.
394
+ - **🔧 Enhanced Tools & Functions API**: Introduced 'event_emitter' and 'event_call', now you can also add citations for better documentation and tracking. Detailed documentation will be provided on our documentation website.
395
+ - **↔️ Sideways Scrolling in Settings**: Settings tabs container now supports horizontal scrolling for easier navigation.
396
+ - **🌑 Darker OLED Theme**: Includes a new, darker OLED theme and improved styling for the light theme, enhancing visual appeal.
397
+ - **🌐 Language Updates**: Updated translations for Indonesian, German, French, and Catalan languages, expanding accessibility.
398
+
399
+ ### Fixed
400
+
401
+ - **⏰ OpenAI Streaming Timeout**: Resolved issues with OpenAI streaming response using the 'AIOHTTP_CLIENT_TIMEOUT' setting, ensuring reliable performance.
402
+ - **💡 User Valves**: Fixed malfunctioning user valves, ensuring proper functionality.
403
+ - **🔄 Collapsible Components**: Addressed issues with collapsible components not working, restoring expected behavior.
404
+
405
+ ### Changed
406
+
407
+ - **🗃️ Database Backend**: Switched from Peewee to SQLAlchemy for improved concurrency support, enhancing database performance.
408
+ - **⬆️ ChromaDB Update**: Upgraded to version 0.5.3. Ensure your remote ChromaDB instance matches this version.
409
+ - **🔤 Primary Font Styling**: Updated primary font to Archivo for better visual consistency.
410
+ - **🔄 Font Change for Windows**: Replaced Arimo with Inter font for Windows users, improving readability.
411
+ - **🚀 Lazy Loading**: Implemented lazy loading for 'faster_whisper' and 'sentence_transformers' to reduce startup memory usage.
412
+ - **📋 Task Generation Payload**: Task generations now include only the "task" field in the body instead of "title".
413
+
414
+ ## [0.3.7] - 2024-06-29
415
+
416
+ ### Added
417
+
418
+ - **🌐 Enhanced Internationalization (i18n)**: Newly introduced Indonesian translation, and updated translations for Turkish, Chinese, and Catalan languages to improve user accessibility.
419
+
420
+ ### Fixed
421
+
422
+ - **🕵️‍♂️ Browser Language Detection**: Corrected the issue where the application was not properly detecting and adapting to the browser's language settings.
423
+ - **🔐 OIDC Admin Role Assignment**: Fixed a bug where the admin role was not being assigned to the first user who signed up via OpenID Connect (OIDC).
424
+ - **💬 Chat/Completions Endpoint**: Resolved an issue where the chat/completions endpoint was non-functional when the stream option was set to False.
425
+ - **🚫 'WEBUI_AUTH' Configuration**: Addressed the problem where setting 'WEBUI_AUTH' to False was not being applied correctly.
426
+
427
+ ### Changed
428
+
429
+ - **📦 Dependency Update**: Upgraded 'authlib' from version 1.3.0 to 1.3.1 to ensure better security and performance enhancements.
430
+
431
+ ## [0.3.6] - 2024-06-27
432
+
433
+ ### Added
434
+
435
+ - **✨ "Functions" Feature**: You can now utilize "Functions" like filters (middleware) and pipe (model) functions directly within the WebUI. While largely compatible with Pipelines, these native functions can be executed easily within Open WebUI. Example use cases for filter functions include usage monitoring, real-time translation, moderation, and automemory. For pipe functions, the scope ranges from Cohere and Anthropic integration directly within Open WebUI, enabling "Valves" for per-user OpenAI API key usage, and much more. If you encounter issues, SAFE_MODE has been introduced.
436
+ - **📁 Files API**: Compatible with OpenAI, this feature allows for custom Retrieval-Augmented Generation (RAG) in conjunction with the Filter Function. More examples will be shared on our community platform and official documentation website.
437
+ - **🛠️ Tool Enhancements**: Tools now support citations and "Valves". Documentation will be available shortly.
438
+ - **🔗 Iframe Support via Files API**: Enables rendering HTML directly into your chat interface using functions and tools. Use cases include playing games like DOOM and Snake, displaying a weather applet, and implementing Anthropic "artifacts"-like features. Stay tuned for updates on our community platform and documentation.
439
+ - **🔒 Experimental OAuth Support**: New experimental OAuth support. Check our documentation for more details.
440
+ - **🖼️ Custom Background Support**: Set a custom background from Settings > Interface to personalize your experience.
441
+ - **🔑 AUTOMATIC1111_API_AUTH Support**: Enhanced security for the AUTOMATIC1111 API.
442
+ - **🎨 Code Highlight Optimization**: Improved code highlighting features.
443
+ - **🎙️ Voice Interruption Feature**: Reintroduced and now toggleable from Settings > Interface.
444
+ - **💤 Wakelock API**: Now in use to prevent screen dimming during important tasks.
445
+ - **🔐 API Key Privacy**: All API keys are now hidden by default for better security.
446
+ - **🔍 New Web Search Provider**: Added jina_search as a new option.
447
+ - **🌐 Enhanced Internationalization (i18n)**: Improved Korean translation and updated Chinese and Ukrainian translations.
448
+
449
+ ### Fixed
450
+
451
+ - **🔧 Conversation Mode Issue**: Fixed the issue where Conversation Mode remained active after being removed from settings.
452
+ - **📏 Scroll Button Obstruction**: Resolved the issue where the scrollToBottom button container obstructed clicks on buttons beneath it.
453
+
454
+ ### Changed
455
+
456
+ - **⏲️ AIOHTTP_CLIENT_TIMEOUT**: Now set to 'None' by default for improved configuration flexibility.
457
+ - **📞 Voice Call Enhancements**: Improved by skipping code blocks and expressions during calls.
458
+ - **🚫 Error Message Handling**: Disabled the continuation of operations with error messages.
459
+ - **🗂️ Playground Relocation**: Moved the Playground from the workspace to the user menu for better user experience.
460
+
461
+ ## [0.3.5] - 2024-06-16
462
+
463
+ ### Added
464
+
465
+ - **📞 Enhanced Voice Call**: Text-to-speech (TTS) callback now operates in real-time for each sentence, reducing latency by not waiting for full completion.
466
+ - **👆 Tap to Interrupt**: During a call, you can now stop the assistant from speaking by simply tapping, instead of using voice. This resolves the issue of the speaker's voice being mistakenly registered as input.
467
+ - **😊 Emoji Call**: Toggle this feature on from the Settings > Interface, allowing LLMs to express emotions using emojis during voice calls for a more dynamic interaction.
468
+ - **🖱️ Quick Archive/Delete**: Use the Shift key + mouseover on the chat list to swiftly archive or delete items.
469
+ - **📝 Markdown Support in Model Descriptions**: You can now format model descriptions with markdown, enabling bold text, links, etc.
470
+ - **🧠 Editable Memories**: Adds the capability to modify memories.
471
+ - **📋 Admin Panel Sorting**: Introduces the ability to sort users/chats within the admin panel.
472
+ - **🌑 Dark Mode for Quick Selectors**: Dark mode now available for chat quick selectors (prompts, models, documents).
473
+ - **🔧 Advanced Parameters**: Adds 'num_keep' and 'num_batch' to advanced parameters for customization.
474
+ - **📅 Dynamic System Prompts**: New variables '{{CURRENT_DATETIME}}', '{{CURRENT_TIME}}', '{{USER_LOCATION}}' added for system prompts. Ensure '{{USER_LOCATION}}' is toggled on from Settings > Interface.
475
+ - **🌐 Tavily Web Search**: Includes Tavily as a web search provider option.
476
+ - **🖊️ Federated Auth Usernames**: Ability to set user names for federated authentication.
477
+ - **🔗 Auto Clean URLs**: When adding connection URLs, trailing slashes are now automatically removed.
478
+ - **🌐 Enhanced Translations**: Improved Chinese and Swedish translations.
479
+
480
+ ### Fixed
481
+
482
+ - **⏳ AIOHTTP_CLIENT_TIMEOUT**: Introduced a new environment variable 'AIOHTTP_CLIENT_TIMEOUT' for requests to Ollama lasting longer than 5 minutes. Default is 300 seconds; set to blank ('') for no timeout.
483
+ - **❌ Message Delete Freeze**: Resolved an issue where message deletion would sometimes cause the web UI to freeze.
484
+
485
+ ## [0.3.4] - 2024-06-12
486
+
487
+ ### Fixed
488
+
489
+ - **🔒 Mixed Content with HTTPS Issue**: Resolved a problem where mixed content (HTTP and HTTPS) was causing security warnings and blocking resources on HTTPS sites.
490
+ - **🔍 Web Search Issue**: Addressed the problem where web search functionality was not working correctly. The 'ENABLE_RAG_LOCAL_WEB_FETCH' option has been reintroduced to restore proper web searching capabilities.
491
+ - **💾 RAG Template Not Being Saved**: Fixed an issue where the RAG template was not being saved correctly, ensuring your custom templates are now preserved as expected.
492
+
493
+ ## [0.3.3] - 2024-06-12
494
+
495
+ ### Added
496
+
497
+ - **🛠️ Native Python Function Calling**: Introducing native Python function calling within Open WebUI. We’ve also included a built-in code editor to seamlessly develop and integrate function code within the 'Tools' workspace. With this, you can significantly enhance your LLM’s capabilities by creating custom RAG pipelines, web search tools, and even agent-like features such as sending Discord messages.
498
+ - **🌐 DuckDuckGo Integration**: Added DuckDuckGo as a web search provider, giving you more search options.
499
+ - **🌏 Enhanced Translations**: Improved translations for Vietnamese and Chinese languages, making the interface more accessible.
500
+
501
+ ### Fixed
502
+
503
+ - **🔗 Web Search URL Error Handling**: Fixed the issue where a single URL error would disrupt the data loading process in Web Search mode. Now, such errors will be handled gracefully to ensure uninterrupted data loading.
504
+ - **🖥️ Frontend Responsiveness**: Resolved the problem where the frontend would stop responding if the backend encounters an error while downloading a model. Improved error handling to maintain frontend stability.
505
+ - **🔧 Dependency Issues in pip**: Fixed issues related to pip installations, ensuring all dependencies are correctly managed to prevent installation errors.
506
+
507
+ ## [0.3.2] - 2024-06-10
508
+
509
+ ### Added
510
+
511
+ - **🔍 Web Search Query Status**: The web search query will now persist in the results section to aid in easier debugging and tracking of search queries.
512
+ - **🌐 New Web Search Provider**: We have added Serply as a new option for web search providers, giving you more choices for your search needs.
513
+ - **🌏 Improved Translations**: We've enhanced translations for Chinese and Portuguese.
514
+
515
+ ### Fixed
516
+
517
+ - **🎤 Audio File Upload Issue**: The bug that prevented audio files from being uploaded in chat input has been fixed, ensuring smooth communication.
518
+ - **💬 Message Input Handling**: Improved the handling of message inputs by instantly clearing images and text after sending, along with immediate visual indications when a response message is loading, enhancing user feedback.
519
+ - **⚙️ Parameter Registration and Validation**: Fixed the issue where parameters were not registering in certain cases and addressed the problem where users were unable to save due to invalid input errors.
520
+
521
+ ## [0.3.1] - 2024-06-09
522
+
523
+ ### Fixed
524
+
525
+ - **💬 Chat Functionality**: Resolved the issue where chat functionality was not working for specific models.
526
+
527
+ ## [0.3.0] - 2024-06-09
528
+
529
+ ### Added
530
+
531
+ - **📚 Knowledge Support for Models**: Attach documents directly to models from the models workspace, enhancing the information available to each model.
532
+ - **🎙️ Hands-Free Voice Call Feature**: Initiate voice calls without needing to use your hands, making interactions more seamless.
533
+ - **📹 Video Call Feature**: Enable video calls with supported vision models like Llava and GPT-4o, adding a visual dimension to your communications.
534
+ - **🎛️ Enhanced UI for Voice Recording**: Improved user interface for the voice recording feature, making it more intuitive and user-friendly.
535
+ - **🌐 External STT Support**: Now support for external Speech-To-Text services, providing more flexibility in choosing your STT provider.
536
+ - **⚙️ Unified Settings**: Consolidated settings including document settings under a new admin settings section for easier management.
537
+ - **🌑 Dark Mode Splash Screen**: A new splash screen for dark mode, ensuring a consistent and visually appealing experience for dark mode users.
538
+ - **📥 Upload Pipeline**: Directly upload pipelines from the admin settings > pipelines section, streamlining the pipeline management process.
539
+ - **🌍 Improved Language Support**: Enhanced support for Chinese and Ukrainian languages, better catering to a global user base.
540
+
541
+ ### Fixed
542
+
543
+ - **🛠️ Playground Issue**: Fixed the playground not functioning properly, ensuring a smoother user experience.
544
+ - **🔥 Temperature Parameter Issue**: Corrected the issue where the temperature value '0' was not being passed correctly.
545
+ - **📝 Prompt Input Clearing**: Resolved prompt input textarea not being cleared right away, ensuring a clean slate for new inputs.
546
+ - **✨ Various UI Styling Issues**: Fixed numerous user interface styling problems for a more cohesive look.
547
+ - **👥 Active Users Display**: Fixed active users showing active sessions instead of actual users, now reflecting accurate user activity.
548
+ - **🌐 Community Platform Compatibility**: The Community Platform is back online and fully compatible with Open WebUI.
549
+
550
+ ### Changed
551
+
552
+ - **📝 RAG Implementation**: Updated the RAG (Retrieval-Augmented Generation) implementation to use a system prompt for context, instead of overriding the user's prompt.
553
+ - **🔄 Settings Relocation**: Moved Models, Connections, Audio, and Images settings to the admin settings for better organization.
554
+ - **✍️ Improved Title Generation**: Enhanced the default prompt for title generation, yielding better results.
555
+ - **🔧 Backend Task Management**: Tasks like title generation and search query generation are now managed on the backend side and controlled only by the admin.
556
+ - **🔍 Editable Search Query Prompt**: You can now edit the search query generation prompt, offering more control over how queries are generated.
557
+ - **📏 Prompt Length Threshold**: Set the prompt length threshold for search query generation from the admin settings, giving more customization options.
558
+ - **📣 Settings Consolidation**: Merged the Banners admin setting with the Interface admin setting for a more streamlined settings area.
559
+
560
+ ## [0.2.5] - 2024-06-05
561
+
562
+ ### Added
563
+
564
+ - **👥 Active Users Indicator**: Now you can see how many people are currently active and what they are running. This helps you gauge when performance might slow down due to a high number of users.
565
+ - **🗂️ Create Ollama Modelfile**: The option to create a modelfile for Ollama has been reintroduced in the Settings > Models section, making it easier to manage your models.
566
+ - **⚙️ Default Model Setting**: Added an option to set the default model from Settings > Interface. This feature is now easily accessible, especially convenient for mobile users as it was previously hidden.
567
+ - **🌐 Enhanced Translations**: We've improved the Chinese translations and added support for Turkmen and Norwegian languages to make the interface more accessible globally.
568
+
569
+ ### Fixed
570
+
571
+ - **📱 Mobile View Improvements**: The UI now uses dvh (dynamic viewport height) instead of vh (viewport height), providing a better and more responsive experience for mobile users.
572
+
573
+ ## [0.2.4] - 2024-06-03
574
+
575
+ ### Added
576
+
577
+ - **👤 Improved Account Pending Page**: The account pending page now displays admin details by default to avoid confusion. You can disable this feature in the admin settings if needed.
578
+ - **🌐 HTTP Proxy Support**: We have enabled the use of the 'http_proxy' environment variable in OpenAI and Ollama API calls, making it easier to configure network settings.
579
+ - **❓ Quick Access to Documentation**: You can now easily access Open WebUI documents via a question mark button located at the bottom right corner of the screen (available on larger screens like PCs).
580
+ - **🌍 Enhanced Translation**: Improvements have been made to translations.
581
+
582
+ ### Fixed
583
+
584
+ - **🔍 SearxNG Web Search**: Fixed the issue where the SearxNG web search functionality was not working properly.
585
+
586
+ ## [0.2.3] - 2024-06-03
587
+
588
+ ### Added
589
+
590
+ - **📁 Export Chat as JSON**: You can now export individual chats as JSON files from the navbar menu by navigating to 'Download > Export Chat'. This makes sharing specific conversations easier.
591
+ - **✏️ Edit Titles with Double Click**: Double-click on titles to rename them quickly and efficiently.
592
+ - **🧩 Batch Multiple Embeddings**: Introduced 'RAG_EMBEDDING_OPENAI_BATCH_SIZE' to process multiple embeddings in a batch, enhancing performance for large datasets.
593
+ - **🌍 Improved Translations**: Enhanced the translation quality across various languages for a better user experience.
594
+
595
+ ### Fixed
596
+
597
+ - **🛠️ Modelfile Migration Script**: Fixed an issue where the modelfile migration script would fail if an invalid modelfile was encountered.
598
+ - **💬 Zhuyin Input Method on Mac**: Resolved an issue where using the Zhuyin input method in the Web UI on a Mac caused text to send immediately upon pressing the enter key, leading to incorrect input.
599
+ - **🔊 Local TTS Voice Selection**: Fixed the issue where the selected local Text-to-Speech (TTS) voice was not being displayed in settings.
600
+
601
+ ## [0.2.2] - 2024-06-02
602
+
603
+ ### Added
604
+
605
+ - **🌊 Mermaid Rendering Support**: We've included support for Mermaid rendering. This allows you to create beautiful diagrams and flowcharts directly within Open WebUI.
606
+ - **🔄 New Environment Variable 'RESET_CONFIG_ON_START'**: Introducing a new environment variable: 'RESET_CONFIG_ON_START'. Set this variable to reset your configuration settings upon starting the application, making it easier to revert to default settings.
607
+
608
+ ### Fixed
609
+
610
+ - **🔧 Pipelines Filter Issue**: We've addressed an issue with the pipelines where filters were not functioning as expected.
611
+
612
+ ## [0.2.1] - 2024-06-02
613
+
614
+ ### Added
615
+
616
+ - **🖱️ Single Model Export Button**: Easily export models with just one click using the new single model export button.
617
+ - **🖥️ Advanced Parameters Support**: Added support for 'num_thread', 'use_mmap', and 'use_mlock' parameters for Ollama.
618
+ - **🌐 Improved Vietnamese Translation**: Enhanced Vietnamese language support for a better user experience for our Vietnamese-speaking community.
619
+
620
+ ### Fixed
621
+
622
+ - **🔧 OpenAI URL API Save Issue**: Corrected a problem preventing the saving of OpenAI URL API settings.
623
+ - **🚫 Display Issue with Disabled Ollama API**: Fixed the display bug causing models to appear in settings when the Ollama API was disabled.
624
+
625
+ ### Changed
626
+
627
+ - **💡 Versioning Update**: As a reminder from our previous update, version 0.2.y will focus primarily on bug fixes, while major updates will be designated as 0.x from now on for better version tracking.
628
+
629
+ ## [0.2.0] - 2024-06-01
630
+
631
+ ### Added
632
+
633
+ - **🔧 Pipelines Support**: Open WebUI now includes a plugin framework for enhanced customization and functionality (https://github.com/open-webui/pipelines). Easily add custom logic and integrate Python libraries, from AI agents to home automation APIs.
634
+ - **🔗 Function Calling via Pipelines**: Integrate function calling seamlessly through Pipelines.
635
+ - **⚖️ User Rate Limiting via Pipelines**: Implement user-specific rate limits to manage API usage efficiently.
636
+ - **📊 Usage Monitoring with Langfuse**: Track and analyze usage statistics with Langfuse integration through Pipelines.
637
+ - **🕒 Conversation Turn Limits**: Set limits on conversation turns to manage interactions better through Pipelines.
638
+ - **🛡️ Toxic Message Filtering**: Automatically filter out toxic messages to maintain a safe environment using Pipelines.
639
+ - **🔍 Web Search Support**: Introducing built-in web search capabilities via RAG API, allowing users to search using SearXNG, Google Programmatic Search Engine, Brave Search, serpstack, and serper. Activate it effortlessly by adding necessary variables from Document settings > Web Params.
640
+ - **🗂️ Models Workspace**: Create and manage model presets for both Ollama/OpenAI API. Note: The old Modelfiles workspace is deprecated.
641
+ - **🛠️ Model Builder Feature**: Build and edit all models with persistent builder mode.
642
+ - **🏷️ Model Tagging Support**: Organize models with tagging features in the models workspace.
643
+ - **📋 Model Ordering Support**: Effortlessly organize models by dragging and dropping them into the desired positions within the models workspace.
644
+ - **📈 OpenAI Generation Stats**: Access detailed generation statistics for OpenAI models.
645
+ - **📅 System Prompt Variables**: New variables added: '{{CURRENT_DATE}}' and '{{USER_NAME}}' for dynamic prompts.
646
+ - **📢 Global Banner Support**: Manage global banners from admin settings > banners.
647
+ - **🗃️ Enhanced Archived Chats Modal**: Search and export archived chats easily.
648
+ - **📂 Archive All Button**: Quickly archive all chats from settings > chats.
649
+ - **🌐 Improved Translations**: Added and improved translations for French, Croatian, Cebuano, and Vietnamese.
650
+
651
+ ### Fixed
652
+
653
+ - **🔍 Archived Chats Visibility**: Resolved issue with archived chats not showing in the admin panel.
654
+ - **💬 Message Styling**: Fixed styling issues affecting message appearance.
655
+ - **🔗 Shared Chat Responses**: Corrected the issue where shared chat response messages were not readonly.
656
+ - **🖥️ UI Enhancement**: Fixed the scrollbar overlapping issue with the message box in the user interface.
657
+
658
+ ### Changed
659
+
660
+ - **💾 User Settings Storage**: User settings are now saved on the backend, ensuring consistency across all devices.
661
+ - **📡 Unified API Requests**: The API request for getting models is now unified to '/api/models' for easier usage.
662
+ - **🔄 Versioning Update**: Our versioning will now follow the format 0.x for major updates and 0.x.y for patches.
663
+ - **📦 Export All Chats (All Users)**: Moved this functionality to the Admin Panel settings for better organization and accessibility.
664
+
665
+ ### Removed
666
+
667
+ - **🚫 Bundled LiteLLM Support Deprecated**: Migrate your LiteLLM config.yaml to a self-hosted LiteLLM instance. LiteLLM can still be added via OpenAI Connections. Download the LiteLLM config.yaml from admin settings > database > export LiteLLM config.yaml.
668
+
669
+ ## [0.1.125] - 2024-05-19
670
+
671
+ ### Added
672
+
673
+ - **🔄 Updated UI**: Chat interface revamped with chat bubbles. Easily switch back to the old style via settings > interface > chat bubble UI.
674
+ - **📂 Enhanced Sidebar UI**: Model files, documents, prompts, and playground merged into Workspace for streamlined access.
675
+ - **🚀 Improved Many Model Interaction**: All responses now displayed simultaneously for a smoother experience.
676
+ - **🐍 Python Code Execution**: Execute Python code locally in the browser with libraries like 'requests', 'beautifulsoup4', 'numpy', 'pandas', 'seaborn', 'matplotlib', 'scikit-learn', 'scipy', 'regex'.
677
+ - **🧠 Experimental Memory Feature**: Manually input personal information you want LLMs to remember via settings > personalization > memory.
678
+ - **💾 Persistent Settings**: Settings now saved as config.json for convenience.
679
+ - **🩺 Health Check Endpoint**: Added for Docker deployment.
680
+ - **↕️ RTL Support**: Toggle chat direction via settings > interface > chat direction.
681
+ - **🖥️ PowerPoint Support**: RAG pipeline now supports PowerPoint documents.
682
+ - **🌐 Language Updates**: Ukrainian, Turkish, Arabic, Chinese, Serbian, Vietnamese updated; Punjabi added.
683
+
684
+ ### Changed
685
+
686
+ - **👤 Shared Chat Update**: Shared chat now includes creator user information.
687
+
688
+ ## [0.1.124] - 2024-05-08
689
+
690
+ ### Added
691
+
692
+ - **🖼️ Improved Chat Sidebar**: Now conveniently displays time ranges and organizes chats by today, yesterday, and more.
693
+ - **📜 Citations in RAG Feature**: Easily track the context fed to the LLM with added citations in the RAG feature.
694
+ - **🔒 Auth Disable Option**: Introducing the ability to disable authentication. Set 'WEBUI_AUTH' to False to disable authentication. Note: Only applicable for fresh installations without existing users.
695
+ - **📹 Enhanced YouTube RAG Pipeline**: Now supports non-English videos for an enriched experience.
696
+ - **🔊 Specify OpenAI TTS Models**: Customize your TTS experience by specifying OpenAI TTS models.
697
+ - **🔧 Additional Environment Variables**: Discover more environment variables in our comprehensive documentation at Open WebUI Documentation (https://docs.openwebui.com).
698
+ - **🌐 Language Support**: Arabic, Finnish, and Hindi added; Improved support for German, Vietnamese, and Chinese.
699
+
700
+ ### Fixed
701
+
702
+ - **🛠️ Model Selector Styling**: Addressed styling issues for improved user experience.
703
+ - **⚠️ Warning Messages**: Resolved backend warning messages.
704
+
705
+ ### Changed
706
+
707
+ - **📝 Title Generation**: Limited output to 50 tokens.
708
+ - **📦 Helm Charts**: Removed Helm charts, now available in a separate repository (https://github.com/open-webui/helm-charts).
709
+
710
+ ## [0.1.123] - 2024-05-02
711
+
712
+ ### Added
713
+
714
+ - **🎨 New Landing Page Design**: Refreshed design for a more modern look and optimized use of screen space.
715
+ - **📹 Youtube RAG Pipeline**: Introduces dedicated RAG pipeline for Youtube videos, enabling interaction with video transcriptions directly.
716
+ - **🔧 Enhanced Admin Panel**: Streamlined user management with options to add users directly or in bulk via CSV import.
717
+ - **👥 '@' Model Integration**: Easily switch to specific models during conversations; old collaborative chat feature phased out.
718
+ - **🌐 Language Enhancements**: Swedish translation added, plus improvements to German, Spanish, and the addition of Doge translation.
719
+
720
+ ### Fixed
721
+
722
+ - **🗑️ Delete Chat Shortcut**: Addressed issue where shortcut wasn't functioning.
723
+ - **🖼️ Modal Closing Bug**: Resolved unexpected closure of modal when dragging from within.
724
+ - **✏️ Edit Button Styling**: Fixed styling inconsistency with edit buttons.
725
+ - **🌐 Image Generation Compatibility Issue**: Rectified image generation compatibility issue with third-party APIs.
726
+ - **📱 iOS PWA Icon Fix**: Corrected iOS PWA home screen icon shape.
727
+ - **🔍 Scroll Gesture Bug**: Adjusted gesture sensitivity to prevent accidental activation when scrolling through code on mobile; now requires scrolling from the leftmost side to open the sidebar.
728
+
729
+ ### Changed
730
+
731
+ - **🔄 Unlimited Context Length**: Advanced settings now allow unlimited max context length (previously limited to 16000).
732
+ - **👑 Super Admin Assignment**: The first signup is automatically assigned a super admin role, unchangeable by other admins.
733
+ - **🛡️ Admin User Restrictions**: User action buttons from the admin panel are now disabled for users with admin roles.
734
+ - **🔝 Default Model Selector**: Set as default model option now exclusively available on the landing page.
735
+
736
+ ## [0.1.122] - 2024-04-27
737
+
738
+ ### Added
739
+
740
+ - **🌟 Enhanced RAG Pipeline**: Now with hybrid searching via 'BM25', reranking powered by 'CrossEncoder', and configurable relevance score thresholds.
741
+ - **🛢️ External Database Support**: Seamlessly connect to custom SQLite or Postgres databases using the 'DATABASE_URL' environment variable.
742
+ - **🌐 Remote ChromaDB Support**: Introducing the capability to connect to remote ChromaDB servers.
743
+ - **👨‍💼 Improved Admin Panel**: Admins can now conveniently check users' chat lists and last active status directly from the admin panel.
744
+ - **🎨 Splash Screen**: Introducing a loading splash screen for a smoother user experience.
745
+ - **🌍 Language Support Expansion**: Added support for Bangla (bn-BD), along with enhancements to Chinese, Spanish, and Ukrainian translations.
746
+ - **💻 Improved LaTeX Rendering Performance**: Enjoy faster rendering times for LaTeX equations.
747
+ - **🔧 More Environment Variables**: Explore additional environment variables in our documentation (https://docs.openwebui.com), including the 'ENABLE_LITELLM' option to manage memory usage.
748
+
749
+ ### Fixed
750
+
751
+ - **🔧 Ollama Compatibility**: Resolved errors occurring when Ollama server version isn't an integer, such as SHA builds or RCs.
752
+ - **🐛 Various OpenAI API Issues**: Addressed several issues related to the OpenAI API.
753
+ - **🛑 Stop Sequence Issue**: Fixed the problem where the stop sequence with a backslash '\' was not functioning.
754
+ - **🔤 Font Fallback**: Corrected font fallback issue.
755
+
756
+ ### Changed
757
+
758
+ - **⌨️ Prompt Input Behavior on Mobile**: Enter key prompt submission disabled on mobile devices for improved user experience.
759
+
760
+ ## [0.1.121] - 2024-04-24
761
+
762
+ ### Fixed
763
+
764
+ - **🔧 Translation Issues**: Addressed various translation discrepancies.
765
+ - **🔒 LiteLLM Security Fix**: Updated LiteLLM version to resolve a security vulnerability.
766
+ - **🖥️ HTML Tag Display**: Rectified the issue where the '< br >' tag wasn't displaying correctly.
767
+ - **🔗 WebSocket Connection**: Resolved the failure of WebSocket connection under HTTPS security for ComfyUI server.
768
+ - **📜 FileReader Optimization**: Implemented FileReader initialization per image in multi-file drag & drop to ensure reusability.
769
+ - **🏷️ Tag Display**: Corrected tag display inconsistencies.
770
+ - **📦 Archived Chat Styling**: Fixed styling issues in archived chat.
771
+ - **🔖 Safari Copy Button Bug**: Addressed the bug where the copy button failed to copy links in Safari.
772
+
773
+ ## [0.1.120] - 2024-04-20
774
+
775
+ ### Added
776
+
777
+ - **📦 Archive Chat Feature**: Easily archive chats with a new sidebar button, and access archived chats via the profile button > archived chats.
778
+ - **🔊 Configurable Text-to-Speech Endpoint**: Customize your Text-to-Speech experience with configurable OpenAI endpoints.
779
+ - **🛠️ Improved Error Handling**: Enhanced error message handling for connection failures.
780
+ - **⌨️ Enhanced Shortcut**: When editing messages, use ctrl/cmd+enter to save and submit, and esc to close.
781
+ - **🌐 Language Support**: Added support for Georgian and enhanced translations for Portuguese and Vietnamese.
782
+
783
+ ### Fixed
784
+
785
+ - **🔧 Model Selector**: Resolved issue where default model selection was not saving.
786
+ - **🔗 Share Link Copy Button**: Fixed bug where the copy button wasn't copying links in Safari.
787
+ - **🎨 Light Theme Styling**: Addressed styling issue with the light theme.
788
+
789
+ ## [0.1.119] - 2024-04-16
790
+
791
+ ### Added
792
+
793
+ - **🌟 Enhanced RAG Embedding Support**: Ollama, and OpenAI models can now be used for RAG embedding model.
794
+ - **🔄 Seamless Integration**: Copy 'ollama run <model name>' directly from Ollama page to easily select and pull models.
795
+ - **🏷️ Tagging Feature**: Add tags to chats directly via the sidebar chat menu.
796
+ - **📱 Mobile Accessibility**: Swipe left and right on mobile to effortlessly open and close the sidebar.
797
+ - **🔍 Improved Navigation**: Admin panel now supports pagination for user list.
798
+ - **🌍 Additional Language Support**: Added Polish language support.
799
+
800
+ ### Fixed
801
+
802
+ - **🌍 Language Enhancements**: Vietnamese and Spanish translations have been improved.
803
+ - **🔧 Helm Fixes**: Resolved issues with Helm trailing slash and manifest.json.
804
+
805
+ ### Changed
806
+
807
+ - **🐳 Docker Optimization**: Updated docker image build process to utilize 'uv' for significantly faster builds compared to 'pip3'.
808
+
809
+ ## [0.1.118] - 2024-04-10
810
+
811
+ ### Added
812
+
813
+ - **🦙 Ollama and CUDA Images**: Added support for ':ollama' and ':cuda' tagged images.
814
+ - **👍 Enhanced Response Rating**: Now you can annotate your ratings for better feedback.
815
+ - **👤 User Initials Profile Photo**: User initials are now the default profile photo.
816
+ - **🔍 Update RAG Embedding Model**: Customize RAG embedding model directly in document settings.
817
+ - **🌍 Additional Language Support**: Added Turkish language support.
818
+
819
+ ### Fixed
820
+
821
+ - **🔒 Share Chat Permission**: Resolved issue with chat sharing permissions.
822
+ - **🛠 Modal Close**: Modals can now be closed using the Esc key.
823
+
824
+ ### Changed
825
+
826
+ - **🎨 Admin Panel Styling**: Refreshed styling for the admin panel.
827
+ - **🐳 Docker Image Build**: Updated docker image build process for improved efficiency.
828
+
829
+ ## [0.1.117] - 2024-04-03
830
+
831
+ ### Added
832
+
833
+ - 🗨️ **Local Chat Sharing**: Share chat links seamlessly between users.
834
+ - 🔑 **API Key Generation Support**: Generate secret keys to leverage Open WebUI with OpenAI libraries.
835
+ - 📄 **Chat Download as PDF**: Easily download chats in PDF format.
836
+ - 📝 **Improved Logging**: Enhancements to logging functionality.
837
+ - 📧 **Trusted Email Authentication**: Authenticate using a trusted email header.
838
+
839
+ ### Fixed
840
+
841
+ - 🌷 **Enhanced Dutch Translation**: Improved translation for Dutch users.
842
+ - ⚪ **White Theme Styling**: Resolved styling issue with the white theme.
843
+ - 📜 **LaTeX Chat Screen Overflow**: Fixed screen overflow issue with LaTeX rendering.
844
+ - 🔒 **Security Patches**: Applied necessary security patches.
845
+
846
+ ## [0.1.116] - 2024-03-31
847
+
848
+ ### Added
849
+
850
+ - **🔄 Enhanced UI**: Model selector now conveniently located in the navbar, enabling seamless switching between multiple models during conversations.
851
+ - **🔍 Improved Model Selector**: Directly pull a model from the selector/Models now display detailed information for better understanding.
852
+ - **💬 Webhook Support**: Now compatible with Google Chat and Microsoft Teams.
853
+ - **🌐 Localization**: Korean translation (I18n) now available.
854
+ - **🌑 Dark Theme**: OLED dark theme introduced for reduced strain during prolonged usage.
855
+ - **🏷️ Tag Autocomplete**: Dropdown feature added for effortless chat tagging.
856
+
857
+ ### Fixed
858
+
859
+ - **🔽 Auto-Scrolling**: Addressed OpenAI auto-scrolling issue.
860
+ - **🏷️ Tag Validation**: Implemented tag validation to prevent empty string tags.
861
+ - **🚫 Model Whitelisting**: Resolved LiteLLM model whitelisting issue.
862
+ - **✅ Spelling**: Corrected various spelling issues for improved readability.
863
+
864
+ ## [0.1.115] - 2024-03-24
865
+
866
+ ### Added
867
+
868
+ - **🔍 Custom Model Selector**: Easily find and select custom models with the new search filter feature.
869
+ - **🛑 Cancel Model Download**: Added the ability to cancel model downloads.
870
+ - **🎨 Image Generation ComfyUI**: Image generation now supports ComfyUI.
871
+ - **🌟 Updated Light Theme**: Updated the light theme for a fresh look.
872
+ - **🌍 Additional Language Support**: Now supporting Bulgarian, Italian, Portuguese, Japanese, and Dutch.
873
+
874
+ ### Fixed
875
+
876
+ - **🔧 Fixed Broken Experimental GGUF Upload**: Resolved issues with experimental GGUF upload functionality.
877
+
878
+ ### Changed
879
+
880
+ - **🔄 Vector Storage Reset Button**: Moved the reset vector storage button to document settings.
881
+
882
+ ## [0.1.114] - 2024-03-20
883
+
884
+ ### Added
885
+
886
+ - **🔗 Webhook Integration**: Now you can subscribe to new user sign-up events via webhook. Simply navigate to the admin panel > admin settings > webhook URL.
887
+ - **🛡️ Enhanced Model Filtering**: Alongside Ollama, OpenAI proxy model whitelisting, we've added model filtering functionality for LiteLLM proxy.
888
+ - **🌍 Expanded Language Support**: Spanish, Catalan, and Vietnamese languages are now available, with improvements made to others.
889
+
890
+ ### Fixed
891
+
892
+ - **🔧 Input Field Spelling**: Resolved issue with spelling mistakes in input fields.
893
+ - **🖊️ Light Mode Styling**: Fixed styling issue with light mode in document adding.
894
+
895
+ ### Changed
896
+
897
+ - **🔄 Language Sorting**: Languages are now sorted alphabetically by their code for improved organization.
898
+
899
+ ## [0.1.113] - 2024-03-18
900
+
901
+ ### Added
902
+
903
+ - 🌍 **Localization**: You can now change the UI language in Settings > General. We support Ukrainian, German, Farsi (Persian), Traditional and Simplified Chinese and French translations. You can help us to translate the UI into your language! More info in our [CONTRIBUTION.md](https://github.com/open-webui/open-webui/blob/main/docs/CONTRIBUTING.md#-translations-and-internationalization).
904
+ - 🎨 **System-wide Theme**: Introducing a new system-wide theme for enhanced visual experience.
905
+
906
+ ### Fixed
907
+
908
+ - 🌑 **Dark Background on Select Fields**: Improved readability by adding a dark background to select fields, addressing issues on certain browsers/devices.
909
+ - **Multiple OPENAI_API_BASE_URLS Issue**: Resolved issue where multiple base URLs caused conflicts when one wasn't functioning.
910
+ - **RAG Encoding Issue**: Fixed encoding problem in RAG.
911
+ - **npm Audit Fix**: Addressed npm audit findings.
912
+ - **Reduced Scroll Threshold**: Improved auto-scroll experience by reducing the scroll threshold from 50px to 5px.
913
+
914
+ ### Changed
915
+
916
+ - 🔄 **Sidebar UI Update**: Updated sidebar UI to feature a chat menu dropdown, replacing two icons for improved navigation.
917
+
918
+ ## [0.1.112] - 2024-03-15
919
+
920
+ ### Fixed
921
+
922
+ - 🗨️ Resolved chat malfunction after image generation.
923
+ - 🎨 Fixed various RAG issues.
924
+ - 🧪 Rectified experimental broken GGUF upload logic.
925
+
926
+ ## [0.1.111] - 2024-03-10
927
+
928
+ ### Added
929
+
930
+ - 🛡️ **Model Whitelisting**: Admins now have the ability to whitelist models for users with the 'user' role.
931
+ - 🔄 **Update All Models**: Added a convenient button to update all models at once.
932
+ - 📄 **Toggle PDF OCR**: Users can now toggle PDF OCR option for improved parsing performance.
933
+ - 🎨 **DALL-E Integration**: Introduced DALL-E integration for image generation alongside automatic1111.
934
+ - 🛠️ **RAG API Refactoring**: Refactored RAG logic and exposed its API, with additional documentation to follow.
935
+
936
+ ### Fixed
937
+
938
+ - 🔒 **Max Token Settings**: Added max token settings for anthropic/claude-3-sonnet-20240229 (Issue #1094).
939
+ - 🔧 **Misalignment Issue**: Corrected misalignment of Edit and Delete Icons when Chat Title is Empty (Issue #1104).
940
+ - 🔄 **Context Loss Fix**: Resolved RAG losing context on model response regeneration with Groq models via API key (Issue #1105).
941
+ - 📁 **File Handling Bug**: Addressed File Not Found Notification when Dropping a Conversation Element (Issue #1098).
942
+ - 🖱️ **Dragged File Styling**: Fixed dragged file layover styling issue.
943
+
944
+ ## [0.1.110] - 2024-03-06
945
+
946
+ ### Added
947
+
948
+ - **🌐 Multiple OpenAI Servers Support**: Enjoy seamless integration with multiple OpenAI-compatible APIs, now supported natively.
949
+
950
+ ### Fixed
951
+
952
+ - **🔍 OCR Issue**: Resolved PDF parsing issue caused by OCR malfunction.
953
+ - **🚫 RAG Issue**: Fixed the RAG functionality, ensuring it operates smoothly.
954
+ - **📄 "Add Docs" Model Button**: Addressed the non-functional behavior of the "Add Docs" model button.
955
+
956
+ ## [0.1.109] - 2024-03-06
957
+
958
+ ### Added
959
+
960
+ - **🔄 Multiple Ollama Servers Support**: Enjoy enhanced scalability and performance with support for multiple Ollama servers in a single WebUI. Load balancing features are now available, providing improved efficiency (#788, #278).
961
+ - **🔧 Support for Claude 3 and Gemini**: Responding to user requests, we've expanded our toolset to include Claude 3 and Gemini, offering a wider range of functionalities within our platform (#1064).
962
+ - **🔍 OCR Functionality for PDF Loader**: We've augmented our PDF loader with Optical Character Recognition (OCR) capabilities. Now, extract text from scanned documents and images within PDFs, broadening the scope of content processing (#1050).
963
+
964
+ ### Fixed
965
+
966
+ - **🛠️ RAG Collection**: Implemented a dynamic mechanism to recreate RAG collections, ensuring users have up-to-date and accurate data (#1031).
967
+ - **📝 User Agent Headers**: Fixed issue of RAG web requests being sent with empty user_agent headers, reducing rejections from certain websites. Realistic headers are now utilized for these requests (#1024).
968
+ - **⏹️ Playground Cancel Functionality**: Introducing a new "Cancel" option for stopping Ollama generation in the Playground, enhancing user control and usability (#1006).
969
+ - **🔤 Typographical Error in 'ASSISTANT' Field**: Corrected a typographical error in the 'ASSISTANT' field within the GGUF model upload template for accuracy and consistency (#1061).
970
+
971
+ ### Changed
972
+
973
+ - **🔄 Refactored Message Deletion Logic**: Streamlined message deletion process for improved efficiency and user experience, simplifying interactions within the platform (#1004).
974
+ - **⚠️ Deprecation of `OLLAMA_API_BASE_URL`**: Deprecated `OLLAMA_API_BASE_URL` environment variable; recommend using `OLLAMA_BASE_URL` instead. Refer to our documentation for further details.
975
+
976
+ ## [0.1.108] - 2024-03-02
977
+
978
+ ### Added
979
+
980
+ - **🎮 Playground Feature (Beta)**: Explore the full potential of the raw API through an intuitive UI with our new playground feature, accessible to admins. Simply click on the bottom name area of the sidebar to access it. The playground feature offers two modes text completion (notebook) and chat completion. As it's in beta, please report any issues you encounter.
981
+ - **🛠️ Direct Database Download for Admins**: Admins can now download the database directly from the WebUI via the admin settings.
982
+ - **🎨 Additional RAG Settings**: Customize your RAG process with the ability to edit the TOP K value. Navigate to Documents > Settings > General to make changes.
983
+ - **🖥️ UI Improvements**: Tooltips now available in the input area and sidebar handle. More tooltips will be added across other parts of the UI.
984
+
985
+ ### Fixed
986
+
987
+ - Resolved input autofocus issue on mobile when the sidebar is open, making it easier to use.
988
+ - Corrected numbered list display issue in Safari (#963).
989
+ - Restricted user ability to delete chats without proper permissions (#993).
990
+
991
+ ### Changed
992
+
993
+ - **Simplified Ollama Settings**: Ollama settings now don't require the `/api` suffix. You can now utilize the Ollama base URL directly, e.g., `http://localhost:11434`. Also, an `OLLAMA_BASE_URL` environment variable has been added.
994
+ - **Database Renaming**: Starting from this release, `ollama.db` will be automatically renamed to `webui.db`.
995
+
996
+ ## [0.1.107] - 2024-03-01
997
+
998
+ ### Added
999
+
1000
+ - **🚀 Makefile and LLM Update Script**: Included Makefile and a script for LLM updates in the repository.
1001
+
1002
+ ### Fixed
1003
+
1004
+ - Corrected issue where links in the settings modal didn't appear clickable (#960).
1005
+ - Fixed problem with web UI port not taking effect due to incorrect environment variable name in run-compose.sh (#996).
1006
+ - Enhanced user experience by displaying chat in browser title and enabling automatic scrolling to the bottom (#992).
1007
+
1008
+ ### Changed
1009
+
1010
+ - Upgraded toast library from `svelte-french-toast` to `svelte-sonner` for a more polished UI.
1011
+ - Enhanced accessibility with the addition of dark mode on the authentication page.
1012
+
1013
+ ## [0.1.106] - 2024-02-27
1014
+
1015
+ ### Added
1016
+
1017
+ - **🎯 Auto-focus Feature**: The input area now automatically focuses when initiating or opening a chat conversation.
1018
+
1019
+ ### Fixed
1020
+
1021
+ - Corrected typo from "HuggingFace" to "Hugging Face" (Issue #924).
1022
+ - Resolved bug causing errors in chat completion API calls to OpenAI due to missing "num_ctx" parameter (Issue #927).
1023
+ - Fixed issues preventing text editing, selection, and cursor retention in the input field (Issue #940).
1024
+ - Fixed a bug where defining an OpenAI-compatible API server using 'OPENAI_API_BASE_URL' containing 'openai' string resulted in hiding models not containing 'gpt' string from the model menu. (Issue #930)
1025
+
1026
+ ## [0.1.105] - 2024-02-25
1027
+
1028
+ ### Added
1029
+
1030
+ - **📄 Document Selection**: Now you can select and delete multiple documents at once for easier management.
1031
+
1032
+ ### Changed
1033
+
1034
+ - **🏷️ Document Pre-tagging**: Simply click the "+" button at the top, enter tag names in the popup window, or select from a list of existing tags. Then, upload files with the added tags for streamlined organization.
1035
+
1036
+ ## [0.1.104] - 2024-02-25
1037
+
1038
+ ### Added
1039
+
1040
+ - **🔄 Check for Updates**: Keep your system current by checking for updates conveniently located in Settings > About.
1041
+ - **🗑️ Automatic Tag Deletion**: Unused tags on the sidebar will now be deleted automatically with just a click.
1042
+
1043
+ ### Changed
1044
+
1045
+ - **🎨 Modernized Styling**: Enjoy a refreshed look with updated styling for a more contemporary experience.
1046
+
1047
+ ## [0.1.103] - 2024-02-25
1048
+
1049
+ ### Added
1050
+
1051
+ - **🔗 Built-in LiteLLM Proxy**: Now includes LiteLLM proxy within Open WebUI for enhanced functionality.
1052
+
1053
+ - Easily integrate existing LiteLLM configurations using `-v /path/to/config.yaml:/app/backend/data/litellm/config.yaml` flag.
1054
+ - When utilizing Docker container to run Open WebUI, ensure connections to localhost use `host.docker.internal`.
1055
+
1056
+ - **🖼️ Image Generation Enhancements**: Introducing Advanced Settings with Image Preview Feature.
1057
+ - Customize image generation by setting the number of steps; defaults to A1111 value.
1058
+
1059
+ ### Fixed
1060
+
1061
+ - Resolved issue with RAG scan halting document loading upon encountering unsupported MIME types or exceptions (Issue #866).
1062
+
1063
+ ### Changed
1064
+
1065
+ - Ollama is no longer required to run Open WebUI.
1066
+ - Access our comprehensive documentation at [Open WebUI Documentation](https://docs.openwebui.com/).
1067
+
1068
+ ## [0.1.102] - 2024-02-22
1069
+
1070
+ ### Added
1071
+
1072
+ - **🖼️ Image Generation**: Generate Images using the AUTOMATIC1111/stable-diffusion-webui API. You can set this up in Settings > Images.
1073
+ - **📝 Change title generation prompt**: Change the prompt used to generate titles for your chats. You can set this up in the Settings > Interface.
1074
+ - **🤖 Change embedding model**: Change the embedding model used to generate embeddings for your chats in the Dockerfile. Use any sentence transformer model from huggingface.co.
1075
+ - **📢 CHANGELOG.md/Popup**: This popup will show you the latest changes.
1076
+
1077
+ ## [0.1.101] - 2024-02-22
1078
+
1079
+ ### Fixed
1080
+
1081
+ - LaTex output formatting issue (#828)
1082
+
1083
+ ### Changed
1084
+
1085
+ - Instead of having the previous 1.0.0-alpha.101, we switched to semantic versioning as a way to respect global conventions.
CODE_OF_CONDUCT.md ADDED
@@ -0,0 +1,77 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Contributor Covenant Code of Conduct
2
+
3
+ ## Our Pledge
4
+
5
+ We as members, contributors, and leaders pledge to make participation in our
6
+ community a harassment-free experience for everyone, regardless of age, body
7
+ size, visible or invisible disability, ethnicity, sex characteristics, gender
8
+ identity and expression, level of experience, education, socio-economic status,
9
+ nationality, personal appearance, race, religion, or sexual identity
10
+ and orientation.
11
+
12
+ We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community.
13
+
14
+ ## Our Standards
15
+
16
+ Examples of behavior that contribute to a positive environment for our community include:
17
+
18
+ - Demonstrating empathy and kindness toward other people
19
+ - Being respectful of differing opinions, viewpoints, and experiences
20
+ - Giving and gracefully accepting constructive feedback
21
+ - Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience
22
+ - Focusing on what is best not just for us as individuals, but for the overall community
23
+
24
+ Examples of unacceptable behavior include:
25
+
26
+ - The use of sexualized language or imagery, and sexual attention or advances of any kind
27
+ - Trolling, insulting or derogatory comments, and personal or political attacks
28
+ - Public or private harassment
29
+ - Publishing others' private information, such as a physical or email address, without their explicit permission
30
+ - **Spamming of any kind**
31
+ - Aggressive sales tactics targeting our community members are strictly prohibited. You can mention your product if it's relevant to the discussion, but under no circumstances should you push it forcefully
32
+ - Other conduct which could reasonably be considered inappropriate in a professional setting
33
+
34
+ ## Enforcement Responsibilities
35
+
36
+ Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful.
37
+
38
+ ## Scope
39
+
40
+ This Code of Conduct applies within all community spaces and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event.
41
+
42
+ ## Enforcement
43
+
44
+ Instances of abusive, harassing, spamming, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at [email protected]. All complaints will be reviewed and investigated promptly and fairly.
45
+
46
+ All community leaders are obligated to respect the privacy and security of the reporter of any incident.
47
+
48
+ ## Enforcement Guidelines
49
+
50
+ Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct:
51
+
52
+ ### 1. Temporary Ban
53
+
54
+ **Community Impact**: Any violation of community standards, including but not limited to inappropriate language, unprofessional behavior, harassment, or spamming.
55
+
56
+ **Consequence**: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban.
57
+
58
+ ### 2. Permanent Ban
59
+
60
+ **Community Impact**: Repeated or severe violations of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals.
61
+
62
+ **Consequence**: A permanent ban from any sort of public interaction within the community.
63
+
64
+ ## Attribution
65
+
66
+ This Code of Conduct is adapted from the [Contributor Covenant][homepage],
67
+ version 2.0, available at
68
+ https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
69
+
70
+ Community Impact Guidelines were inspired by [Mozilla's code of conduct
71
+ enforcement ladder](https://github.com/mozilla/diversity).
72
+
73
+ [homepage]: https://www.contributor-covenant.org
74
+
75
+ For answers to common questions about this code of conduct, see the FAQ at
76
+ https://www.contributor-covenant.org/faq. Translations are available at
77
+ https://www.contributor-covenant.org/translations.
Caddyfile.localhost ADDED
@@ -0,0 +1,64 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Run with
2
+ # caddy run --envfile ./example.env --config ./Caddyfile.localhost
3
+ #
4
+ # This is configured for
5
+ # - Automatic HTTPS (even for localhost)
6
+ # - Reverse Proxying to Ollama API Base URL (http://localhost:11434/api)
7
+ # - CORS
8
+ # - HTTP Basic Auth API Tokens (uncomment basicauth section)
9
+
10
+
11
+ # CORS Preflight (OPTIONS) + Request (GET, POST, PATCH, PUT, DELETE)
12
+ (cors-api) {
13
+ @match-cors-api-preflight method OPTIONS
14
+ handle @match-cors-api-preflight {
15
+ header {
16
+ Access-Control-Allow-Origin "{http.request.header.origin}"
17
+ Access-Control-Allow-Methods "GET, POST, PUT, PATCH, DELETE, OPTIONS"
18
+ Access-Control-Allow-Headers "Origin, Accept, Authorization, Content-Type, X-Requested-With"
19
+ Access-Control-Allow-Credentials "true"
20
+ Access-Control-Max-Age "3600"
21
+ defer
22
+ }
23
+ respond "" 204
24
+ }
25
+
26
+ @match-cors-api-request {
27
+ not {
28
+ header Origin "{http.request.scheme}://{http.request.host}"
29
+ }
30
+ header Origin "{http.request.header.origin}"
31
+ }
32
+ handle @match-cors-api-request {
33
+ header {
34
+ Access-Control-Allow-Origin "{http.request.header.origin}"
35
+ Access-Control-Allow-Methods "GET, POST, PUT, PATCH, DELETE, OPTIONS"
36
+ Access-Control-Allow-Headers "Origin, Accept, Authorization, Content-Type, X-Requested-With"
37
+ Access-Control-Allow-Credentials "true"
38
+ Access-Control-Max-Age "3600"
39
+ defer
40
+ }
41
+ }
42
+ }
43
+
44
+ # replace localhost with example.com or whatever
45
+ localhost {
46
+ ## HTTP Basic Auth
47
+ ## (uncomment to enable)
48
+ # basicauth {
49
+ # # see .example.env for how to generate tokens
50
+ # {env.OLLAMA_API_ID} {env.OLLAMA_API_TOKEN_DIGEST}
51
+ # }
52
+
53
+ handle /api/* {
54
+ # Comment to disable CORS
55
+ import cors-api
56
+
57
+ reverse_proxy localhost:11434
58
+ }
59
+
60
+ # Same-Origin Static Web Server
61
+ file_server {
62
+ root ./build/
63
+ }
64
+ }
Dockerfile ADDED
@@ -0,0 +1,166 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # syntax=docker/dockerfile:1
2
+ # Initialize device type args
3
+ # use build args in the docker build commmand with --build-arg="BUILDARG=true"
4
+ ARG USE_CUDA=false
5
+ ARG USE_OLLAMA=false
6
+ # Tested with cu117 for CUDA 11 and cu121 for CUDA 12 (default)
7
+ ARG USE_CUDA_VER=cu121
8
+ # any sentence transformer model; models to use can be found at https://huggingface.co/models?library=sentence-transformers
9
+ # Leaderboard: https://huggingface.co/spaces/mteb/leaderboard
10
+ # for better performance and multilangauge support use "intfloat/multilingual-e5-large" (~2.5GB) or "intfloat/multilingual-e5-base" (~1.5GB)
11
+ # IMPORTANT: If you change the embedding model (sentence-transformers/all-MiniLM-L6-v2) and vice versa, you aren't able to use RAG Chat with your previous documents loaded in the WebUI! You need to re-embed them.
12
+ ARG USE_EMBEDDING_MODEL=sentence-transformers/all-MiniLM-L6-v2
13
+ ARG USE_RERANKING_MODEL=""
14
+ ARG BUILD_HASH=dev-build
15
+ # Override at your own risk - non-root configurations are untested
16
+ ARG UID=0
17
+ ARG GID=0
18
+
19
+ ######## WebUI frontend ########
20
+ FROM --platform=$BUILDPLATFORM node:21-alpine3.19 as build
21
+ ARG BUILD_HASH
22
+
23
+ WORKDIR /app
24
+
25
+ COPY package.json package-lock.json ./
26
+ RUN npm ci
27
+
28
+ COPY . .
29
+ ENV APP_BUILD_HASH=${BUILD_HASH}
30
+ RUN npm run build
31
+
32
+ ######## WebUI backend ########
33
+ FROM python:3.11-slim-bookworm as base
34
+
35
+ # Use args
36
+ ARG USE_CUDA
37
+ ARG USE_OLLAMA
38
+ ARG USE_CUDA_VER
39
+ ARG USE_EMBEDDING_MODEL
40
+ ARG USE_RERANKING_MODEL
41
+ ARG UID
42
+ ARG GID
43
+
44
+ ## Basis ##
45
+ ENV ENV=prod \
46
+ PORT=8080 \
47
+ # pass build args to the build
48
+ USE_OLLAMA_DOCKER=${USE_OLLAMA} \
49
+ USE_CUDA_DOCKER=${USE_CUDA} \
50
+ USE_CUDA_DOCKER_VER=${USE_CUDA_VER} \
51
+ USE_EMBEDDING_MODEL_DOCKER=${USE_EMBEDDING_MODEL} \
52
+ USE_RERANKING_MODEL_DOCKER=${USE_RERANKING_MODEL}
53
+
54
+ ## Basis URL Config ##
55
+ ENV OLLAMA_BASE_URL="/ollama" \
56
+ OPENAI_API_BASE_URL=""
57
+
58
+ ## API Key and Security Config ##
59
+ ENV OPENAI_API_KEY="" \
60
+ WEBUI_SECRET_KEY="" \
61
+ SCARF_NO_ANALYTICS=true \
62
+ DO_NOT_TRACK=true \
63
+ ANONYMIZED_TELEMETRY=false
64
+
65
+ #### Other models #########################################################
66
+ ## whisper TTS model settings ##
67
+ ENV WHISPER_MODEL="base" \
68
+ WHISPER_MODEL_DIR="/app/backend/data/cache/whisper/models"
69
+
70
+ ## RAG Embedding model settings ##
71
+ ENV RAG_EMBEDDING_MODEL="$USE_EMBEDDING_MODEL_DOCKER" \
72
+ RAG_RERANKING_MODEL="$USE_RERANKING_MODEL_DOCKER" \
73
+ SENTENCE_TRANSFORMERS_HOME="/app/backend/data/cache/embedding/models"
74
+
75
+ ## Hugging Face download cache ##
76
+ ENV HF_HOME="/app/backend/data/cache/embedding/models"
77
+
78
+ ## Torch Extensions ##
79
+ # ENV TORCH_EXTENSIONS_DIR="/.cache/torch_extensions"
80
+
81
+ #### Other models ##########################################################
82
+
83
+ WORKDIR /app/backend
84
+
85
+ ENV HOME /root
86
+ # Create user and group if not root
87
+ RUN if [ $UID -ne 0 ]; then \
88
+ if [ $GID -ne 0 ]; then \
89
+ addgroup --gid $GID app; \
90
+ fi; \
91
+ adduser --uid $UID --gid $GID --home $HOME --disabled-password --no-create-home app; \
92
+ fi
93
+
94
+ RUN mkdir -p $HOME/.cache/chroma
95
+ RUN echo -n 00000000-0000-0000-0000-000000000000 > $HOME/.cache/chroma/telemetry_user_id
96
+
97
+ # Make sure the user has access to the app and root directory
98
+ RUN chown -R $UID:$GID /app $HOME
99
+
100
+ RUN if [ "$USE_OLLAMA" = "true" ]; then \
101
+ apt-get update && \
102
+ # Install pandoc and netcat
103
+ apt-get install -y --no-install-recommends git build-essential pandoc netcat-openbsd curl && \
104
+ apt-get install -y --no-install-recommends gcc python3-dev && \
105
+ # for RAG OCR
106
+ apt-get install -y --no-install-recommends ffmpeg libsm6 libxext6 && \
107
+ # install helper tools
108
+ apt-get install -y --no-install-recommends curl jq && \
109
+ # install ollama
110
+ curl -fsSL https://ollama.com/install.sh | sh && \
111
+ # cleanup
112
+ rm -rf /var/lib/apt/lists/*; \
113
+ else \
114
+ apt-get update && \
115
+ # Install pandoc, netcat and gcc
116
+ apt-get install -y --no-install-recommends git build-essential pandoc gcc netcat-openbsd curl jq && \
117
+ apt-get install -y --no-install-recommends gcc python3-dev && \
118
+ # for RAG OCR
119
+ apt-get install -y --no-install-recommends ffmpeg libsm6 libxext6 && \
120
+ # cleanup
121
+ rm -rf /var/lib/apt/lists/*; \
122
+ fi
123
+
124
+ # install python dependencies
125
+ COPY --chown=$UID:$GID ./backend/requirements.txt ./requirements.txt
126
+
127
+ RUN pip3 install uv && \
128
+ if [ "$USE_CUDA" = "true" ]; then \
129
+ # If you use CUDA the whisper and embedding model will be downloaded on first use
130
+ pip3 install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/$USE_CUDA_DOCKER_VER --no-cache-dir && \
131
+ uv pip install --system -r requirements.txt --no-cache-dir && \
132
+ python -c "import os; from sentence_transformers import SentenceTransformer; SentenceTransformer(os.environ['RAG_EMBEDDING_MODEL'], device='cpu')" && \
133
+ 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'])"; \
134
+ else \
135
+ pip3 install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cpu --no-cache-dir && \
136
+ uv pip install --system -r requirements.txt --no-cache-dir && \
137
+ python -c "import os; from sentence_transformers import SentenceTransformer; SentenceTransformer(os.environ['RAG_EMBEDDING_MODEL'], device='cpu')" && \
138
+ 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'])"; \
139
+ fi; \
140
+ chown -R $UID:$GID /app/backend/data/
141
+
142
+
143
+
144
+ # copy embedding weight from build
145
+ # RUN mkdir -p /root/.cache/chroma/onnx_models/all-MiniLM-L6-v2
146
+ # COPY --from=build /app/onnx /root/.cache/chroma/onnx_models/all-MiniLM-L6-v2/onnx
147
+
148
+ # copy built frontend files
149
+ COPY --chown=$UID:$GID --from=build /app/build /app/build
150
+ COPY --chown=$UID:$GID --from=build /app/CHANGELOG.md /app/CHANGELOG.md
151
+ COPY --chown=$UID:$GID --from=build /app/package.json /app/package.json
152
+
153
+ # copy backend files
154
+ COPY --chown=$UID:$GID ./backend .
155
+
156
+ EXPOSE 8080
157
+
158
+ HEALTHCHECK CMD curl --silent --fail http://localhost:${PORT:-8080}/health | jq -ne 'input.status == true' || exit 1
159
+
160
+ USER $UID:$GID
161
+
162
+ ARG BUILD_HASH
163
+ ENV WEBUI_BUILD_VERSION=${BUILD_HASH}
164
+ ENV DOCKER true
165
+
166
+ CMD [ "bash", "start.sh"]
INSTALLATION.md ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ### Installing Both Ollama and Open WebUI Using Kustomize
2
+
3
+ For cpu-only pod
4
+
5
+ ```bash
6
+ kubectl apply -f ./kubernetes/manifest/base
7
+ ```
8
+
9
+ For gpu-enabled pod
10
+
11
+ ```bash
12
+ kubectl apply -k ./kubernetes/manifest
13
+ ```
14
+
15
+ ### Installing Both Ollama and Open WebUI Using Helm
16
+
17
+ Package Helm file first
18
+
19
+ ```bash
20
+ helm package ./kubernetes/helm/
21
+ ```
22
+
23
+ For cpu-only pod
24
+
25
+ ```bash
26
+ helm install ollama-webui ./ollama-webui-*.tgz
27
+ ```
28
+
29
+ For gpu-enabled pod
30
+
31
+ ```bash
32
+ helm install ollama-webui ./ollama-webui-*.tgz --set ollama.resources.limits.nvidia.com/gpu="1"
33
+ ```
34
+
35
+ Check the `kubernetes/helm/values.yaml` file to know which parameters are available for customization
LICENSE ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ MIT License
2
+
3
+ Copyright (c) 2023 Timothy Jaeryang Baek
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
Makefile ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ ifneq ($(shell which docker-compose 2>/dev/null),)
3
+ DOCKER_COMPOSE := docker-compose
4
+ else
5
+ DOCKER_COMPOSE := docker compose
6
+ endif
7
+
8
+ install:
9
+ $(DOCKER_COMPOSE) up -d
10
+
11
+ remove:
12
+ @chmod +x confirm_remove.sh
13
+ @./confirm_remove.sh
14
+
15
+ start:
16
+ $(DOCKER_COMPOSE) start
17
+ startAndBuild:
18
+ $(DOCKER_COMPOSE) up -d --build
19
+
20
+ stop:
21
+ $(DOCKER_COMPOSE) stop
22
+
23
+ update:
24
+ # Calls the LLM update script
25
+ chmod +x update_ollama_models.sh
26
+ @./update_ollama_models.sh
27
+ @git pull
28
+ $(DOCKER_COMPOSE) down
29
+ # Make sure the ollama-webui container is stopped before rebuilding
30
+ @docker stop open-webui || true
31
+ $(DOCKER_COMPOSE) up --build -d
32
+ $(DOCKER_COMPOSE) start
33
+
README.md ADDED
@@ -0,0 +1,230 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: AI Station
3
+ emoji: 🐳
4
+ colorFrom: purple
5
+ colorTo: gray
6
+ sdk: docker
7
+ app_port: 8080
8
+ ---
9
+ # Open WebUI (原 Ollama WebUI) 👋
10
+
11
+ ![GitHub stars](https://img.shields.io/github/stars/open-webui/open-webui?style=social)
12
+ ![GitHub forks](https://img.shields.io/github/forks/open-webui/open-webui?style=social)
13
+ ![GitHub watchers](https://img.shields.io/github/watchers/open-webui/open-webui?style=social)
14
+ ![GitHub repo size](https://img.shields.io/github/repo-size/open-webui/open-webui)
15
+ ![GitHub language count](https://img.shields.io/github/languages/count/open-webui/open-webui)
16
+ ![GitHub top language](https://img.shields.io/github/languages/top/open-webui/open-webui)
17
+ ![GitHub last commit](https://img.shields.io/github/last-commit/open-webui/open-webui?color=red)
18
+ ![Hits](https://hits.seeyoufarm.com/api/count/incr/badge.svg?url=https%3A%2F%2Fgithub.com%2Follama-webui%2Follama-wbui&count_bg=%2379C83D&title_bg=%23555555&icon=&icon_color=%23E7E7E7&title=hits&edge_flat=false)
19
+ [![Discord](https://img.shields.io/badge/Discord-Open_WebUI-blue?logo=discord&logoColor=white)](https://discord.gg/5rJgQTnV4s)
20
+ [![](https://img.shields.io/static/v1?label=Sponsor&message=%E2%9D%A4&logo=GitHub&color=%23fe8e86)](https://github.com/sponsors/tjbck)
21
+
22
+ Open WebUI 是一个[可扩展](https://github.com/open-webui/pipelines)、功能丰富且用户友好的自托管 WebUI,设计用于完全离线运行。它支持多种 LLM 运行器,包括 Ollama 和兼容 OpenAI 的 API。有关更多信息,请查看我们的 [Open WebUI 文档](https://docs.openwebui.com/)。
23
+
24
+ ![Open WebUI 演示](./demo.gif)
25
+
26
+ ## Open WebUI 主要功能 ⭐
27
+
28
+ - 🚀 **无缝安装**:通过 Docker 或 Kubernetes(kubectl、kustomize 或 helm)轻松安装,支持 `:ollama` 和 `:cuda` 标记的镜像。
29
+
30
+ - 🤝 **Ollama/OpenAI API 集成**:轻松集成兼容 OpenAI 的 API,支持与 Ollama 模型一起使用的多样化对话。可自定义 OpenAI API URL,以连接 **LMStudio、GroqCloud、Mistral、OpenRouter 等**。
31
+
32
+ - 🧩 **流水线,Open WebUI 插件支持**:通过 [Pipelines 插件框架](https://github.com/open-webui/pipelines),将自定义逻辑和 Python 库无缝集成到 Open WebUI 中。启动 Pipelines 实例,将 OpenAI URL 设置为 Pipelines URL,探索无限可能。包括 **函数调用**、用户 **访问控制**、与 Langfuse 结合的**使用监控**、**LibreTranslate 的实时翻译**以支持多语言、**有害消息过滤**等功能的[示例](https://github.com/open-webui/pipelines/tree/main/examples)。
33
+
34
+ - 📱 **响应式设计**:在台式电脑、笔记本电脑和移动设备上享受无缝体验。
35
+
36
+ - 📱 **移动端渐进式 Web 应用 (PWA)**:在移动设备上提供类似原生应用的体验,支持离线访问并提供流畅的用户界面。
37
+
38
+ - ✒️🔢 **支持完整 Markdown 和 LaTeX**:使用全面的 Markdown 和 LaTeX 功能提升 LLM 互动体验。
39
+
40
+ - 🎤📹 **免提语音/视频通话**:体验集成语音和视频通话的无缝通信,提供更具互动性的聊天环境。
41
+
42
+ - 🛠️ **模型构建器**:通过 Web UI 轻松创建 Ollama 模型。通过 [Open WebUI 社区](https://openwebui.com/) 集成,轻松创建并添加自定义角色/代理、定制聊天元素及导入模型。
43
+
44
+ - 🐍 **原生 Python 函数调用工具**:在工具工作区中为 LLM 提供内置代码编辑器支持。通过添加纯 Python 函数,实现与 LLM 的无缝集成。
45
+
46
+ - 📚 **本地 RAG 集成**:借助开创性的检索增强生成 (RAG) 支持,深入探索聊天互动的未来。该功能将文档互动无缝集成到聊天体验中。您可以将文档直接加载到聊天中,或将文件添加到文档库中,使用 `#` 命令在查询前轻松访问它们。
47
+
48
+ - 🔍 **用于 RAG 的网页搜索**:使用 `SearXNG`、`Google PSE`、`Brave Search`、`serpstack`、`serper`、`Serply`、`DuckDuckGo`、`TavilySearch` 和 `SearchApi` 等提供商进行网页搜索,并将结果直接注入到您的聊天体验中。
49
+
50
+ - 🌐 **网页浏览功能**:使用 `#` 命令后接 URL,将网页内容无缝集成到聊天体验中,增强互动的丰富性和深度。
51
+
52
+ - 🎨 **图片生成集成**:通过 AUTOMATIC1111 API 或 ComfyUI(本地),以及 OpenAI 的 DALL-E(外部)轻松集成图片生成功能,使聊天体验更加生动。
53
+
54
+ - ⚙️ **多模型对话**:轻松同时与多个模型互动,利用它们各自的优势获得最佳响应。通过并行使用多样化模型,提升体验。
55
+
56
+ - 🔐 **基于角色的访问控制 (RBAC)**:确保访问安全;只有授权人员才能访问您的 Ollama,且管理员保留创建/拉取模型的专有权利。
57
+
58
+ - 🌐🌍 **多语言支持**:通过国际化 (i18n) 支持,以您偏好的语言体验 Open WebUI。我们正在积极寻找贡献者,帮助扩展我们支持的语言!
59
+
60
+ - 🌟 **持续更新**:我们致力于通过定期更新、修复和新功能不断改进 Open WebUI。
61
+
62
+ 想了解更多关于 Open WebUI 的功能?请查看我��的 [Open WebUI 文档](https://docs.openwebui.com/features) 获取全面概述!
63
+
64
+ ## 🔗 也别忘了查看 Open WebUI 社区!
65
+
66
+ 不要忘记探索我们的姐妹项目 [Open WebUI 社区](https://openwebui.com/),您可以在这里发现、下载和探索定制的模型文件。Open WebUI 社区为增强您与 Open WebUI 的聊天互动提供了广泛的可能性!🚀
67
+
68
+ ## 如何安装 🚀
69
+
70
+ ### 通过 Python pip 安装 🐍
71
+
72
+ Open WebUI 可以通过 pip(Python 包管理器)安装。安装前请确保您使用的是 **Python 3.11** 以避免兼容性问题。
73
+
74
+ 1. **安装 Open WebUI**:
75
+ 打开终端并运行以下命令安装 Open WebUI:
76
+
77
+ ```bash
78
+ pip install open-webui
79
+ ```
80
+
81
+ 2. **运行 Open WebUI**:
82
+ 安装后,您可以通过执行以下命令启动 Open WebUI:
83
+
84
+ ```bash
85
+ open-webui serve
86
+ ```
87
+
88
+ 这将启动 Open WebUI 服务器,您可以通过 [http://localhost:8080](http://localhost:8080) 访问。
89
+
90
+ ### 使用 Docker 快速启动 🐳
91
+
92
+ > [!注意]
93
+ > 请注意,某些 Docker 环境可能需要额外的配置。如果遇到连接问题,我们的 [Open WebUI 文档](https://docs.openwebui.com/) 中提供了详细的指南。
94
+
95
+ > [!警告]
96
+ > 使用 Docker 安装 Open WebUI 时,请确保在命令中包含 `-v open-webui:/app/backend/data`。此步骤至关重要,它可确保正确挂载数据库并防止数据丢失。
97
+
98
+ > [!提示]
99
+ > 如果您希望使用带有 Ollama 或 CUDA 加速的 Open WebUI,我们建议使用标有 `:cuda` 或 `:ollama` 标签的官方镜像。要启用 CUDA,您必须在 Linux/WSL 系统上安装 [Nvidia CUDA 容器工具包](https://docs.nvidia.com/dgx/nvidia-container-runtime-upgrade/)。
100
+
101
+ ### 默认配置安装
102
+
103
+ - **如果 Ollama 在您的计算机上**,请使用以下命令:
104
+
105
+ ```bash
106
+ docker run -d -p 3000:8080 --add-host=host.docker.internal:host-gateway -v open-webui:/app/backend/data --name open-webui --restart always ghcr.io/open-webui/open-webui:main
107
+ ```
108
+
109
+ - **如果 Ollama 在不同的服务器上**,请使用以下命令:
110
+
111
+ 要连接到另一台服务器上的 Ollama,请将 `OLLAMA_BASE_URL` 更改为服务器的 URL:
112
+
113
+ ```bash
114
+ docker run -d -p 3000:8080 -e OLLAMA_BASE_URL=https://example.com -v open-webui:/app/backend/data --name open-webui --restart always ghcr.io/open-webui/open-webui:main
115
+ ```
116
+
117
+ - **使用 Nvidia GPU 支持运行 Open WebUI**,请使用以下命令:
118
+
119
+ ```bash
120
+ docker run -d -p 3000:8080 --gpus all --add-host=host.docker.internal:host-gateway -v open-webui:/app/backend/data --name open-webui --restart always ghcr.io/open-webui/open-webui:cuda
121
+ ```
122
+
123
+ ### 仅用于 OpenAI API 的安装
124
+
125
+ - **如果您只使用 OpenAI API**,请使用以下命令:
126
+
127
+ ```bash
128
+ docker run -d -p 3000:8080 -e OPENAI_API_KEY=your_secret_key -v open-webui:/app/backend/data --name open-webui --restart always ghcr.io/open-webui/open-webui:main
129
+ ```
130
+
131
+ ### 安装包含 Ollama 支持的 Open WebUI
132
+
133
+ 此安装方法使用一个打包了 Open WebUI 和 Ollama 的单一容器镜像,允许通过单一命令进行简化安装。根据您的硬件设置选择合适的命令:
134
+
135
+ - **支持 GPU**:
136
+ 通过运行以下命令利用 GPU 资源:
137
+
138
+ ```bash
139
+ docker run -d -p 3000:8080 --gpus=all -v ollama:/root/.ollama -v open-webui:/app/backend/data --name open-webui --restart always ghcr.io/open-webui/open-webui:ollama
140
+ ```
141
+
142
+ - **仅使用 CPU**:
143
+ 如果不使用 GPU,请使用以下命令:
144
+
145
+ ```bash
146
+ docker run -d -p 3000:8080 -v ollama:/root/.ollama -v open-webui:/app/backend/data --name open-webui --restart always ghcr.io/open-webui/open-webui:ollama
147
+ ```
148
+
149
+ 两种命令都可以方便内建安装 Open WebUI 和 Ollama,确保您快速完成安装并运行。
150
+
151
+ 安装完成后,您可以访问 [http://localhost:3000](http://localhost:3000) 的 Open WebUI。享受吧!😄
152
+
153
+ ### 其他安装方法
154
+
155
+ 我们提供多种安装替代方案,包括非 Docker 的原生安装方法、Docker Compose、Kustomize 和 Helm。访问我们的 [Open WebUI 文档](https://docs.openwebui.com/getting-started/) 或加入我们的 [Discord 社区](https://discord.gg/5rJgQTnV4s) 获取完整指南。
156
+
157
+ ### 疑难解答
158
+
159
+ 遇到连接问题?我们的 [Open WebUI 文档](https://docs.openwebui.com/troubleshooting/) 可以帮助您解决问题。有关进一步的帮助并加入我们的活跃社区,请访问 [Open WebUI Discord](https://discord.gg/5rJgQTnV4s)。
160
+
161
+ #### Open WebUI:服务器连接错误
162
+
163
+ 如果您遇到连接问题,通常是由于 WebUI docker 容器无法在容器内到达 127.0.0.1:11434(host.docker.internal:11434)的 Ollama 服务器所致。在您的 docker 命令中使用 `--network=host` 标志来解决此问题。请注意,端口从 3000 变为 8080,链接为:`http://localhost:8080`。
164
+
165
+ **示例 Docker 命令**:
166
+
167
+ ```bash
168
+ docker run -d --network=host -v open-webui:/app/backend/data -e OLLAMA_BASE_URL=http://127.0.0.1:11434 --name open-webui --restart always ghcr.io/open-webui/open-webui:main
169
+ ```
170
+
171
+ ### 保持 Docker 安���最新
172
+
173
+ 如果您希望将本地 Docker 安装更新到最新版本,可以使用 [Watchtower](https://containrrr.dev/watchtower/) 进行:
174
+
175
+ ```bash
176
+ docker run --rm --volume /var/run/docker.sock:/var/run/docker.sock containrrr/watchtower --run-once open-webui
177
+ ```
178
+
179
+ 在命令的最后部分,如果容器名称不同,请将 `open-webui` 替换为您的容器名称。
180
+
181
+ 请查看我们在 [Open WebUI 文档](https://docs.openwebui.com/migration/) 中的迁移指南。
182
+
183
+ ### 使用开发分支 🌙
184
+
185
+ > [!WARNING]
186
+ > `:dev` 分支包含最新的不稳定功能和变更。使用时需自行承担风险,因为可能存在错误或不完整功能。
187
+
188
+ 如果你想尝试最新的尖端功能并能接受偶尔的不稳定,可以使用 `:dev` 标签,如下:
189
+
190
+ ```bash
191
+ docker run -d -p 3000:8080 -v open-webui:/app/backend/data --name open-webui --add-host=host.docker.internal:host-gateway --restart always ghcr.io/open-webui/open-webui:dev
192
+ ```
193
+
194
+ ## 下一步是什么?🌟
195
+
196
+ 在 [Open WebUI 文档](https://docs.openwebui.com/roadmap/) 中发现即将推出的功能。
197
+
198
+ ## 支持者 ✨
199
+
200
+ 向我们的杰出支持者致敬,他们帮助这个项目成为现实!🙏
201
+
202
+ ### 白金赞助商 🤍
203
+
204
+ - 我们正在寻找赞助商!
205
+
206
+ ### 鸣谢
207
+
208
+ 特别感谢 [Lawrence Kim 教授](https://www.lhkim.com/) 和 [Nick Vincent 教授](https://www.nickmvincent.com/) 对该项目成为研究工作的支持和指导。感激您在整个过程中的指导!🙌
209
+
210
+ ## 许可 📜
211
+
212
+ 本项目使用 [MIT 许可](LICENSE) - 详细信息请参阅 [LICENSE](LICENSE) 文件。📄
213
+
214
+ ## 支持 💬
215
+
216
+ 如果您有任何问题、建议或需要帮助,请打开一个问题,或加入我们的 [Open WebUI Discord 社区](https://discord.gg/5rJgQTnV4s) 以与我们联系!🤝
217
+
218
+ ## 星标历史
219
+
220
+ <a href="https://star-history.com/#open-webui/open-webui&Date">
221
+ <picture>
222
+ <source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=open-webui/open-webui&type=Date&theme=dark" />
223
+ <source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=open-webui/open-webui&type=Date" />
224
+ <img alt="Star History Chart" src="https://api.star-history.com/svg?repos=open-webui/open-webui&type=Date" />
225
+ </picture>
226
+ </a>
227
+
228
+ ---
229
+
230
+ 由 [Timothy J. Baek](https://github.com/tjbck) 创建 - 让我们一起让 Open WebUI 更加出色!💪
TROUBLESHOOTING.md ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Open WebUI Troubleshooting Guide
2
+
3
+ ## Understanding the Open WebUI Architecture
4
+
5
+ The Open WebUI system is designed to streamline interactions between the client (your browser) and the Ollama API. At the heart of this design is a backend reverse proxy, enhancing security and resolving CORS issues.
6
+
7
+ - **How it Works**: The Open WebUI is designed to interact with the Ollama API through a specific route. When a request is made from the WebUI to Ollama, it is not directly sent to the Ollama API. Initially, the request is sent to the Open WebUI backend via `/ollama` route. From there, the backend is responsible for forwarding the request to the Ollama API. This forwarding is accomplished by using the route specified in the `OLLAMA_BASE_URL` environment variable. Therefore, a request made to `/ollama` in the WebUI is effectively the same as making a request to `OLLAMA_BASE_URL` in the backend. For instance, a request to `/ollama/api/tags` in the WebUI is equivalent to `OLLAMA_BASE_URL/api/tags` in the backend.
8
+
9
+ - **Security Benefits**: This design prevents direct exposure of the Ollama API to the frontend, safeguarding against potential CORS (Cross-Origin Resource Sharing) issues and unauthorized access. Requiring authentication to access the Ollama API further enhances this security layer.
10
+
11
+ ## Open WebUI: Server Connection Error
12
+
13
+ If you're experiencing connection issues, it’s often due to the WebUI docker container not being able to reach the Ollama server at 127.0.0.1:11434 (host.docker.internal:11434) inside the container . Use the `--network=host` flag in your docker command to resolve this. Note that the port changes from 3000 to 8080, resulting in the link: `http://localhost:8080`.
14
+
15
+ **Example Docker Command**:
16
+
17
+ ```bash
18
+ docker run -d --network=host -v open-webui:/app/backend/data -e OLLAMA_BASE_URL=http://127.0.0.1:11434 --name open-webui --restart always ghcr.io/open-webui/open-webui:main
19
+ ```
20
+
21
+ ### Error on Slow Reponses for Ollama
22
+
23
+ Open WebUI has a default timeout of 5 minutes for Ollama to finish generating the response. If needed, this can be adjusted via the environment variable AIOHTTP_CLIENT_TIMEOUT, which sets the timeout in seconds.
24
+
25
+ ### General Connection Errors
26
+
27
+ **Ensure Ollama Version is Up-to-Date**: Always start by checking that you have the latest version of Ollama. Visit [Ollama's official site](https://ollama.com/) for the latest updates.
28
+
29
+ **Troubleshooting Steps**:
30
+
31
+ 1. **Verify Ollama URL Format**:
32
+ - When running the Web UI container, ensure the `OLLAMA_BASE_URL` is correctly set. (e.g., `http://192.168.1.1:11434` for different host setups).
33
+ - In the Open WebUI, navigate to "Settings" > "General".
34
+ - Confirm that the Ollama Server URL is correctly set to `[OLLAMA URL]` (e.g., `http://localhost:11434`).
35
+
36
+ By following these enhanced troubleshooting steps, connection issues should be effectively resolved. For further assistance or queries, feel free to reach out to us on our community Discord.
backend/.dockerignore ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ __pycache__
2
+ .env
3
+ _old
4
+ uploads
5
+ .ipynb_checkpoints
6
+ *.db
7
+ _test
8
+ !/data
9
+ /data/*
10
+ !/data/litellm
11
+ /data/litellm/*
12
+ !data/litellm/config.yaml
13
+
14
+ !data/config.json
backend/.gitignore ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ __pycache__
2
+ .env
3
+ _old
4
+ uploads
5
+ .ipynb_checkpoints
6
+ *.db
7
+ _test
8
+ Pipfile
9
+ !/data
10
+ /data/*
11
+ /open_webui/data/*
12
+ .webui_secret_key
backend/dev.sh ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ PORT="${PORT:-8080}"
2
+ uvicorn open_webui.main:app --port $PORT --host 0.0.0.0 --forwarded-allow-ips '*' --reload
backend/open_webui/__init__.py ADDED
@@ -0,0 +1,77 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import base64
2
+ import os
3
+ import random
4
+ from pathlib import Path
5
+
6
+ import typer
7
+ import uvicorn
8
+
9
+ app = typer.Typer()
10
+
11
+ KEY_FILE = Path.cwd() / ".webui_secret_key"
12
+
13
+
14
+ @app.command()
15
+ def serve(
16
+ host: str = "0.0.0.0",
17
+ port: int = 8080,
18
+ ):
19
+ os.environ["FROM_INIT_PY"] = "true"
20
+ if os.getenv("WEBUI_SECRET_KEY") is None:
21
+ typer.echo(
22
+ "Loading WEBUI_SECRET_KEY from file, not provided as an environment variable."
23
+ )
24
+ if not KEY_FILE.exists():
25
+ typer.echo(f"Generating a new secret key and saving it to {KEY_FILE}")
26
+ KEY_FILE.write_bytes(base64.b64encode(random.randbytes(12)))
27
+ typer.echo(f"Loading WEBUI_SECRET_KEY from {KEY_FILE}")
28
+ os.environ["WEBUI_SECRET_KEY"] = KEY_FILE.read_text()
29
+
30
+ if os.getenv("USE_CUDA_DOCKER", "false") == "true":
31
+ typer.echo(
32
+ "CUDA is enabled, appending LD_LIBRARY_PATH to include torch/cudnn & cublas libraries."
33
+ )
34
+ LD_LIBRARY_PATH = os.getenv("LD_LIBRARY_PATH", "").split(":")
35
+ os.environ["LD_LIBRARY_PATH"] = ":".join(
36
+ LD_LIBRARY_PATH
37
+ + [
38
+ "/usr/local/lib/python3.11/site-packages/torch/lib",
39
+ "/usr/local/lib/python3.11/site-packages/nvidia/cudnn/lib",
40
+ ]
41
+ )
42
+ try:
43
+ import torch
44
+
45
+ assert torch.cuda.is_available(), "CUDA not available"
46
+ typer.echo("CUDA seems to be working")
47
+ except Exception as e:
48
+ typer.echo(
49
+ "Error when testing CUDA but USE_CUDA_DOCKER is true. "
50
+ "Resetting USE_CUDA_DOCKER to false and removing "
51
+ f"LD_LIBRARY_PATH modifications: {e}"
52
+ )
53
+ os.environ["USE_CUDA_DOCKER"] = "false"
54
+ os.environ["LD_LIBRARY_PATH"] = ":".join(LD_LIBRARY_PATH)
55
+
56
+ import open_webui.main # we need set environment variables before importing main
57
+
58
+ uvicorn.run(open_webui.main.app, host=host, port=port, forwarded_allow_ips="*")
59
+
60
+
61
+ @app.command()
62
+ def dev(
63
+ host: str = "0.0.0.0",
64
+ port: int = 8080,
65
+ reload: bool = True,
66
+ ):
67
+ uvicorn.run(
68
+ "open_webui.main:app",
69
+ host=host,
70
+ port=port,
71
+ reload=reload,
72
+ forwarded_allow_ips="*",
73
+ )
74
+
75
+
76
+ if __name__ == "__main__":
77
+ app()
backend/open_webui/alembic.ini ADDED
@@ -0,0 +1,114 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # A generic, single database configuration.
2
+
3
+ [alembic]
4
+ # path to migration scripts
5
+ script_location = migrations
6
+
7
+ # template used to generate migration file names; The default value is %%(rev)s_%%(slug)s
8
+ # Uncomment the line below if you want the files to be prepended with date and time
9
+ # file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s
10
+
11
+ # sys.path path, will be prepended to sys.path if present.
12
+ # defaults to the current working directory.
13
+ prepend_sys_path = .
14
+
15
+ # timezone to use when rendering the date within the migration file
16
+ # as well as the filename.
17
+ # If specified, requires the python>=3.9 or backports.zoneinfo library.
18
+ # Any required deps can installed by adding `alembic[tz]` to the pip requirements
19
+ # string value is passed to ZoneInfo()
20
+ # leave blank for localtime
21
+ # timezone =
22
+
23
+ # max length of characters to apply to the
24
+ # "slug" field
25
+ # truncate_slug_length = 40
26
+
27
+ # set to 'true' to run the environment during
28
+ # the 'revision' command, regardless of autogenerate
29
+ # revision_environment = false
30
+
31
+ # set to 'true' to allow .pyc and .pyo files without
32
+ # a source .py file to be detected as revisions in the
33
+ # versions/ directory
34
+ # sourceless = false
35
+
36
+ # version location specification; This defaults
37
+ # to migrations/versions. When using multiple version
38
+ # directories, initial revisions must be specified with --version-path.
39
+ # The path separator used here should be the separator specified by "version_path_separator" below.
40
+ # version_locations = %(here)s/bar:%(here)s/bat:migrations/versions
41
+
42
+ # version path separator; As mentioned above, this is the character used to split
43
+ # version_locations. The default within new alembic.ini files is "os", which uses os.pathsep.
44
+ # If this key is omitted entirely, it falls back to the legacy behavior of splitting on spaces and/or commas.
45
+ # Valid values for version_path_separator are:
46
+ #
47
+ # version_path_separator = :
48
+ # version_path_separator = ;
49
+ # version_path_separator = space
50
+ version_path_separator = os # Use os.pathsep. Default configuration used for new projects.
51
+
52
+ # set to 'true' to search source files recursively
53
+ # in each "version_locations" directory
54
+ # new in Alembic version 1.10
55
+ # recursive_version_locations = false
56
+
57
+ # the output encoding used when revision files
58
+ # are written from script.py.mako
59
+ # output_encoding = utf-8
60
+
61
+ # sqlalchemy.url = REPLACE_WITH_DATABASE_URL
62
+
63
+
64
+ [post_write_hooks]
65
+ # post_write_hooks defines scripts or Python functions that are run
66
+ # on newly generated revision scripts. See the documentation for further
67
+ # detail and examples
68
+
69
+ # format using "black" - use the console_scripts runner, against the "black" entrypoint
70
+ # hooks = black
71
+ # black.type = console_scripts
72
+ # black.entrypoint = black
73
+ # black.options = -l 79 REVISION_SCRIPT_FILENAME
74
+
75
+ # lint with attempts to fix using "ruff" - use the exec runner, execute a binary
76
+ # hooks = ruff
77
+ # ruff.type = exec
78
+ # ruff.executable = %(here)s/.venv/bin/ruff
79
+ # ruff.options = --fix REVISION_SCRIPT_FILENAME
80
+
81
+ # Logging configuration
82
+ [loggers]
83
+ keys = root,sqlalchemy,alembic
84
+
85
+ [handlers]
86
+ keys = console
87
+
88
+ [formatters]
89
+ keys = generic
90
+
91
+ [logger_root]
92
+ level = WARN
93
+ handlers = console
94
+ qualname =
95
+
96
+ [logger_sqlalchemy]
97
+ level = WARN
98
+ handlers =
99
+ qualname = sqlalchemy.engine
100
+
101
+ [logger_alembic]
102
+ level = INFO
103
+ handlers =
104
+ qualname = alembic
105
+
106
+ [handler_console]
107
+ class = StreamHandler
108
+ args = (sys.stderr,)
109
+ level = NOTSET
110
+ formatter = generic
111
+
112
+ [formatter_generic]
113
+ format = %(levelname)-5.5s [%(name)s] %(message)s
114
+ datefmt = %H:%M:%S
backend/open_webui/apps/audio/main.py ADDED
@@ -0,0 +1,583 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import hashlib
2
+ import json
3
+ import logging
4
+ import os
5
+ import uuid
6
+ from functools import lru_cache
7
+ from pathlib import Path
8
+
9
+ import requests
10
+ from open_webui.config import (
11
+ AUDIO_STT_ENGINE,
12
+ AUDIO_STT_MODEL,
13
+ AUDIO_STT_OPENAI_API_BASE_URL,
14
+ AUDIO_STT_OPENAI_API_KEY,
15
+ AUDIO_TTS_API_KEY,
16
+ AUDIO_TTS_ENGINE,
17
+ AUDIO_TTS_MODEL,
18
+ AUDIO_TTS_OPENAI_API_BASE_URL,
19
+ AUDIO_TTS_OPENAI_API_KEY,
20
+ AUDIO_TTS_SPLIT_ON,
21
+ AUDIO_TTS_VOICE,
22
+ AUDIO_TTS_AZURE_SPEECH_REGION,
23
+ AUDIO_TTS_AZURE_SPEECH_OUTPUT_FORMAT,
24
+ CACHE_DIR,
25
+ CORS_ALLOW_ORIGIN,
26
+ WHISPER_MODEL,
27
+ WHISPER_MODEL_AUTO_UPDATE,
28
+ WHISPER_MODEL_DIR,
29
+ AppConfig,
30
+ )
31
+
32
+ from open_webui.constants import ERROR_MESSAGES
33
+ from open_webui.env import SRC_LOG_LEVELS, DEVICE_TYPE
34
+ from fastapi import Depends, FastAPI, File, HTTPException, Request, UploadFile, status
35
+ from fastapi.middleware.cors import CORSMiddleware
36
+ from fastapi.responses import FileResponse
37
+ from pydantic import BaseModel
38
+ from open_webui.utils.utils import get_admin_user, get_current_user, get_verified_user
39
+
40
+ log = logging.getLogger(__name__)
41
+ log.setLevel(SRC_LOG_LEVELS["AUDIO"])
42
+
43
+ app = FastAPI()
44
+ app.add_middleware(
45
+ CORSMiddleware,
46
+ allow_origins=CORS_ALLOW_ORIGIN,
47
+ allow_credentials=True,
48
+ allow_methods=["*"],
49
+ allow_headers=["*"],
50
+ )
51
+
52
+ app.state.config = AppConfig()
53
+
54
+ app.state.config.STT_OPENAI_API_BASE_URL = AUDIO_STT_OPENAI_API_BASE_URL
55
+ app.state.config.STT_OPENAI_API_KEY = AUDIO_STT_OPENAI_API_KEY
56
+ app.state.config.STT_ENGINE = AUDIO_STT_ENGINE
57
+ app.state.config.STT_MODEL = AUDIO_STT_MODEL
58
+
59
+ app.state.config.TTS_OPENAI_API_BASE_URL = AUDIO_TTS_OPENAI_API_BASE_URL
60
+ app.state.config.TTS_OPENAI_API_KEY = AUDIO_TTS_OPENAI_API_KEY
61
+ app.state.config.TTS_ENGINE = AUDIO_TTS_ENGINE
62
+ app.state.config.TTS_MODEL = AUDIO_TTS_MODEL
63
+ app.state.config.TTS_VOICE = AUDIO_TTS_VOICE
64
+ app.state.config.TTS_API_KEY = AUDIO_TTS_API_KEY
65
+ app.state.config.TTS_SPLIT_ON = AUDIO_TTS_SPLIT_ON
66
+
67
+ app.state.config.TTS_AZURE_SPEECH_REGION = AUDIO_TTS_AZURE_SPEECH_REGION
68
+ app.state.config.TTS_AZURE_SPEECH_OUTPUT_FORMAT = AUDIO_TTS_AZURE_SPEECH_OUTPUT_FORMAT
69
+
70
+ # setting device type for whisper model
71
+ whisper_device_type = DEVICE_TYPE if DEVICE_TYPE and DEVICE_TYPE == "cuda" else "cpu"
72
+ log.info(f"whisper_device_type: {whisper_device_type}")
73
+
74
+ SPEECH_CACHE_DIR = Path(CACHE_DIR).joinpath("./audio/speech/")
75
+ SPEECH_CACHE_DIR.mkdir(parents=True, exist_ok=True)
76
+
77
+
78
+ class TTSConfigForm(BaseModel):
79
+ OPENAI_API_BASE_URL: str
80
+ OPENAI_API_KEY: str
81
+ API_KEY: str
82
+ ENGINE: str
83
+ MODEL: str
84
+ VOICE: str
85
+ SPLIT_ON: str
86
+ AZURE_SPEECH_REGION: str
87
+ AZURE_SPEECH_OUTPUT_FORMAT: str
88
+
89
+
90
+ class STTConfigForm(BaseModel):
91
+ OPENAI_API_BASE_URL: str
92
+ OPENAI_API_KEY: str
93
+ ENGINE: str
94
+ MODEL: str
95
+
96
+
97
+ class AudioConfigUpdateForm(BaseModel):
98
+ tts: TTSConfigForm
99
+ stt: STTConfigForm
100
+
101
+
102
+ from pydub import AudioSegment
103
+ from pydub.utils import mediainfo
104
+
105
+
106
+ def is_mp4_audio(file_path):
107
+ """Check if the given file is an MP4 audio file."""
108
+ if not os.path.isfile(file_path):
109
+ print(f"File not found: {file_path}")
110
+ return False
111
+
112
+ info = mediainfo(file_path)
113
+ if (
114
+ info.get("codec_name") == "aac"
115
+ and info.get("codec_type") == "audio"
116
+ and info.get("codec_tag_string") == "mp4a"
117
+ ):
118
+ return True
119
+ return False
120
+
121
+
122
+ def convert_mp4_to_wav(file_path, output_path):
123
+ """Convert MP4 audio file to WAV format."""
124
+ audio = AudioSegment.from_file(file_path, format="mp4")
125
+ audio.export(output_path, format="wav")
126
+ print(f"Converted {file_path} to {output_path}")
127
+
128
+
129
+ @app.get("/config")
130
+ async def get_audio_config(user=Depends(get_admin_user)):
131
+ return {
132
+ "tts": {
133
+ "OPENAI_API_BASE_URL": app.state.config.TTS_OPENAI_API_BASE_URL,
134
+ "OPENAI_API_KEY": app.state.config.TTS_OPENAI_API_KEY,
135
+ "API_KEY": app.state.config.TTS_API_KEY,
136
+ "ENGINE": app.state.config.TTS_ENGINE,
137
+ "MODEL": app.state.config.TTS_MODEL,
138
+ "VOICE": app.state.config.TTS_VOICE,
139
+ "SPLIT_ON": app.state.config.TTS_SPLIT_ON,
140
+ "AZURE_SPEECH_REGION": app.state.config.TTS_AZURE_SPEECH_REGION,
141
+ "AZURE_SPEECH_OUTPUT_FORMAT": app.state.config.TTS_AZURE_SPEECH_OUTPUT_FORMAT,
142
+ },
143
+ "stt": {
144
+ "OPENAI_API_BASE_URL": app.state.config.STT_OPENAI_API_BASE_URL,
145
+ "OPENAI_API_KEY": app.state.config.STT_OPENAI_API_KEY,
146
+ "ENGINE": app.state.config.STT_ENGINE,
147
+ "MODEL": app.state.config.STT_MODEL,
148
+ },
149
+ }
150
+
151
+
152
+ @app.post("/config/update")
153
+ async def update_audio_config(
154
+ form_data: AudioConfigUpdateForm, user=Depends(get_admin_user)
155
+ ):
156
+ app.state.config.TTS_OPENAI_API_BASE_URL = form_data.tts.OPENAI_API_BASE_URL
157
+ app.state.config.TTS_OPENAI_API_KEY = form_data.tts.OPENAI_API_KEY
158
+ app.state.config.TTS_API_KEY = form_data.tts.API_KEY
159
+ app.state.config.TTS_ENGINE = form_data.tts.ENGINE
160
+ app.state.config.TTS_MODEL = form_data.tts.MODEL
161
+ app.state.config.TTS_VOICE = form_data.tts.VOICE
162
+ app.state.config.TTS_SPLIT_ON = form_data.tts.SPLIT_ON
163
+ app.state.config.TTS_AZURE_SPEECH_REGION = form_data.tts.AZURE_SPEECH_REGION
164
+ app.state.config.TTS_AZURE_SPEECH_OUTPUT_FORMAT = (
165
+ form_data.tts.AZURE_SPEECH_OUTPUT_FORMAT
166
+ )
167
+
168
+ app.state.config.STT_OPENAI_API_BASE_URL = form_data.stt.OPENAI_API_BASE_URL
169
+ app.state.config.STT_OPENAI_API_KEY = form_data.stt.OPENAI_API_KEY
170
+ app.state.config.STT_ENGINE = form_data.stt.ENGINE
171
+ app.state.config.STT_MODEL = form_data.stt.MODEL
172
+
173
+ return {
174
+ "tts": {
175
+ "OPENAI_API_BASE_URL": app.state.config.TTS_OPENAI_API_BASE_URL,
176
+ "OPENAI_API_KEY": app.state.config.TTS_OPENAI_API_KEY,
177
+ "API_KEY": app.state.config.TTS_API_KEY,
178
+ "ENGINE": app.state.config.TTS_ENGINE,
179
+ "MODEL": app.state.config.TTS_MODEL,
180
+ "VOICE": app.state.config.TTS_VOICE,
181
+ "SPLIT_ON": app.state.config.TTS_SPLIT_ON,
182
+ "AZURE_SPEECH_REGION": app.state.config.TTS_AZURE_SPEECH_REGION,
183
+ "AZURE_SPEECH_OUTPUT_FORMAT": app.state.config.TTS_AZURE_SPEECH_OUTPUT_FORMAT,
184
+ },
185
+ "stt": {
186
+ "OPENAI_API_BASE_URL": app.state.config.STT_OPENAI_API_BASE_URL,
187
+ "OPENAI_API_KEY": app.state.config.STT_OPENAI_API_KEY,
188
+ "ENGINE": app.state.config.STT_ENGINE,
189
+ "MODEL": app.state.config.STT_MODEL,
190
+ },
191
+ }
192
+
193
+
194
+ @app.post("/speech")
195
+ async def speech(request: Request, user=Depends(get_verified_user)):
196
+ body = await request.body()
197
+ name = hashlib.sha256(body).hexdigest()
198
+
199
+ file_path = SPEECH_CACHE_DIR.joinpath(f"{name}.mp3")
200
+ file_body_path = SPEECH_CACHE_DIR.joinpath(f"{name}.json")
201
+
202
+ # Check if the file already exists in the cache
203
+ if file_path.is_file():
204
+ return FileResponse(file_path)
205
+
206
+ if app.state.config.TTS_ENGINE == "openai":
207
+ headers = {}
208
+ headers["Authorization"] = f"Bearer {app.state.config.TTS_OPENAI_API_KEY}"
209
+ headers["Content-Type"] = "application/json"
210
+
211
+ try:
212
+ body = body.decode("utf-8")
213
+ body = json.loads(body)
214
+ body["model"] = app.state.config.TTS_MODEL
215
+ body = json.dumps(body).encode("utf-8")
216
+ except Exception:
217
+ pass
218
+
219
+ r = None
220
+ try:
221
+ r = requests.post(
222
+ url=f"{app.state.config.TTS_OPENAI_API_BASE_URL}/audio/speech",
223
+ data=body,
224
+ headers=headers,
225
+ stream=True,
226
+ )
227
+
228
+ r.raise_for_status()
229
+
230
+ # Save the streaming content to a file
231
+ with open(file_path, "wb") as f:
232
+ for chunk in r.iter_content(chunk_size=8192):
233
+ f.write(chunk)
234
+
235
+ with open(file_body_path, "w") as f:
236
+ json.dump(json.loads(body.decode("utf-8")), f)
237
+
238
+ # Return the saved file
239
+ return FileResponse(file_path)
240
+
241
+ except Exception as e:
242
+ log.exception(e)
243
+ error_detail = "Open WebUI: Server Connection Error"
244
+ if r is not None:
245
+ try:
246
+ res = r.json()
247
+ if "error" in res:
248
+ error_detail = f"External: {res['error']['message']}"
249
+ except Exception:
250
+ error_detail = f"External: {e}"
251
+
252
+ raise HTTPException(
253
+ status_code=r.status_code if r != None else 500,
254
+ detail=error_detail,
255
+ )
256
+
257
+ elif app.state.config.TTS_ENGINE == "elevenlabs":
258
+ payload = None
259
+ try:
260
+ payload = json.loads(body.decode("utf-8"))
261
+ except Exception as e:
262
+ log.exception(e)
263
+ raise HTTPException(status_code=400, detail="Invalid JSON payload")
264
+
265
+ voice_id = payload.get("voice", "")
266
+
267
+ if voice_id not in get_available_voices():
268
+ raise HTTPException(
269
+ status_code=400,
270
+ detail="Invalid voice id",
271
+ )
272
+
273
+ url = f"https://api.elevenlabs.io/v1/text-to-speech/{voice_id}"
274
+
275
+ headers = {
276
+ "Accept": "audio/mpeg",
277
+ "Content-Type": "application/json",
278
+ "xi-api-key": app.state.config.TTS_API_KEY,
279
+ }
280
+
281
+ data = {
282
+ "text": payload["input"],
283
+ "model_id": app.state.config.TTS_MODEL,
284
+ "voice_settings": {"stability": 0.5, "similarity_boost": 0.5},
285
+ }
286
+
287
+ try:
288
+ r = requests.post(url, json=data, headers=headers)
289
+
290
+ r.raise_for_status()
291
+
292
+ # Save the streaming content to a file
293
+ with open(file_path, "wb") as f:
294
+ for chunk in r.iter_content(chunk_size=8192):
295
+ f.write(chunk)
296
+
297
+ with open(file_body_path, "w") as f:
298
+ json.dump(json.loads(body.decode("utf-8")), f)
299
+
300
+ # Return the saved file
301
+ return FileResponse(file_path)
302
+
303
+ except Exception as e:
304
+ log.exception(e)
305
+ error_detail = "Open WebUI: Server Connection Error"
306
+ if r is not None:
307
+ try:
308
+ res = r.json()
309
+ if "error" in res:
310
+ error_detail = f"External: {res['error']['message']}"
311
+ except Exception:
312
+ error_detail = f"External: {e}"
313
+
314
+ raise HTTPException(
315
+ status_code=r.status_code if r != None else 500,
316
+ detail=error_detail,
317
+ )
318
+
319
+ elif app.state.config.TTS_ENGINE == "azure":
320
+ payload = None
321
+ try:
322
+ payload = json.loads(body.decode("utf-8"))
323
+ except Exception as e:
324
+ log.exception(e)
325
+ raise HTTPException(status_code=400, detail="Invalid JSON payload")
326
+
327
+ region = app.state.config.TTS_AZURE_SPEECH_REGION
328
+ language = app.state.config.TTS_VOICE
329
+ locale = "-".join(app.state.config.TTS_VOICE.split("-")[:1])
330
+ output_format = app.state.config.TTS_AZURE_SPEECH_OUTPUT_FORMAT
331
+ url = f"https://{region}.tts.speech.microsoft.com/cognitiveservices/v1"
332
+
333
+ headers = {
334
+ "Ocp-Apim-Subscription-Key": app.state.config.TTS_API_KEY,
335
+ "Content-Type": "application/ssml+xml",
336
+ "X-Microsoft-OutputFormat": output_format,
337
+ }
338
+
339
+ data = f"""<speak version="1.0" xmlns="http://www.w3.org/2001/10/synthesis" xml:lang="{locale}">
340
+ <voice name="{language}">{payload["input"]}</voice>
341
+ </speak>"""
342
+
343
+ response = requests.post(url, headers=headers, data=data)
344
+
345
+ if response.status_code == 200:
346
+ with open(file_path, "wb") as f:
347
+ f.write(response.content)
348
+ return FileResponse(file_path)
349
+ else:
350
+ log.error(f"Error synthesizing speech - {response.reason}")
351
+ raise HTTPException(
352
+ status_code=500, detail=f"Error synthesizing speech - {response.reason}"
353
+ )
354
+
355
+
356
+ @app.post("/transcriptions")
357
+ def transcribe(
358
+ file: UploadFile = File(...),
359
+ user=Depends(get_current_user),
360
+ ):
361
+ log.info(f"file.content_type: {file.content_type}")
362
+
363
+ if file.content_type not in ["audio/mpeg", "audio/wav", "audio/ogg", "audio/x-m4a"]:
364
+ raise HTTPException(
365
+ status_code=status.HTTP_400_BAD_REQUEST,
366
+ detail=ERROR_MESSAGES.FILE_NOT_SUPPORTED,
367
+ )
368
+
369
+ try:
370
+ ext = file.filename.split(".")[-1]
371
+
372
+ id = uuid.uuid4()
373
+ filename = f"{id}.{ext}"
374
+
375
+ file_dir = f"{CACHE_DIR}/audio/transcriptions"
376
+ os.makedirs(file_dir, exist_ok=True)
377
+ file_path = f"{file_dir}/{filename}"
378
+
379
+ print(filename)
380
+
381
+ contents = file.file.read()
382
+ with open(file_path, "wb") as f:
383
+ f.write(contents)
384
+ f.close()
385
+
386
+ if app.state.config.STT_ENGINE == "":
387
+ from faster_whisper import WhisperModel
388
+
389
+ whisper_kwargs = {
390
+ "model_size_or_path": WHISPER_MODEL,
391
+ "device": whisper_device_type,
392
+ "compute_type": "int8",
393
+ "download_root": WHISPER_MODEL_DIR,
394
+ "local_files_only": not WHISPER_MODEL_AUTO_UPDATE,
395
+ }
396
+
397
+ log.debug(f"whisper_kwargs: {whisper_kwargs}")
398
+
399
+ try:
400
+ model = WhisperModel(**whisper_kwargs)
401
+ except Exception:
402
+ log.warning(
403
+ "WhisperModel initialization failed, attempting download with local_files_only=False"
404
+ )
405
+ whisper_kwargs["local_files_only"] = False
406
+ model = WhisperModel(**whisper_kwargs)
407
+
408
+ segments, info = model.transcribe(file_path, beam_size=5)
409
+ log.info(
410
+ "Detected language '%s' with probability %f"
411
+ % (info.language, info.language_probability)
412
+ )
413
+
414
+ transcript = "".join([segment.text for segment in list(segments)])
415
+
416
+ data = {"text": transcript.strip()}
417
+
418
+ # save the transcript to a json file
419
+ transcript_file = f"{file_dir}/{id}.json"
420
+ with open(transcript_file, "w") as f:
421
+ json.dump(data, f)
422
+
423
+ print(data)
424
+
425
+ return data
426
+
427
+ elif app.state.config.STT_ENGINE == "openai":
428
+ if is_mp4_audio(file_path):
429
+ print("is_mp4_audio")
430
+ os.rename(file_path, file_path.replace(".wav", ".mp4"))
431
+ # Convert MP4 audio file to WAV format
432
+ convert_mp4_to_wav(file_path.replace(".wav", ".mp4"), file_path)
433
+
434
+ headers = {"Authorization": f"Bearer {app.state.config.STT_OPENAI_API_KEY}"}
435
+
436
+ files = {"file": (filename, open(file_path, "rb"))}
437
+ data = {"model": app.state.config.STT_MODEL}
438
+
439
+ print(files, data)
440
+
441
+ r = None
442
+ try:
443
+ r = requests.post(
444
+ url=f"{app.state.config.STT_OPENAI_API_BASE_URL}/audio/transcriptions",
445
+ headers=headers,
446
+ files=files,
447
+ data=data,
448
+ )
449
+
450
+ r.raise_for_status()
451
+
452
+ data = r.json()
453
+
454
+ # save the transcript to a json file
455
+ transcript_file = f"{file_dir}/{id}.json"
456
+ with open(transcript_file, "w") as f:
457
+ json.dump(data, f)
458
+
459
+ print(data)
460
+ return data
461
+ except Exception as e:
462
+ log.exception(e)
463
+ error_detail = "Open WebUI: Server Connection Error"
464
+ if r is not None:
465
+ try:
466
+ res = r.json()
467
+ if "error" in res:
468
+ error_detail = f"External: {res['error']['message']}"
469
+ except Exception:
470
+ error_detail = f"External: {e}"
471
+
472
+ raise HTTPException(
473
+ status_code=r.status_code if r != None else 500,
474
+ detail=error_detail,
475
+ )
476
+
477
+ except Exception as e:
478
+ log.exception(e)
479
+
480
+ raise HTTPException(
481
+ status_code=status.HTTP_400_BAD_REQUEST,
482
+ detail=ERROR_MESSAGES.DEFAULT(e),
483
+ )
484
+
485
+
486
+ def get_available_models() -> list[dict]:
487
+ if app.state.config.TTS_ENGINE == "openai":
488
+ return [{"id": "tts-1"}, {"id": "tts-1-hd"}]
489
+ elif app.state.config.TTS_ENGINE == "elevenlabs":
490
+ headers = {
491
+ "xi-api-key": app.state.config.TTS_API_KEY,
492
+ "Content-Type": "application/json",
493
+ }
494
+
495
+ try:
496
+ response = requests.get(
497
+ "https://api.elevenlabs.io/v1/models", headers=headers, timeout=5
498
+ )
499
+ response.raise_for_status()
500
+ models = response.json()
501
+ return [
502
+ {"name": model["name"], "id": model["model_id"]} for model in models
503
+ ]
504
+ except requests.RequestException as e:
505
+ log.error(f"Error fetching voices: {str(e)}")
506
+ return []
507
+
508
+
509
+ @app.get("/models")
510
+ async def get_models(user=Depends(get_verified_user)):
511
+ return {"models": get_available_models()}
512
+
513
+
514
+ def get_available_voices() -> dict:
515
+ """Returns {voice_id: voice_name} dict"""
516
+ ret = {}
517
+ if app.state.config.TTS_ENGINE == "openai":
518
+ ret = {
519
+ "alloy": "alloy",
520
+ "echo": "echo",
521
+ "fable": "fable",
522
+ "onyx": "onyx",
523
+ "nova": "nova",
524
+ "shimmer": "shimmer",
525
+ }
526
+ elif app.state.config.TTS_ENGINE == "elevenlabs":
527
+ try:
528
+ ret = get_elevenlabs_voices()
529
+ except Exception:
530
+ # Avoided @lru_cache with exception
531
+ pass
532
+ elif app.state.config.TTS_ENGINE == "azure":
533
+ try:
534
+ region = app.state.config.TTS_AZURE_SPEECH_REGION
535
+ url = f"https://{region}.tts.speech.microsoft.com/cognitiveservices/voices/list"
536
+ headers = {"Ocp-Apim-Subscription-Key": app.state.config.TTS_API_KEY}
537
+
538
+ response = requests.get(url, headers=headers)
539
+ response.raise_for_status()
540
+ voices = response.json()
541
+ for voice in voices:
542
+ ret[voice["ShortName"]] = (
543
+ f"{voice['DisplayName']} ({voice['ShortName']})"
544
+ )
545
+ except requests.RequestException as e:
546
+ log.error(f"Error fetching voices: {str(e)}")
547
+
548
+ return ret
549
+
550
+
551
+ @lru_cache
552
+ def get_elevenlabs_voices() -> dict:
553
+ """
554
+ Note, set the following in your .env file to use Elevenlabs:
555
+ AUDIO_TTS_ENGINE=elevenlabs
556
+ AUDIO_TTS_API_KEY=sk_... # Your Elevenlabs API key
557
+ AUDIO_TTS_VOICE=EXAVITQu4vr4xnSDxMaL # From https://api.elevenlabs.io/v1/voices
558
+ AUDIO_TTS_MODEL=eleven_multilingual_v2
559
+ """
560
+ headers = {
561
+ "xi-api-key": app.state.config.TTS_API_KEY,
562
+ "Content-Type": "application/json",
563
+ }
564
+ try:
565
+ # TODO: Add retries
566
+ response = requests.get("https://api.elevenlabs.io/v1/voices", headers=headers)
567
+ response.raise_for_status()
568
+ voices_data = response.json()
569
+
570
+ voices = {}
571
+ for voice in voices_data.get("voices", []):
572
+ voices[voice["voice_id"]] = voice["name"]
573
+ except requests.RequestException as e:
574
+ # Avoid @lru_cache with exception
575
+ log.error(f"Error fetching voices: {str(e)}")
576
+ raise RuntimeError(f"Error fetching voices: {str(e)}")
577
+
578
+ return voices
579
+
580
+
581
+ @app.get("/voices")
582
+ async def get_voices(user=Depends(get_verified_user)):
583
+ return {"voices": [{"id": k, "name": v} for k, v in get_available_voices().items()]}
backend/open_webui/apps/images/main.py ADDED
@@ -0,0 +1,597 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import asyncio
2
+ import base64
3
+ import json
4
+ import logging
5
+ import mimetypes
6
+ import re
7
+ import uuid
8
+ from pathlib import Path
9
+ from typing import Optional
10
+
11
+ import requests
12
+ from open_webui.apps.images.utils.comfyui import (
13
+ ComfyUIGenerateImageForm,
14
+ ComfyUIWorkflow,
15
+ comfyui_generate_image,
16
+ )
17
+ from open_webui.config import (
18
+ AUTOMATIC1111_API_AUTH,
19
+ AUTOMATIC1111_BASE_URL,
20
+ AUTOMATIC1111_CFG_SCALE,
21
+ AUTOMATIC1111_SAMPLER,
22
+ AUTOMATIC1111_SCHEDULER,
23
+ CACHE_DIR,
24
+ COMFYUI_BASE_URL,
25
+ COMFYUI_WORKFLOW,
26
+ COMFYUI_WORKFLOW_NODES,
27
+ CORS_ALLOW_ORIGIN,
28
+ ENABLE_IMAGE_GENERATION,
29
+ IMAGE_GENERATION_ENGINE,
30
+ IMAGE_GENERATION_MODEL,
31
+ IMAGE_SIZE,
32
+ IMAGE_STEPS,
33
+ IMAGES_OPENAI_API_BASE_URL,
34
+ IMAGES_OPENAI_API_KEY,
35
+ AppConfig,
36
+ )
37
+ from open_webui.constants import ERROR_MESSAGES
38
+ from open_webui.env import SRC_LOG_LEVELS
39
+ from fastapi import Depends, FastAPI, HTTPException, Request
40
+ from fastapi.middleware.cors import CORSMiddleware
41
+ from pydantic import BaseModel
42
+ from open_webui.utils.utils import get_admin_user, get_verified_user
43
+
44
+ log = logging.getLogger(__name__)
45
+ log.setLevel(SRC_LOG_LEVELS["IMAGES"])
46
+
47
+ IMAGE_CACHE_DIR = Path(CACHE_DIR).joinpath("./image/generations/")
48
+ IMAGE_CACHE_DIR.mkdir(parents=True, exist_ok=True)
49
+
50
+ app = FastAPI()
51
+ app.add_middleware(
52
+ CORSMiddleware,
53
+ allow_origins=CORS_ALLOW_ORIGIN,
54
+ allow_credentials=True,
55
+ allow_methods=["*"],
56
+ allow_headers=["*"],
57
+ )
58
+
59
+ app.state.config = AppConfig()
60
+
61
+ app.state.config.ENGINE = IMAGE_GENERATION_ENGINE
62
+ app.state.config.ENABLED = ENABLE_IMAGE_GENERATION
63
+
64
+ app.state.config.OPENAI_API_BASE_URL = IMAGES_OPENAI_API_BASE_URL
65
+ app.state.config.OPENAI_API_KEY = IMAGES_OPENAI_API_KEY
66
+
67
+ app.state.config.MODEL = IMAGE_GENERATION_MODEL
68
+
69
+ app.state.config.AUTOMATIC1111_BASE_URL = AUTOMATIC1111_BASE_URL
70
+ app.state.config.AUTOMATIC1111_API_AUTH = AUTOMATIC1111_API_AUTH
71
+ app.state.config.AUTOMATIC1111_CFG_SCALE = AUTOMATIC1111_CFG_SCALE
72
+ app.state.config.AUTOMATIC1111_SAMPLER = AUTOMATIC1111_SAMPLER
73
+ app.state.config.AUTOMATIC1111_SCHEDULER = AUTOMATIC1111_SCHEDULER
74
+ app.state.config.COMFYUI_BASE_URL = COMFYUI_BASE_URL
75
+ app.state.config.COMFYUI_WORKFLOW = COMFYUI_WORKFLOW
76
+ app.state.config.COMFYUI_WORKFLOW_NODES = COMFYUI_WORKFLOW_NODES
77
+
78
+ app.state.config.IMAGE_SIZE = IMAGE_SIZE
79
+ app.state.config.IMAGE_STEPS = IMAGE_STEPS
80
+
81
+
82
+ @app.get("/config")
83
+ async def get_config(request: Request, user=Depends(get_admin_user)):
84
+ return {
85
+ "enabled": app.state.config.ENABLED,
86
+ "engine": app.state.config.ENGINE,
87
+ "openai": {
88
+ "OPENAI_API_BASE_URL": app.state.config.OPENAI_API_BASE_URL,
89
+ "OPENAI_API_KEY": app.state.config.OPENAI_API_KEY,
90
+ },
91
+ "automatic1111": {
92
+ "AUTOMATIC1111_BASE_URL": app.state.config.AUTOMATIC1111_BASE_URL,
93
+ "AUTOMATIC1111_API_AUTH": app.state.config.AUTOMATIC1111_API_AUTH,
94
+ "AUTOMATIC1111_CFG_SCALE": app.state.config.AUTOMATIC1111_CFG_SCALE,
95
+ "AUTOMATIC1111_SAMPLER": app.state.config.AUTOMATIC1111_SAMPLER,
96
+ "AUTOMATIC1111_SCHEDULER": app.state.config.AUTOMATIC1111_SCHEDULER,
97
+ },
98
+ "comfyui": {
99
+ "COMFYUI_BASE_URL": app.state.config.COMFYUI_BASE_URL,
100
+ "COMFYUI_WORKFLOW": app.state.config.COMFYUI_WORKFLOW,
101
+ "COMFYUI_WORKFLOW_NODES": app.state.config.COMFYUI_WORKFLOW_NODES,
102
+ },
103
+ }
104
+
105
+
106
+ class OpenAIConfigForm(BaseModel):
107
+ OPENAI_API_BASE_URL: str
108
+ OPENAI_API_KEY: str
109
+
110
+
111
+ class Automatic1111ConfigForm(BaseModel):
112
+ AUTOMATIC1111_BASE_URL: str
113
+ AUTOMATIC1111_API_AUTH: str
114
+ AUTOMATIC1111_CFG_SCALE: Optional[str]
115
+ AUTOMATIC1111_SAMPLER: Optional[str]
116
+ AUTOMATIC1111_SCHEDULER: Optional[str]
117
+
118
+
119
+ class ComfyUIConfigForm(BaseModel):
120
+ COMFYUI_BASE_URL: str
121
+ COMFYUI_WORKFLOW: str
122
+ COMFYUI_WORKFLOW_NODES: list[dict]
123
+
124
+
125
+ class ConfigForm(BaseModel):
126
+ enabled: bool
127
+ engine: str
128
+ openai: OpenAIConfigForm
129
+ automatic1111: Automatic1111ConfigForm
130
+ comfyui: ComfyUIConfigForm
131
+
132
+
133
+ @app.post("/config/update")
134
+ async def update_config(form_data: ConfigForm, user=Depends(get_admin_user)):
135
+ app.state.config.ENGINE = form_data.engine
136
+ app.state.config.ENABLED = form_data.enabled
137
+
138
+ app.state.config.OPENAI_API_BASE_URL = form_data.openai.OPENAI_API_BASE_URL
139
+ app.state.config.OPENAI_API_KEY = form_data.openai.OPENAI_API_KEY
140
+
141
+ app.state.config.AUTOMATIC1111_BASE_URL = (
142
+ form_data.automatic1111.AUTOMATIC1111_BASE_URL
143
+ )
144
+ app.state.config.AUTOMATIC1111_API_AUTH = (
145
+ form_data.automatic1111.AUTOMATIC1111_API_AUTH
146
+ )
147
+
148
+ app.state.config.AUTOMATIC1111_CFG_SCALE = (
149
+ float(form_data.automatic1111.AUTOMATIC1111_CFG_SCALE)
150
+ if form_data.automatic1111.AUTOMATIC1111_CFG_SCALE
151
+ else None
152
+ )
153
+ app.state.config.AUTOMATIC1111_SAMPLER = (
154
+ form_data.automatic1111.AUTOMATIC1111_SAMPLER
155
+ if form_data.automatic1111.AUTOMATIC1111_SAMPLER
156
+ else None
157
+ )
158
+ app.state.config.AUTOMATIC1111_SCHEDULER = (
159
+ form_data.automatic1111.AUTOMATIC1111_SCHEDULER
160
+ if form_data.automatic1111.AUTOMATIC1111_SCHEDULER
161
+ else None
162
+ )
163
+
164
+ app.state.config.COMFYUI_BASE_URL = form_data.comfyui.COMFYUI_BASE_URL.strip("/")
165
+ app.state.config.COMFYUI_WORKFLOW = form_data.comfyui.COMFYUI_WORKFLOW
166
+ app.state.config.COMFYUI_WORKFLOW_NODES = form_data.comfyui.COMFYUI_WORKFLOW_NODES
167
+
168
+ return {
169
+ "enabled": app.state.config.ENABLED,
170
+ "engine": app.state.config.ENGINE,
171
+ "openai": {
172
+ "OPENAI_API_BASE_URL": app.state.config.OPENAI_API_BASE_URL,
173
+ "OPENAI_API_KEY": app.state.config.OPENAI_API_KEY,
174
+ },
175
+ "automatic1111": {
176
+ "AUTOMATIC1111_BASE_URL": app.state.config.AUTOMATIC1111_BASE_URL,
177
+ "AUTOMATIC1111_API_AUTH": app.state.config.AUTOMATIC1111_API_AUTH,
178
+ "AUTOMATIC1111_CFG_SCALE": app.state.config.AUTOMATIC1111_CFG_SCALE,
179
+ "AUTOMATIC1111_SAMPLER": app.state.config.AUTOMATIC1111_SAMPLER,
180
+ "AUTOMATIC1111_SCHEDULER": app.state.config.AUTOMATIC1111_SCHEDULER,
181
+ },
182
+ "comfyui": {
183
+ "COMFYUI_BASE_URL": app.state.config.COMFYUI_BASE_URL,
184
+ "COMFYUI_WORKFLOW": app.state.config.COMFYUI_WORKFLOW,
185
+ "COMFYUI_WORKFLOW_NODES": app.state.config.COMFYUI_WORKFLOW_NODES,
186
+ },
187
+ }
188
+
189
+
190
+ def get_automatic1111_api_auth():
191
+ if app.state.config.AUTOMATIC1111_API_AUTH is None:
192
+ return ""
193
+ else:
194
+ auth1111_byte_string = app.state.config.AUTOMATIC1111_API_AUTH.encode("utf-8")
195
+ auth1111_base64_encoded_bytes = base64.b64encode(auth1111_byte_string)
196
+ auth1111_base64_encoded_string = auth1111_base64_encoded_bytes.decode("utf-8")
197
+ return f"Basic {auth1111_base64_encoded_string}"
198
+
199
+
200
+ @app.get("/config/url/verify")
201
+ async def verify_url(user=Depends(get_admin_user)):
202
+ if app.state.config.ENGINE == "automatic1111":
203
+ try:
204
+ r = requests.get(
205
+ url=f"{app.state.config.AUTOMATIC1111_BASE_URL}/sdapi/v1/options",
206
+ headers={"authorization": get_automatic1111_api_auth()},
207
+ )
208
+ r.raise_for_status()
209
+ return True
210
+ except Exception:
211
+ app.state.config.ENABLED = False
212
+ raise HTTPException(status_code=400, detail=ERROR_MESSAGES.INVALID_URL)
213
+ elif app.state.config.ENGINE == "comfyui":
214
+ try:
215
+ r = requests.get(url=f"{app.state.config.COMFYUI_BASE_URL}/object_info")
216
+ r.raise_for_status()
217
+ return True
218
+ except Exception:
219
+ app.state.config.ENABLED = False
220
+ raise HTTPException(status_code=400, detail=ERROR_MESSAGES.INVALID_URL)
221
+ else:
222
+ return True
223
+
224
+
225
+ def set_image_model(model: str):
226
+ log.info(f"Setting image model to {model}")
227
+ app.state.config.MODEL = model
228
+ if app.state.config.ENGINE in ["", "automatic1111"]:
229
+ api_auth = get_automatic1111_api_auth()
230
+ r = requests.get(
231
+ url=f"{app.state.config.AUTOMATIC1111_BASE_URL}/sdapi/v1/options",
232
+ headers={"authorization": api_auth},
233
+ )
234
+ options = r.json()
235
+ if model != options["sd_model_checkpoint"]:
236
+ options["sd_model_checkpoint"] = model
237
+ r = requests.post(
238
+ url=f"{app.state.config.AUTOMATIC1111_BASE_URL}/sdapi/v1/options",
239
+ json=options,
240
+ headers={"authorization": api_auth},
241
+ )
242
+ return app.state.config.MODEL
243
+
244
+
245
+ def get_image_model():
246
+ if app.state.config.ENGINE == "openai":
247
+ return app.state.config.MODEL if app.state.config.MODEL else "dall-e-2"
248
+ elif app.state.config.ENGINE == "comfyui":
249
+ return app.state.config.MODEL if app.state.config.MODEL else ""
250
+ elif app.state.config.ENGINE == "automatic1111" or app.state.config.ENGINE == "":
251
+ try:
252
+ r = requests.get(
253
+ url=f"{app.state.config.AUTOMATIC1111_BASE_URL}/sdapi/v1/options",
254
+ headers={"authorization": get_automatic1111_api_auth()},
255
+ )
256
+ options = r.json()
257
+ return options["sd_model_checkpoint"]
258
+ except Exception as e:
259
+ app.state.config.ENABLED = False
260
+ raise HTTPException(status_code=400, detail=ERROR_MESSAGES.DEFAULT(e))
261
+
262
+
263
+ class ImageConfigForm(BaseModel):
264
+ MODEL: str
265
+ IMAGE_SIZE: str
266
+ IMAGE_STEPS: int
267
+
268
+
269
+ @app.get("/image/config")
270
+ async def get_image_config(user=Depends(get_admin_user)):
271
+ return {
272
+ "MODEL": app.state.config.MODEL,
273
+ "IMAGE_SIZE": app.state.config.IMAGE_SIZE,
274
+ "IMAGE_STEPS": app.state.config.IMAGE_STEPS,
275
+ }
276
+
277
+
278
+ @app.post("/image/config/update")
279
+ async def update_image_config(form_data: ImageConfigForm, user=Depends(get_admin_user)):
280
+
281
+ set_image_model(form_data.MODEL)
282
+
283
+ pattern = r"^\d+x\d+$"
284
+ if re.match(pattern, form_data.IMAGE_SIZE):
285
+ app.state.config.IMAGE_SIZE = form_data.IMAGE_SIZE
286
+ else:
287
+ raise HTTPException(
288
+ status_code=400,
289
+ detail=ERROR_MESSAGES.INCORRECT_FORMAT(" (e.g., 512x512)."),
290
+ )
291
+
292
+ if form_data.IMAGE_STEPS >= 0:
293
+ app.state.config.IMAGE_STEPS = form_data.IMAGE_STEPS
294
+ else:
295
+ raise HTTPException(
296
+ status_code=400,
297
+ detail=ERROR_MESSAGES.INCORRECT_FORMAT(" (e.g., 50)."),
298
+ )
299
+
300
+ return {
301
+ "MODEL": app.state.config.MODEL,
302
+ "IMAGE_SIZE": app.state.config.IMAGE_SIZE,
303
+ "IMAGE_STEPS": app.state.config.IMAGE_STEPS,
304
+ }
305
+
306
+
307
+ @app.get("/models")
308
+ def get_models(user=Depends(get_verified_user)):
309
+ try:
310
+ if app.state.config.ENGINE == "openai":
311
+ return [
312
+ {"id": "dall-e-2", "name": "DALL·E 2"},
313
+ {"id": "dall-e-3", "name": "DALL·E 3"},
314
+ ]
315
+ elif app.state.config.ENGINE == "comfyui":
316
+ # TODO - get models from comfyui
317
+ r = requests.get(url=f"{app.state.config.COMFYUI_BASE_URL}/object_info")
318
+ info = r.json()
319
+
320
+ workflow = json.loads(app.state.config.COMFYUI_WORKFLOW)
321
+ model_node_id = None
322
+
323
+ for node in app.state.config.COMFYUI_WORKFLOW_NODES:
324
+ if node["type"] == "model":
325
+ if node["node_ids"]:
326
+ model_node_id = node["node_ids"][0]
327
+ break
328
+
329
+ if model_node_id:
330
+ model_list_key = None
331
+
332
+ print(workflow[model_node_id]["class_type"])
333
+ for key in info[workflow[model_node_id]["class_type"]]["input"][
334
+ "required"
335
+ ]:
336
+ if "_name" in key:
337
+ model_list_key = key
338
+ break
339
+
340
+ if model_list_key:
341
+ return list(
342
+ map(
343
+ lambda model: {"id": model, "name": model},
344
+ info[workflow[model_node_id]["class_type"]]["input"][
345
+ "required"
346
+ ][model_list_key][0],
347
+ )
348
+ )
349
+ else:
350
+ return list(
351
+ map(
352
+ lambda model: {"id": model, "name": model},
353
+ info["CheckpointLoaderSimple"]["input"]["required"][
354
+ "ckpt_name"
355
+ ][0],
356
+ )
357
+ )
358
+ elif (
359
+ app.state.config.ENGINE == "automatic1111" or app.state.config.ENGINE == ""
360
+ ):
361
+ r = requests.get(
362
+ url=f"{app.state.config.AUTOMATIC1111_BASE_URL}/sdapi/v1/sd-models",
363
+ headers={"authorization": get_automatic1111_api_auth()},
364
+ )
365
+ models = r.json()
366
+ return list(
367
+ map(
368
+ lambda model: {"id": model["title"], "name": model["model_name"]},
369
+ models,
370
+ )
371
+ )
372
+ except Exception as e:
373
+ app.state.config.ENABLED = False
374
+ raise HTTPException(status_code=400, detail=ERROR_MESSAGES.DEFAULT(e))
375
+
376
+
377
+ class GenerateImageForm(BaseModel):
378
+ model: Optional[str] = None
379
+ prompt: str
380
+ size: Optional[str] = None
381
+ n: int = 1
382
+ negative_prompt: Optional[str] = None
383
+
384
+
385
+ def save_b64_image(b64_str):
386
+ try:
387
+ image_id = str(uuid.uuid4())
388
+
389
+ if "," in b64_str:
390
+ header, encoded = b64_str.split(",", 1)
391
+ mime_type = header.split(";")[0]
392
+
393
+ img_data = base64.b64decode(encoded)
394
+ image_format = mimetypes.guess_extension(mime_type)
395
+
396
+ image_filename = f"{image_id}{image_format}"
397
+ file_path = IMAGE_CACHE_DIR / f"{image_filename}"
398
+ with open(file_path, "wb") as f:
399
+ f.write(img_data)
400
+ return image_filename
401
+ else:
402
+ image_filename = f"{image_id}.png"
403
+ file_path = IMAGE_CACHE_DIR.joinpath(image_filename)
404
+
405
+ img_data = base64.b64decode(b64_str)
406
+
407
+ # Write the image data to a file
408
+ with open(file_path, "wb") as f:
409
+ f.write(img_data)
410
+ return image_filename
411
+
412
+ except Exception as e:
413
+ log.exception(f"Error saving image: {e}")
414
+ return None
415
+
416
+
417
+ def save_url_image(url):
418
+ image_id = str(uuid.uuid4())
419
+ try:
420
+ r = requests.get(url)
421
+ r.raise_for_status()
422
+ if r.headers["content-type"].split("/")[0] == "image":
423
+ mime_type = r.headers["content-type"]
424
+ image_format = mimetypes.guess_extension(mime_type)
425
+
426
+ if not image_format:
427
+ raise ValueError("Could not determine image type from MIME type")
428
+
429
+ image_filename = f"{image_id}{image_format}"
430
+
431
+ file_path = IMAGE_CACHE_DIR.joinpath(f"{image_filename}")
432
+ with open(file_path, "wb") as image_file:
433
+ for chunk in r.iter_content(chunk_size=8192):
434
+ image_file.write(chunk)
435
+ return image_filename
436
+ else:
437
+ log.error("Url does not point to an image.")
438
+ return None
439
+
440
+ except Exception as e:
441
+ log.exception(f"Error saving image: {e}")
442
+ return None
443
+
444
+
445
+ @app.post("/generations")
446
+ async def image_generations(
447
+ form_data: GenerateImageForm,
448
+ user=Depends(get_verified_user),
449
+ ):
450
+ width, height = tuple(map(int, app.state.config.IMAGE_SIZE.split("x")))
451
+
452
+ r = None
453
+ try:
454
+ if app.state.config.ENGINE == "openai":
455
+ headers = {}
456
+ headers["Authorization"] = f"Bearer {app.state.config.OPENAI_API_KEY}"
457
+ headers["Content-Type"] = "application/json"
458
+
459
+ data = {
460
+ "model": (
461
+ app.state.config.MODEL
462
+ if app.state.config.MODEL != ""
463
+ else "dall-e-2"
464
+ ),
465
+ "prompt": form_data.prompt,
466
+ "n": form_data.n,
467
+ "size": (
468
+ form_data.size if form_data.size else app.state.config.IMAGE_SIZE
469
+ ),
470
+ "response_format": "b64_json",
471
+ }
472
+
473
+ # Use asyncio.to_thread for the requests.post call
474
+ r = await asyncio.to_thread(
475
+ requests.post,
476
+ url=f"{app.state.config.OPENAI_API_BASE_URL}/images/generations",
477
+ json=data,
478
+ headers=headers,
479
+ )
480
+
481
+ r.raise_for_status()
482
+ res = r.json()
483
+
484
+ images = []
485
+
486
+ for image in res["data"]:
487
+ image_filename = save_b64_image(image["b64_json"])
488
+ images.append({"url": f"/cache/image/generations/{image_filename}"})
489
+ file_body_path = IMAGE_CACHE_DIR.joinpath(f"{image_filename}.json")
490
+
491
+ with open(file_body_path, "w") as f:
492
+ json.dump(data, f)
493
+
494
+ return images
495
+
496
+ elif app.state.config.ENGINE == "comfyui":
497
+ data = {
498
+ "prompt": form_data.prompt,
499
+ "width": width,
500
+ "height": height,
501
+ "n": form_data.n,
502
+ }
503
+
504
+ if app.state.config.IMAGE_STEPS is not None:
505
+ data["steps"] = app.state.config.IMAGE_STEPS
506
+
507
+ if form_data.negative_prompt is not None:
508
+ data["negative_prompt"] = form_data.negative_prompt
509
+
510
+ form_data = ComfyUIGenerateImageForm(
511
+ **{
512
+ "workflow": ComfyUIWorkflow(
513
+ **{
514
+ "workflow": app.state.config.COMFYUI_WORKFLOW,
515
+ "nodes": app.state.config.COMFYUI_WORKFLOW_NODES,
516
+ }
517
+ ),
518
+ **data,
519
+ }
520
+ )
521
+ res = await comfyui_generate_image(
522
+ app.state.config.MODEL,
523
+ form_data,
524
+ user.id,
525
+ app.state.config.COMFYUI_BASE_URL,
526
+ )
527
+ log.debug(f"res: {res}")
528
+
529
+ images = []
530
+
531
+ for image in res["data"]:
532
+ image_filename = save_url_image(image["url"])
533
+ images.append({"url": f"/cache/image/generations/{image_filename}"})
534
+ file_body_path = IMAGE_CACHE_DIR.joinpath(f"{image_filename}.json")
535
+
536
+ with open(file_body_path, "w") as f:
537
+ json.dump(form_data.model_dump(exclude_none=True), f)
538
+
539
+ log.debug(f"images: {images}")
540
+ return images
541
+ elif (
542
+ app.state.config.ENGINE == "automatic1111" or app.state.config.ENGINE == ""
543
+ ):
544
+ if form_data.model:
545
+ set_image_model(form_data.model)
546
+
547
+ data = {
548
+ "prompt": form_data.prompt,
549
+ "batch_size": form_data.n,
550
+ "width": width,
551
+ "height": height,
552
+ }
553
+
554
+ if app.state.config.IMAGE_STEPS is not None:
555
+ data["steps"] = app.state.config.IMAGE_STEPS
556
+
557
+ if form_data.negative_prompt is not None:
558
+ data["negative_prompt"] = form_data.negative_prompt
559
+
560
+ if app.state.config.AUTOMATIC1111_CFG_SCALE:
561
+ data["cfg_scale"] = app.state.config.AUTOMATIC1111_CFG_SCALE
562
+
563
+ if app.state.config.AUTOMATIC1111_SAMPLER:
564
+ data["sampler_name"] = app.state.config.AUTOMATIC1111_SAMPLER
565
+
566
+ if app.state.config.AUTOMATIC1111_SCHEDULER:
567
+ data["scheduler"] = app.state.config.AUTOMATIC1111_SCHEDULER
568
+
569
+ # Use asyncio.to_thread for the requests.post call
570
+ r = await asyncio.to_thread(
571
+ requests.post,
572
+ url=f"{app.state.config.AUTOMATIC1111_BASE_URL}/sdapi/v1/txt2img",
573
+ json=data,
574
+ headers={"authorization": get_automatic1111_api_auth()},
575
+ )
576
+
577
+ res = r.json()
578
+ log.debug(f"res: {res}")
579
+
580
+ images = []
581
+
582
+ for image in res["images"]:
583
+ image_filename = save_b64_image(image)
584
+ images.append({"url": f"/cache/image/generations/{image_filename}"})
585
+ file_body_path = IMAGE_CACHE_DIR.joinpath(f"{image_filename}.json")
586
+
587
+ with open(file_body_path, "w") as f:
588
+ json.dump({**data, "info": res["info"]}, f)
589
+
590
+ return images
591
+ except Exception as e:
592
+ error = e
593
+ if r != None:
594
+ data = r.json()
595
+ if "error" in data:
596
+ error = data["error"]["message"]
597
+ raise HTTPException(status_code=400, detail=ERROR_MESSAGES.DEFAULT(error))
backend/open_webui/apps/images/utils/comfyui.py ADDED
@@ -0,0 +1,174 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import asyncio
2
+ import json
3
+ import logging
4
+ import random
5
+ import urllib.parse
6
+ import urllib.request
7
+ from typing import Optional
8
+
9
+ import websocket # NOTE: websocket-client (https://github.com/websocket-client/websocket-client)
10
+ from open_webui.env import SRC_LOG_LEVELS
11
+ from pydantic import BaseModel
12
+
13
+ log = logging.getLogger(__name__)
14
+ log.setLevel(SRC_LOG_LEVELS["COMFYUI"])
15
+
16
+ default_headers = {"User-Agent": "Mozilla/5.0"}
17
+
18
+
19
+ def queue_prompt(prompt, client_id, base_url):
20
+ log.info("queue_prompt")
21
+ p = {"prompt": prompt, "client_id": client_id}
22
+ data = json.dumps(p).encode("utf-8")
23
+ log.debug(f"queue_prompt data: {data}")
24
+ try:
25
+ req = urllib.request.Request(
26
+ f"{base_url}/prompt", data=data, headers=default_headers
27
+ )
28
+ response = urllib.request.urlopen(req).read()
29
+ return json.loads(response)
30
+ except Exception as e:
31
+ log.exception(f"Error while queuing prompt: {e}")
32
+ raise e
33
+
34
+
35
+ def get_image(filename, subfolder, folder_type, base_url):
36
+ log.info("get_image")
37
+ data = {"filename": filename, "subfolder": subfolder, "type": folder_type}
38
+ url_values = urllib.parse.urlencode(data)
39
+ req = urllib.request.Request(
40
+ f"{base_url}/view?{url_values}", headers=default_headers
41
+ )
42
+ with urllib.request.urlopen(req) as response:
43
+ return response.read()
44
+
45
+
46
+ def get_image_url(filename, subfolder, folder_type, base_url):
47
+ log.info("get_image")
48
+ data = {"filename": filename, "subfolder": subfolder, "type": folder_type}
49
+ url_values = urllib.parse.urlencode(data)
50
+ return f"{base_url}/view?{url_values}"
51
+
52
+
53
+ def get_history(prompt_id, base_url):
54
+ log.info("get_history")
55
+
56
+ req = urllib.request.Request(
57
+ f"{base_url}/history/{prompt_id}", headers=default_headers
58
+ )
59
+ with urllib.request.urlopen(req) as response:
60
+ return json.loads(response.read())
61
+
62
+
63
+ def get_images(ws, prompt, client_id, base_url):
64
+ prompt_id = queue_prompt(prompt, client_id, base_url)["prompt_id"]
65
+ output_images = []
66
+ while True:
67
+ out = ws.recv()
68
+ if isinstance(out, str):
69
+ message = json.loads(out)
70
+ if message["type"] == "executing":
71
+ data = message["data"]
72
+ if data["node"] is None and data["prompt_id"] == prompt_id:
73
+ break # Execution is done
74
+ else:
75
+ continue # previews are binary data
76
+
77
+ history = get_history(prompt_id, base_url)[prompt_id]
78
+ for o in history["outputs"]:
79
+ for node_id in history["outputs"]:
80
+ node_output = history["outputs"][node_id]
81
+ if "images" in node_output:
82
+ for image in node_output["images"]:
83
+ url = get_image_url(
84
+ image["filename"], image["subfolder"], image["type"], base_url
85
+ )
86
+ output_images.append({"url": url})
87
+ return {"data": output_images}
88
+
89
+
90
+ class ComfyUINodeInput(BaseModel):
91
+ type: Optional[str] = None
92
+ node_ids: list[str] = []
93
+ key: Optional[str] = "text"
94
+ value: Optional[str] = None
95
+
96
+
97
+ class ComfyUIWorkflow(BaseModel):
98
+ workflow: str
99
+ nodes: list[ComfyUINodeInput]
100
+
101
+
102
+ class ComfyUIGenerateImageForm(BaseModel):
103
+ workflow: ComfyUIWorkflow
104
+
105
+ prompt: str
106
+ negative_prompt: Optional[str] = None
107
+ width: int
108
+ height: int
109
+ n: int = 1
110
+
111
+ steps: Optional[int] = None
112
+ seed: Optional[int] = None
113
+
114
+
115
+ async def comfyui_generate_image(
116
+ model: str, payload: ComfyUIGenerateImageForm, client_id, base_url
117
+ ):
118
+ ws_url = base_url.replace("http://", "ws://").replace("https://", "wss://")
119
+ workflow = json.loads(payload.workflow.workflow)
120
+
121
+ for node in payload.workflow.nodes:
122
+ if node.type:
123
+ if node.type == "model":
124
+ for node_id in node.node_ids:
125
+ workflow[node_id]["inputs"][node.key] = model
126
+ elif node.type == "prompt":
127
+ for node_id in node.node_ids:
128
+ workflow[node_id]["inputs"]["text"] = payload.prompt
129
+ elif node.type == "negative_prompt":
130
+ for node_id in node.node_ids:
131
+ workflow[node_id]["inputs"]["text"] = payload.negative_prompt
132
+ elif node.type == "width":
133
+ for node_id in node.node_ids:
134
+ workflow[node_id]["inputs"]["width"] = payload.width
135
+ elif node.type == "height":
136
+ for node_id in node.node_ids:
137
+ workflow[node_id]["inputs"]["height"] = payload.height
138
+ elif node.type == "n":
139
+ for node_id in node.node_ids:
140
+ workflow[node_id]["inputs"]["batch_size"] = payload.n
141
+ elif node.type == "steps":
142
+ for node_id in node.node_ids:
143
+ workflow[node_id]["inputs"]["steps"] = payload.steps
144
+ elif node.type == "seed":
145
+ seed = (
146
+ payload.seed
147
+ if payload.seed
148
+ else random.randint(0, 18446744073709551614)
149
+ )
150
+ for node_id in node.node_ids:
151
+ workflow[node_id]["inputs"][node.key] = seed
152
+ else:
153
+ for node_id in node.node_ids:
154
+ workflow[node_id]["inputs"][node.key] = node.value
155
+
156
+ try:
157
+ ws = websocket.WebSocket()
158
+ ws.connect(f"{ws_url}/ws?clientId={client_id}")
159
+ log.info("WebSocket connection established.")
160
+ except Exception as e:
161
+ log.exception(f"Failed to connect to WebSocket server: {e}")
162
+ return None
163
+
164
+ try:
165
+ log.info("Sending workflow to WebSocket server.")
166
+ log.info(f"Workflow: {workflow}")
167
+ images = await asyncio.to_thread(get_images, ws, workflow, client_id, base_url)
168
+ except Exception as e:
169
+ log.exception(f"Error while receiving images: {e}")
170
+ images = None
171
+
172
+ ws.close()
173
+
174
+ return images
backend/open_webui/apps/ollama/main.py ADDED
@@ -0,0 +1,1135 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import asyncio
2
+ import json
3
+ import logging
4
+ import os
5
+ import random
6
+ import re
7
+ import time
8
+ from typing import Optional, Union
9
+ from urllib.parse import urlparse
10
+
11
+ import aiohttp
12
+ import requests
13
+ from open_webui.apps.webui.models.models import Models
14
+ from open_webui.config import (
15
+ AIOHTTP_CLIENT_TIMEOUT,
16
+ CORS_ALLOW_ORIGIN,
17
+ ENABLE_MODEL_FILTER,
18
+ ENABLE_OLLAMA_API,
19
+ MODEL_FILTER_LIST,
20
+ OLLAMA_BASE_URLS,
21
+ UPLOAD_DIR,
22
+ AppConfig,
23
+ )
24
+ from open_webui.constants import ERROR_MESSAGES
25
+ from open_webui.env import SRC_LOG_LEVELS
26
+ from fastapi import Depends, FastAPI, File, HTTPException, Request, UploadFile
27
+ from fastapi.middleware.cors import CORSMiddleware
28
+ from fastapi.responses import StreamingResponse
29
+ from pydantic import BaseModel, ConfigDict
30
+ from starlette.background import BackgroundTask
31
+
32
+
33
+ from open_webui.utils.misc import (
34
+ calculate_sha256,
35
+ )
36
+ from open_webui.utils.payload import (
37
+ apply_model_params_to_body_ollama,
38
+ apply_model_params_to_body_openai,
39
+ apply_model_system_prompt_to_body,
40
+ )
41
+ from open_webui.utils.utils import get_admin_user, get_verified_user
42
+
43
+ log = logging.getLogger(__name__)
44
+ log.setLevel(SRC_LOG_LEVELS["OLLAMA"])
45
+
46
+ app = FastAPI()
47
+ app.add_middleware(
48
+ CORSMiddleware,
49
+ allow_origins=CORS_ALLOW_ORIGIN,
50
+ allow_credentials=True,
51
+ allow_methods=["*"],
52
+ allow_headers=["*"],
53
+ )
54
+
55
+ app.state.config = AppConfig()
56
+
57
+ app.state.config.ENABLE_MODEL_FILTER = ENABLE_MODEL_FILTER
58
+ app.state.config.MODEL_FILTER_LIST = MODEL_FILTER_LIST
59
+
60
+ app.state.config.ENABLE_OLLAMA_API = ENABLE_OLLAMA_API
61
+ app.state.config.OLLAMA_BASE_URLS = OLLAMA_BASE_URLS
62
+ app.state.MODELS = {}
63
+
64
+
65
+ # TODO: Implement a more intelligent load balancing mechanism for distributing requests among multiple backend instances.
66
+ # Current implementation uses a simple round-robin approach (random.choice). Consider incorporating algorithms like weighted round-robin,
67
+ # least connections, or least response time for better resource utilization and performance optimization.
68
+
69
+
70
+ @app.middleware("http")
71
+ async def check_url(request: Request, call_next):
72
+ if len(app.state.MODELS) == 0:
73
+ await get_all_models()
74
+ else:
75
+ pass
76
+
77
+ response = await call_next(request)
78
+ return response
79
+
80
+
81
+ @app.head("/")
82
+ @app.get("/")
83
+ async def get_status():
84
+ return {"status": True}
85
+
86
+
87
+ @app.get("/config")
88
+ async def get_config(user=Depends(get_admin_user)):
89
+ return {"ENABLE_OLLAMA_API": app.state.config.ENABLE_OLLAMA_API}
90
+
91
+
92
+ class OllamaConfigForm(BaseModel):
93
+ enable_ollama_api: Optional[bool] = None
94
+
95
+
96
+ @app.post("/config/update")
97
+ async def update_config(form_data: OllamaConfigForm, user=Depends(get_admin_user)):
98
+ app.state.config.ENABLE_OLLAMA_API = form_data.enable_ollama_api
99
+ return {"ENABLE_OLLAMA_API": app.state.config.ENABLE_OLLAMA_API}
100
+
101
+
102
+ @app.get("/urls")
103
+ async def get_ollama_api_urls(user=Depends(get_admin_user)):
104
+ return {"OLLAMA_BASE_URLS": app.state.config.OLLAMA_BASE_URLS}
105
+
106
+
107
+ class UrlUpdateForm(BaseModel):
108
+ urls: list[str]
109
+
110
+
111
+ @app.post("/urls/update")
112
+ async def update_ollama_api_url(form_data: UrlUpdateForm, user=Depends(get_admin_user)):
113
+ app.state.config.OLLAMA_BASE_URLS = form_data.urls
114
+
115
+ log.info(f"app.state.config.OLLAMA_BASE_URLS: {app.state.config.OLLAMA_BASE_URLS}")
116
+ return {"OLLAMA_BASE_URLS": app.state.config.OLLAMA_BASE_URLS}
117
+
118
+
119
+ async def fetch_url(url):
120
+ timeout = aiohttp.ClientTimeout(total=5)
121
+ try:
122
+ async with aiohttp.ClientSession(timeout=timeout, trust_env=True) as session:
123
+ async with session.get(url) as response:
124
+ return await response.json()
125
+ except Exception as e:
126
+ # Handle connection error here
127
+ log.error(f"Connection error: {e}")
128
+ return None
129
+
130
+
131
+ async def cleanup_response(
132
+ response: Optional[aiohttp.ClientResponse],
133
+ session: Optional[aiohttp.ClientSession],
134
+ ):
135
+ if response:
136
+ response.close()
137
+ if session:
138
+ await session.close()
139
+
140
+
141
+ async def post_streaming_url(
142
+ url: str, payload: Union[str, bytes], stream: bool = True, content_type=None
143
+ ):
144
+ r = None
145
+ try:
146
+ session = aiohttp.ClientSession(
147
+ trust_env=True, timeout=aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT)
148
+ )
149
+ r = await session.post(
150
+ url,
151
+ data=payload,
152
+ headers={"Content-Type": "application/json"},
153
+ )
154
+ r.raise_for_status()
155
+
156
+ if stream:
157
+ headers = dict(r.headers)
158
+ if content_type:
159
+ headers["Content-Type"] = content_type
160
+ return StreamingResponse(
161
+ r.content,
162
+ status_code=r.status,
163
+ headers=headers,
164
+ background=BackgroundTask(
165
+ cleanup_response, response=r, session=session
166
+ ),
167
+ )
168
+ else:
169
+ res = await r.json()
170
+ await cleanup_response(r, session)
171
+ return res
172
+
173
+ except Exception as e:
174
+ error_detail = "Open WebUI: Server Connection Error"
175
+ if r is not None:
176
+ try:
177
+ res = await r.json()
178
+ if "error" in res:
179
+ error_detail = f"Ollama: {res['error']}"
180
+ except Exception:
181
+ error_detail = f"Ollama: {e}"
182
+
183
+ raise HTTPException(
184
+ status_code=r.status if r else 500,
185
+ detail=error_detail,
186
+ )
187
+
188
+
189
+ def merge_models_lists(model_lists):
190
+ merged_models = {}
191
+
192
+ for idx, model_list in enumerate(model_lists):
193
+ if model_list is not None:
194
+ for model in model_list:
195
+ digest = model["digest"]
196
+ if digest not in merged_models:
197
+ model["urls"] = [idx]
198
+ merged_models[digest] = model
199
+ else:
200
+ merged_models[digest]["urls"].append(idx)
201
+
202
+ return list(merged_models.values())
203
+
204
+
205
+ async def get_all_models():
206
+ log.info("get_all_models()")
207
+
208
+ if app.state.config.ENABLE_OLLAMA_API:
209
+ tasks = [
210
+ fetch_url(f"{url}/api/tags") for url in app.state.config.OLLAMA_BASE_URLS
211
+ ]
212
+ responses = await asyncio.gather(*tasks)
213
+
214
+ models = {
215
+ "models": merge_models_lists(
216
+ map(
217
+ lambda response: response["models"] if response else None, responses
218
+ )
219
+ )
220
+ }
221
+
222
+ else:
223
+ models = {"models": []}
224
+
225
+ app.state.MODELS = {model["model"]: model for model in models["models"]}
226
+
227
+ return models
228
+
229
+
230
+ @app.get("/api/tags")
231
+ @app.get("/api/tags/{url_idx}")
232
+ async def get_ollama_tags(
233
+ url_idx: Optional[int] = None, user=Depends(get_verified_user)
234
+ ):
235
+ if url_idx is None:
236
+ models = await get_all_models()
237
+
238
+ if app.state.config.ENABLE_MODEL_FILTER:
239
+ if user.role == "user":
240
+ models["models"] = list(
241
+ filter(
242
+ lambda model: model["name"]
243
+ in app.state.config.MODEL_FILTER_LIST,
244
+ models["models"],
245
+ )
246
+ )
247
+ return models
248
+ return models
249
+ else:
250
+ url = app.state.config.OLLAMA_BASE_URLS[url_idx]
251
+
252
+ r = None
253
+ try:
254
+ r = requests.request(method="GET", url=f"{url}/api/tags")
255
+ r.raise_for_status()
256
+
257
+ return r.json()
258
+ except Exception as e:
259
+ log.exception(e)
260
+ error_detail = "Open WebUI: Server Connection Error"
261
+ if r is not None:
262
+ try:
263
+ res = r.json()
264
+ if "error" in res:
265
+ error_detail = f"Ollama: {res['error']}"
266
+ except Exception:
267
+ error_detail = f"Ollama: {e}"
268
+
269
+ raise HTTPException(
270
+ status_code=r.status_code if r else 500,
271
+ detail=error_detail,
272
+ )
273
+
274
+
275
+ @app.get("/api/version")
276
+ @app.get("/api/version/{url_idx}")
277
+ async def get_ollama_versions(url_idx: Optional[int] = None):
278
+ if app.state.config.ENABLE_OLLAMA_API:
279
+ if url_idx is None:
280
+ # returns lowest version
281
+ tasks = [
282
+ fetch_url(f"{url}/api/version")
283
+ for url in app.state.config.OLLAMA_BASE_URLS
284
+ ]
285
+ responses = await asyncio.gather(*tasks)
286
+ responses = list(filter(lambda x: x is not None, responses))
287
+
288
+ if len(responses) > 0:
289
+ lowest_version = min(
290
+ responses,
291
+ key=lambda x: tuple(
292
+ map(int, re.sub(r"^v|-.*", "", x["version"]).split("."))
293
+ ),
294
+ )
295
+
296
+ return {"version": lowest_version["version"]}
297
+ else:
298
+ raise HTTPException(
299
+ status_code=500,
300
+ detail=ERROR_MESSAGES.OLLAMA_NOT_FOUND,
301
+ )
302
+ else:
303
+ url = app.state.config.OLLAMA_BASE_URLS[url_idx]
304
+
305
+ r = None
306
+ try:
307
+ r = requests.request(method="GET", url=f"{url}/api/version")
308
+ r.raise_for_status()
309
+
310
+ return r.json()
311
+ except Exception as e:
312
+ log.exception(e)
313
+ error_detail = "Open WebUI: Server Connection Error"
314
+ if r is not None:
315
+ try:
316
+ res = r.json()
317
+ if "error" in res:
318
+ error_detail = f"Ollama: {res['error']}"
319
+ except Exception:
320
+ error_detail = f"Ollama: {e}"
321
+
322
+ raise HTTPException(
323
+ status_code=r.status_code if r else 500,
324
+ detail=error_detail,
325
+ )
326
+ else:
327
+ return {"version": False}
328
+
329
+
330
+ class ModelNameForm(BaseModel):
331
+ name: str
332
+
333
+
334
+ @app.post("/api/pull")
335
+ @app.post("/api/pull/{url_idx}")
336
+ async def pull_model(
337
+ form_data: ModelNameForm, url_idx: int = 0, user=Depends(get_admin_user)
338
+ ):
339
+ url = app.state.config.OLLAMA_BASE_URLS[url_idx]
340
+ log.info(f"url: {url}")
341
+
342
+ # Admin should be able to pull models from any source
343
+ payload = {**form_data.model_dump(exclude_none=True), "insecure": True}
344
+
345
+ return await post_streaming_url(f"{url}/api/pull", json.dumps(payload))
346
+
347
+
348
+ class PushModelForm(BaseModel):
349
+ name: str
350
+ insecure: Optional[bool] = None
351
+ stream: Optional[bool] = None
352
+
353
+
354
+ @app.delete("/api/push")
355
+ @app.delete("/api/push/{url_idx}")
356
+ async def push_model(
357
+ form_data: PushModelForm,
358
+ url_idx: Optional[int] = None,
359
+ user=Depends(get_admin_user),
360
+ ):
361
+ if url_idx is None:
362
+ if form_data.name in app.state.MODELS:
363
+ url_idx = app.state.MODELS[form_data.name]["urls"][0]
364
+ else:
365
+ raise HTTPException(
366
+ status_code=400,
367
+ detail=ERROR_MESSAGES.MODEL_NOT_FOUND(form_data.name),
368
+ )
369
+
370
+ url = app.state.config.OLLAMA_BASE_URLS[url_idx]
371
+ log.debug(f"url: {url}")
372
+
373
+ return await post_streaming_url(
374
+ f"{url}/api/push", form_data.model_dump_json(exclude_none=True).encode()
375
+ )
376
+
377
+
378
+ class CreateModelForm(BaseModel):
379
+ name: str
380
+ modelfile: Optional[str] = None
381
+ stream: Optional[bool] = None
382
+ path: Optional[str] = None
383
+
384
+
385
+ @app.post("/api/create")
386
+ @app.post("/api/create/{url_idx}")
387
+ async def create_model(
388
+ form_data: CreateModelForm, url_idx: int = 0, user=Depends(get_admin_user)
389
+ ):
390
+ log.debug(f"form_data: {form_data}")
391
+ url = app.state.config.OLLAMA_BASE_URLS[url_idx]
392
+ log.info(f"url: {url}")
393
+
394
+ return await post_streaming_url(
395
+ f"{url}/api/create", form_data.model_dump_json(exclude_none=True).encode()
396
+ )
397
+
398
+
399
+ class CopyModelForm(BaseModel):
400
+ source: str
401
+ destination: str
402
+
403
+
404
+ @app.post("/api/copy")
405
+ @app.post("/api/copy/{url_idx}")
406
+ async def copy_model(
407
+ form_data: CopyModelForm,
408
+ url_idx: Optional[int] = None,
409
+ user=Depends(get_admin_user),
410
+ ):
411
+ if url_idx is None:
412
+ if form_data.source in app.state.MODELS:
413
+ url_idx = app.state.MODELS[form_data.source]["urls"][0]
414
+ else:
415
+ raise HTTPException(
416
+ status_code=400,
417
+ detail=ERROR_MESSAGES.MODEL_NOT_FOUND(form_data.source),
418
+ )
419
+
420
+ url = app.state.config.OLLAMA_BASE_URLS[url_idx]
421
+ log.info(f"url: {url}")
422
+ r = requests.request(
423
+ method="POST",
424
+ url=f"{url}/api/copy",
425
+ headers={"Content-Type": "application/json"},
426
+ data=form_data.model_dump_json(exclude_none=True).encode(),
427
+ )
428
+
429
+ try:
430
+ r.raise_for_status()
431
+
432
+ log.debug(f"r.text: {r.text}")
433
+
434
+ return True
435
+ except Exception as e:
436
+ log.exception(e)
437
+ error_detail = "Open WebUI: Server Connection Error"
438
+ if r is not None:
439
+ try:
440
+ res = r.json()
441
+ if "error" in res:
442
+ error_detail = f"Ollama: {res['error']}"
443
+ except Exception:
444
+ error_detail = f"Ollama: {e}"
445
+
446
+ raise HTTPException(
447
+ status_code=r.status_code if r else 500,
448
+ detail=error_detail,
449
+ )
450
+
451
+
452
+ @app.delete("/api/delete")
453
+ @app.delete("/api/delete/{url_idx}")
454
+ async def delete_model(
455
+ form_data: ModelNameForm,
456
+ url_idx: Optional[int] = None,
457
+ user=Depends(get_admin_user),
458
+ ):
459
+ if url_idx is None:
460
+ if form_data.name in app.state.MODELS:
461
+ url_idx = app.state.MODELS[form_data.name]["urls"][0]
462
+ else:
463
+ raise HTTPException(
464
+ status_code=400,
465
+ detail=ERROR_MESSAGES.MODEL_NOT_FOUND(form_data.name),
466
+ )
467
+
468
+ url = app.state.config.OLLAMA_BASE_URLS[url_idx]
469
+ log.info(f"url: {url}")
470
+
471
+ r = requests.request(
472
+ method="DELETE",
473
+ url=f"{url}/api/delete",
474
+ headers={"Content-Type": "application/json"},
475
+ data=form_data.model_dump_json(exclude_none=True).encode(),
476
+ )
477
+ try:
478
+ r.raise_for_status()
479
+
480
+ log.debug(f"r.text: {r.text}")
481
+
482
+ return True
483
+ except Exception as e:
484
+ log.exception(e)
485
+ error_detail = "Open WebUI: Server Connection Error"
486
+ if r is not None:
487
+ try:
488
+ res = r.json()
489
+ if "error" in res:
490
+ error_detail = f"Ollama: {res['error']}"
491
+ except Exception:
492
+ error_detail = f"Ollama: {e}"
493
+
494
+ raise HTTPException(
495
+ status_code=r.status_code if r else 500,
496
+ detail=error_detail,
497
+ )
498
+
499
+
500
+ @app.post("/api/show")
501
+ async def show_model_info(form_data: ModelNameForm, user=Depends(get_verified_user)):
502
+ if form_data.name not in app.state.MODELS:
503
+ raise HTTPException(
504
+ status_code=400,
505
+ detail=ERROR_MESSAGES.MODEL_NOT_FOUND(form_data.name),
506
+ )
507
+
508
+ url_idx = random.choice(app.state.MODELS[form_data.name]["urls"])
509
+ url = app.state.config.OLLAMA_BASE_URLS[url_idx]
510
+ log.info(f"url: {url}")
511
+
512
+ r = requests.request(
513
+ method="POST",
514
+ url=f"{url}/api/show",
515
+ headers={"Content-Type": "application/json"},
516
+ data=form_data.model_dump_json(exclude_none=True).encode(),
517
+ )
518
+ try:
519
+ r.raise_for_status()
520
+
521
+ return r.json()
522
+ except Exception as e:
523
+ log.exception(e)
524
+ error_detail = "Open WebUI: Server Connection Error"
525
+ if r is not None:
526
+ try:
527
+ res = r.json()
528
+ if "error" in res:
529
+ error_detail = f"Ollama: {res['error']}"
530
+ except Exception:
531
+ error_detail = f"Ollama: {e}"
532
+
533
+ raise HTTPException(
534
+ status_code=r.status_code if r else 500,
535
+ detail=error_detail,
536
+ )
537
+
538
+
539
+ class GenerateEmbeddingsForm(BaseModel):
540
+ model: str
541
+ prompt: str
542
+ options: Optional[dict] = None
543
+ keep_alive: Optional[Union[int, str]] = None
544
+
545
+
546
+ @app.post("/api/embed")
547
+ @app.post("/api/embed/{url_idx}")
548
+ async def generate_embeddings(
549
+ form_data: GenerateEmbeddingsForm,
550
+ url_idx: Optional[int] = None,
551
+ user=Depends(get_verified_user),
552
+ ):
553
+ if url_idx is None:
554
+ model = form_data.model
555
+
556
+ if ":" not in model:
557
+ model = f"{model}:latest"
558
+
559
+ if model in app.state.MODELS:
560
+ url_idx = random.choice(app.state.MODELS[model]["urls"])
561
+ else:
562
+ raise HTTPException(
563
+ status_code=400,
564
+ detail=ERROR_MESSAGES.MODEL_NOT_FOUND(form_data.model),
565
+ )
566
+
567
+ url = app.state.config.OLLAMA_BASE_URLS[url_idx]
568
+ log.info(f"url: {url}")
569
+
570
+ r = requests.request(
571
+ method="POST",
572
+ url=f"{url}/api/embed",
573
+ headers={"Content-Type": "application/json"},
574
+ data=form_data.model_dump_json(exclude_none=True).encode(),
575
+ )
576
+ try:
577
+ r.raise_for_status()
578
+
579
+ return r.json()
580
+ except Exception as e:
581
+ log.exception(e)
582
+ error_detail = "Open WebUI: Server Connection Error"
583
+ if r is not None:
584
+ try:
585
+ res = r.json()
586
+ if "error" in res:
587
+ error_detail = f"Ollama: {res['error']}"
588
+ except Exception:
589
+ error_detail = f"Ollama: {e}"
590
+
591
+ raise HTTPException(
592
+ status_code=r.status_code if r else 500,
593
+ detail=error_detail,
594
+ )
595
+
596
+
597
+ @app.post("/api/embeddings")
598
+ @app.post("/api/embeddings/{url_idx}")
599
+ async def generate_embeddings(
600
+ form_data: GenerateEmbeddingsForm,
601
+ url_idx: Optional[int] = None,
602
+ user=Depends(get_verified_user),
603
+ ):
604
+ if url_idx is None:
605
+ model = form_data.model
606
+
607
+ if ":" not in model:
608
+ model = f"{model}:latest"
609
+
610
+ if model in app.state.MODELS:
611
+ url_idx = random.choice(app.state.MODELS[model]["urls"])
612
+ else:
613
+ raise HTTPException(
614
+ status_code=400,
615
+ detail=ERROR_MESSAGES.MODEL_NOT_FOUND(form_data.model),
616
+ )
617
+
618
+ url = app.state.config.OLLAMA_BASE_URLS[url_idx]
619
+ log.info(f"url: {url}")
620
+
621
+ r = requests.request(
622
+ method="POST",
623
+ url=f"{url}/api/embeddings",
624
+ headers={"Content-Type": "application/json"},
625
+ data=form_data.model_dump_json(exclude_none=True).encode(),
626
+ )
627
+ try:
628
+ r.raise_for_status()
629
+
630
+ return r.json()
631
+ except Exception as e:
632
+ log.exception(e)
633
+ error_detail = "Open WebUI: Server Connection Error"
634
+ if r is not None:
635
+ try:
636
+ res = r.json()
637
+ if "error" in res:
638
+ error_detail = f"Ollama: {res['error']}"
639
+ except Exception:
640
+ error_detail = f"Ollama: {e}"
641
+
642
+ raise HTTPException(
643
+ status_code=r.status_code if r else 500,
644
+ detail=error_detail,
645
+ )
646
+
647
+
648
+ def generate_ollama_embeddings(
649
+ form_data: GenerateEmbeddingsForm,
650
+ url_idx: Optional[int] = None,
651
+ ):
652
+ log.info(f"generate_ollama_embeddings {form_data}")
653
+
654
+ if url_idx is None:
655
+ model = form_data.model
656
+
657
+ if ":" not in model:
658
+ model = f"{model}:latest"
659
+
660
+ if model in app.state.MODELS:
661
+ url_idx = random.choice(app.state.MODELS[model]["urls"])
662
+ else:
663
+ raise HTTPException(
664
+ status_code=400,
665
+ detail=ERROR_MESSAGES.MODEL_NOT_FOUND(form_data.model),
666
+ )
667
+
668
+ url = app.state.config.OLLAMA_BASE_URLS[url_idx]
669
+ log.info(f"url: {url}")
670
+
671
+ r = requests.request(
672
+ method="POST",
673
+ url=f"{url}/api/embeddings",
674
+ headers={"Content-Type": "application/json"},
675
+ data=form_data.model_dump_json(exclude_none=True).encode(),
676
+ )
677
+ try:
678
+ r.raise_for_status()
679
+
680
+ data = r.json()
681
+
682
+ log.info(f"generate_ollama_embeddings {data}")
683
+
684
+ if "embedding" in data:
685
+ return data["embedding"]
686
+ else:
687
+ raise Exception("Something went wrong :/")
688
+ except Exception as e:
689
+ log.exception(e)
690
+ error_detail = "Open WebUI: Server Connection Error"
691
+ if r is not None:
692
+ try:
693
+ res = r.json()
694
+ if "error" in res:
695
+ error_detail = f"Ollama: {res['error']}"
696
+ except Exception:
697
+ error_detail = f"Ollama: {e}"
698
+
699
+ raise Exception(error_detail)
700
+
701
+
702
+ class GenerateCompletionForm(BaseModel):
703
+ model: str
704
+ prompt: str
705
+ images: Optional[list[str]] = None
706
+ format: Optional[str] = None
707
+ options: Optional[dict] = None
708
+ system: Optional[str] = None
709
+ template: Optional[str] = None
710
+ context: Optional[str] = None
711
+ stream: Optional[bool] = True
712
+ raw: Optional[bool] = None
713
+ keep_alive: Optional[Union[int, str]] = None
714
+
715
+
716
+ @app.post("/api/generate")
717
+ @app.post("/api/generate/{url_idx}")
718
+ async def generate_completion(
719
+ form_data: GenerateCompletionForm,
720
+ url_idx: Optional[int] = None,
721
+ user=Depends(get_verified_user),
722
+ ):
723
+ if url_idx is None:
724
+ model = form_data.model
725
+
726
+ if ":" not in model:
727
+ model = f"{model}:latest"
728
+
729
+ if model in app.state.MODELS:
730
+ url_idx = random.choice(app.state.MODELS[model]["urls"])
731
+ else:
732
+ raise HTTPException(
733
+ status_code=400,
734
+ detail=ERROR_MESSAGES.MODEL_NOT_FOUND(form_data.model),
735
+ )
736
+
737
+ url = app.state.config.OLLAMA_BASE_URLS[url_idx]
738
+ log.info(f"url: {url}")
739
+
740
+ return await post_streaming_url(
741
+ f"{url}/api/generate", form_data.model_dump_json(exclude_none=True).encode()
742
+ )
743
+
744
+
745
+ class ChatMessage(BaseModel):
746
+ role: str
747
+ content: str
748
+ images: Optional[list[str]] = None
749
+
750
+
751
+ class GenerateChatCompletionForm(BaseModel):
752
+ model: str
753
+ messages: list[ChatMessage]
754
+ format: Optional[str] = None
755
+ options: Optional[dict] = None
756
+ template: Optional[str] = None
757
+ stream: Optional[bool] = None
758
+ keep_alive: Optional[Union[int, str]] = None
759
+
760
+
761
+ def get_ollama_url(url_idx: Optional[int], model: str):
762
+ if url_idx is None:
763
+ if model not in app.state.MODELS:
764
+ raise HTTPException(
765
+ status_code=400,
766
+ detail=ERROR_MESSAGES.MODEL_NOT_FOUND(model),
767
+ )
768
+ url_idx = random.choice(app.state.MODELS[model]["urls"])
769
+ url = app.state.config.OLLAMA_BASE_URLS[url_idx]
770
+ return url
771
+
772
+
773
+ @app.post("/api/chat")
774
+ @app.post("/api/chat/{url_idx}")
775
+ async def generate_chat_completion(
776
+ form_data: GenerateChatCompletionForm,
777
+ url_idx: Optional[int] = None,
778
+ user=Depends(get_verified_user),
779
+ ):
780
+ payload = {**form_data.model_dump(exclude_none=True)}
781
+ log.debug(f"{payload = }")
782
+ if "metadata" in payload:
783
+ del payload["metadata"]
784
+
785
+ model_id = form_data.model
786
+
787
+ if app.state.config.ENABLE_MODEL_FILTER:
788
+ if user.role == "user" and model_id not in app.state.config.MODEL_FILTER_LIST:
789
+ raise HTTPException(
790
+ status_code=403,
791
+ detail="Model not found",
792
+ )
793
+
794
+ model_info = Models.get_model_by_id(model_id)
795
+
796
+ if model_info:
797
+ if model_info.base_model_id:
798
+ payload["model"] = model_info.base_model_id
799
+
800
+ params = model_info.params.model_dump()
801
+
802
+ if params:
803
+ if payload.get("options") is None:
804
+ payload["options"] = {}
805
+
806
+ payload["options"] = apply_model_params_to_body_ollama(
807
+ params, payload["options"]
808
+ )
809
+ payload = apply_model_system_prompt_to_body(params, payload, user)
810
+
811
+ if ":" not in payload["model"]:
812
+ payload["model"] = f"{payload['model']}:latest"
813
+
814
+ url = get_ollama_url(url_idx, payload["model"])
815
+ log.info(f"url: {url}")
816
+ log.debug(payload)
817
+
818
+ return await post_streaming_url(
819
+ f"{url}/api/chat",
820
+ json.dumps(payload),
821
+ stream=form_data.stream,
822
+ content_type="application/x-ndjson",
823
+ )
824
+
825
+
826
+ # TODO: we should update this part once Ollama supports other types
827
+ class OpenAIChatMessageContent(BaseModel):
828
+ type: str
829
+ model_config = ConfigDict(extra="allow")
830
+
831
+
832
+ class OpenAIChatMessage(BaseModel):
833
+ role: str
834
+ content: Union[str, OpenAIChatMessageContent]
835
+
836
+ model_config = ConfigDict(extra="allow")
837
+
838
+
839
+ class OpenAIChatCompletionForm(BaseModel):
840
+ model: str
841
+ messages: list[OpenAIChatMessage]
842
+
843
+ model_config = ConfigDict(extra="allow")
844
+
845
+
846
+ @app.post("/v1/chat/completions")
847
+ @app.post("/v1/chat/completions/{url_idx}")
848
+ async def generate_openai_chat_completion(
849
+ form_data: dict,
850
+ url_idx: Optional[int] = None,
851
+ user=Depends(get_verified_user),
852
+ ):
853
+ completion_form = OpenAIChatCompletionForm(**form_data)
854
+ payload = {**completion_form.model_dump(exclude_none=True, exclude=["metadata"])}
855
+ if "metadata" in payload:
856
+ del payload["metadata"]
857
+
858
+ model_id = completion_form.model
859
+
860
+ if app.state.config.ENABLE_MODEL_FILTER:
861
+ if user.role == "user" and model_id not in app.state.config.MODEL_FILTER_LIST:
862
+ raise HTTPException(
863
+ status_code=403,
864
+ detail="Model not found",
865
+ )
866
+
867
+ model_info = Models.get_model_by_id(model_id)
868
+
869
+ if model_info:
870
+ if model_info.base_model_id:
871
+ payload["model"] = model_info.base_model_id
872
+
873
+ params = model_info.params.model_dump()
874
+
875
+ if params:
876
+ payload = apply_model_params_to_body_openai(params, payload)
877
+ payload = apply_model_system_prompt_to_body(params, payload, user)
878
+
879
+ if ":" not in payload["model"]:
880
+ payload["model"] = f"{payload['model']}:latest"
881
+
882
+ url = get_ollama_url(url_idx, payload["model"])
883
+ log.info(f"url: {url}")
884
+
885
+ return await post_streaming_url(
886
+ f"{url}/v1/chat/completions",
887
+ json.dumps(payload),
888
+ stream=payload.get("stream", False),
889
+ )
890
+
891
+
892
+ @app.get("/v1/models")
893
+ @app.get("/v1/models/{url_idx}")
894
+ async def get_openai_models(
895
+ url_idx: Optional[int] = None,
896
+ user=Depends(get_verified_user),
897
+ ):
898
+ if url_idx is None:
899
+ models = await get_all_models()
900
+
901
+ if app.state.config.ENABLE_MODEL_FILTER:
902
+ if user.role == "user":
903
+ models["models"] = list(
904
+ filter(
905
+ lambda model: model["name"]
906
+ in app.state.config.MODEL_FILTER_LIST,
907
+ models["models"],
908
+ )
909
+ )
910
+
911
+ return {
912
+ "data": [
913
+ {
914
+ "id": model["model"],
915
+ "object": "model",
916
+ "created": int(time.time()),
917
+ "owned_by": "openai",
918
+ }
919
+ for model in models["models"]
920
+ ],
921
+ "object": "list",
922
+ }
923
+
924
+ else:
925
+ url = app.state.config.OLLAMA_BASE_URLS[url_idx]
926
+ try:
927
+ r = requests.request(method="GET", url=f"{url}/api/tags")
928
+ r.raise_for_status()
929
+
930
+ models = r.json()
931
+
932
+ return {
933
+ "data": [
934
+ {
935
+ "id": model["model"],
936
+ "object": "model",
937
+ "created": int(time.time()),
938
+ "owned_by": "openai",
939
+ }
940
+ for model in models["models"]
941
+ ],
942
+ "object": "list",
943
+ }
944
+
945
+ except Exception as e:
946
+ log.exception(e)
947
+ error_detail = "Open WebUI: Server Connection Error"
948
+ if r is not None:
949
+ try:
950
+ res = r.json()
951
+ if "error" in res:
952
+ error_detail = f"Ollama: {res['error']}"
953
+ except Exception:
954
+ error_detail = f"Ollama: {e}"
955
+
956
+ raise HTTPException(
957
+ status_code=r.status_code if r else 500,
958
+ detail=error_detail,
959
+ )
960
+
961
+
962
+ class UrlForm(BaseModel):
963
+ url: str
964
+
965
+
966
+ class UploadBlobForm(BaseModel):
967
+ filename: str
968
+
969
+
970
+ def parse_huggingface_url(hf_url):
971
+ try:
972
+ # Parse the URL
973
+ parsed_url = urlparse(hf_url)
974
+
975
+ # Get the path and split it into components
976
+ path_components = parsed_url.path.split("/")
977
+
978
+ # Extract the desired output
979
+ model_file = path_components[-1]
980
+
981
+ return model_file
982
+ except ValueError:
983
+ return None
984
+
985
+
986
+ async def download_file_stream(
987
+ ollama_url, file_url, file_path, file_name, chunk_size=1024 * 1024
988
+ ):
989
+ done = False
990
+
991
+ if os.path.exists(file_path):
992
+ current_size = os.path.getsize(file_path)
993
+ else:
994
+ current_size = 0
995
+
996
+ headers = {"Range": f"bytes={current_size}-"} if current_size > 0 else {}
997
+
998
+ timeout = aiohttp.ClientTimeout(total=600) # Set the timeout
999
+
1000
+ async with aiohttp.ClientSession(timeout=timeout, trust_env=True) as session:
1001
+ async with session.get(file_url, headers=headers) as response:
1002
+ total_size = int(response.headers.get("content-length", 0)) + current_size
1003
+
1004
+ with open(file_path, "ab+") as file:
1005
+ async for data in response.content.iter_chunked(chunk_size):
1006
+ current_size += len(data)
1007
+ file.write(data)
1008
+
1009
+ done = current_size == total_size
1010
+ progress = round((current_size / total_size) * 100, 2)
1011
+
1012
+ yield f'data: {{"progress": {progress}, "completed": {current_size}, "total": {total_size}}}\n\n'
1013
+
1014
+ if done:
1015
+ file.seek(0)
1016
+ hashed = calculate_sha256(file)
1017
+ file.seek(0)
1018
+
1019
+ url = f"{ollama_url}/api/blobs/sha256:{hashed}"
1020
+ response = requests.post(url, data=file)
1021
+
1022
+ if response.ok:
1023
+ res = {
1024
+ "done": done,
1025
+ "blob": f"sha256:{hashed}",
1026
+ "name": file_name,
1027
+ }
1028
+ os.remove(file_path)
1029
+
1030
+ yield f"data: {json.dumps(res)}\n\n"
1031
+ else:
1032
+ raise "Ollama: Could not create blob, Please try again."
1033
+
1034
+
1035
+ # url = "https://huggingface.co/TheBloke/stablelm-zephyr-3b-GGUF/resolve/main/stablelm-zephyr-3b.Q2_K.gguf"
1036
+ @app.post("/models/download")
1037
+ @app.post("/models/download/{url_idx}")
1038
+ async def download_model(
1039
+ form_data: UrlForm,
1040
+ url_idx: Optional[int] = None,
1041
+ user=Depends(get_admin_user),
1042
+ ):
1043
+ allowed_hosts = ["https://huggingface.co/", "https://github.com/"]
1044
+
1045
+ if not any(form_data.url.startswith(host) for host in allowed_hosts):
1046
+ raise HTTPException(
1047
+ status_code=400,
1048
+ detail="Invalid file_url. Only URLs from allowed hosts are permitted.",
1049
+ )
1050
+
1051
+ if url_idx is None:
1052
+ url_idx = 0
1053
+ url = app.state.config.OLLAMA_BASE_URLS[url_idx]
1054
+
1055
+ file_name = parse_huggingface_url(form_data.url)
1056
+
1057
+ if file_name:
1058
+ file_path = f"{UPLOAD_DIR}/{file_name}"
1059
+
1060
+ return StreamingResponse(
1061
+ download_file_stream(url, form_data.url, file_path, file_name),
1062
+ )
1063
+ else:
1064
+ return None
1065
+
1066
+
1067
+ @app.post("/models/upload")
1068
+ @app.post("/models/upload/{url_idx}")
1069
+ def upload_model(
1070
+ file: UploadFile = File(...),
1071
+ url_idx: Optional[int] = None,
1072
+ user=Depends(get_admin_user),
1073
+ ):
1074
+ if url_idx is None:
1075
+ url_idx = 0
1076
+ ollama_url = app.state.config.OLLAMA_BASE_URLS[url_idx]
1077
+
1078
+ file_path = f"{UPLOAD_DIR}/{file.filename}"
1079
+
1080
+ # Save file in chunks
1081
+ with open(file_path, "wb+") as f:
1082
+ for chunk in file.file:
1083
+ f.write(chunk)
1084
+
1085
+ def file_process_stream():
1086
+ nonlocal ollama_url
1087
+ total_size = os.path.getsize(file_path)
1088
+ chunk_size = 1024 * 1024
1089
+ try:
1090
+ with open(file_path, "rb") as f:
1091
+ total = 0
1092
+ done = False
1093
+
1094
+ while not done:
1095
+ chunk = f.read(chunk_size)
1096
+ if not chunk:
1097
+ done = True
1098
+ continue
1099
+
1100
+ total += len(chunk)
1101
+ progress = round((total / total_size) * 100, 2)
1102
+
1103
+ res = {
1104
+ "progress": progress,
1105
+ "total": total_size,
1106
+ "completed": total,
1107
+ }
1108
+ yield f"data: {json.dumps(res)}\n\n"
1109
+
1110
+ if done:
1111
+ f.seek(0)
1112
+ hashed = calculate_sha256(f)
1113
+ f.seek(0)
1114
+
1115
+ url = f"{ollama_url}/api/blobs/sha256:{hashed}"
1116
+ response = requests.post(url, data=f)
1117
+
1118
+ if response.ok:
1119
+ res = {
1120
+ "done": done,
1121
+ "blob": f"sha256:{hashed}",
1122
+ "name": file.filename,
1123
+ }
1124
+ os.remove(file_path)
1125
+ yield f"data: {json.dumps(res)}\n\n"
1126
+ else:
1127
+ raise Exception(
1128
+ "Ollama: Could not create blob, Please try again."
1129
+ )
1130
+
1131
+ except Exception as e:
1132
+ res = {"error": str(e)}
1133
+ yield f"data: {json.dumps(res)}\n\n"
1134
+
1135
+ return StreamingResponse(file_process_stream(), media_type="text/event-stream")
backend/open_webui/apps/openai/main.py ADDED
@@ -0,0 +1,546 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import asyncio
2
+ import hashlib
3
+ import json
4
+ import logging
5
+ from pathlib import Path
6
+ from typing import Literal, Optional, overload
7
+
8
+ import aiohttp
9
+ import requests
10
+ from open_webui.apps.webui.models.models import Models
11
+ from open_webui.config import (
12
+ AIOHTTP_CLIENT_TIMEOUT,
13
+ CACHE_DIR,
14
+ CORS_ALLOW_ORIGIN,
15
+ ENABLE_MODEL_FILTER,
16
+ ENABLE_OPENAI_API,
17
+ MODEL_FILTER_LIST,
18
+ OPENAI_API_BASE_URLS,
19
+ OPENAI_API_KEYS,
20
+ AppConfig,
21
+ )
22
+ from open_webui.constants import ERROR_MESSAGES
23
+ from open_webui.env import SRC_LOG_LEVELS
24
+ from fastapi import Depends, FastAPI, HTTPException, Request
25
+ from fastapi.middleware.cors import CORSMiddleware
26
+ from fastapi.responses import FileResponse, StreamingResponse
27
+ from pydantic import BaseModel
28
+ from starlette.background import BackgroundTask
29
+
30
+
31
+ from open_webui.utils.payload import (
32
+ apply_model_params_to_body_openai,
33
+ apply_model_system_prompt_to_body,
34
+ )
35
+
36
+ from open_webui.utils.utils import get_admin_user, get_verified_user
37
+
38
+ log = logging.getLogger(__name__)
39
+ log.setLevel(SRC_LOG_LEVELS["OPENAI"])
40
+
41
+ app = FastAPI()
42
+ app.add_middleware(
43
+ CORSMiddleware,
44
+ allow_origins=CORS_ALLOW_ORIGIN,
45
+ allow_credentials=True,
46
+ allow_methods=["*"],
47
+ allow_headers=["*"],
48
+ )
49
+
50
+
51
+ app.state.config = AppConfig()
52
+
53
+ app.state.config.ENABLE_MODEL_FILTER = ENABLE_MODEL_FILTER
54
+ app.state.config.MODEL_FILTER_LIST = MODEL_FILTER_LIST
55
+
56
+ app.state.config.ENABLE_OPENAI_API = ENABLE_OPENAI_API
57
+ app.state.config.OPENAI_API_BASE_URLS = OPENAI_API_BASE_URLS
58
+ app.state.config.OPENAI_API_KEYS = OPENAI_API_KEYS
59
+
60
+ app.state.MODELS = {}
61
+
62
+
63
+ @app.middleware("http")
64
+ async def check_url(request: Request, call_next):
65
+ if len(app.state.MODELS) == 0:
66
+ await get_all_models()
67
+
68
+ response = await call_next(request)
69
+ return response
70
+
71
+
72
+ @app.get("/config")
73
+ async def get_config(user=Depends(get_admin_user)):
74
+ return {"ENABLE_OPENAI_API": app.state.config.ENABLE_OPENAI_API}
75
+
76
+
77
+ class OpenAIConfigForm(BaseModel):
78
+ enable_openai_api: Optional[bool] = None
79
+
80
+
81
+ @app.post("/config/update")
82
+ async def update_config(form_data: OpenAIConfigForm, user=Depends(get_admin_user)):
83
+ app.state.config.ENABLE_OPENAI_API = form_data.enable_openai_api
84
+ return {"ENABLE_OPENAI_API": app.state.config.ENABLE_OPENAI_API}
85
+
86
+
87
+ class UrlsUpdateForm(BaseModel):
88
+ urls: list[str]
89
+
90
+
91
+ class KeysUpdateForm(BaseModel):
92
+ keys: list[str]
93
+
94
+
95
+ @app.get("/urls")
96
+ async def get_openai_urls(user=Depends(get_admin_user)):
97
+ return {"OPENAI_API_BASE_URLS": app.state.config.OPENAI_API_BASE_URLS}
98
+
99
+
100
+ @app.post("/urls/update")
101
+ async def update_openai_urls(form_data: UrlsUpdateForm, user=Depends(get_admin_user)):
102
+ await get_all_models()
103
+ app.state.config.OPENAI_API_BASE_URLS = form_data.urls
104
+ return {"OPENAI_API_BASE_URLS": app.state.config.OPENAI_API_BASE_URLS}
105
+
106
+
107
+ @app.get("/keys")
108
+ async def get_openai_keys(user=Depends(get_admin_user)):
109
+ return {"OPENAI_API_KEYS": app.state.config.OPENAI_API_KEYS}
110
+
111
+
112
+ @app.post("/keys/update")
113
+ async def update_openai_key(form_data: KeysUpdateForm, user=Depends(get_admin_user)):
114
+ app.state.config.OPENAI_API_KEYS = form_data.keys
115
+ return {"OPENAI_API_KEYS": app.state.config.OPENAI_API_KEYS}
116
+
117
+
118
+ @app.post("/audio/speech")
119
+ async def speech(request: Request, user=Depends(get_verified_user)):
120
+ idx = None
121
+ try:
122
+ idx = app.state.config.OPENAI_API_BASE_URLS.index("https://api.openai.com/v1")
123
+ body = await request.body()
124
+ name = hashlib.sha256(body).hexdigest()
125
+
126
+ SPEECH_CACHE_DIR = Path(CACHE_DIR).joinpath("./audio/speech/")
127
+ SPEECH_CACHE_DIR.mkdir(parents=True, exist_ok=True)
128
+ file_path = SPEECH_CACHE_DIR.joinpath(f"{name}.mp3")
129
+ file_body_path = SPEECH_CACHE_DIR.joinpath(f"{name}.json")
130
+
131
+ # Check if the file already exists in the cache
132
+ if file_path.is_file():
133
+ return FileResponse(file_path)
134
+
135
+ headers = {}
136
+ headers["Authorization"] = f"Bearer {app.state.config.OPENAI_API_KEYS[idx]}"
137
+ headers["Content-Type"] = "application/json"
138
+ if "openrouter.ai" in app.state.config.OPENAI_API_BASE_URLS[idx]:
139
+ headers["HTTP-Referer"] = "https://openwebui.com/"
140
+ headers["X-Title"] = "Open WebUI"
141
+ r = None
142
+ try:
143
+ r = requests.post(
144
+ url=f"{app.state.config.OPENAI_API_BASE_URLS[idx]}/audio/speech",
145
+ data=body,
146
+ headers=headers,
147
+ stream=True,
148
+ )
149
+
150
+ r.raise_for_status()
151
+
152
+ # Save the streaming content to a file
153
+ with open(file_path, "wb") as f:
154
+ for chunk in r.iter_content(chunk_size=8192):
155
+ f.write(chunk)
156
+
157
+ with open(file_body_path, "w") as f:
158
+ json.dump(json.loads(body.decode("utf-8")), f)
159
+
160
+ # Return the saved file
161
+ return FileResponse(file_path)
162
+
163
+ except Exception as e:
164
+ log.exception(e)
165
+ error_detail = "Open WebUI: Server Connection Error"
166
+ if r is not None:
167
+ try:
168
+ res = r.json()
169
+ if "error" in res:
170
+ error_detail = f"External: {res['error']}"
171
+ except Exception:
172
+ error_detail = f"External: {e}"
173
+
174
+ raise HTTPException(
175
+ status_code=r.status_code if r else 500, detail=error_detail
176
+ )
177
+
178
+ except ValueError:
179
+ raise HTTPException(status_code=401, detail=ERROR_MESSAGES.OPENAI_NOT_FOUND)
180
+
181
+
182
+ async def fetch_url(url, key):
183
+ timeout = aiohttp.ClientTimeout(total=5)
184
+ try:
185
+ headers = {"Authorization": f"Bearer {key}"}
186
+ async with aiohttp.ClientSession(timeout=timeout, trust_env=True) as session:
187
+ async with session.get(url, headers=headers) as response:
188
+ return await response.json()
189
+ except Exception as e:
190
+ # Handle connection error here
191
+ log.error(f"Connection error: {e}")
192
+ return None
193
+
194
+
195
+ async def cleanup_response(
196
+ response: Optional[aiohttp.ClientResponse],
197
+ session: Optional[aiohttp.ClientSession],
198
+ ):
199
+ if response:
200
+ response.close()
201
+ if session:
202
+ await session.close()
203
+
204
+
205
+ def merge_models_lists(model_lists):
206
+ log.debug(f"merge_models_lists {model_lists}")
207
+ merged_list = []
208
+
209
+ for idx, models in enumerate(model_lists):
210
+ if models is not None and "error" not in models:
211
+ merged_list.extend(
212
+ [
213
+ {
214
+ **model,
215
+ "name": model.get("name", model["id"]),
216
+ "owned_by": "openai",
217
+ "openai": model,
218
+ "urlIdx": idx,
219
+ }
220
+ for model in models
221
+ if "api.openai.com"
222
+ not in app.state.config.OPENAI_API_BASE_URLS[idx]
223
+ or not any(
224
+ name in model["id"]
225
+ for name in [
226
+ "babbage",
227
+ "dall-e",
228
+ "davinci",
229
+ "embedding",
230
+ "tts",
231
+ "whisper",
232
+ ]
233
+ )
234
+ ]
235
+ )
236
+
237
+ return merged_list
238
+
239
+
240
+ def is_openai_api_disabled():
241
+ api_keys = app.state.config.OPENAI_API_KEYS
242
+ no_keys = len(api_keys) == 1 and api_keys[0] == ""
243
+ return no_keys or not app.state.config.ENABLE_OPENAI_API
244
+
245
+
246
+ async def get_all_models_raw() -> list:
247
+ if is_openai_api_disabled():
248
+ return []
249
+
250
+ # Check if API KEYS length is same than API URLS length
251
+ num_urls = len(app.state.config.OPENAI_API_BASE_URLS)
252
+ num_keys = len(app.state.config.OPENAI_API_KEYS)
253
+
254
+ if num_keys != num_urls:
255
+ # if there are more keys than urls, remove the extra keys
256
+ if num_keys > num_urls:
257
+ new_keys = app.state.config.OPENAI_API_KEYS[:num_urls]
258
+ app.state.config.OPENAI_API_KEYS = new_keys
259
+ # if there are more urls than keys, add empty keys
260
+ else:
261
+ app.state.config.OPENAI_API_KEYS += [""] * (num_urls - num_keys)
262
+
263
+ tasks = [
264
+ fetch_url(f"{url}/models", app.state.config.OPENAI_API_KEYS[idx])
265
+ for idx, url in enumerate(app.state.config.OPENAI_API_BASE_URLS)
266
+ ]
267
+
268
+ responses = await asyncio.gather(*tasks)
269
+ log.debug(f"get_all_models:responses() {responses}")
270
+
271
+ return responses
272
+
273
+
274
+ @overload
275
+ async def get_all_models(raw: Literal[True]) -> list: ...
276
+
277
+
278
+ @overload
279
+ async def get_all_models(raw: Literal[False] = False) -> dict[str, list]: ...
280
+
281
+
282
+ async def get_all_models(raw=False) -> dict[str, list] | list:
283
+ log.info("get_all_models()")
284
+ if is_openai_api_disabled():
285
+ return [] if raw else {"data": []}
286
+
287
+ responses = await get_all_models_raw()
288
+ if raw:
289
+ return responses
290
+
291
+ def extract_data(response):
292
+ if response and "data" in response:
293
+ return response["data"]
294
+ if isinstance(response, list):
295
+ return response
296
+ return None
297
+
298
+ models = {"data": merge_models_lists(map(extract_data, responses))}
299
+
300
+ log.debug(f"models: {models}")
301
+ app.state.MODELS = {model["id"]: model for model in models["data"]}
302
+
303
+ return models
304
+
305
+
306
+ @app.get("/models")
307
+ @app.get("/models/{url_idx}")
308
+ async def get_models(url_idx: Optional[int] = None, user=Depends(get_verified_user)):
309
+ if url_idx is None:
310
+ models = await get_all_models()
311
+ if app.state.config.ENABLE_MODEL_FILTER:
312
+ if user.role == "user":
313
+ models["data"] = list(
314
+ filter(
315
+ lambda model: model["id"] in app.state.config.MODEL_FILTER_LIST,
316
+ models["data"],
317
+ )
318
+ )
319
+ return models
320
+ return models
321
+ else:
322
+ url = app.state.config.OPENAI_API_BASE_URLS[url_idx]
323
+ key = app.state.config.OPENAI_API_KEYS[url_idx]
324
+
325
+ headers = {}
326
+ headers["Authorization"] = f"Bearer {key}"
327
+ headers["Content-Type"] = "application/json"
328
+
329
+ r = None
330
+
331
+ try:
332
+ r = requests.request(method="GET", url=f"{url}/models", headers=headers)
333
+ r.raise_for_status()
334
+
335
+ response_data = r.json()
336
+
337
+ if "api.openai.com" in url:
338
+ # Filter the response data
339
+ response_data["data"] = [
340
+ model
341
+ for model in response_data["data"]
342
+ if not any(
343
+ name in model["id"]
344
+ for name in [
345
+ "babbage",
346
+ "dall-e",
347
+ "davinci",
348
+ "embedding",
349
+ "tts",
350
+ "whisper",
351
+ ]
352
+ )
353
+ ]
354
+
355
+ return response_data
356
+ except Exception as e:
357
+ log.exception(e)
358
+ error_detail = "Open WebUI: Server Connection Error"
359
+ if r is not None:
360
+ try:
361
+ res = r.json()
362
+ if "error" in res:
363
+ error_detail = f"External: {res['error']}"
364
+ except Exception:
365
+ error_detail = f"External: {e}"
366
+
367
+ raise HTTPException(
368
+ status_code=r.status_code if r else 500,
369
+ detail=error_detail,
370
+ )
371
+
372
+
373
+ @app.post("/chat/completions")
374
+ @app.post("/chat/completions/{url_idx}")
375
+ async def generate_chat_completion(
376
+ form_data: dict,
377
+ url_idx: Optional[int] = None,
378
+ user=Depends(get_verified_user),
379
+ ):
380
+ idx = 0
381
+ payload = {**form_data}
382
+
383
+ if "metadata" in payload:
384
+ del payload["metadata"]
385
+
386
+ model_id = form_data.get("model")
387
+ model_info = Models.get_model_by_id(model_id)
388
+
389
+ if model_info:
390
+ if model_info.base_model_id:
391
+ payload["model"] = model_info.base_model_id
392
+
393
+ params = model_info.params.model_dump()
394
+ payload = apply_model_params_to_body_openai(params, payload)
395
+ payload = apply_model_system_prompt_to_body(params, payload, user)
396
+
397
+ model = app.state.MODELS[payload.get("model")]
398
+ idx = model["urlIdx"]
399
+
400
+ if "pipeline" in model and model.get("pipeline"):
401
+ payload["user"] = {
402
+ "name": user.name,
403
+ "id": user.id,
404
+ "email": user.email,
405
+ "role": user.role,
406
+ }
407
+
408
+ url = app.state.config.OPENAI_API_BASE_URLS[idx]
409
+ key = app.state.config.OPENAI_API_KEYS[idx]
410
+
411
+ # Change max_completion_tokens to max_tokens (Backward compatible)
412
+ if "api.openai.com" not in url and not payload["model"].lower().startswith("o1-"):
413
+ if "max_completion_tokens" in payload:
414
+ # Remove "max_completion_tokens" from the payload
415
+ payload["max_tokens"] = payload["max_completion_tokens"]
416
+ del payload["max_completion_tokens"]
417
+ else:
418
+ if "max_tokens" in payload and "max_completion_tokens" in payload:
419
+ del payload["max_tokens"]
420
+
421
+ # Convert the modified body back to JSON
422
+ payload = json.dumps(payload)
423
+
424
+ log.debug(payload)
425
+
426
+ headers = {}
427
+ headers["Authorization"] = f"Bearer {key}"
428
+ headers["Content-Type"] = "application/json"
429
+ if "openrouter.ai" in app.state.config.OPENAI_API_BASE_URLS[idx]:
430
+ headers["HTTP-Referer"] = "https://openwebui.com/"
431
+ headers["X-Title"] = "Open WebUI"
432
+
433
+ r = None
434
+ session = None
435
+ streaming = False
436
+ response = None
437
+
438
+ try:
439
+ session = aiohttp.ClientSession(
440
+ trust_env=True, timeout=aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT)
441
+ )
442
+ r = await session.request(
443
+ method="POST",
444
+ url=f"{url}/chat/completions",
445
+ data=payload,
446
+ headers=headers,
447
+ )
448
+
449
+ # Check if response is SSE
450
+ if "text/event-stream" in r.headers.get("Content-Type", ""):
451
+ streaming = True
452
+ return StreamingResponse(
453
+ r.content,
454
+ status_code=r.status,
455
+ headers=dict(r.headers),
456
+ background=BackgroundTask(
457
+ cleanup_response, response=r, session=session
458
+ ),
459
+ )
460
+ else:
461
+ try:
462
+ response = await r.json()
463
+ except Exception as e:
464
+ log.error(e)
465
+ response = await r.text()
466
+
467
+ r.raise_for_status()
468
+ return response
469
+ except Exception as e:
470
+ log.exception(e)
471
+ error_detail = "Open WebUI: Server Connection Error"
472
+ if isinstance(response, dict):
473
+ if "error" in response:
474
+ error_detail = f"{response['error']['message'] if 'message' in response['error'] else response['error']}"
475
+ elif isinstance(response, str):
476
+ error_detail = response
477
+
478
+ raise HTTPException(status_code=r.status if r else 500, detail=error_detail)
479
+ finally:
480
+ if not streaming and session:
481
+ if r:
482
+ r.close()
483
+ await session.close()
484
+
485
+
486
+ @app.api_route("/{path:path}", methods=["GET", "POST", "PUT", "DELETE"])
487
+ async def proxy(path: str, request: Request, user=Depends(get_verified_user)):
488
+ idx = 0
489
+
490
+ body = await request.body()
491
+
492
+ url = app.state.config.OPENAI_API_BASE_URLS[idx]
493
+ key = app.state.config.OPENAI_API_KEYS[idx]
494
+
495
+ target_url = f"{url}/{path}"
496
+
497
+ headers = {}
498
+ headers["Authorization"] = f"Bearer {key}"
499
+ headers["Content-Type"] = "application/json"
500
+
501
+ r = None
502
+ session = None
503
+ streaming = False
504
+
505
+ try:
506
+ session = aiohttp.ClientSession(trust_env=True)
507
+ r = await session.request(
508
+ method=request.method,
509
+ url=target_url,
510
+ data=body,
511
+ headers=headers,
512
+ )
513
+
514
+ r.raise_for_status()
515
+
516
+ # Check if response is SSE
517
+ if "text/event-stream" in r.headers.get("Content-Type", ""):
518
+ streaming = True
519
+ return StreamingResponse(
520
+ r.content,
521
+ status_code=r.status,
522
+ headers=dict(r.headers),
523
+ background=BackgroundTask(
524
+ cleanup_response, response=r, session=session
525
+ ),
526
+ )
527
+ else:
528
+ response_data = await r.json()
529
+ return response_data
530
+ except Exception as e:
531
+ log.exception(e)
532
+ error_detail = "Open WebUI: Server Connection Error"
533
+ if r is not None:
534
+ try:
535
+ res = await r.json()
536
+ print(res)
537
+ if "error" in res:
538
+ error_detail = f"External: {res['error']['message'] if 'message' in res['error'] else res['error']}"
539
+ except Exception:
540
+ error_detail = f"External: {e}"
541
+ raise HTTPException(status_code=r.status if r else 500, detail=error_detail)
542
+ finally:
543
+ if not streaming and session:
544
+ if r:
545
+ r.close()
546
+ await session.close()
backend/open_webui/apps/rag/main.py ADDED
@@ -0,0 +1,1577 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ import logging
3
+ import mimetypes
4
+ import os
5
+ import shutil
6
+ import socket
7
+ import urllib.parse
8
+ import uuid
9
+ from datetime import datetime
10
+ from pathlib import Path
11
+ from typing import Iterator, Optional, Sequence, Union
12
+
13
+
14
+ import numpy as np
15
+ import torch
16
+ import requests
17
+ import validators
18
+
19
+ from fastapi import Depends, FastAPI, File, Form, HTTPException, UploadFile, status
20
+ from fastapi.middleware.cors import CORSMiddleware
21
+ from pydantic import BaseModel
22
+
23
+ from open_webui.apps.rag.search.main import SearchResult
24
+ from open_webui.apps.rag.search.brave import search_brave
25
+ from open_webui.apps.rag.search.duckduckgo import search_duckduckgo
26
+ from open_webui.apps.rag.search.google_pse import search_google_pse
27
+ from open_webui.apps.rag.search.jina_search import search_jina
28
+ from open_webui.apps.rag.search.searchapi import search_searchapi
29
+ from open_webui.apps.rag.search.searxng import search_searxng
30
+ from open_webui.apps.rag.search.serper import search_serper
31
+ from open_webui.apps.rag.search.serply import search_serply
32
+ from open_webui.apps.rag.search.serpstack import search_serpstack
33
+ from open_webui.apps.rag.search.tavily import search_tavily
34
+ from open_webui.apps.rag.utils import (
35
+ get_embedding_function,
36
+ get_model_path,
37
+ query_collection,
38
+ query_collection_with_hybrid_search,
39
+ query_doc,
40
+ query_doc_with_hybrid_search,
41
+ )
42
+ from open_webui.apps.webui.models.documents import DocumentForm, Documents
43
+ from open_webui.apps.webui.models.files import Files
44
+ from open_webui.config import (
45
+ BRAVE_SEARCH_API_KEY,
46
+ CHUNK_OVERLAP,
47
+ CHUNK_SIZE,
48
+ CONTENT_EXTRACTION_ENGINE,
49
+ CORS_ALLOW_ORIGIN,
50
+ DOCS_DIR,
51
+ ENABLE_RAG_HYBRID_SEARCH,
52
+ ENABLE_RAG_LOCAL_WEB_FETCH,
53
+ ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION,
54
+ ENABLE_RAG_WEB_SEARCH,
55
+ ENV,
56
+ GOOGLE_PSE_API_KEY,
57
+ GOOGLE_PSE_ENGINE_ID,
58
+ PDF_EXTRACT_IMAGES,
59
+ RAG_EMBEDDING_ENGINE,
60
+ RAG_EMBEDDING_MODEL,
61
+ RAG_EMBEDDING_MODEL_AUTO_UPDATE,
62
+ RAG_EMBEDDING_MODEL_TRUST_REMOTE_CODE,
63
+ RAG_EMBEDDING_OPENAI_BATCH_SIZE,
64
+ RAG_FILE_MAX_COUNT,
65
+ RAG_FILE_MAX_SIZE,
66
+ RAG_OPENAI_API_BASE_URL,
67
+ RAG_OPENAI_API_KEY,
68
+ RAG_RELEVANCE_THRESHOLD,
69
+ RAG_RERANKING_MODEL,
70
+ RAG_RERANKING_MODEL_AUTO_UPDATE,
71
+ RAG_RERANKING_MODEL_TRUST_REMOTE_CODE,
72
+ DEFAULT_RAG_TEMPLATE,
73
+ RAG_TEMPLATE,
74
+ RAG_TOP_K,
75
+ RAG_WEB_SEARCH_CONCURRENT_REQUESTS,
76
+ RAG_WEB_SEARCH_DOMAIN_FILTER_LIST,
77
+ RAG_WEB_SEARCH_ENGINE,
78
+ RAG_WEB_SEARCH_RESULT_COUNT,
79
+ SEARCHAPI_API_KEY,
80
+ SEARCHAPI_ENGINE,
81
+ SEARXNG_QUERY_URL,
82
+ SERPER_API_KEY,
83
+ SERPLY_API_KEY,
84
+ SERPSTACK_API_KEY,
85
+ SERPSTACK_HTTPS,
86
+ TAVILY_API_KEY,
87
+ TIKA_SERVER_URL,
88
+ UPLOAD_DIR,
89
+ YOUTUBE_LOADER_LANGUAGE,
90
+ AppConfig,
91
+ )
92
+ from open_webui.constants import ERROR_MESSAGES
93
+ from open_webui.env import SRC_LOG_LEVELS, DEVICE_TYPE, DOCKER
94
+ from open_webui.utils.misc import (
95
+ calculate_sha256,
96
+ calculate_sha256_string,
97
+ extract_folders_after_data_docs,
98
+ sanitize_filename,
99
+ )
100
+ from open_webui.utils.utils import get_admin_user, get_verified_user
101
+ from open_webui.apps.rag.vector.connector import VECTOR_DB_CLIENT
102
+
103
+ from langchain.text_splitter import RecursiveCharacterTextSplitter
104
+ from langchain_community.document_loaders import (
105
+ BSHTMLLoader,
106
+ CSVLoader,
107
+ Docx2txtLoader,
108
+ OutlookMessageLoader,
109
+ PyPDFLoader,
110
+ TextLoader,
111
+ UnstructuredEPubLoader,
112
+ UnstructuredExcelLoader,
113
+ UnstructuredMarkdownLoader,
114
+ UnstructuredPowerPointLoader,
115
+ UnstructuredRSTLoader,
116
+ UnstructuredXMLLoader,
117
+ WebBaseLoader,
118
+ YoutubeLoader,
119
+ )
120
+ from langchain_core.documents import Document
121
+ from colbert.infra import ColBERTConfig
122
+ from colbert.modeling.checkpoint import Checkpoint
123
+
124
+ log = logging.getLogger(__name__)
125
+ log.setLevel(SRC_LOG_LEVELS["RAG"])
126
+
127
+ app = FastAPI()
128
+
129
+ app.state.config = AppConfig()
130
+
131
+ app.state.config.TOP_K = RAG_TOP_K
132
+ app.state.config.RELEVANCE_THRESHOLD = RAG_RELEVANCE_THRESHOLD
133
+ app.state.config.FILE_MAX_SIZE = RAG_FILE_MAX_SIZE
134
+ app.state.config.FILE_MAX_COUNT = RAG_FILE_MAX_COUNT
135
+
136
+ app.state.config.ENABLE_RAG_HYBRID_SEARCH = ENABLE_RAG_HYBRID_SEARCH
137
+ app.state.config.ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION = (
138
+ ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION
139
+ )
140
+
141
+ app.state.config.CONTENT_EXTRACTION_ENGINE = CONTENT_EXTRACTION_ENGINE
142
+ app.state.config.TIKA_SERVER_URL = TIKA_SERVER_URL
143
+
144
+ app.state.config.CHUNK_SIZE = CHUNK_SIZE
145
+ app.state.config.CHUNK_OVERLAP = CHUNK_OVERLAP
146
+
147
+ app.state.config.RAG_EMBEDDING_ENGINE = RAG_EMBEDDING_ENGINE
148
+ app.state.config.RAG_EMBEDDING_MODEL = RAG_EMBEDDING_MODEL
149
+ app.state.config.RAG_EMBEDDING_OPENAI_BATCH_SIZE = RAG_EMBEDDING_OPENAI_BATCH_SIZE
150
+ app.state.config.RAG_RERANKING_MODEL = RAG_RERANKING_MODEL
151
+ app.state.config.RAG_TEMPLATE = RAG_TEMPLATE
152
+
153
+ app.state.config.OPENAI_API_BASE_URL = RAG_OPENAI_API_BASE_URL
154
+ app.state.config.OPENAI_API_KEY = RAG_OPENAI_API_KEY
155
+
156
+ app.state.config.PDF_EXTRACT_IMAGES = PDF_EXTRACT_IMAGES
157
+
158
+ app.state.config.YOUTUBE_LOADER_LANGUAGE = YOUTUBE_LOADER_LANGUAGE
159
+ app.state.YOUTUBE_LOADER_TRANSLATION = None
160
+
161
+
162
+ app.state.config.ENABLE_RAG_WEB_SEARCH = ENABLE_RAG_WEB_SEARCH
163
+ app.state.config.RAG_WEB_SEARCH_ENGINE = RAG_WEB_SEARCH_ENGINE
164
+ app.state.config.RAG_WEB_SEARCH_DOMAIN_FILTER_LIST = RAG_WEB_SEARCH_DOMAIN_FILTER_LIST
165
+
166
+ app.state.config.SEARXNG_QUERY_URL = SEARXNG_QUERY_URL
167
+ app.state.config.GOOGLE_PSE_API_KEY = GOOGLE_PSE_API_KEY
168
+ app.state.config.GOOGLE_PSE_ENGINE_ID = GOOGLE_PSE_ENGINE_ID
169
+ app.state.config.BRAVE_SEARCH_API_KEY = BRAVE_SEARCH_API_KEY
170
+ app.state.config.SERPSTACK_API_KEY = SERPSTACK_API_KEY
171
+ app.state.config.SERPSTACK_HTTPS = SERPSTACK_HTTPS
172
+ app.state.config.SERPER_API_KEY = SERPER_API_KEY
173
+ app.state.config.SERPLY_API_KEY = SERPLY_API_KEY
174
+ app.state.config.TAVILY_API_KEY = TAVILY_API_KEY
175
+ app.state.config.SEARCHAPI_API_KEY = SEARCHAPI_API_KEY
176
+ app.state.config.SEARCHAPI_ENGINE = SEARCHAPI_ENGINE
177
+ app.state.config.RAG_WEB_SEARCH_RESULT_COUNT = RAG_WEB_SEARCH_RESULT_COUNT
178
+ app.state.config.RAG_WEB_SEARCH_CONCURRENT_REQUESTS = RAG_WEB_SEARCH_CONCURRENT_REQUESTS
179
+
180
+
181
+ def update_embedding_model(
182
+ embedding_model: str,
183
+ auto_update: bool = False,
184
+ ):
185
+ if embedding_model and app.state.config.RAG_EMBEDDING_ENGINE == "":
186
+ import sentence_transformers
187
+
188
+ app.state.sentence_transformer_ef = sentence_transformers.SentenceTransformer(
189
+ get_model_path(embedding_model, auto_update),
190
+ device=DEVICE_TYPE,
191
+ trust_remote_code=RAG_EMBEDDING_MODEL_TRUST_REMOTE_CODE,
192
+ )
193
+ else:
194
+ app.state.sentence_transformer_ef = None
195
+
196
+
197
+ def update_reranking_model(
198
+ reranking_model: str,
199
+ auto_update: bool = False,
200
+ ):
201
+ if reranking_model:
202
+ if any(model in reranking_model for model in ["jinaai/jina-colbert-v2"]):
203
+
204
+ class ColBERT:
205
+ def __init__(self, name) -> None:
206
+ print("ColBERT: Loading model", name)
207
+ self.device = "cuda" if torch.cuda.is_available() else "cpu"
208
+
209
+ if DOCKER:
210
+ # This is a workaround for the issue with the docker container
211
+ # where the torch extension is not loaded properly
212
+ # and the following error is thrown:
213
+ # /root/.cache/torch_extensions/py311_cpu/segmented_maxsim_cpp/segmented_maxsim_cpp.so: cannot open shared object file: No such file or directory
214
+
215
+ lock_file = "/root/.cache/torch_extensions/py311_cpu/segmented_maxsim_cpp/lock"
216
+ if os.path.exists(lock_file):
217
+ os.remove(lock_file)
218
+
219
+ self.ckpt = Checkpoint(
220
+ name,
221
+ colbert_config=ColBERTConfig(model_name=name),
222
+ ).to(self.device)
223
+ pass
224
+
225
+ def calculate_similarity_scores(
226
+ self, query_embeddings, document_embeddings
227
+ ):
228
+
229
+ query_embeddings = query_embeddings.to(self.device)
230
+ document_embeddings = document_embeddings.to(self.device)
231
+
232
+ # Validate dimensions to ensure compatibility
233
+ if query_embeddings.dim() != 3:
234
+ raise ValueError(
235
+ f"Expected query embeddings to have 3 dimensions, but got {query_embeddings.dim()}."
236
+ )
237
+ if document_embeddings.dim() != 3:
238
+ raise ValueError(
239
+ f"Expected document embeddings to have 3 dimensions, but got {document_embeddings.dim()}."
240
+ )
241
+ if query_embeddings.size(0) not in [1, document_embeddings.size(0)]:
242
+ raise ValueError(
243
+ "There should be either one query or queries equal to the number of documents."
244
+ )
245
+
246
+ # Transpose the query embeddings to align for matrix multiplication
247
+ transposed_query_embeddings = query_embeddings.permute(0, 2, 1)
248
+ # Compute similarity scores using batch matrix multiplication
249
+ computed_scores = torch.matmul(
250
+ document_embeddings, transposed_query_embeddings
251
+ )
252
+ # Apply max pooling to extract the highest semantic similarity across each document's sequence
253
+ maximum_scores = torch.max(computed_scores, dim=1).values
254
+
255
+ # Sum up the maximum scores across features to get the overall document relevance scores
256
+ final_scores = maximum_scores.sum(dim=1)
257
+
258
+ normalized_scores = torch.softmax(final_scores, dim=0)
259
+
260
+ return normalized_scores.detach().cpu().numpy().astype(np.float32)
261
+
262
+ def predict(self, sentences):
263
+
264
+ query = sentences[0][0]
265
+ docs = [i[1] for i in sentences]
266
+
267
+ # Embedding the documents
268
+ embedded_docs = self.ckpt.docFromText(docs, bsize=32)[0]
269
+ # Embedding the queries
270
+ embedded_queries = self.ckpt.queryFromText([query], bsize=32)
271
+ embedded_query = embedded_queries[0]
272
+
273
+ # Calculate retrieval scores for the query against all documents
274
+ scores = self.calculate_similarity_scores(
275
+ embedded_query.unsqueeze(0), embedded_docs
276
+ )
277
+
278
+ return scores
279
+
280
+ try:
281
+ app.state.sentence_transformer_rf = ColBERT(
282
+ get_model_path(reranking_model, auto_update)
283
+ )
284
+ except Exception as e:
285
+ log.error(f"ColBERT: {e}")
286
+ app.state.sentence_transformer_rf = None
287
+ app.state.config.ENABLE_RAG_HYBRID_SEARCH = False
288
+ else:
289
+ import sentence_transformers
290
+
291
+ try:
292
+ app.state.sentence_transformer_rf = sentence_transformers.CrossEncoder(
293
+ get_model_path(reranking_model, auto_update),
294
+ device=DEVICE_TYPE,
295
+ trust_remote_code=RAG_RERANKING_MODEL_TRUST_REMOTE_CODE,
296
+ )
297
+ except:
298
+ log.error("CrossEncoder error")
299
+ app.state.sentence_transformer_rf = None
300
+ app.state.config.ENABLE_RAG_HYBRID_SEARCH = False
301
+ else:
302
+ app.state.sentence_transformer_rf = None
303
+
304
+
305
+ update_embedding_model(
306
+ app.state.config.RAG_EMBEDDING_MODEL,
307
+ RAG_EMBEDDING_MODEL_AUTO_UPDATE,
308
+ )
309
+
310
+ update_reranking_model(
311
+ app.state.config.RAG_RERANKING_MODEL,
312
+ RAG_RERANKING_MODEL_AUTO_UPDATE,
313
+ )
314
+
315
+
316
+ app.state.EMBEDDING_FUNCTION = get_embedding_function(
317
+ app.state.config.RAG_EMBEDDING_ENGINE,
318
+ app.state.config.RAG_EMBEDDING_MODEL,
319
+ app.state.sentence_transformer_ef,
320
+ app.state.config.OPENAI_API_KEY,
321
+ app.state.config.OPENAI_API_BASE_URL,
322
+ app.state.config.RAG_EMBEDDING_OPENAI_BATCH_SIZE,
323
+ )
324
+
325
+ app.add_middleware(
326
+ CORSMiddleware,
327
+ allow_origins=CORS_ALLOW_ORIGIN,
328
+ allow_credentials=True,
329
+ allow_methods=["*"],
330
+ allow_headers=["*"],
331
+ )
332
+
333
+
334
+ class CollectionNameForm(BaseModel):
335
+ collection_name: Optional[str] = "test"
336
+
337
+
338
+ class UrlForm(CollectionNameForm):
339
+ url: str
340
+
341
+
342
+ class SearchForm(CollectionNameForm):
343
+ query: str
344
+
345
+
346
+ @app.get("/")
347
+ async def get_status():
348
+ return {
349
+ "status": True,
350
+ "chunk_size": app.state.config.CHUNK_SIZE,
351
+ "chunk_overlap": app.state.config.CHUNK_OVERLAP,
352
+ "template": app.state.config.RAG_TEMPLATE,
353
+ "embedding_engine": app.state.config.RAG_EMBEDDING_ENGINE,
354
+ "embedding_model": app.state.config.RAG_EMBEDDING_MODEL,
355
+ "reranking_model": app.state.config.RAG_RERANKING_MODEL,
356
+ "openai_batch_size": app.state.config.RAG_EMBEDDING_OPENAI_BATCH_SIZE,
357
+ }
358
+
359
+
360
+ @app.get("/embedding")
361
+ async def get_embedding_config(user=Depends(get_admin_user)):
362
+ return {
363
+ "status": True,
364
+ "embedding_engine": app.state.config.RAG_EMBEDDING_ENGINE,
365
+ "embedding_model": app.state.config.RAG_EMBEDDING_MODEL,
366
+ "openai_config": {
367
+ "url": app.state.config.OPENAI_API_BASE_URL,
368
+ "key": app.state.config.OPENAI_API_KEY,
369
+ "batch_size": app.state.config.RAG_EMBEDDING_OPENAI_BATCH_SIZE,
370
+ },
371
+ }
372
+
373
+
374
+ @app.get("/reranking")
375
+ async def get_reraanking_config(user=Depends(get_admin_user)):
376
+ return {
377
+ "status": True,
378
+ "reranking_model": app.state.config.RAG_RERANKING_MODEL,
379
+ }
380
+
381
+
382
+ class OpenAIConfigForm(BaseModel):
383
+ url: str
384
+ key: str
385
+ batch_size: Optional[int] = None
386
+
387
+
388
+ class EmbeddingModelUpdateForm(BaseModel):
389
+ openai_config: Optional[OpenAIConfigForm] = None
390
+ embedding_engine: str
391
+ embedding_model: str
392
+
393
+
394
+ @app.post("/embedding/update")
395
+ async def update_embedding_config(
396
+ form_data: EmbeddingModelUpdateForm, user=Depends(get_admin_user)
397
+ ):
398
+ log.info(
399
+ f"Updating embedding model: {app.state.config.RAG_EMBEDDING_MODEL} to {form_data.embedding_model}"
400
+ )
401
+ try:
402
+ app.state.config.RAG_EMBEDDING_ENGINE = form_data.embedding_engine
403
+ app.state.config.RAG_EMBEDDING_MODEL = form_data.embedding_model
404
+
405
+ if app.state.config.RAG_EMBEDDING_ENGINE in ["ollama", "openai"]:
406
+ if form_data.openai_config is not None:
407
+ app.state.config.OPENAI_API_BASE_URL = form_data.openai_config.url
408
+ app.state.config.OPENAI_API_KEY = form_data.openai_config.key
409
+ app.state.config.RAG_EMBEDDING_OPENAI_BATCH_SIZE = (
410
+ form_data.openai_config.batch_size
411
+ if form_data.openai_config.batch_size
412
+ else 1
413
+ )
414
+
415
+ update_embedding_model(app.state.config.RAG_EMBEDDING_MODEL)
416
+
417
+ app.state.EMBEDDING_FUNCTION = get_embedding_function(
418
+ app.state.config.RAG_EMBEDDING_ENGINE,
419
+ app.state.config.RAG_EMBEDDING_MODEL,
420
+ app.state.sentence_transformer_ef,
421
+ app.state.config.OPENAI_API_KEY,
422
+ app.state.config.OPENAI_API_BASE_URL,
423
+ app.state.config.RAG_EMBEDDING_OPENAI_BATCH_SIZE,
424
+ )
425
+
426
+ return {
427
+ "status": True,
428
+ "embedding_engine": app.state.config.RAG_EMBEDDING_ENGINE,
429
+ "embedding_model": app.state.config.RAG_EMBEDDING_MODEL,
430
+ "openai_config": {
431
+ "url": app.state.config.OPENAI_API_BASE_URL,
432
+ "key": app.state.config.OPENAI_API_KEY,
433
+ "batch_size": app.state.config.RAG_EMBEDDING_OPENAI_BATCH_SIZE,
434
+ },
435
+ }
436
+ except Exception as e:
437
+ log.exception(f"Problem updating embedding model: {e}")
438
+ raise HTTPException(
439
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
440
+ detail=ERROR_MESSAGES.DEFAULT(e),
441
+ )
442
+
443
+
444
+ class RerankingModelUpdateForm(BaseModel):
445
+ reranking_model: str
446
+
447
+
448
+ @app.post("/reranking/update")
449
+ async def update_reranking_config(
450
+ form_data: RerankingModelUpdateForm, user=Depends(get_admin_user)
451
+ ):
452
+ log.info(
453
+ f"Updating reranking model: {app.state.config.RAG_RERANKING_MODEL} to {form_data.reranking_model}"
454
+ )
455
+ try:
456
+ app.state.config.RAG_RERANKING_MODEL = form_data.reranking_model
457
+
458
+ update_reranking_model(app.state.config.RAG_RERANKING_MODEL, True)
459
+
460
+ return {
461
+ "status": True,
462
+ "reranking_model": app.state.config.RAG_RERANKING_MODEL,
463
+ }
464
+ except Exception as e:
465
+ log.exception(f"Problem updating reranking model: {e}")
466
+ raise HTTPException(
467
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
468
+ detail=ERROR_MESSAGES.DEFAULT(e),
469
+ )
470
+
471
+
472
+ @app.get("/config")
473
+ async def get_rag_config(user=Depends(get_admin_user)):
474
+ return {
475
+ "status": True,
476
+ "pdf_extract_images": app.state.config.PDF_EXTRACT_IMAGES,
477
+ "file": {
478
+ "max_size": app.state.config.FILE_MAX_SIZE,
479
+ "max_count": app.state.config.FILE_MAX_COUNT,
480
+ },
481
+ "content_extraction": {
482
+ "engine": app.state.config.CONTENT_EXTRACTION_ENGINE,
483
+ "tika_server_url": app.state.config.TIKA_SERVER_URL,
484
+ },
485
+ "chunk": {
486
+ "chunk_size": app.state.config.CHUNK_SIZE,
487
+ "chunk_overlap": app.state.config.CHUNK_OVERLAP,
488
+ },
489
+ "youtube": {
490
+ "language": app.state.config.YOUTUBE_LOADER_LANGUAGE,
491
+ "translation": app.state.YOUTUBE_LOADER_TRANSLATION,
492
+ },
493
+ "web": {
494
+ "ssl_verification": app.state.config.ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION,
495
+ "search": {
496
+ "enabled": app.state.config.ENABLE_RAG_WEB_SEARCH,
497
+ "engine": app.state.config.RAG_WEB_SEARCH_ENGINE,
498
+ "searxng_query_url": app.state.config.SEARXNG_QUERY_URL,
499
+ "google_pse_api_key": app.state.config.GOOGLE_PSE_API_KEY,
500
+ "google_pse_engine_id": app.state.config.GOOGLE_PSE_ENGINE_ID,
501
+ "brave_search_api_key": app.state.config.BRAVE_SEARCH_API_KEY,
502
+ "serpstack_api_key": app.state.config.SERPSTACK_API_KEY,
503
+ "serpstack_https": app.state.config.SERPSTACK_HTTPS,
504
+ "serper_api_key": app.state.config.SERPER_API_KEY,
505
+ "serply_api_key": app.state.config.SERPLY_API_KEY,
506
+ "tavily_api_key": app.state.config.TAVILY_API_KEY,
507
+ "searchapi_api_key": app.state.config.SEARCHAPI_API_KEY,
508
+ "seaarchapi_engine": app.state.config.SEARCHAPI_ENGINE,
509
+ "result_count": app.state.config.RAG_WEB_SEARCH_RESULT_COUNT,
510
+ "concurrent_requests": app.state.config.RAG_WEB_SEARCH_CONCURRENT_REQUESTS,
511
+ },
512
+ },
513
+ }
514
+
515
+
516
+ class FileConfig(BaseModel):
517
+ max_size: Optional[int] = None
518
+ max_count: Optional[int] = None
519
+
520
+
521
+ class ContentExtractionConfig(BaseModel):
522
+ engine: str = ""
523
+ tika_server_url: Optional[str] = None
524
+
525
+
526
+ class ChunkParamUpdateForm(BaseModel):
527
+ chunk_size: int
528
+ chunk_overlap: int
529
+
530
+
531
+ class YoutubeLoaderConfig(BaseModel):
532
+ language: list[str]
533
+ translation: Optional[str] = None
534
+
535
+
536
+ class WebSearchConfig(BaseModel):
537
+ enabled: bool
538
+ engine: Optional[str] = None
539
+ searxng_query_url: Optional[str] = None
540
+ google_pse_api_key: Optional[str] = None
541
+ google_pse_engine_id: Optional[str] = None
542
+ brave_search_api_key: Optional[str] = None
543
+ serpstack_api_key: Optional[str] = None
544
+ serpstack_https: Optional[bool] = None
545
+ serper_api_key: Optional[str] = None
546
+ serply_api_key: Optional[str] = None
547
+ tavily_api_key: Optional[str] = None
548
+ searchapi_api_key: Optional[str] = None
549
+ searchapi_engine: Optional[str] = None
550
+ result_count: Optional[int] = None
551
+ concurrent_requests: Optional[int] = None
552
+
553
+
554
+ class WebConfig(BaseModel):
555
+ search: WebSearchConfig
556
+ web_loader_ssl_verification: Optional[bool] = None
557
+
558
+
559
+ class ConfigUpdateForm(BaseModel):
560
+ pdf_extract_images: Optional[bool] = None
561
+ file: Optional[FileConfig] = None
562
+ content_extraction: Optional[ContentExtractionConfig] = None
563
+ chunk: Optional[ChunkParamUpdateForm] = None
564
+ youtube: Optional[YoutubeLoaderConfig] = None
565
+ web: Optional[WebConfig] = None
566
+
567
+
568
+ @app.post("/config/update")
569
+ async def update_rag_config(form_data: ConfigUpdateForm, user=Depends(get_admin_user)):
570
+ app.state.config.PDF_EXTRACT_IMAGES = (
571
+ form_data.pdf_extract_images
572
+ if form_data.pdf_extract_images is not None
573
+ else app.state.config.PDF_EXTRACT_IMAGES
574
+ )
575
+
576
+ if form_data.file is not None:
577
+ app.state.config.FILE_MAX_SIZE = form_data.file.max_size
578
+ app.state.config.FILE_MAX_COUNT = form_data.file.max_count
579
+
580
+ if form_data.content_extraction is not None:
581
+ log.info(f"Updating text settings: {form_data.content_extraction}")
582
+ app.state.config.CONTENT_EXTRACTION_ENGINE = form_data.content_extraction.engine
583
+ app.state.config.TIKA_SERVER_URL = form_data.content_extraction.tika_server_url
584
+
585
+ if form_data.chunk is not None:
586
+ app.state.config.CHUNK_SIZE = form_data.chunk.chunk_size
587
+ app.state.config.CHUNK_OVERLAP = form_data.chunk.chunk_overlap
588
+
589
+ if form_data.youtube is not None:
590
+ app.state.config.YOUTUBE_LOADER_LANGUAGE = form_data.youtube.language
591
+ app.state.YOUTUBE_LOADER_TRANSLATION = form_data.youtube.translation
592
+
593
+ if form_data.web is not None:
594
+ app.state.config.ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION = (
595
+ form_data.web.web_loader_ssl_verification
596
+ )
597
+
598
+ app.state.config.ENABLE_RAG_WEB_SEARCH = form_data.web.search.enabled
599
+ app.state.config.RAG_WEB_SEARCH_ENGINE = form_data.web.search.engine
600
+ app.state.config.SEARXNG_QUERY_URL = form_data.web.search.searxng_query_url
601
+ app.state.config.GOOGLE_PSE_API_KEY = form_data.web.search.google_pse_api_key
602
+ app.state.config.GOOGLE_PSE_ENGINE_ID = (
603
+ form_data.web.search.google_pse_engine_id
604
+ )
605
+ app.state.config.BRAVE_SEARCH_API_KEY = (
606
+ form_data.web.search.brave_search_api_key
607
+ )
608
+ app.state.config.SERPSTACK_API_KEY = form_data.web.search.serpstack_api_key
609
+ app.state.config.SERPSTACK_HTTPS = form_data.web.search.serpstack_https
610
+ app.state.config.SERPER_API_KEY = form_data.web.search.serper_api_key
611
+ app.state.config.SERPLY_API_KEY = form_data.web.search.serply_api_key
612
+ app.state.config.TAVILY_API_KEY = form_data.web.search.tavily_api_key
613
+ app.state.config.SEARCHAPI_API_KEY = form_data.web.search.searchapi_api_key
614
+ app.state.config.SEARCHAPI_ENGINE = form_data.web.search.searchapi_engine
615
+ app.state.config.RAG_WEB_SEARCH_RESULT_COUNT = form_data.web.search.result_count
616
+ app.state.config.RAG_WEB_SEARCH_CONCURRENT_REQUESTS = (
617
+ form_data.web.search.concurrent_requests
618
+ )
619
+
620
+ return {
621
+ "status": True,
622
+ "pdf_extract_images": app.state.config.PDF_EXTRACT_IMAGES,
623
+ "file": {
624
+ "max_size": app.state.config.FILE_MAX_SIZE,
625
+ "max_count": app.state.config.FILE_MAX_COUNT,
626
+ },
627
+ "content_extraction": {
628
+ "engine": app.state.config.CONTENT_EXTRACTION_ENGINE,
629
+ "tika_server_url": app.state.config.TIKA_SERVER_URL,
630
+ },
631
+ "chunk": {
632
+ "chunk_size": app.state.config.CHUNK_SIZE,
633
+ "chunk_overlap": app.state.config.CHUNK_OVERLAP,
634
+ },
635
+ "youtube": {
636
+ "language": app.state.config.YOUTUBE_LOADER_LANGUAGE,
637
+ "translation": app.state.YOUTUBE_LOADER_TRANSLATION,
638
+ },
639
+ "web": {
640
+ "ssl_verification": app.state.config.ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION,
641
+ "search": {
642
+ "enabled": app.state.config.ENABLE_RAG_WEB_SEARCH,
643
+ "engine": app.state.config.RAG_WEB_SEARCH_ENGINE,
644
+ "searxng_query_url": app.state.config.SEARXNG_QUERY_URL,
645
+ "google_pse_api_key": app.state.config.GOOGLE_PSE_API_KEY,
646
+ "google_pse_engine_id": app.state.config.GOOGLE_PSE_ENGINE_ID,
647
+ "brave_search_api_key": app.state.config.BRAVE_SEARCH_API_KEY,
648
+ "serpstack_api_key": app.state.config.SERPSTACK_API_KEY,
649
+ "serpstack_https": app.state.config.SERPSTACK_HTTPS,
650
+ "serper_api_key": app.state.config.SERPER_API_KEY,
651
+ "serply_api_key": app.state.config.SERPLY_API_KEY,
652
+ "serachapi_api_key": app.state.config.SEARCHAPI_API_KEY,
653
+ "searchapi_engine": app.state.config.SEARCHAPI_ENGINE,
654
+ "tavily_api_key": app.state.config.TAVILY_API_KEY,
655
+ "result_count": app.state.config.RAG_WEB_SEARCH_RESULT_COUNT,
656
+ "concurrent_requests": app.state.config.RAG_WEB_SEARCH_CONCURRENT_REQUESTS,
657
+ },
658
+ },
659
+ }
660
+
661
+
662
+ @app.get("/template")
663
+ async def get_rag_template(user=Depends(get_verified_user)):
664
+ return {
665
+ "status": True,
666
+ "template": app.state.config.RAG_TEMPLATE,
667
+ }
668
+
669
+
670
+ @app.get("/query/settings")
671
+ async def get_query_settings(user=Depends(get_admin_user)):
672
+ return {
673
+ "status": True,
674
+ "template": app.state.config.RAG_TEMPLATE,
675
+ "k": app.state.config.TOP_K,
676
+ "r": app.state.config.RELEVANCE_THRESHOLD,
677
+ "hybrid": app.state.config.ENABLE_RAG_HYBRID_SEARCH,
678
+ }
679
+
680
+
681
+ class QuerySettingsForm(BaseModel):
682
+ k: Optional[int] = None
683
+ r: Optional[float] = None
684
+ template: Optional[str] = None
685
+ hybrid: Optional[bool] = None
686
+
687
+
688
+ @app.post("/query/settings/update")
689
+ async def update_query_settings(
690
+ form_data: QuerySettingsForm, user=Depends(get_admin_user)
691
+ ):
692
+ app.state.config.RAG_TEMPLATE = (
693
+ form_data.template if form_data.template != "" else DEFAULT_RAG_TEMPLATE
694
+ )
695
+ app.state.config.TOP_K = form_data.k if form_data.k else 4
696
+ app.state.config.RELEVANCE_THRESHOLD = form_data.r if form_data.r else 0.0
697
+ app.state.config.ENABLE_RAG_HYBRID_SEARCH = (
698
+ form_data.hybrid if form_data.hybrid else False
699
+ )
700
+
701
+ return {
702
+ "status": True,
703
+ "template": app.state.config.RAG_TEMPLATE,
704
+ "k": app.state.config.TOP_K,
705
+ "r": app.state.config.RELEVANCE_THRESHOLD,
706
+ "hybrid": app.state.config.ENABLE_RAG_HYBRID_SEARCH,
707
+ }
708
+
709
+
710
+ class QueryDocForm(BaseModel):
711
+ collection_name: str
712
+ query: str
713
+ k: Optional[int] = None
714
+ r: Optional[float] = None
715
+ hybrid: Optional[bool] = None
716
+
717
+
718
+ @app.post("/query/doc")
719
+ def query_doc_handler(
720
+ form_data: QueryDocForm,
721
+ user=Depends(get_verified_user),
722
+ ):
723
+ try:
724
+ if app.state.config.ENABLE_RAG_HYBRID_SEARCH:
725
+ return query_doc_with_hybrid_search(
726
+ collection_name=form_data.collection_name,
727
+ query=form_data.query,
728
+ embedding_function=app.state.EMBEDDING_FUNCTION,
729
+ k=form_data.k if form_data.k else app.state.config.TOP_K,
730
+ reranking_function=app.state.sentence_transformer_rf,
731
+ r=(
732
+ form_data.r if form_data.r else app.state.config.RELEVANCE_THRESHOLD
733
+ ),
734
+ )
735
+ else:
736
+ return query_doc(
737
+ collection_name=form_data.collection_name,
738
+ query=form_data.query,
739
+ embedding_function=app.state.EMBEDDING_FUNCTION,
740
+ k=form_data.k if form_data.k else app.state.config.TOP_K,
741
+ )
742
+ except Exception as e:
743
+ log.exception(e)
744
+ raise HTTPException(
745
+ status_code=status.HTTP_400_BAD_REQUEST,
746
+ detail=ERROR_MESSAGES.DEFAULT(e),
747
+ )
748
+
749
+
750
+ class QueryCollectionsForm(BaseModel):
751
+ collection_names: list[str]
752
+ query: str
753
+ k: Optional[int] = None
754
+ r: Optional[float] = None
755
+ hybrid: Optional[bool] = None
756
+
757
+
758
+ @app.post("/query/collection")
759
+ def query_collection_handler(
760
+ form_data: QueryCollectionsForm,
761
+ user=Depends(get_verified_user),
762
+ ):
763
+ try:
764
+ if app.state.config.ENABLE_RAG_HYBRID_SEARCH:
765
+ return query_collection_with_hybrid_search(
766
+ collection_names=form_data.collection_names,
767
+ query=form_data.query,
768
+ embedding_function=app.state.EMBEDDING_FUNCTION,
769
+ k=form_data.k if form_data.k else app.state.config.TOP_K,
770
+ reranking_function=app.state.sentence_transformer_rf,
771
+ r=(
772
+ form_data.r if form_data.r else app.state.config.RELEVANCE_THRESHOLD
773
+ ),
774
+ )
775
+ else:
776
+ return query_collection(
777
+ collection_names=form_data.collection_names,
778
+ query=form_data.query,
779
+ embedding_function=app.state.EMBEDDING_FUNCTION,
780
+ k=form_data.k if form_data.k else app.state.config.TOP_K,
781
+ )
782
+
783
+ except Exception as e:
784
+ log.exception(e)
785
+ raise HTTPException(
786
+ status_code=status.HTTP_400_BAD_REQUEST,
787
+ detail=ERROR_MESSAGES.DEFAULT(e),
788
+ )
789
+
790
+
791
+ @app.post("/youtube")
792
+ def store_youtube_video(form_data: UrlForm, user=Depends(get_verified_user)):
793
+ try:
794
+ loader = YoutubeLoader.from_youtube_url(
795
+ form_data.url,
796
+ add_video_info=True,
797
+ language=app.state.config.YOUTUBE_LOADER_LANGUAGE,
798
+ translation=app.state.YOUTUBE_LOADER_TRANSLATION,
799
+ )
800
+ data = loader.load()
801
+
802
+ collection_name = form_data.collection_name
803
+ if collection_name == "":
804
+ collection_name = calculate_sha256_string(form_data.url)[:63]
805
+
806
+ store_data_in_vector_db(data, collection_name, overwrite=True)
807
+ return {
808
+ "status": True,
809
+ "collection_name": collection_name,
810
+ "filename": form_data.url,
811
+ }
812
+ except Exception as e:
813
+ log.exception(e)
814
+ raise HTTPException(
815
+ status_code=status.HTTP_400_BAD_REQUEST,
816
+ detail=ERROR_MESSAGES.DEFAULT(e),
817
+ )
818
+
819
+
820
+ @app.post("/web")
821
+ def store_web(form_data: UrlForm, user=Depends(get_verified_user)):
822
+ # "https://www.gutenberg.org/files/1727/1727-h/1727-h.htm"
823
+ try:
824
+ loader = get_web_loader(
825
+ form_data.url,
826
+ verify_ssl=app.state.config.ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION,
827
+ )
828
+ data = loader.load()
829
+
830
+ collection_name = form_data.collection_name
831
+ if collection_name == "":
832
+ collection_name = calculate_sha256_string(form_data.url)[:63]
833
+
834
+ store_data_in_vector_db(data, collection_name, overwrite=True)
835
+ return {
836
+ "status": True,
837
+ "collection_name": collection_name,
838
+ "filename": form_data.url,
839
+ }
840
+ except Exception as e:
841
+ log.exception(e)
842
+ raise HTTPException(
843
+ status_code=status.HTTP_400_BAD_REQUEST,
844
+ detail=ERROR_MESSAGES.DEFAULT(e),
845
+ )
846
+
847
+
848
+ def get_web_loader(url: Union[str, Sequence[str]], verify_ssl: bool = True):
849
+ # Check if the URL is valid
850
+ if not validate_url(url):
851
+ raise ValueError(ERROR_MESSAGES.INVALID_URL)
852
+ return SafeWebBaseLoader(
853
+ url,
854
+ verify_ssl=verify_ssl,
855
+ requests_per_second=RAG_WEB_SEARCH_CONCURRENT_REQUESTS,
856
+ continue_on_failure=True,
857
+ )
858
+
859
+
860
+ def validate_url(url: Union[str, Sequence[str]]):
861
+ if isinstance(url, str):
862
+ if isinstance(validators.url(url), validators.ValidationError):
863
+ raise ValueError(ERROR_MESSAGES.INVALID_URL)
864
+ if not ENABLE_RAG_LOCAL_WEB_FETCH:
865
+ # Local web fetch is disabled, filter out any URLs that resolve to private IP addresses
866
+ parsed_url = urllib.parse.urlparse(url)
867
+ # Get IPv4 and IPv6 addresses
868
+ ipv4_addresses, ipv6_addresses = resolve_hostname(parsed_url.hostname)
869
+ # Check if any of the resolved addresses are private
870
+ # This is technically still vulnerable to DNS rebinding attacks, as we don't control WebBaseLoader
871
+ for ip in ipv4_addresses:
872
+ if validators.ipv4(ip, private=True):
873
+ raise ValueError(ERROR_MESSAGES.INVALID_URL)
874
+ for ip in ipv6_addresses:
875
+ if validators.ipv6(ip, private=True):
876
+ raise ValueError(ERROR_MESSAGES.INVALID_URL)
877
+ return True
878
+ elif isinstance(url, Sequence):
879
+ return all(validate_url(u) for u in url)
880
+ else:
881
+ return False
882
+
883
+
884
+ def resolve_hostname(hostname):
885
+ # Get address information
886
+ addr_info = socket.getaddrinfo(hostname, None)
887
+
888
+ # Extract IP addresses from address information
889
+ ipv4_addresses = [info[4][0] for info in addr_info if info[0] == socket.AF_INET]
890
+ ipv6_addresses = [info[4][0] for info in addr_info if info[0] == socket.AF_INET6]
891
+
892
+ return ipv4_addresses, ipv6_addresses
893
+
894
+
895
+ def search_web(engine: str, query: str) -> list[SearchResult]:
896
+ """Search the web using a search engine and return the results as a list of SearchResult objects.
897
+ Will look for a search engine API key in environment variables in the following order:
898
+ - SEARXNG_QUERY_URL
899
+ - GOOGLE_PSE_API_KEY + GOOGLE_PSE_ENGINE_ID
900
+ - BRAVE_SEARCH_API_KEY
901
+ - SERPSTACK_API_KEY
902
+ - SERPER_API_KEY
903
+ - SERPLY_API_KEY
904
+ - TAVILY_API_KEY
905
+ - SEARCHAPI_API_KEY + SEARCHAPI_ENGINE (by default `google`)
906
+ Args:
907
+ query (str): The query to search for
908
+ """
909
+
910
+ # TODO: add playwright to search the web
911
+ if engine == "searxng":
912
+ if app.state.config.SEARXNG_QUERY_URL:
913
+ return search_searxng(
914
+ app.state.config.SEARXNG_QUERY_URL,
915
+ query,
916
+ app.state.config.RAG_WEB_SEARCH_RESULT_COUNT,
917
+ app.state.config.RAG_WEB_SEARCH_DOMAIN_FILTER_LIST,
918
+ )
919
+ else:
920
+ raise Exception("No SEARXNG_QUERY_URL found in environment variables")
921
+ elif engine == "google_pse":
922
+ if (
923
+ app.state.config.GOOGLE_PSE_API_KEY
924
+ and app.state.config.GOOGLE_PSE_ENGINE_ID
925
+ ):
926
+ return search_google_pse(
927
+ app.state.config.GOOGLE_PSE_API_KEY,
928
+ app.state.config.GOOGLE_PSE_ENGINE_ID,
929
+ query,
930
+ app.state.config.RAG_WEB_SEARCH_RESULT_COUNT,
931
+ app.state.config.RAG_WEB_SEARCH_DOMAIN_FILTER_LIST,
932
+ )
933
+ else:
934
+ raise Exception(
935
+ "No GOOGLE_PSE_API_KEY or GOOGLE_PSE_ENGINE_ID found in environment variables"
936
+ )
937
+ elif engine == "brave":
938
+ if app.state.config.BRAVE_SEARCH_API_KEY:
939
+ return search_brave(
940
+ app.state.config.BRAVE_SEARCH_API_KEY,
941
+ query,
942
+ app.state.config.RAG_WEB_SEARCH_RESULT_COUNT,
943
+ app.state.config.RAG_WEB_SEARCH_DOMAIN_FILTER_LIST,
944
+ )
945
+ else:
946
+ raise Exception("No BRAVE_SEARCH_API_KEY found in environment variables")
947
+ elif engine == "serpstack":
948
+ if app.state.config.SERPSTACK_API_KEY:
949
+ return search_serpstack(
950
+ app.state.config.SERPSTACK_API_KEY,
951
+ query,
952
+ app.state.config.RAG_WEB_SEARCH_RESULT_COUNT,
953
+ app.state.config.RAG_WEB_SEARCH_DOMAIN_FILTER_LIST,
954
+ https_enabled=app.state.config.SERPSTACK_HTTPS,
955
+ )
956
+ else:
957
+ raise Exception("No SERPSTACK_API_KEY found in environment variables")
958
+ elif engine == "serper":
959
+ if app.state.config.SERPER_API_KEY:
960
+ return search_serper(
961
+ app.state.config.SERPER_API_KEY,
962
+ query,
963
+ app.state.config.RAG_WEB_SEARCH_RESULT_COUNT,
964
+ app.state.config.RAG_WEB_SEARCH_DOMAIN_FILTER_LIST,
965
+ )
966
+ else:
967
+ raise Exception("No SERPER_API_KEY found in environment variables")
968
+ elif engine == "serply":
969
+ if app.state.config.SERPLY_API_KEY:
970
+ return search_serply(
971
+ app.state.config.SERPLY_API_KEY,
972
+ query,
973
+ app.state.config.RAG_WEB_SEARCH_RESULT_COUNT,
974
+ app.state.config.RAG_WEB_SEARCH_DOMAIN_FILTER_LIST,
975
+ )
976
+ else:
977
+ raise Exception("No SERPLY_API_KEY found in environment variables")
978
+ elif engine == "duckduckgo":
979
+ return search_duckduckgo(
980
+ query,
981
+ app.state.config.RAG_WEB_SEARCH_RESULT_COUNT,
982
+ app.state.config.RAG_WEB_SEARCH_DOMAIN_FILTER_LIST,
983
+ )
984
+ elif engine == "tavily":
985
+ if app.state.config.TAVILY_API_KEY:
986
+ return search_tavily(
987
+ app.state.config.TAVILY_API_KEY,
988
+ query,
989
+ app.state.config.RAG_WEB_SEARCH_RESULT_COUNT,
990
+ )
991
+ else:
992
+ raise Exception("No TAVILY_API_KEY found in environment variables")
993
+ elif engine == "searchapi":
994
+ if app.state.config.SEARCHAPI_API_KEY:
995
+ return search_searchapi(
996
+ app.state.config.SEARCHAPI_API_KEY,
997
+ app.state.config.SEARCHAPI_ENGINE,
998
+ query,
999
+ app.state.config.RAG_WEB_SEARCH_RESULT_COUNT,
1000
+ app.state.config.RAG_WEB_SEARCH_DOMAIN_FILTER_LIST,
1001
+ )
1002
+ else:
1003
+ raise Exception("No SEARCHAPI_API_KEY found in environment variables")
1004
+ elif engine == "jina":
1005
+ return search_jina(query, app.state.config.RAG_WEB_SEARCH_RESULT_COUNT)
1006
+ else:
1007
+ raise Exception("No search engine API key found in environment variables")
1008
+
1009
+
1010
+ @app.post("/web/search")
1011
+ def store_web_search(form_data: SearchForm, user=Depends(get_verified_user)):
1012
+ try:
1013
+ logging.info(
1014
+ f"trying to web search with {app.state.config.RAG_WEB_SEARCH_ENGINE, form_data.query}"
1015
+ )
1016
+ web_results = search_web(
1017
+ app.state.config.RAG_WEB_SEARCH_ENGINE, form_data.query
1018
+ )
1019
+ except Exception as e:
1020
+ log.exception(e)
1021
+
1022
+ print(e)
1023
+ raise HTTPException(
1024
+ status_code=status.HTTP_400_BAD_REQUEST,
1025
+ detail=ERROR_MESSAGES.WEB_SEARCH_ERROR(e),
1026
+ )
1027
+
1028
+ try:
1029
+ urls = [result.link for result in web_results]
1030
+ loader = get_web_loader(urls)
1031
+ data = loader.load()
1032
+
1033
+ collection_name = form_data.collection_name
1034
+ if collection_name == "":
1035
+ collection_name = calculate_sha256_string(form_data.query)[:63]
1036
+
1037
+ store_data_in_vector_db(data, collection_name, overwrite=True)
1038
+ return {
1039
+ "status": True,
1040
+ "collection_name": collection_name,
1041
+ "filenames": urls,
1042
+ }
1043
+ except Exception as e:
1044
+ log.exception(e)
1045
+ raise HTTPException(
1046
+ status_code=status.HTTP_400_BAD_REQUEST,
1047
+ detail=ERROR_MESSAGES.DEFAULT(e),
1048
+ )
1049
+
1050
+
1051
+ def store_data_in_vector_db(
1052
+ data, collection_name, metadata: Optional[dict] = None, overwrite: bool = False
1053
+ ) -> bool:
1054
+ text_splitter = RecursiveCharacterTextSplitter(
1055
+ chunk_size=app.state.config.CHUNK_SIZE,
1056
+ chunk_overlap=app.state.config.CHUNK_OVERLAP,
1057
+ add_start_index=True,
1058
+ )
1059
+
1060
+ docs = text_splitter.split_documents(data)
1061
+
1062
+ if len(docs) > 0:
1063
+ log.info(f"store_data_in_vector_db {docs}")
1064
+ return store_docs_in_vector_db(docs, collection_name, metadata, overwrite), None
1065
+ else:
1066
+ raise ValueError(ERROR_MESSAGES.EMPTY_CONTENT)
1067
+
1068
+
1069
+ def store_text_in_vector_db(
1070
+ text, metadata, collection_name, overwrite: bool = False
1071
+ ) -> bool:
1072
+ text_splitter = RecursiveCharacterTextSplitter(
1073
+ chunk_size=app.state.config.CHUNK_SIZE,
1074
+ chunk_overlap=app.state.config.CHUNK_OVERLAP,
1075
+ add_start_index=True,
1076
+ )
1077
+ docs = text_splitter.create_documents([text], metadatas=[metadata])
1078
+ return store_docs_in_vector_db(docs, collection_name, overwrite=overwrite)
1079
+
1080
+
1081
+ def store_docs_in_vector_db(
1082
+ docs, collection_name, metadata: Optional[dict] = None, overwrite: bool = False
1083
+ ) -> bool:
1084
+ log.info(f"store_docs_in_vector_db {docs} {collection_name}")
1085
+
1086
+ texts = [doc.page_content for doc in docs]
1087
+ metadatas = [{**doc.metadata, **(metadata if metadata else {})} for doc in docs]
1088
+
1089
+ # ChromaDB does not like datetime formats
1090
+ # for meta-data so convert them to string.
1091
+ for metadata in metadatas:
1092
+ for key, value in metadata.items():
1093
+ if isinstance(value, datetime):
1094
+ metadata[key] = str(value)
1095
+
1096
+ try:
1097
+ if overwrite:
1098
+ if VECTOR_DB_CLIENT.has_collection(collection_name=collection_name):
1099
+ log.info(f"deleting existing collection {collection_name}")
1100
+ VECTOR_DB_CLIENT.delete_collection(collection_name=collection_name)
1101
+
1102
+ if VECTOR_DB_CLIENT.has_collection(collection_name=collection_name):
1103
+ log.info(f"collection {collection_name} already exists")
1104
+ return True
1105
+ else:
1106
+ embedding_function = get_embedding_function(
1107
+ app.state.config.RAG_EMBEDDING_ENGINE,
1108
+ app.state.config.RAG_EMBEDDING_MODEL,
1109
+ app.state.sentence_transformer_ef,
1110
+ app.state.config.OPENAI_API_KEY,
1111
+ app.state.config.OPENAI_API_BASE_URL,
1112
+ app.state.config.RAG_EMBEDDING_OPENAI_BATCH_SIZE,
1113
+ )
1114
+
1115
+ VECTOR_DB_CLIENT.insert(
1116
+ collection_name=collection_name,
1117
+ items=[
1118
+ {
1119
+ "id": str(uuid.uuid4()),
1120
+ "text": text,
1121
+ "vector": embedding_function(text.replace("\n", " ")),
1122
+ "metadata": metadatas[idx],
1123
+ }
1124
+ for idx, text in enumerate(texts)
1125
+ ],
1126
+ )
1127
+
1128
+ return True
1129
+ except Exception as e:
1130
+ log.exception(e)
1131
+ return False
1132
+
1133
+
1134
+ class TikaLoader:
1135
+ def __init__(self, file_path, mime_type=None):
1136
+ self.file_path = file_path
1137
+ self.mime_type = mime_type
1138
+
1139
+ def load(self) -> list[Document]:
1140
+ with open(self.file_path, "rb") as f:
1141
+ data = f.read()
1142
+
1143
+ if self.mime_type is not None:
1144
+ headers = {"Content-Type": self.mime_type}
1145
+ else:
1146
+ headers = {}
1147
+
1148
+ endpoint = app.state.config.TIKA_SERVER_URL
1149
+ if not endpoint.endswith("/"):
1150
+ endpoint += "/"
1151
+ endpoint += "tika/text"
1152
+
1153
+ r = requests.put(endpoint, data=data, headers=headers)
1154
+
1155
+ if r.ok:
1156
+ raw_metadata = r.json()
1157
+ text = raw_metadata.get("X-TIKA:content", "<No text content found>")
1158
+
1159
+ if "Content-Type" in raw_metadata:
1160
+ headers["Content-Type"] = raw_metadata["Content-Type"]
1161
+
1162
+ log.info("Tika extracted text: %s", text)
1163
+
1164
+ return [Document(page_content=text, metadata=headers)]
1165
+ else:
1166
+ raise Exception(f"Error calling Tika: {r.reason}")
1167
+
1168
+
1169
+ def get_loader(filename: str, file_content_type: str, file_path: str):
1170
+ file_ext = filename.split(".")[-1].lower()
1171
+ known_type = True
1172
+
1173
+ known_source_ext = [
1174
+ "go",
1175
+ "py",
1176
+ "java",
1177
+ "sh",
1178
+ "bat",
1179
+ "ps1",
1180
+ "cmd",
1181
+ "js",
1182
+ "ts",
1183
+ "css",
1184
+ "cpp",
1185
+ "hpp",
1186
+ "h",
1187
+ "c",
1188
+ "cs",
1189
+ "sql",
1190
+ "log",
1191
+ "ini",
1192
+ "pl",
1193
+ "pm",
1194
+ "r",
1195
+ "dart",
1196
+ "dockerfile",
1197
+ "env",
1198
+ "php",
1199
+ "hs",
1200
+ "hsc",
1201
+ "lua",
1202
+ "nginxconf",
1203
+ "conf",
1204
+ "m",
1205
+ "mm",
1206
+ "plsql",
1207
+ "perl",
1208
+ "rb",
1209
+ "rs",
1210
+ "db2",
1211
+ "scala",
1212
+ "bash",
1213
+ "swift",
1214
+ "vue",
1215
+ "svelte",
1216
+ "msg",
1217
+ "ex",
1218
+ "exs",
1219
+ "erl",
1220
+ "tsx",
1221
+ "jsx",
1222
+ "hs",
1223
+ "lhs",
1224
+ ]
1225
+
1226
+ if (
1227
+ app.state.config.CONTENT_EXTRACTION_ENGINE == "tika"
1228
+ and app.state.config.TIKA_SERVER_URL
1229
+ ):
1230
+ if file_ext in known_source_ext or (
1231
+ file_content_type and file_content_type.find("text/") >= 0
1232
+ ):
1233
+ loader = TextLoader(file_path, autodetect_encoding=True)
1234
+ else:
1235
+ loader = TikaLoader(file_path, file_content_type)
1236
+ else:
1237
+ if file_ext == "pdf":
1238
+ loader = PyPDFLoader(
1239
+ file_path, extract_images=app.state.config.PDF_EXTRACT_IMAGES
1240
+ )
1241
+ elif file_ext == "csv":
1242
+ loader = CSVLoader(file_path)
1243
+ elif file_ext == "rst":
1244
+ loader = UnstructuredRSTLoader(file_path, mode="elements")
1245
+ elif file_ext == "xml":
1246
+ loader = UnstructuredXMLLoader(file_path)
1247
+ elif file_ext in ["htm", "html"]:
1248
+ loader = BSHTMLLoader(file_path, open_encoding="unicode_escape")
1249
+ elif file_ext == "md":
1250
+ loader = UnstructuredMarkdownLoader(file_path)
1251
+ elif file_content_type == "application/epub+zip":
1252
+ loader = UnstructuredEPubLoader(file_path)
1253
+ elif (
1254
+ file_content_type
1255
+ == "application/vnd.openxmlformats-officedocument.wordprocessingml.document"
1256
+ or file_ext == "docx"
1257
+ ):
1258
+ loader = Docx2txtLoader(file_path)
1259
+ elif file_content_type in [
1260
+ "application/vnd.ms-excel",
1261
+ "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
1262
+ ] or file_ext in ["xls", "xlsx"]:
1263
+ loader = UnstructuredExcelLoader(file_path)
1264
+ elif file_content_type in [
1265
+ "application/vnd.ms-powerpoint",
1266
+ "application/vnd.openxmlformats-officedocument.presentationml.presentation",
1267
+ ] or file_ext in ["ppt", "pptx"]:
1268
+ loader = UnstructuredPowerPointLoader(file_path)
1269
+ elif file_ext == "msg":
1270
+ loader = OutlookMessageLoader(file_path)
1271
+ elif file_ext in known_source_ext or (
1272
+ file_content_type and file_content_type.find("text/") >= 0
1273
+ ):
1274
+ loader = TextLoader(file_path, autodetect_encoding=True)
1275
+ else:
1276
+ loader = TextLoader(file_path, autodetect_encoding=True)
1277
+ known_type = False
1278
+
1279
+ return loader, known_type
1280
+
1281
+
1282
+ @app.post("/doc")
1283
+ def store_doc(
1284
+ collection_name: Optional[str] = Form(None),
1285
+ file: UploadFile = File(...),
1286
+ user=Depends(get_verified_user),
1287
+ ):
1288
+ # "https://www.gutenberg.org/files/1727/1727-h/1727-h.htm"
1289
+
1290
+ log.info(f"file.content_type: {file.content_type}")
1291
+ try:
1292
+ unsanitized_filename = file.filename
1293
+ filename = os.path.basename(unsanitized_filename)
1294
+
1295
+ file_path = f"{UPLOAD_DIR}/{filename}"
1296
+
1297
+ contents = file.file.read()
1298
+ with open(file_path, "wb") as f:
1299
+ f.write(contents)
1300
+ f.close()
1301
+
1302
+ f = open(file_path, "rb")
1303
+ if collection_name is None:
1304
+ collection_name = calculate_sha256(f)[:63]
1305
+ f.close()
1306
+
1307
+ loader, known_type = get_loader(filename, file.content_type, file_path)
1308
+ data = loader.load()
1309
+
1310
+ try:
1311
+ result = store_data_in_vector_db(data, collection_name)
1312
+
1313
+ if result:
1314
+ return {
1315
+ "status": True,
1316
+ "collection_name": collection_name,
1317
+ "filename": filename,
1318
+ "known_type": known_type,
1319
+ }
1320
+ except Exception as e:
1321
+ raise HTTPException(
1322
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
1323
+ detail=e,
1324
+ )
1325
+ except Exception as e:
1326
+ log.exception(e)
1327
+ if "No pandoc was found" in str(e):
1328
+ raise HTTPException(
1329
+ status_code=status.HTTP_400_BAD_REQUEST,
1330
+ detail=ERROR_MESSAGES.PANDOC_NOT_INSTALLED,
1331
+ )
1332
+ else:
1333
+ raise HTTPException(
1334
+ status_code=status.HTTP_400_BAD_REQUEST,
1335
+ detail=ERROR_MESSAGES.DEFAULT(e),
1336
+ )
1337
+
1338
+
1339
+ class ProcessDocForm(BaseModel):
1340
+ file_id: str
1341
+ collection_name: Optional[str] = None
1342
+
1343
+
1344
+ @app.post("/process/doc")
1345
+ def process_doc(
1346
+ form_data: ProcessDocForm,
1347
+ user=Depends(get_verified_user),
1348
+ ):
1349
+ try:
1350
+ file = Files.get_file_by_id(form_data.file_id)
1351
+ file_path = file.meta.get("path", f"{UPLOAD_DIR}/{file.filename}")
1352
+
1353
+ f = open(file_path, "rb")
1354
+
1355
+ collection_name = form_data.collection_name
1356
+ if collection_name is None:
1357
+ collection_name = calculate_sha256(f)[:63]
1358
+ f.close()
1359
+
1360
+ loader, known_type = get_loader(
1361
+ file.filename, file.meta.get("content_type"), file_path
1362
+ )
1363
+ data = loader.load()
1364
+
1365
+ try:
1366
+ result = store_data_in_vector_db(
1367
+ data,
1368
+ collection_name,
1369
+ {
1370
+ "file_id": form_data.file_id,
1371
+ "name": file.meta.get("name", file.filename),
1372
+ },
1373
+ )
1374
+
1375
+ if result:
1376
+ return {
1377
+ "status": True,
1378
+ "collection_name": collection_name,
1379
+ "known_type": known_type,
1380
+ "filename": file.meta.get("name", file.filename),
1381
+ }
1382
+ except Exception as e:
1383
+ raise HTTPException(
1384
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
1385
+ detail=e,
1386
+ )
1387
+ except Exception as e:
1388
+ log.exception(e)
1389
+ if "No pandoc was found" in str(e):
1390
+ raise HTTPException(
1391
+ status_code=status.HTTP_400_BAD_REQUEST,
1392
+ detail=ERROR_MESSAGES.PANDOC_NOT_INSTALLED,
1393
+ )
1394
+ else:
1395
+ raise HTTPException(
1396
+ status_code=status.HTTP_400_BAD_REQUEST,
1397
+ detail=ERROR_MESSAGES.DEFAULT(e),
1398
+ )
1399
+
1400
+
1401
+ class TextRAGForm(BaseModel):
1402
+ name: str
1403
+ content: str
1404
+ collection_name: Optional[str] = None
1405
+
1406
+
1407
+ @app.post("/text")
1408
+ def store_text(
1409
+ form_data: TextRAGForm,
1410
+ user=Depends(get_verified_user),
1411
+ ):
1412
+ collection_name = form_data.collection_name
1413
+ if collection_name is None:
1414
+ collection_name = calculate_sha256_string(form_data.content)
1415
+
1416
+ result = store_text_in_vector_db(
1417
+ form_data.content,
1418
+ metadata={"name": form_data.name, "created_by": user.id},
1419
+ collection_name=collection_name,
1420
+ )
1421
+
1422
+ if result:
1423
+ return {"status": True, "collection_name": collection_name}
1424
+ else:
1425
+ raise HTTPException(
1426
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
1427
+ detail=ERROR_MESSAGES.DEFAULT(),
1428
+ )
1429
+
1430
+
1431
+ @app.get("/scan")
1432
+ def scan_docs_dir(user=Depends(get_admin_user)):
1433
+ for path in Path(DOCS_DIR).rglob("./**/*"):
1434
+ try:
1435
+ if path.is_file() and not path.name.startswith("."):
1436
+ tags = extract_folders_after_data_docs(path)
1437
+ filename = path.name
1438
+ file_content_type = mimetypes.guess_type(path)
1439
+
1440
+ f = open(path, "rb")
1441
+ collection_name = calculate_sha256(f)[:63]
1442
+ f.close()
1443
+
1444
+ loader, known_type = get_loader(
1445
+ filename, file_content_type[0], str(path)
1446
+ )
1447
+ data = loader.load()
1448
+
1449
+ try:
1450
+ result = store_data_in_vector_db(data, collection_name)
1451
+
1452
+ if result:
1453
+ sanitized_filename = sanitize_filename(filename)
1454
+ doc = Documents.get_doc_by_name(sanitized_filename)
1455
+
1456
+ if doc is None:
1457
+ doc = Documents.insert_new_doc(
1458
+ user.id,
1459
+ DocumentForm(
1460
+ **{
1461
+ "name": sanitized_filename,
1462
+ "title": filename,
1463
+ "collection_name": collection_name,
1464
+ "filename": filename,
1465
+ "content": (
1466
+ json.dumps(
1467
+ {
1468
+ "tags": list(
1469
+ map(
1470
+ lambda name: {"name": name},
1471
+ tags,
1472
+ )
1473
+ )
1474
+ }
1475
+ )
1476
+ if len(tags)
1477
+ else "{}"
1478
+ ),
1479
+ }
1480
+ ),
1481
+ )
1482
+ except Exception as e:
1483
+ log.exception(e)
1484
+ pass
1485
+
1486
+ except Exception as e:
1487
+ log.exception(e)
1488
+
1489
+ return True
1490
+
1491
+
1492
+ @app.post("/reset/db")
1493
+ def reset_vector_db(user=Depends(get_admin_user)):
1494
+ VECTOR_DB_CLIENT.reset()
1495
+
1496
+
1497
+ @app.post("/reset/uploads")
1498
+ def reset_upload_dir(user=Depends(get_admin_user)) -> bool:
1499
+ folder = f"{UPLOAD_DIR}"
1500
+ try:
1501
+ # Check if the directory exists
1502
+ if os.path.exists(folder):
1503
+ # Iterate over all the files and directories in the specified directory
1504
+ for filename in os.listdir(folder):
1505
+ file_path = os.path.join(folder, filename)
1506
+ try:
1507
+ if os.path.isfile(file_path) or os.path.islink(file_path):
1508
+ os.unlink(file_path) # Remove the file or link
1509
+ elif os.path.isdir(file_path):
1510
+ shutil.rmtree(file_path) # Remove the directory
1511
+ except Exception as e:
1512
+ print(f"Failed to delete {file_path}. Reason: {e}")
1513
+ else:
1514
+ print(f"The directory {folder} does not exist")
1515
+ except Exception as e:
1516
+ print(f"Failed to process the directory {folder}. Reason: {e}")
1517
+
1518
+ return True
1519
+
1520
+
1521
+ @app.post("/reset")
1522
+ def reset(user=Depends(get_admin_user)) -> bool:
1523
+ folder = f"{UPLOAD_DIR}"
1524
+ for filename in os.listdir(folder):
1525
+ file_path = os.path.join(folder, filename)
1526
+ try:
1527
+ if os.path.isfile(file_path) or os.path.islink(file_path):
1528
+ os.unlink(file_path)
1529
+ elif os.path.isdir(file_path):
1530
+ shutil.rmtree(file_path)
1531
+ except Exception as e:
1532
+ log.error("Failed to delete %s. Reason: %s" % (file_path, e))
1533
+
1534
+ try:
1535
+ VECTOR_DB_CLIENT.reset()
1536
+ except Exception as e:
1537
+ log.exception(e)
1538
+
1539
+ return True
1540
+
1541
+
1542
+ class SafeWebBaseLoader(WebBaseLoader):
1543
+ """WebBaseLoader with enhanced error handling for URLs."""
1544
+
1545
+ def lazy_load(self) -> Iterator[Document]:
1546
+ """Lazy load text from the url(s) in web_path with error handling."""
1547
+ for path in self.web_paths:
1548
+ try:
1549
+ soup = self._scrape(path, bs_kwargs=self.bs_kwargs)
1550
+ text = soup.get_text(**self.bs_get_text_kwargs)
1551
+
1552
+ # Build metadata
1553
+ metadata = {"source": path}
1554
+ if title := soup.find("title"):
1555
+ metadata["title"] = title.get_text()
1556
+ if description := soup.find("meta", attrs={"name": "description"}):
1557
+ metadata["description"] = description.get(
1558
+ "content", "No description found."
1559
+ )
1560
+ if html := soup.find("html"):
1561
+ metadata["language"] = html.get("lang", "No language found.")
1562
+
1563
+ yield Document(page_content=text, metadata=metadata)
1564
+ except Exception as e:
1565
+ # Log the error and continue with the next URL
1566
+ log.error(f"Error loading {path}: {e}")
1567
+
1568
+
1569
+ if ENV == "dev":
1570
+
1571
+ @app.get("/ef")
1572
+ async def get_embeddings():
1573
+ return {"result": app.state.EMBEDDING_FUNCTION("hello world")}
1574
+
1575
+ @app.get("/ef/{text}")
1576
+ async def get_embeddings_text(text: str):
1577
+ return {"result": app.state.EMBEDDING_FUNCTION(text)}
backend/open_webui/apps/rag/search/brave.py ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import logging
2
+ from typing import Optional
3
+
4
+ import requests
5
+ from open_webui.apps.rag.search.main import SearchResult, get_filtered_results
6
+ from open_webui.env import SRC_LOG_LEVELS
7
+
8
+ log = logging.getLogger(__name__)
9
+ log.setLevel(SRC_LOG_LEVELS["RAG"])
10
+
11
+
12
+ def search_brave(
13
+ api_key: str, query: str, count: int, filter_list: Optional[list[str]] = None
14
+ ) -> list[SearchResult]:
15
+ """Search using Brave's Search API and return the results as a list of SearchResult objects.
16
+
17
+ Args:
18
+ api_key (str): A Brave Search API key
19
+ query (str): The query to search for
20
+ """
21
+ url = "https://api.search.brave.com/res/v1/web/search"
22
+ headers = {
23
+ "Accept": "application/json",
24
+ "Accept-Encoding": "gzip",
25
+ "X-Subscription-Token": api_key,
26
+ }
27
+ params = {"q": query, "count": count}
28
+
29
+ response = requests.get(url, headers=headers, params=params)
30
+ response.raise_for_status()
31
+
32
+ json_response = response.json()
33
+ results = json_response.get("web", {}).get("results", [])
34
+ if filter_list:
35
+ results = get_filtered_results(results, filter_list)
36
+
37
+ return [
38
+ SearchResult(
39
+ link=result["url"], title=result.get("title"), snippet=result.get("snippet")
40
+ )
41
+ for result in results[:count]
42
+ ]
backend/open_webui/apps/rag/search/duckduckgo.py ADDED
@@ -0,0 +1,50 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import logging
2
+ from typing import Optional
3
+
4
+ from open_webui.apps.rag.search.main import SearchResult, get_filtered_results
5
+ from duckduckgo_search import DDGS
6
+ from open_webui.env import SRC_LOG_LEVELS
7
+
8
+ log = logging.getLogger(__name__)
9
+ log.setLevel(SRC_LOG_LEVELS["RAG"])
10
+
11
+
12
+ def search_duckduckgo(
13
+ query: str, count: int, filter_list: Optional[list[str]] = None
14
+ ) -> list[SearchResult]:
15
+ """
16
+ Search using DuckDuckGo's Search API and return the results as a list of SearchResult objects.
17
+ Args:
18
+ query (str): The query to search for
19
+ count (int): The number of results to return
20
+
21
+ Returns:
22
+ list[SearchResult]: A list of search results
23
+ """
24
+ # Use the DDGS context manager to create a DDGS object
25
+ with DDGS() as ddgs:
26
+ # Use the ddgs.text() method to perform the search
27
+ ddgs_gen = ddgs.text(
28
+ query, safesearch="moderate", max_results=count, backend="api"
29
+ )
30
+ # Check if there are search results
31
+ if ddgs_gen:
32
+ # Convert the search results into a list
33
+ search_results = [r for r in ddgs_gen]
34
+
35
+ # Create an empty list to store the SearchResult objects
36
+ results = []
37
+ # Iterate over each search result
38
+ for result in search_results:
39
+ # Create a SearchResult object and append it to the results list
40
+ results.append(
41
+ SearchResult(
42
+ link=result["href"],
43
+ title=result.get("title"),
44
+ snippet=result.get("body"),
45
+ )
46
+ )
47
+ if filter_list:
48
+ results = get_filtered_results(results, filter_list)
49
+ # Return the list of search results
50
+ return results
backend/open_webui/apps/rag/search/google_pse.py ADDED
@@ -0,0 +1,50 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import logging
2
+ from typing import Optional
3
+
4
+ import requests
5
+ from open_webui.apps.rag.search.main import SearchResult, get_filtered_results
6
+ from open_webui.env import SRC_LOG_LEVELS
7
+
8
+ log = logging.getLogger(__name__)
9
+ log.setLevel(SRC_LOG_LEVELS["RAG"])
10
+
11
+
12
+ def search_google_pse(
13
+ api_key: str,
14
+ search_engine_id: str,
15
+ query: str,
16
+ count: int,
17
+ filter_list: Optional[list[str]] = None,
18
+ ) -> list[SearchResult]:
19
+ """Search using Google's Programmable Search Engine API and return the results as a list of SearchResult objects.
20
+
21
+ Args:
22
+ api_key (str): A Programmable Search Engine API key
23
+ search_engine_id (str): A Programmable Search Engine ID
24
+ query (str): The query to search for
25
+ """
26
+ url = "https://www.googleapis.com/customsearch/v1"
27
+
28
+ headers = {"Content-Type": "application/json"}
29
+ params = {
30
+ "cx": search_engine_id,
31
+ "q": query,
32
+ "key": api_key,
33
+ "num": count,
34
+ }
35
+
36
+ response = requests.request("GET", url, headers=headers, params=params)
37
+ response.raise_for_status()
38
+
39
+ json_response = response.json()
40
+ results = json_response.get("items", [])
41
+ if filter_list:
42
+ results = get_filtered_results(results, filter_list)
43
+ return [
44
+ SearchResult(
45
+ link=result["link"],
46
+ title=result.get("title"),
47
+ snippet=result.get("snippet"),
48
+ )
49
+ for result in results
50
+ ]
backend/open_webui/apps/rag/search/jina_search.py ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import logging
2
+
3
+ import requests
4
+ from open_webui.apps.rag.search.main import SearchResult
5
+ from open_webui.env import SRC_LOG_LEVELS
6
+ from yarl import URL
7
+
8
+ log = logging.getLogger(__name__)
9
+ log.setLevel(SRC_LOG_LEVELS["RAG"])
10
+
11
+
12
+ def search_jina(query: str, count: int) -> list[SearchResult]:
13
+ """
14
+ Search using Jina's Search API and return the results as a list of SearchResult objects.
15
+ Args:
16
+ query (str): The query to search for
17
+ count (int): The number of results to return
18
+
19
+ Returns:
20
+ list[SearchResult]: A list of search results
21
+ """
22
+ jina_search_endpoint = "https://s.jina.ai/"
23
+ headers = {
24
+ "Accept": "application/json",
25
+ }
26
+ url = str(URL(jina_search_endpoint + query))
27
+ response = requests.get(url, headers=headers)
28
+ response.raise_for_status()
29
+ data = response.json()
30
+
31
+ results = []
32
+ for result in data["data"][:count]:
33
+ results.append(
34
+ SearchResult(
35
+ link=result["url"],
36
+ title=result.get("title"),
37
+ snippet=result.get("content"),
38
+ )
39
+ )
40
+
41
+ return results
backend/open_webui/apps/rag/search/main.py ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import Optional
2
+ from urllib.parse import urlparse
3
+
4
+ from pydantic import BaseModel
5
+
6
+
7
+ def get_filtered_results(results, filter_list):
8
+ if not filter_list:
9
+ return results
10
+ filtered_results = []
11
+ for result in results:
12
+ url = result.get("url") or result.get("link", "")
13
+ domain = urlparse(url).netloc
14
+ if any(domain.endswith(filtered_domain) for filtered_domain in filter_list):
15
+ filtered_results.append(result)
16
+ return filtered_results
17
+
18
+
19
+ class SearchResult(BaseModel):
20
+ link: str
21
+ title: Optional[str]
22
+ snippet: Optional[str]
backend/open_webui/apps/rag/search/searchapi.py ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import logging
2
+ from typing import Optional
3
+ from urllib.parse import urlencode
4
+
5
+ import requests
6
+ from open_webui.apps.rag.search.main import SearchResult, get_filtered_results
7
+ from open_webui.env import SRC_LOG_LEVELS
8
+
9
+ log = logging.getLogger(__name__)
10
+ log.setLevel(SRC_LOG_LEVELS["RAG"])
11
+
12
+
13
+ def search_searchapi(
14
+ api_key: str,
15
+ engine: str,
16
+ query: str,
17
+ count: int,
18
+ filter_list: Optional[list[str]] = None,
19
+ ) -> list[SearchResult]:
20
+ """Search using searchapi.io's API and return the results as a list of SearchResult objects.
21
+
22
+ Args:
23
+ api_key (str): A searchapi.io API key
24
+ query (str): The query to search for
25
+ """
26
+ url = "https://www.searchapi.io/api/v1/search"
27
+
28
+ engine = engine or "google"
29
+
30
+ payload = {"engine": engine, "q": query, "api_key": api_key}
31
+
32
+ url = f"{url}?{urlencode(payload)}"
33
+ response = requests.request("GET", url)
34
+
35
+ json_response = response.json()
36
+ log.info(f"results from searchapi search: {json_response}")
37
+
38
+ results = sorted(
39
+ json_response.get("organic_results", []), key=lambda x: x.get("position", 0)
40
+ )
41
+ if filter_list:
42
+ results = get_filtered_results(results, filter_list)
43
+ return [
44
+ SearchResult(
45
+ link=result["link"], title=result["title"], snippet=result["snippet"]
46
+ )
47
+ for result in results[:count]
48
+ ]
backend/open_webui/apps/rag/search/searxng.py ADDED
@@ -0,0 +1,91 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import logging
2
+ from typing import Optional
3
+
4
+ import requests
5
+ from open_webui.apps.rag.search.main import SearchResult, get_filtered_results
6
+ from open_webui.env import SRC_LOG_LEVELS
7
+
8
+ log = logging.getLogger(__name__)
9
+ log.setLevel(SRC_LOG_LEVELS["RAG"])
10
+
11
+
12
+ def search_searxng(
13
+ query_url: str,
14
+ query: str,
15
+ count: int,
16
+ filter_list: Optional[list[str]] = None,
17
+ **kwargs,
18
+ ) -> list[SearchResult]:
19
+ """
20
+ Search a SearXNG instance for a given query and return the results as a list of SearchResult objects.
21
+
22
+ The function allows passing additional parameters such as language or time_range to tailor the search result.
23
+
24
+ Args:
25
+ query_url (str): The base URL of the SearXNG server.
26
+ query (str): The search term or question to find in the SearXNG database.
27
+ count (int): The maximum number of results to retrieve from the search.
28
+
29
+ Keyword Args:
30
+ language (str): Language filter for the search results; e.g., "en-US". Defaults to an empty string.
31
+ safesearch (int): Safe search filter for safer web results; 0 = off, 1 = moderate, 2 = strict. Defaults to 1 (moderate).
32
+ time_range (str): Time range for filtering results by date; e.g., "2023-04-05..today" or "all-time". Defaults to ''.
33
+ categories: (Optional[list[str]]): Specific categories within which the search should be performed, defaulting to an empty string if not provided.
34
+
35
+ Returns:
36
+ list[SearchResult]: A list of SearchResults sorted by relevance score in descending order.
37
+
38
+ Raise:
39
+ requests.exceptions.RequestException: If a request error occurs during the search process.
40
+ """
41
+
42
+ # Default values for optional parameters are provided as empty strings or None when not specified.
43
+ language = kwargs.get("language", "en-US")
44
+ safesearch = kwargs.get("safesearch", "1")
45
+ time_range = kwargs.get("time_range", "")
46
+ categories = "".join(kwargs.get("categories", []))
47
+
48
+ params = {
49
+ "q": query,
50
+ "format": "json",
51
+ "pageno": 1,
52
+ "safesearch": safesearch,
53
+ "language": language,
54
+ "time_range": time_range,
55
+ "categories": categories,
56
+ "theme": "simple",
57
+ "image_proxy": 0,
58
+ }
59
+
60
+ # Legacy query format
61
+ if "<query>" in query_url:
62
+ # Strip all query parameters from the URL
63
+ query_url = query_url.split("?")[0]
64
+
65
+ log.debug(f"searching {query_url}")
66
+
67
+ response = requests.get(
68
+ query_url,
69
+ headers={
70
+ "User-Agent": "Open WebUI (https://github.com/open-webui/open-webui) RAG Bot",
71
+ "Accept": "text/html",
72
+ "Accept-Encoding": "gzip, deflate",
73
+ "Accept-Language": "en-US,en;q=0.5",
74
+ "Connection": "keep-alive",
75
+ },
76
+ params=params,
77
+ )
78
+
79
+ response.raise_for_status() # Raise an exception for HTTP errors.
80
+
81
+ json_response = response.json()
82
+ results = json_response.get("results", [])
83
+ sorted_results = sorted(results, key=lambda x: x.get("score", 0), reverse=True)
84
+ if filter_list:
85
+ sorted_results = get_filtered_results(sorted_results, filter_list)
86
+ return [
87
+ SearchResult(
88
+ link=result["url"], title=result.get("title"), snippet=result.get("content")
89
+ )
90
+ for result in sorted_results[:count]
91
+ ]