Spaces:
Running
Running
github-actions[bot]
commited on
Commit
•
9258713
0
Parent(s):
GitHub deploy: 936896f546adff4433dc2695ee03436cf32be3a8
Browse filesThis view is limited to 50 files because it contains too many changes.
See raw diff
- .dockerignore +20 -0
- .env.example +13 -0
- .eslintignore +13 -0
- .eslintrc.cjs +31 -0
- .gitattributes +3 -0
- .github/FUNDING.yml +1 -0
- .github/ISSUE_TEMPLATE/bug_report.md +80 -0
- .github/ISSUE_TEMPLATE/feature_request.md +35 -0
- .github/dependabot.yml +12 -0
- .github/pull_request_template.md +72 -0
- .github/workflows/build-release.yml +72 -0
- .github/workflows/deploy-to-hf-spaces.yml +66 -0
- .github/workflows/docker-build.yaml +477 -0
- .github/workflows/format-backend.yaml +39 -0
- .github/workflows/format-build-frontend.yaml +57 -0
- .github/workflows/integration-test.yml +253 -0
- .github/workflows/lint-backend.disabled +27 -0
- .github/workflows/lint-frontend.disabled +21 -0
- .github/workflows/release-pypi.yml +32 -0
- .github/workflows/syns.yml +45 -0
- .gitignore +309 -0
- .npmrc +1 -0
- .prettierignore +316 -0
- .prettierrc +9 -0
- CHANGELOG.md +0 -0
- CODE_OF_CONDUCT.md +99 -0
- Caddyfile.localhost +64 -0
- Dockerfile +176 -0
- INSTALLATION.md +35 -0
- LICENSE +21 -0
- Makefile +33 -0
- README.md +221 -0
- TROUBLESHOOTING.md +36 -0
- backend/.dockerignore +14 -0
- backend/.gitignore +12 -0
- backend/dev.sh +2 -0
- backend/open_webui/__init__.py +77 -0
- backend/open_webui/alembic.ini +114 -0
- backend/open_webui/config.py +1959 -0
- backend/open_webui/constants.py +118 -0
- backend/open_webui/env.py +394 -0
- backend/open_webui/functions.py +316 -0
- backend/open_webui/internal/db.py +114 -0
- backend/open_webui/internal/migrations/001_initial_schema.py +254 -0
- backend/open_webui/internal/migrations/002_add_local_sharing.py +48 -0
- backend/open_webui/internal/migrations/003_add_auth_api_key.py +48 -0
- backend/open_webui/internal/migrations/004_add_archived.py +46 -0
- backend/open_webui/internal/migrations/005_add_updated_at.py +130 -0
- backend/open_webui/internal/migrations/006_migrate_timestamps_and_charfields.py +130 -0
- backend/open_webui/internal/migrations/007_add_user_last_active_at.py +79 -0
.dockerignore
ADDED
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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
|
20 |
+
backend/data/*
|
.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,3 @@
|
|
|
|
|
|
|
|
|
1 |
+
*.sh text eol=lf
|
2 |
+
*.ttf filter=lfs diff=lfs merge=lfs -text
|
3 |
+
*.jpg 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,80 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 |
+
## Important Notes
|
12 |
+
|
13 |
+
- **Before submitting a bug report**: Please check the Issues or Discussions section to see if a similar issue or feature request has already been posted. It's likely we're already tracking it! If you’re unsure, start a discussion post first. This will help us efficiently focus on improving the project.
|
14 |
+
|
15 |
+
- **Collaborate respectfully**: We value a constructive attitude, so please be mindful of your communication. If negativity is part of your approach, our capacity to engage may be limited. We’re here to help if you’re open to learning and communicating positively. Remember, Open WebUI is a volunteer-driven project managed by a single maintainer and supported by contributors who also have full-time jobs. We appreciate your time and ask that you respect ours.
|
16 |
+
|
17 |
+
- **Contributing**: If you encounter an issue, we highly encourage you to submit a pull request or fork the project. We actively work to prevent contributor burnout to maintain the quality and continuity of Open WebUI.
|
18 |
+
|
19 |
+
- **Bug reproducibility**: If a bug cannot be reproduced with a `:main` or `:dev` Docker setup, or a pip install with Python 3.11, it may require additional help from the community. In such cases, we will move it to the "issues" Discussions section due to our limited resources. We encourage the community to assist with these issues. Remember, it’s not that the issue doesn’t exist; we need your help!
|
20 |
+
|
21 |
+
Note: Please remove the notes above when submitting your post. Thank you for your understanding and support!
|
22 |
+
|
23 |
+
---
|
24 |
+
|
25 |
+
## Installation Method
|
26 |
+
|
27 |
+
[Describe the method you used to install the project, e.g., git clone, Docker, pip, etc.]
|
28 |
+
|
29 |
+
## Environment
|
30 |
+
|
31 |
+
- **Open WebUI Version:** [e.g., v0.3.11]
|
32 |
+
- **Ollama (if applicable):** [e.g., v0.2.0, v0.1.32-rc1]
|
33 |
+
|
34 |
+
- **Operating System:** [e.g., Windows 10, macOS Big Sur, Ubuntu 20.04]
|
35 |
+
- **Browser (if applicable):** [e.g., Chrome 100.0, Firefox 98.0]
|
36 |
+
|
37 |
+
**Confirmation:**
|
38 |
+
|
39 |
+
- [ ] I have read and followed all the instructions provided in the README.md.
|
40 |
+
- [ ] I am on the latest version of both Open WebUI and Ollama.
|
41 |
+
- [ ] I have included the browser console logs.
|
42 |
+
- [ ] I have included the Docker container logs.
|
43 |
+
- [ ] I have provided the exact steps to reproduce the bug in the "Steps to Reproduce" section below.
|
44 |
+
|
45 |
+
## Expected Behavior:
|
46 |
+
|
47 |
+
[Describe what you expected to happen.]
|
48 |
+
|
49 |
+
## Actual Behavior:
|
50 |
+
|
51 |
+
[Describe what actually happened.]
|
52 |
+
|
53 |
+
## Description
|
54 |
+
|
55 |
+
**Bug Summary:**
|
56 |
+
[Provide a brief but clear summary of the bug]
|
57 |
+
|
58 |
+
## Reproduction Details
|
59 |
+
|
60 |
+
**Steps to Reproduce:**
|
61 |
+
[Outline the steps to reproduce the bug. Be as detailed as possible.]
|
62 |
+
|
63 |
+
## Logs and Screenshots
|
64 |
+
|
65 |
+
**Browser Console Logs:**
|
66 |
+
[Include relevant browser console logs, if applicable]
|
67 |
+
|
68 |
+
**Docker Container Logs:**
|
69 |
+
[Include relevant Docker container logs, if applicable]
|
70 |
+
|
71 |
+
**Screenshots/Screen Recordings (if applicable):**
|
72 |
+
[Attach any relevant screenshots to help illustrate the issue]
|
73 |
+
|
74 |
+
## Additional Information
|
75 |
+
|
76 |
+
[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.]
|
77 |
+
|
78 |
+
## Note
|
79 |
+
|
80 |
+
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,35 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
---
|
2 |
+
name: Feature request
|
3 |
+
about: Suggest an idea for this project
|
4 |
+
title: ''
|
5 |
+
labels: ''
|
6 |
+
assignees: ''
|
7 |
+
---
|
8 |
+
|
9 |
+
# Feature Request
|
10 |
+
|
11 |
+
## Important Notes
|
12 |
+
|
13 |
+
- **Before submitting a report**: Please check the Issues or Discussions section to see if a similar issue or feature request has already been posted. It's likely we're already tracking it! If you’re unsure, start a discussion post first. This will help us efficiently focus on improving the project.
|
14 |
+
|
15 |
+
- **Collaborate respectfully**: We value a constructive attitude, so please be mindful of your communication. If negativity is part of your approach, our capacity to engage may be limited. We’re here to help if you’re open to learning and communicating positively. Remember, Open WebUI is a volunteer-driven project managed by a single maintainer and supported by contributors who also have full-time jobs. We appreciate your time and ask that you respect ours.
|
16 |
+
|
17 |
+
- **Contributing**: If you encounter an issue, we highly encourage you to submit a pull request or fork the project. We actively work to prevent contributor burnout to maintain the quality and continuity of Open WebUI.
|
18 |
+
|
19 |
+
- **Bug reproducibility**: If a bug cannot be reproduced with a `:main` or `:dev` Docker setup, or a pip install with Python 3.11, it may require additional help from the community. In such cases, we will move it to the "issues" Discussions section due to our limited resources. We encourage the community to assist with these issues. Remember, it’s not that the issue doesn’t exist; we need your help!
|
20 |
+
|
21 |
+
Note: Please remove the notes above when submitting your post. Thank you for your understanding and support!
|
22 |
+
|
23 |
+
---
|
24 |
+
|
25 |
+
**Is your feature request related to a problem? Please describe.**
|
26 |
+
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
27 |
+
|
28 |
+
**Describe the solution you'd like**
|
29 |
+
A clear and concise description of what you want to happen.
|
30 |
+
|
31 |
+
**Describe alternatives you've considered**
|
32 |
+
A clear and concise description of any alternative solutions or features you've considered.
|
33 |
+
|
34 |
+
**Additional context**
|
35 |
+
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,66 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
name: Deploy to HuggingFace Spaces
|
2 |
+
|
3 |
+
on:
|
4 |
+
schedule:
|
5 |
+
# 每天UTC时间凌晨3点执行
|
6 |
+
- cron: '0 3 * * *'
|
7 |
+
push:
|
8 |
+
branches:
|
9 |
+
- dev
|
10 |
+
- main
|
11 |
+
workflow_dispatch:
|
12 |
+
|
13 |
+
jobs:
|
14 |
+
check-secret:
|
15 |
+
runs-on: ubuntu-latest
|
16 |
+
outputs:
|
17 |
+
token-set: ${{ steps.check-key.outputs.defined }}
|
18 |
+
steps:
|
19 |
+
- id: check-key
|
20 |
+
env:
|
21 |
+
HF_TOKEN: ${{ secrets.HF_TOKEN }}
|
22 |
+
if: "${{ env.HF_TOKEN != '' }}"
|
23 |
+
run: echo "defined=true" >> $GITHUB_OUTPUT
|
24 |
+
|
25 |
+
deploy:
|
26 |
+
runs-on: ubuntu-latest
|
27 |
+
needs: [check-secret]
|
28 |
+
if: needs.check-secret.outputs.token-set == 'true'
|
29 |
+
env:
|
30 |
+
HF_TOKEN: ${{ secrets.HF_TOKEN }}
|
31 |
+
steps:
|
32 |
+
- name: Checkout repository
|
33 |
+
uses: actions/checkout@v4
|
34 |
+
with:
|
35 |
+
lfs: true
|
36 |
+
|
37 |
+
- name: Remove git history
|
38 |
+
run: rm -rf .git
|
39 |
+
|
40 |
+
- name: Prepend YAML front matter to README.md
|
41 |
+
run: |
|
42 |
+
echo "---" > temp_readme.md
|
43 |
+
echo "title: Open WebUI" >> temp_readme.md
|
44 |
+
echo "emoji: 🐳" >> temp_readme.md
|
45 |
+
echo "colorFrom: purple" >> temp_readme.md
|
46 |
+
echo "colorTo: gray" >> temp_readme.md
|
47 |
+
echo "sdk: docker" >> temp_readme.md
|
48 |
+
echo "app_port: 8080" >> temp_readme.md
|
49 |
+
echo "---" >> temp_readme.md
|
50 |
+
cat README.md >> temp_readme.md
|
51 |
+
mv temp_readme.md README.md
|
52 |
+
|
53 |
+
- name: Configure git
|
54 |
+
run: |
|
55 |
+
git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
56 |
+
git config --global user.name "github-actions[bot]"
|
57 |
+
- name: Set up Git and push to Space
|
58 |
+
run: |
|
59 |
+
git init --initial-branch=main
|
60 |
+
git lfs install
|
61 |
+
git lfs track "*.ttf"
|
62 |
+
git lfs track "*.jpg"
|
63 |
+
rm demo.gif
|
64 |
+
git add .
|
65 |
+
git commit -m "GitHub deploy: ${{ github.sha }}"
|
66 |
+
git push --force https://open-webui:${HF_TOKEN}@huggingface.co/spaces/houin/open-webui 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@v5
|
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: '22' # 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: '22'
|
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,253 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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/remove-unwanted-software@v4.1
|
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@v5
|
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 |
+
DATABASE_POOL_SIZE: 10
|
186 |
+
DATABASE_POOL_MAX_OVERFLOW: 10
|
187 |
+
DATABASE_POOL_TIMEOUT: 30
|
188 |
+
run: |
|
189 |
+
cd backend
|
190 |
+
uvicorn open_webui.main:app --port "8081" --forwarded-allow-ips '*' &
|
191 |
+
UVICORN_PID=$!
|
192 |
+
# Wait up to 20 seconds for the server to start
|
193 |
+
for i in {1..20}; do
|
194 |
+
curl -s http://localhost:8081/api/config > /dev/null && break
|
195 |
+
sleep 1
|
196 |
+
if [ $i -eq 20 ]; then
|
197 |
+
echo "Server failed to start"
|
198 |
+
kill -9 $UVICORN_PID
|
199 |
+
exit 1
|
200 |
+
fi
|
201 |
+
done
|
202 |
+
# Check that the server is still running after 5 seconds
|
203 |
+
sleep 5
|
204 |
+
if ! kill -0 $UVICORN_PID; then
|
205 |
+
echo "Server has stopped"
|
206 |
+
exit 1
|
207 |
+
fi
|
208 |
+
|
209 |
+
# Check that service will reconnect to postgres when connection will be closed
|
210 |
+
status_code=$(curl --write-out %{http_code} -s --output /dev/null http://localhost:8081/health/db)
|
211 |
+
if [[ "$status_code" -ne 200 ]] ; then
|
212 |
+
echo "Server has failed before postgres reconnect check"
|
213 |
+
exit 1
|
214 |
+
fi
|
215 |
+
|
216 |
+
echo "Terminating all connections to postgres..."
|
217 |
+
python -c "import os, psycopg2 as pg2; \
|
218 |
+
conn = pg2.connect(dsn=os.environ['DATABASE_URL'].replace('+pool', '')); \
|
219 |
+
cur = conn.cursor(); \
|
220 |
+
cur.execute('SELECT pg_terminate_backend(psa.pid) FROM pg_stat_activity psa WHERE datname = current_database() AND pid <> pg_backend_pid();')"
|
221 |
+
|
222 |
+
status_code=$(curl --write-out %{http_code} -s --output /dev/null http://localhost:8081/health/db)
|
223 |
+
if [[ "$status_code" -ne 200 ]] ; then
|
224 |
+
echo "Server has not reconnected to postgres after connection was closed: returned status $status_code"
|
225 |
+
exit 1
|
226 |
+
fi
|
227 |
+
|
228 |
+
# - name: Test backend with MySQL
|
229 |
+
# if: success() || steps.sqlite.conclusion == 'failure' || steps.postgres.conclusion == 'failure'
|
230 |
+
# env:
|
231 |
+
# WEBUI_SECRET_KEY: secret-key
|
232 |
+
# GLOBAL_LOG_LEVEL: debug
|
233 |
+
# DATABASE_URL: mysql://root:mysql@localhost:3306/mysql
|
234 |
+
# run: |
|
235 |
+
# cd backend
|
236 |
+
# uvicorn open_webui.main:app --port "8083" --forwarded-allow-ips '*' &
|
237 |
+
# UVICORN_PID=$!
|
238 |
+
# # Wait up to 20 seconds for the server to start
|
239 |
+
# for i in {1..20}; do
|
240 |
+
# curl -s http://localhost:8083/api/config > /dev/null && break
|
241 |
+
# sleep 1
|
242 |
+
# if [ $i -eq 20 ]; then
|
243 |
+
# echo "Server failed to start"
|
244 |
+
# kill -9 $UVICORN_PID
|
245 |
+
# exit 1
|
246 |
+
# fi
|
247 |
+
# done
|
248 |
+
# # Check that the server is still running after 5 seconds
|
249 |
+
# sleep 5
|
250 |
+
# if ! kill -0 $UVICORN_PID; then
|
251 |
+
# echo "Server has stopped"
|
252 |
+
# exit 1
|
253 |
+
# 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@v5
|
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
|
.github/workflows/syns.yml
ADDED
@@ -0,0 +1,45 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
name: Sync with upstream
|
2 |
+
|
3 |
+
on:
|
4 |
+
schedule:
|
5 |
+
# 每天UTC时间凌晨2点执行
|
6 |
+
- cron: '0 2 * * *'
|
7 |
+
watch:
|
8 |
+
types: [ started ]
|
9 |
+
|
10 |
+
jobs:
|
11 |
+
sync:
|
12 |
+
runs-on: ubuntu-latest
|
13 |
+
|
14 |
+
steps:
|
15 |
+
# 检查仓库代码
|
16 |
+
- name: Checkout repository
|
17 |
+
uses: actions/checkout@v4
|
18 |
+
with:
|
19 |
+
# 获取完整的 Git 历史
|
20 |
+
fetch-depth: 0
|
21 |
+
|
22 |
+
# 设置 git 用户信息
|
23 |
+
- name: Set git user
|
24 |
+
run: |
|
25 |
+
git config user.name "github-actions[bot]"
|
26 |
+
git config user.email "github-actions[bot]@users.noreply.github.com"
|
27 |
+
|
28 |
+
# 添加上游仓库为 remote
|
29 |
+
- name: Add upstream
|
30 |
+
run: |
|
31 |
+
git remote add upstream https://github.com/open-webui/open-webui
|
32 |
+
git fetch upstream
|
33 |
+
|
34 |
+
# 合并上游更改,忽略 deploy-to-hf-spaces.yml
|
35 |
+
- name: Merge upstream changes
|
36 |
+
run: |
|
37 |
+
git merge upstream/main --no-commit --strategy-option=theirs || true
|
38 |
+
git restore --source=HEAD --staged --worktree .github/workflows/deploy-to-hf-spaces.yml
|
39 |
+
git commit -m "Merged upstream changes except .github/workflows/deploy-to-hf-spaces.yml" || true
|
40 |
+
|
41 |
+
# 推送合并后的更改到主分支
|
42 |
+
- name: Push changes
|
43 |
+
run: |
|
44 |
+
git push https://${{ secrets.PAT }}@github.com/cuxt/open-webui.git main
|
45 |
+
|
.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
The diff for this file is too large to render.
See raw diff
|
|
CODE_OF_CONDUCT.md
ADDED
@@ -0,0 +1,99 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Contributor Covenant Code of Conduct
|
2 |
+
|
3 |
+
## Our Pledge
|
4 |
+
|
5 |
+
As members, contributors, and leaders of this community, we pledge to make participation in our open-source project a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation.
|
6 |
+
|
7 |
+
We are committed to creating and maintaining an open, respectful, and professional environment where positive contributions and meaningful discussions can flourish. By participating in this project, you agree to uphold these values and align your behavior to the standards outlined in this Code of Conduct.
|
8 |
+
|
9 |
+
## Why These Standards Are Important
|
10 |
+
|
11 |
+
Open-source projects rely on a community of volunteers dedicating their time, expertise, and effort toward a shared goal. These projects are inherently collaborative but also fragile, as the success of the project depends on the goodwill, energy, and productivity of those involved.
|
12 |
+
|
13 |
+
Maintaining a positive and respectful environment is essential to safeguarding the integrity of this project and protecting contributors' efforts. Behavior that disrupts this atmosphere—whether through hostility, entitlement, or unprofessional conduct—can severely harm the morale and productivity of the community. **Strict enforcement of these standards ensures a safe and supportive space for meaningful collaboration.**
|
14 |
+
|
15 |
+
This is a community where **respect and professionalism are mandatory.** Violations of these standards will result in **zero tolerance** and immediate enforcement to prevent disruption and ensure the well-being of all participants.
|
16 |
+
|
17 |
+
## Our Standards
|
18 |
+
|
19 |
+
Examples of behavior that contribute to a positive and professional community include:
|
20 |
+
|
21 |
+
- **Respecting others.** Be considerate, listen actively, and engage with empathy toward others' viewpoints and experiences.
|
22 |
+
- **Constructive feedback.** Provide actionable, thoughtful, and respectful feedback that helps improve the project and encourages collaboration. Avoid unproductive negativity or hypercriticism.
|
23 |
+
- **Recognizing volunteer contributions.** Appreciate that contributors dedicate their free time and resources selflessly. Approach them with gratitude and patience.
|
24 |
+
- **Focusing on shared goals.** Collaborate in ways that prioritize the health, success, and sustainability of the community over individual agendas.
|
25 |
+
|
26 |
+
Examples of unacceptable behavior include:
|
27 |
+
|
28 |
+
- The use of discriminatory, demeaning, or sexualized language or behavior.
|
29 |
+
- Personal attacks, derogatory comments, trolling, or inflammatory political or ideological arguments.
|
30 |
+
- Harassment, intimidation, or any behavior intended to create a hostile, uncomfortable, or unsafe environment.
|
31 |
+
- Publishing others' private information (e.g., physical or email addresses) without explicit permission.
|
32 |
+
- **Entitlement, demand, or aggression toward contributors.** Volunteers are under no obligation to provide immediate or personalized support. Rude or dismissive behavior will not be tolerated.
|
33 |
+
- **Unproductive or destructive behavior.** This includes venting frustration as hostility ("tantrums"), hypercriticism, attention-seeking negativity, or anything that distracts from the project's goals.
|
34 |
+
- **Spamming and promotional exploitation.** Sharing irrelevant product promotions or self-promotion in the community is not allowed unless it directly contributes value to the discussion.
|
35 |
+
|
36 |
+
### Feedback and Community Engagement
|
37 |
+
|
38 |
+
- **Constructive feedback is encouraged, but hostile or entitled behavior will result in immediate action.** If you disagree with elements of the project, we encourage you to offer meaningful improvements or fork the project if necessary. Healthy discussions and technical disagreements are welcome only when handled with professionalism.
|
39 |
+
- **Respect contributors' time and efforts.** No one is entitled to personalized or on-demand assistance. This is a community built on collaboration and shared effort; demanding or demeaning behavior undermines that trust and will not be allowed.
|
40 |
+
|
41 |
+
### Zero Tolerance: No Warnings, Immediate Action
|
42 |
+
|
43 |
+
This community operates under a **zero-tolerance policy.** Any behavior deemed unacceptable under this Code of Conduct will result in **immediate enforcement, without prior warning.**
|
44 |
+
|
45 |
+
We employ this approach to ensure that unproductive or disruptive behavior does not escalate further or cause unnecessary harm to other contributors. The standards are clear, and violations of any kind—whether mild or severe—will be addressed decisively to protect the community.
|
46 |
+
|
47 |
+
## Enforcement Responsibilities
|
48 |
+
|
49 |
+
Community leaders are responsible for upholding and enforcing these standards. They are empowered to take **immediate and appropriate action** to address any behaviors they deem unacceptable under this Code of Conduct. These actions are taken with the goal of protecting the community and preserving its safe, positive, and productive environment.
|
50 |
+
|
51 |
+
## Scope
|
52 |
+
|
53 |
+
This Code of Conduct applies to all community spaces, including forums, repositories, social media accounts, and in-person events. It also applies when an individual represents the community in public settings, such as conferences or official communications.
|
54 |
+
|
55 |
+
Additionally, any behavior outside of these defined spaces that negatively impacts the community or its members may fall within the scope of this Code of Conduct.
|
56 |
+
|
57 |
+
## Reporting Violations
|
58 |
+
|
59 |
+
Instances of unacceptable behavior can be reported to the leadership team at **hello@openwebui.com**. Reports will be handled promptly, confidentially, and with consideration for the safety and well-being of the reporter.
|
60 |
+
|
61 |
+
All community leaders are required to uphold confidentiality and impartiality when addressing reports of violations.
|
62 |
+
|
63 |
+
## Enforcement Guidelines
|
64 |
+
|
65 |
+
### Ban
|
66 |
+
|
67 |
+
**Community Impact**: Community leaders will issue a ban to any participant whose behavior is deemed unacceptable according to this Code of Conduct. Bans are enforced immediately and without prior notice.
|
68 |
+
|
69 |
+
A ban may be temporary or permanent, depending on the severity of the violation. This includes—but is not limited to—behavior such as:
|
70 |
+
|
71 |
+
- Harassment or abusive behavior toward contributors.
|
72 |
+
- Persistent negativity or hostility that disrupts the collaborative environment.
|
73 |
+
- Disrespectful, demanding, or aggressive interactions with others.
|
74 |
+
- Attempts to cause harm or sabotage the community.
|
75 |
+
|
76 |
+
**Consequence**: A banned individual is immediately removed from access to all community spaces, communication channels, and events. Community leaders reserve the right to enforce either a time-limited suspension or a permanent ban based on the specific circumstances of the violation.
|
77 |
+
|
78 |
+
This approach ensures that disruptive behaviors are addressed swiftly and decisively in order to maintain the integrity and productivity of the community.
|
79 |
+
|
80 |
+
## Why Zero Tolerance Is Necessary
|
81 |
+
|
82 |
+
Open-source projects thrive on collaboration, goodwill, and mutual respect. Toxic behaviors—such as entitlement, hostility, or persistent negativity—threaten not just individual contributors but the health of the project as a whole. Allowing such behaviors to persist robs contributors of their time, energy, and enthusiasm for the work they do.
|
83 |
+
|
84 |
+
By enforcing a zero-tolerance policy, we ensure that the community remains a safe, welcoming space for all participants. These measures are not about harshness—they are about protecting contributors and fostering a productive environment where innovation can thrive.
|
85 |
+
|
86 |
+
Our expectations are clear, and our enforcement reflects our commitment to this project's long-term success.
|
87 |
+
|
88 |
+
## Attribution
|
89 |
+
|
90 |
+
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.0, available at
|
91 |
+
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
|
92 |
+
|
93 |
+
Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/diversity).
|
94 |
+
|
95 |
+
[homepage]: https://www.contributor-covenant.org
|
96 |
+
|
97 |
+
For answers to common questions about this code of conduct, see the FAQ at
|
98 |
+
https://www.contributor-covenant.org/faq. Translations are available at
|
99 |
+
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,176 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# syntax=docker/dockerfile:1
|
2 |
+
# Initialize device type args
|
3 |
+
# use build args in the docker build command 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 |
+
|
15 |
+
# Tiktoken encoding name; models to use can be found at https://huggingface.co/models?library=tiktoken
|
16 |
+
ARG USE_TIKTOKEN_ENCODING_NAME="cl100k_base"
|
17 |
+
|
18 |
+
ARG BUILD_HASH=dev-build
|
19 |
+
# Override at your own risk - non-root configurations are untested
|
20 |
+
ARG UID=0
|
21 |
+
ARG GID=0
|
22 |
+
|
23 |
+
######## WebUI frontend ########
|
24 |
+
FROM --platform=$BUILDPLATFORM node:22-alpine3.20 AS build
|
25 |
+
ARG BUILD_HASH
|
26 |
+
|
27 |
+
WORKDIR /app
|
28 |
+
|
29 |
+
COPY package.json package-lock.json ./
|
30 |
+
RUN npm ci
|
31 |
+
|
32 |
+
COPY . .
|
33 |
+
ENV APP_BUILD_HASH=${BUILD_HASH}
|
34 |
+
RUN npm run build
|
35 |
+
|
36 |
+
######## WebUI backend ########
|
37 |
+
FROM python:3.11-slim-bookworm AS base
|
38 |
+
|
39 |
+
# Use args
|
40 |
+
ARG USE_CUDA
|
41 |
+
ARG USE_OLLAMA
|
42 |
+
ARG USE_CUDA_VER
|
43 |
+
ARG USE_EMBEDDING_MODEL
|
44 |
+
ARG USE_RERANKING_MODEL
|
45 |
+
ARG UID
|
46 |
+
ARG GID
|
47 |
+
|
48 |
+
## Basis ##
|
49 |
+
ENV ENV=prod \
|
50 |
+
PORT=8080 \
|
51 |
+
# pass build args to the build
|
52 |
+
USE_OLLAMA_DOCKER=${USE_OLLAMA} \
|
53 |
+
USE_CUDA_DOCKER=${USE_CUDA} \
|
54 |
+
USE_CUDA_DOCKER_VER=${USE_CUDA_VER} \
|
55 |
+
USE_EMBEDDING_MODEL_DOCKER=${USE_EMBEDDING_MODEL} \
|
56 |
+
USE_RERANKING_MODEL_DOCKER=${USE_RERANKING_MODEL}
|
57 |
+
|
58 |
+
## Basis URL Config ##
|
59 |
+
ENV OLLAMA_BASE_URL="/ollama" \
|
60 |
+
OPENAI_API_BASE_URL=""
|
61 |
+
|
62 |
+
## API Key and Security Config ##
|
63 |
+
ENV OPENAI_API_KEY="" \
|
64 |
+
WEBUI_SECRET_KEY="" \
|
65 |
+
SCARF_NO_ANALYTICS=true \
|
66 |
+
DO_NOT_TRACK=true \
|
67 |
+
ANONYMIZED_TELEMETRY=false
|
68 |
+
|
69 |
+
#### Other models #########################################################
|
70 |
+
## whisper TTS model settings ##
|
71 |
+
ENV WHISPER_MODEL="base" \
|
72 |
+
WHISPER_MODEL_DIR="/app/backend/data/cache/whisper/models"
|
73 |
+
|
74 |
+
## RAG Embedding model settings ##
|
75 |
+
ENV RAG_EMBEDDING_MODEL="$USE_EMBEDDING_MODEL_DOCKER" \
|
76 |
+
RAG_RERANKING_MODEL="$USE_RERANKING_MODEL_DOCKER" \
|
77 |
+
SENTENCE_TRANSFORMERS_HOME="/app/backend/data/cache/embedding/models"
|
78 |
+
|
79 |
+
## Tiktoken model settings ##
|
80 |
+
ENV TIKTOKEN_ENCODING_NAME="cl100k_base" \
|
81 |
+
TIKTOKEN_CACHE_DIR="/app/backend/data/cache/tiktoken"
|
82 |
+
|
83 |
+
## Hugging Face download cache ##
|
84 |
+
ENV HF_HOME="/app/backend/data/cache/embedding/models"
|
85 |
+
|
86 |
+
## Torch Extensions ##
|
87 |
+
# ENV TORCH_EXTENSIONS_DIR="/.cache/torch_extensions"
|
88 |
+
|
89 |
+
#### Other models ##########################################################
|
90 |
+
|
91 |
+
WORKDIR /app/backend
|
92 |
+
|
93 |
+
ENV HOME=/root
|
94 |
+
# Create user and group if not root
|
95 |
+
RUN if [ $UID -ne 0 ]; then \
|
96 |
+
if [ $GID -ne 0 ]; then \
|
97 |
+
addgroup --gid $GID app; \
|
98 |
+
fi; \
|
99 |
+
adduser --uid $UID --gid $GID --home $HOME --disabled-password --no-create-home app; \
|
100 |
+
fi
|
101 |
+
|
102 |
+
RUN mkdir -p $HOME/.cache/chroma
|
103 |
+
RUN echo -n 00000000-0000-0000-0000-000000000000 > $HOME/.cache/chroma/telemetry_user_id
|
104 |
+
|
105 |
+
# Make sure the user has access to the app and root directory
|
106 |
+
RUN chown -R $UID:$GID /app $HOME
|
107 |
+
|
108 |
+
RUN if [ "$USE_OLLAMA" = "true" ]; then \
|
109 |
+
apt-get update && \
|
110 |
+
# Install pandoc and netcat
|
111 |
+
apt-get install -y --no-install-recommends git build-essential pandoc netcat-openbsd curl && \
|
112 |
+
apt-get install -y --no-install-recommends gcc python3-dev && \
|
113 |
+
# for RAG OCR
|
114 |
+
apt-get install -y --no-install-recommends ffmpeg libsm6 libxext6 && \
|
115 |
+
# install helper tools
|
116 |
+
apt-get install -y --no-install-recommends curl jq && \
|
117 |
+
# install ollama
|
118 |
+
curl -fsSL https://ollama.com/install.sh | sh && \
|
119 |
+
# cleanup
|
120 |
+
rm -rf /var/lib/apt/lists/*; \
|
121 |
+
else \
|
122 |
+
apt-get update && \
|
123 |
+
# Install pandoc, netcat and gcc
|
124 |
+
apt-get install -y --no-install-recommends git build-essential pandoc gcc netcat-openbsd curl jq && \
|
125 |
+
apt-get install -y --no-install-recommends gcc python3-dev && \
|
126 |
+
# for RAG OCR
|
127 |
+
apt-get install -y --no-install-recommends ffmpeg libsm6 libxext6 && \
|
128 |
+
# cleanup
|
129 |
+
rm -rf /var/lib/apt/lists/*; \
|
130 |
+
fi
|
131 |
+
|
132 |
+
# install python dependencies
|
133 |
+
COPY --chown=$UID:$GID ./backend/requirements.txt ./requirements.txt
|
134 |
+
|
135 |
+
RUN pip3 install uv && \
|
136 |
+
if [ "$USE_CUDA" = "true" ]; then \
|
137 |
+
# If you use CUDA the whisper and embedding model will be downloaded on first use
|
138 |
+
pip3 install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/$USE_CUDA_DOCKER_VER --no-cache-dir && \
|
139 |
+
uv pip install --system -r requirements.txt --no-cache-dir && \
|
140 |
+
python -c "import os; from sentence_transformers import SentenceTransformer; SentenceTransformer(os.environ['RAG_EMBEDDING_MODEL'], device='cpu')" && \
|
141 |
+
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'])"; \
|
142 |
+
python -c "import os; import tiktoken; tiktoken.get_encoding(os.environ['TIKTOKEN_ENCODING_NAME'])"; \
|
143 |
+
else \
|
144 |
+
pip3 install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cpu --no-cache-dir && \
|
145 |
+
uv pip install --system -r requirements.txt --no-cache-dir && \
|
146 |
+
python -c "import os; from sentence_transformers import SentenceTransformer; SentenceTransformer(os.environ['RAG_EMBEDDING_MODEL'], device='cpu')" && \
|
147 |
+
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'])"; \
|
148 |
+
python -c "import os; import tiktoken; tiktoken.get_encoding(os.environ['TIKTOKEN_ENCODING_NAME'])"; \
|
149 |
+
fi; \
|
150 |
+
chown -R $UID:$GID /app/backend/data/
|
151 |
+
|
152 |
+
|
153 |
+
|
154 |
+
# copy embedding weight from build
|
155 |
+
# RUN mkdir -p /root/.cache/chroma/onnx_models/all-MiniLM-L6-v2
|
156 |
+
# COPY --from=build /app/onnx /root/.cache/chroma/onnx_models/all-MiniLM-L6-v2/onnx
|
157 |
+
|
158 |
+
# copy built frontend files
|
159 |
+
COPY --chown=$UID:$GID --from=build /app/build /app/build
|
160 |
+
COPY --chown=$UID:$GID --from=build /app/CHANGELOG.md /app/CHANGELOG.md
|
161 |
+
COPY --chown=$UID:$GID --from=build /app/package.json /app/package.json
|
162 |
+
|
163 |
+
# copy backend files
|
164 |
+
COPY --chown=$UID:$GID ./backend .
|
165 |
+
|
166 |
+
EXPOSE 8080
|
167 |
+
|
168 |
+
HEALTHCHECK CMD curl --silent --fail http://localhost:${PORT:-8080}/health | jq -ne 'input.status == true' || exit 1
|
169 |
+
|
170 |
+
USER $UID:$GID
|
171 |
+
|
172 |
+
ARG BUILD_HASH
|
173 |
+
ENV WEBUI_BUILD_VERSION=${BUILD_HASH}
|
174 |
+
ENV DOCKER=true
|
175 |
+
|
176 |
+
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,221 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
---
|
2 |
+
title: Open WebUI
|
3 |
+
emoji: 🐳
|
4 |
+
colorFrom: purple
|
5 |
+
colorTo: gray
|
6 |
+
sdk: docker
|
7 |
+
app_port: 8080
|
8 |
+
---
|
9 |
+
# Open 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 is an [extensible](https://github.com/open-webui/pipelines), feature-rich, and user-friendly self-hosted WebUI designed to operate entirely offline. It supports various LLM runners, including Ollama and OpenAI-compatible APIs. For more information, be sure to check out our [Open WebUI Documentation](https://docs.openwebui.com/).
|
23 |
+
|
24 |
+
![Open WebUI Demo](./demo.gif)
|
25 |
+
|
26 |
+
## Key Features of Open WebUI ⭐
|
27 |
+
|
28 |
+
- 🚀 **Effortless Setup**: Install seamlessly using Docker or Kubernetes (kubectl, kustomize or helm) for a hassle-free experience with support for both `:ollama` and `:cuda` tagged images.
|
29 |
+
|
30 |
+
- 🤝 **Ollama/OpenAI API Integration**: Effortlessly integrate OpenAI-compatible APIs for versatile conversations alongside Ollama models. Customize the OpenAI API URL to link with **LMStudio, GroqCloud, Mistral, OpenRouter, and more**.
|
31 |
+
|
32 |
+
- 🛡️ **Granular Permissions and User Groups**: By allowing administrators to create detailed user roles and permissions, we ensure a secure user environment. This granularity not only enhances security but also allows for customized user experiences, fostering a sense of ownership and responsibility amongst users.
|
33 |
+
|
34 |
+
- 📱 **Responsive Design**: Enjoy a seamless experience across Desktop PC, Laptop, and Mobile devices.
|
35 |
+
|
36 |
+
- 📱 **Progressive Web App (PWA) for Mobile**: Enjoy a native app-like experience on your mobile device with our PWA, providing offline access on localhost and a seamless user interface.
|
37 |
+
|
38 |
+
- ✒️🔢 **Full Markdown and LaTeX Support**: Elevate your LLM experience with comprehensive Markdown and LaTeX capabilities for enriched interaction.
|
39 |
+
|
40 |
+
- 🎤📹 **Hands-Free Voice/Video Call**: Experience seamless communication with integrated hands-free voice and video call features, allowing for a more dynamic and interactive chat environment.
|
41 |
+
|
42 |
+
- 🛠️ **Model Builder**: Easily create Ollama models via the Web UI. Create and add custom characters/agents, customize chat elements, and import models effortlessly through [Open WebUI Community](https://openwebui.com/) integration.
|
43 |
+
|
44 |
+
- 🐍 **Native Python Function Calling Tool**: Enhance your LLMs with built-in code editor support in the tools workspace. Bring Your Own Function (BYOF) by simply adding your pure Python functions, enabling seamless integration with LLMs.
|
45 |
+
|
46 |
+
- 📚 **Local RAG Integration**: Dive into the future of chat interactions with groundbreaking Retrieval Augmented Generation (RAG) support. This feature seamlessly integrates document interactions into your chat experience. You can load documents directly into the chat or add files to your document library, effortlessly accessing them using the `#` command before a query.
|
47 |
+
|
48 |
+
- 🔍 **Web Search for RAG**: Perform web searches using providers like `SearXNG`, `Google PSE`, `Brave Search`, `serpstack`, `serper`, `Serply`, `DuckDuckGo`, `TavilySearch`, `SearchApi` and `Bing` and inject the results directly into your chat experience.
|
49 |
+
|
50 |
+
- 🌐 **Web Browsing Capability**: Seamlessly integrate websites into your chat experience using the `#` command followed by a URL. This feature allows you to incorporate web content directly into your conversations, enhancing the richness and depth of your interactions.
|
51 |
+
|
52 |
+
- 🎨 **Image Generation Integration**: Seamlessly incorporate image generation capabilities using options such as AUTOMATIC1111 API or ComfyUI (local), and OpenAI's DALL-E (external), enriching your chat experience with dynamic visual content.
|
53 |
+
|
54 |
+
- ⚙️ **Many Models Conversations**: Effortlessly engage with various models simultaneously, harnessing their unique strengths for optimal responses. Enhance your experience by leveraging a diverse set of models in parallel.
|
55 |
+
|
56 |
+
- 🔐 **Role-Based Access Control (RBAC)**: Ensure secure access with restricted permissions; only authorized individuals can access your Ollama, and exclusive model creation/pulling rights are reserved for administrators.
|
57 |
+
|
58 |
+
- 🌐🌍 **Multilingual Support**: Experience Open WebUI in your preferred language with our internationalization (i18n) support. Join us in expanding our supported languages! We're actively seeking contributors!
|
59 |
+
|
60 |
+
- 🧩 **Pipelines, Open WebUI Plugin Support**: Seamlessly integrate custom logic and Python libraries into Open WebUI using [Pipelines Plugin Framework](https://github.com/open-webui/pipelines). Launch your Pipelines instance, set the OpenAI URL to the Pipelines URL, and explore endless possibilities. [Examples](https://github.com/open-webui/pipelines/tree/main/examples) include **Function Calling**, User **Rate Limiting** to control access, **Usage Monitoring** with tools like Langfuse, **Live Translation with LibreTranslate** for multilingual support, **Toxic Message Filtering** and much more.
|
61 |
+
|
62 |
+
- 🌟 **Continuous Updates**: We are committed to improving Open WebUI with regular updates, fixes, and new features.
|
63 |
+
|
64 |
+
Want to learn more about Open WebUI's features? Check out our [Open WebUI documentation](https://docs.openwebui.com/features) for a comprehensive overview!
|
65 |
+
|
66 |
+
## 🔗 Also Check Out Open WebUI Community!
|
67 |
+
|
68 |
+
Don't forget to explore our sibling project, [Open WebUI Community](https://openwebui.com/), where you can discover, download, and explore customized Modelfiles. Open WebUI Community offers a wide range of exciting possibilities for enhancing your chat interactions with Open WebUI! 🚀
|
69 |
+
|
70 |
+
## How to Install 🚀
|
71 |
+
|
72 |
+
### Installation via Python pip 🐍
|
73 |
+
|
74 |
+
Open WebUI can be installed using pip, the Python package installer. Before proceeding, ensure you're using **Python 3.11** to avoid compatibility issues.
|
75 |
+
|
76 |
+
1. **Install Open WebUI**:
|
77 |
+
Open your terminal and run the following command to install Open WebUI:
|
78 |
+
|
79 |
+
```bash
|
80 |
+
pip install open-webui
|
81 |
+
```
|
82 |
+
|
83 |
+
2. **Running Open WebUI**:
|
84 |
+
After installation, you can start Open WebUI by executing:
|
85 |
+
|
86 |
+
```bash
|
87 |
+
open-webui serve
|
88 |
+
```
|
89 |
+
|
90 |
+
This will start the Open WebUI server, which you can access at [http://localhost:8080](http://localhost:8080)
|
91 |
+
|
92 |
+
### Quick Start with Docker 🐳
|
93 |
+
|
94 |
+
> [!NOTE]
|
95 |
+
> Please note that for certain Docker environments, additional configurations might be needed. If you encounter any connection issues, our detailed guide on [Open WebUI Documentation](https://docs.openwebui.com/) is ready to assist you.
|
96 |
+
|
97 |
+
> [!WARNING]
|
98 |
+
> When using Docker to install Open WebUI, make sure to include the `-v open-webui:/app/backend/data` in your Docker command. This step is crucial as it ensures your database is properly mounted and prevents any loss of data.
|
99 |
+
|
100 |
+
> [!TIP]
|
101 |
+
> If you wish to utilize Open WebUI with Ollama included or CUDA acceleration, we recommend utilizing our official images tagged with either `:cuda` or `:ollama`. To enable CUDA, you must install the [Nvidia CUDA container toolkit](https://docs.nvidia.com/dgx/nvidia-container-runtime-upgrade/) on your Linux/WSL system.
|
102 |
+
|
103 |
+
### Installation with Default Configuration
|
104 |
+
|
105 |
+
- **If Ollama is on your computer**, use this command:
|
106 |
+
|
107 |
+
```bash
|
108 |
+
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
|
109 |
+
```
|
110 |
+
|
111 |
+
- **If Ollama is on a Different Server**, use this command:
|
112 |
+
|
113 |
+
To connect to Ollama on another server, change the `OLLAMA_BASE_URL` to the server's URL:
|
114 |
+
|
115 |
+
```bash
|
116 |
+
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
|
117 |
+
```
|
118 |
+
|
119 |
+
- **To run Open WebUI with Nvidia GPU support**, use this command:
|
120 |
+
|
121 |
+
```bash
|
122 |
+
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
|
123 |
+
```
|
124 |
+
|
125 |
+
### Installation for OpenAI API Usage Only
|
126 |
+
|
127 |
+
- **If you're only using OpenAI API**, use this command:
|
128 |
+
|
129 |
+
```bash
|
130 |
+
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
|
131 |
+
```
|
132 |
+
|
133 |
+
### Installing Open WebUI with Bundled Ollama Support
|
134 |
+
|
135 |
+
This installation method uses a single container image that bundles Open WebUI with Ollama, allowing for a streamlined setup via a single command. Choose the appropriate command based on your hardware setup:
|
136 |
+
|
137 |
+
- **With GPU Support**:
|
138 |
+
Utilize GPU resources by running the following command:
|
139 |
+
|
140 |
+
```bash
|
141 |
+
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
|
142 |
+
```
|
143 |
+
|
144 |
+
- **For CPU Only**:
|
145 |
+
If you're not using a GPU, use this command instead:
|
146 |
+
|
147 |
+
```bash
|
148 |
+
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
|
149 |
+
```
|
150 |
+
|
151 |
+
Both commands facilitate a built-in, hassle-free installation of both Open WebUI and Ollama, ensuring that you can get everything up and running swiftly.
|
152 |
+
|
153 |
+
After installation, you can access Open WebUI at [http://localhost:3000](http://localhost:3000). Enjoy! 😄
|
154 |
+
|
155 |
+
### Other Installation Methods
|
156 |
+
|
157 |
+
We offer various installation alternatives, including non-Docker native installation methods, Docker Compose, Kustomize, and Helm. Visit our [Open WebUI Documentation](https://docs.openwebui.com/getting-started/) or join our [Discord community](https://discord.gg/5rJgQTnV4s) for comprehensive guidance.
|
158 |
+
|
159 |
+
### Troubleshooting
|
160 |
+
|
161 |
+
Encountering connection issues? Our [Open WebUI Documentation](https://docs.openwebui.com/troubleshooting/) has got you covered. For further assistance and to join our vibrant community, visit the [Open WebUI Discord](https://discord.gg/5rJgQTnV4s).
|
162 |
+
|
163 |
+
#### Open WebUI: Server Connection Error
|
164 |
+
|
165 |
+
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`.
|
166 |
+
|
167 |
+
**Example Docker Command**:
|
168 |
+
|
169 |
+
```bash
|
170 |
+
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
|
171 |
+
```
|
172 |
+
|
173 |
+
### Keeping Your Docker Installation Up-to-Date
|
174 |
+
|
175 |
+
In case you want to update your local Docker installation to the latest version, you can do it with [Watchtower](https://containrrr.dev/watchtower/):
|
176 |
+
|
177 |
+
```bash
|
178 |
+
docker run --rm --volume /var/run/docker.sock:/var/run/docker.sock containrrr/watchtower --run-once open-webui
|
179 |
+
```
|
180 |
+
|
181 |
+
In the last part of the command, replace `open-webui` with your container name if it is different.
|
182 |
+
|
183 |
+
Check our Migration Guide available in our [Open WebUI Documentation](https://docs.openwebui.com/tutorials/migration/).
|
184 |
+
|
185 |
+
### Using the Dev Branch 🌙
|
186 |
+
|
187 |
+
> [!WARNING]
|
188 |
+
> The `:dev` branch contains the latest unstable features and changes. Use it at your own risk as it may have bugs or incomplete features.
|
189 |
+
|
190 |
+
If you want to try out the latest bleeding-edge features and are okay with occasional instability, you can use the `:dev` tag like this:
|
191 |
+
|
192 |
+
```bash
|
193 |
+
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
|
194 |
+
```
|
195 |
+
|
196 |
+
## What's Next? 🌟
|
197 |
+
|
198 |
+
Discover upcoming features on our roadmap in the [Open WebUI Documentation](https://docs.openwebui.com/roadmap/).
|
199 |
+
|
200 |
+
## License 📜
|
201 |
+
|
202 |
+
This project is licensed under the [MIT License](LICENSE) - see the [LICENSE](LICENSE) file for details. 📄
|
203 |
+
|
204 |
+
## Support 💬
|
205 |
+
|
206 |
+
If you have any questions, suggestions, or need assistance, please open an issue or join our
|
207 |
+
[Open WebUI Discord community](https://discord.gg/5rJgQTnV4s) to connect with us! 🤝
|
208 |
+
|
209 |
+
## Star History
|
210 |
+
|
211 |
+
<a href="https://star-history.com/#open-webui/open-webui&Date">
|
212 |
+
<picture>
|
213 |
+
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=open-webui/open-webui&type=Date&theme=dark" />
|
214 |
+
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=open-webui/open-webui&type=Date" />
|
215 |
+
<img alt="Star History Chart" src="https://api.star-history.com/svg?repos=open-webui/open-webui&type=Date" />
|
216 |
+
</picture>
|
217 |
+
</a>
|
218 |
+
|
219 |
+
---
|
220 |
+
|
221 |
+
Created by [Timothy Jaeryang Baek](https://github.com/tjbck) - Let's make Open WebUI even more amazing together! 💪
|
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 Responses 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/config.py
ADDED
@@ -0,0 +1,1959 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import json
|
2 |
+
import logging
|
3 |
+
import os
|
4 |
+
import shutil
|
5 |
+
from datetime import datetime
|
6 |
+
from pathlib import Path
|
7 |
+
from typing import Generic, Optional, TypeVar
|
8 |
+
from urllib.parse import urlparse
|
9 |
+
|
10 |
+
import chromadb
|
11 |
+
import requests
|
12 |
+
import yaml
|
13 |
+
from open_webui.internal.db import Base, get_db
|
14 |
+
from open_webui.env import (
|
15 |
+
OPEN_WEBUI_DIR,
|
16 |
+
DATA_DIR,
|
17 |
+
ENV,
|
18 |
+
FRONTEND_BUILD_DIR,
|
19 |
+
WEBUI_AUTH,
|
20 |
+
WEBUI_FAVICON_URL,
|
21 |
+
WEBUI_NAME,
|
22 |
+
log,
|
23 |
+
DATABASE_URL,
|
24 |
+
OFFLINE_MODE,
|
25 |
+
)
|
26 |
+
from pydantic import BaseModel
|
27 |
+
from sqlalchemy import JSON, Column, DateTime, Integer, func
|
28 |
+
|
29 |
+
|
30 |
+
class EndpointFilter(logging.Filter):
|
31 |
+
def filter(self, record: logging.LogRecord) -> bool:
|
32 |
+
return record.getMessage().find("/health") == -1
|
33 |
+
|
34 |
+
|
35 |
+
# Filter out /endpoint
|
36 |
+
logging.getLogger("uvicorn.access").addFilter(EndpointFilter())
|
37 |
+
|
38 |
+
####################################
|
39 |
+
# Config helpers
|
40 |
+
####################################
|
41 |
+
|
42 |
+
|
43 |
+
# Function to run the alembic migrations
|
44 |
+
def run_migrations():
|
45 |
+
print("Running migrations")
|
46 |
+
try:
|
47 |
+
from alembic import command
|
48 |
+
from alembic.config import Config
|
49 |
+
|
50 |
+
alembic_cfg = Config(OPEN_WEBUI_DIR / "alembic.ini")
|
51 |
+
|
52 |
+
# Set the script location dynamically
|
53 |
+
migrations_path = OPEN_WEBUI_DIR / "migrations"
|
54 |
+
alembic_cfg.set_main_option("script_location", str(migrations_path))
|
55 |
+
|
56 |
+
command.upgrade(alembic_cfg, "head")
|
57 |
+
except Exception as e:
|
58 |
+
print(f"Error: {e}")
|
59 |
+
|
60 |
+
|
61 |
+
run_migrations()
|
62 |
+
|
63 |
+
|
64 |
+
class Config(Base):
|
65 |
+
__tablename__ = "config"
|
66 |
+
|
67 |
+
id = Column(Integer, primary_key=True)
|
68 |
+
data = Column(JSON, nullable=False)
|
69 |
+
version = Column(Integer, nullable=False, default=0)
|
70 |
+
created_at = Column(DateTime, nullable=False, server_default=func.now())
|
71 |
+
updated_at = Column(DateTime, nullable=True, onupdate=func.now())
|
72 |
+
|
73 |
+
|
74 |
+
def load_json_config():
|
75 |
+
with open(f"{DATA_DIR}/config.json", "r") as file:
|
76 |
+
return json.load(file)
|
77 |
+
|
78 |
+
|
79 |
+
def save_to_db(data):
|
80 |
+
with get_db() as db:
|
81 |
+
existing_config = db.query(Config).first()
|
82 |
+
if not existing_config:
|
83 |
+
new_config = Config(data=data, version=0)
|
84 |
+
db.add(new_config)
|
85 |
+
else:
|
86 |
+
existing_config.data = data
|
87 |
+
existing_config.updated_at = datetime.now()
|
88 |
+
db.add(existing_config)
|
89 |
+
db.commit()
|
90 |
+
|
91 |
+
|
92 |
+
def reset_config():
|
93 |
+
with get_db() as db:
|
94 |
+
db.query(Config).delete()
|
95 |
+
db.commit()
|
96 |
+
|
97 |
+
|
98 |
+
# When initializing, check if config.json exists and migrate it to the database
|
99 |
+
if os.path.exists(f"{DATA_DIR}/config.json"):
|
100 |
+
data = load_json_config()
|
101 |
+
save_to_db(data)
|
102 |
+
os.rename(f"{DATA_DIR}/config.json", f"{DATA_DIR}/old_config.json")
|
103 |
+
|
104 |
+
DEFAULT_CONFIG = {
|
105 |
+
"version": 0,
|
106 |
+
"ui": {
|
107 |
+
"default_locale": "",
|
108 |
+
"prompt_suggestions": [
|
109 |
+
{
|
110 |
+
"title": [
|
111 |
+
"Help me study",
|
112 |
+
"vocabulary for a college entrance exam",
|
113 |
+
],
|
114 |
+
"content": "Help me study vocabulary: write a sentence for me to fill in the blank, and I'll try to pick the correct option.",
|
115 |
+
},
|
116 |
+
{
|
117 |
+
"title": [
|
118 |
+
"Give me ideas",
|
119 |
+
"for what to do with my kids' art",
|
120 |
+
],
|
121 |
+
"content": "What are 5 creative things I could do with my kids' art? I don't want to throw them away, but it's also so much clutter.",
|
122 |
+
},
|
123 |
+
{
|
124 |
+
"title": ["Tell me a fun fact", "about the Roman Empire"],
|
125 |
+
"content": "Tell me a random fun fact about the Roman Empire",
|
126 |
+
},
|
127 |
+
{
|
128 |
+
"title": [
|
129 |
+
"Show me a code snippet",
|
130 |
+
"of a website's sticky header",
|
131 |
+
],
|
132 |
+
"content": "Show me a code snippet of a website's sticky header in CSS and JavaScript.",
|
133 |
+
},
|
134 |
+
{
|
135 |
+
"title": [
|
136 |
+
"Explain options trading",
|
137 |
+
"if I'm familiar with buying and selling stocks",
|
138 |
+
],
|
139 |
+
"content": "Explain options trading in simple terms if I'm familiar with buying and selling stocks.",
|
140 |
+
},
|
141 |
+
{
|
142 |
+
"title": ["Overcome procrastination", "give me tips"],
|
143 |
+
"content": "Could you start by asking me about instances when I procrastinate the most and then give me some suggestions to overcome it?",
|
144 |
+
},
|
145 |
+
{
|
146 |
+
"title": [
|
147 |
+
"Grammar check",
|
148 |
+
"rewrite it for better readability ",
|
149 |
+
],
|
150 |
+
"content": 'Check the following sentence for grammar and clarity: "[sentence]". Rewrite it for better readability while maintaining its original meaning.',
|
151 |
+
},
|
152 |
+
],
|
153 |
+
},
|
154 |
+
}
|
155 |
+
|
156 |
+
|
157 |
+
def get_config():
|
158 |
+
with get_db() as db:
|
159 |
+
config_entry = db.query(Config).order_by(Config.id.desc()).first()
|
160 |
+
return config_entry.data if config_entry else DEFAULT_CONFIG
|
161 |
+
|
162 |
+
|
163 |
+
CONFIG_DATA = get_config()
|
164 |
+
|
165 |
+
|
166 |
+
def get_config_value(config_path: str):
|
167 |
+
path_parts = config_path.split(".")
|
168 |
+
cur_config = CONFIG_DATA
|
169 |
+
for key in path_parts:
|
170 |
+
if key in cur_config:
|
171 |
+
cur_config = cur_config[key]
|
172 |
+
else:
|
173 |
+
return None
|
174 |
+
return cur_config
|
175 |
+
|
176 |
+
|
177 |
+
PERSISTENT_CONFIG_REGISTRY = []
|
178 |
+
|
179 |
+
|
180 |
+
def save_config(config):
|
181 |
+
global CONFIG_DATA
|
182 |
+
global PERSISTENT_CONFIG_REGISTRY
|
183 |
+
try:
|
184 |
+
save_to_db(config)
|
185 |
+
CONFIG_DATA = config
|
186 |
+
|
187 |
+
# Trigger updates on all registered PersistentConfig entries
|
188 |
+
for config_item in PERSISTENT_CONFIG_REGISTRY:
|
189 |
+
config_item.update()
|
190 |
+
except Exception as e:
|
191 |
+
log.exception(e)
|
192 |
+
return False
|
193 |
+
return True
|
194 |
+
|
195 |
+
|
196 |
+
T = TypeVar("T")
|
197 |
+
|
198 |
+
|
199 |
+
class PersistentConfig(Generic[T]):
|
200 |
+
def __init__(self, env_name: str, config_path: str, env_value: T):
|
201 |
+
self.env_name = env_name
|
202 |
+
self.config_path = config_path
|
203 |
+
self.env_value = env_value
|
204 |
+
self.config_value = get_config_value(config_path)
|
205 |
+
if self.config_value is not None:
|
206 |
+
log.info(f"'{env_name}' loaded from the latest database entry")
|
207 |
+
self.value = self.config_value
|
208 |
+
else:
|
209 |
+
self.value = env_value
|
210 |
+
|
211 |
+
PERSISTENT_CONFIG_REGISTRY.append(self)
|
212 |
+
|
213 |
+
def __str__(self):
|
214 |
+
return str(self.value)
|
215 |
+
|
216 |
+
@property
|
217 |
+
def __dict__(self):
|
218 |
+
raise TypeError(
|
219 |
+
"PersistentConfig object cannot be converted to dict, use config_get or .value instead."
|
220 |
+
)
|
221 |
+
|
222 |
+
def __getattribute__(self, item):
|
223 |
+
if item == "__dict__":
|
224 |
+
raise TypeError(
|
225 |
+
"PersistentConfig object cannot be converted to dict, use config_get or .value instead."
|
226 |
+
)
|
227 |
+
return super().__getattribute__(item)
|
228 |
+
|
229 |
+
def update(self):
|
230 |
+
new_value = get_config_value(self.config_path)
|
231 |
+
if new_value is not None:
|
232 |
+
self.value = new_value
|
233 |
+
log.info(f"Updated {self.env_name} to new value {self.value}")
|
234 |
+
|
235 |
+
def save(self):
|
236 |
+
log.info(f"Saving '{self.env_name}' to the database")
|
237 |
+
path_parts = self.config_path.split(".")
|
238 |
+
sub_config = CONFIG_DATA
|
239 |
+
for key in path_parts[:-1]:
|
240 |
+
if key not in sub_config:
|
241 |
+
sub_config[key] = {}
|
242 |
+
sub_config = sub_config[key]
|
243 |
+
sub_config[path_parts[-1]] = self.value
|
244 |
+
save_to_db(CONFIG_DATA)
|
245 |
+
self.config_value = self.value
|
246 |
+
|
247 |
+
|
248 |
+
class AppConfig:
|
249 |
+
_state: dict[str, PersistentConfig]
|
250 |
+
|
251 |
+
def __init__(self):
|
252 |
+
super().__setattr__("_state", {})
|
253 |
+
|
254 |
+
def __setattr__(self, key, value):
|
255 |
+
if isinstance(value, PersistentConfig):
|
256 |
+
self._state[key] = value
|
257 |
+
else:
|
258 |
+
self._state[key].value = value
|
259 |
+
self._state[key].save()
|
260 |
+
|
261 |
+
def __getattr__(self, key):
|
262 |
+
return self._state[key].value
|
263 |
+
|
264 |
+
|
265 |
+
####################################
|
266 |
+
# WEBUI_AUTH (Required for security)
|
267 |
+
####################################
|
268 |
+
|
269 |
+
ENABLE_API_KEY = PersistentConfig(
|
270 |
+
"ENABLE_API_KEY",
|
271 |
+
"auth.api_key.enable",
|
272 |
+
os.environ.get("ENABLE_API_KEY", "True").lower() == "true",
|
273 |
+
)
|
274 |
+
|
275 |
+
|
276 |
+
JWT_EXPIRES_IN = PersistentConfig(
|
277 |
+
"JWT_EXPIRES_IN", "auth.jwt_expiry", os.environ.get("JWT_EXPIRES_IN", "-1")
|
278 |
+
)
|
279 |
+
|
280 |
+
####################################
|
281 |
+
# OAuth config
|
282 |
+
####################################
|
283 |
+
|
284 |
+
ENABLE_OAUTH_SIGNUP = PersistentConfig(
|
285 |
+
"ENABLE_OAUTH_SIGNUP",
|
286 |
+
"oauth.enable_signup",
|
287 |
+
os.environ.get("ENABLE_OAUTH_SIGNUP", "False").lower() == "true",
|
288 |
+
)
|
289 |
+
|
290 |
+
OAUTH_MERGE_ACCOUNTS_BY_EMAIL = PersistentConfig(
|
291 |
+
"OAUTH_MERGE_ACCOUNTS_BY_EMAIL",
|
292 |
+
"oauth.merge_accounts_by_email",
|
293 |
+
os.environ.get("OAUTH_MERGE_ACCOUNTS_BY_EMAIL", "False").lower() == "true",
|
294 |
+
)
|
295 |
+
|
296 |
+
OAUTH_PROVIDERS = {}
|
297 |
+
|
298 |
+
GOOGLE_CLIENT_ID = PersistentConfig(
|
299 |
+
"GOOGLE_CLIENT_ID",
|
300 |
+
"oauth.google.client_id",
|
301 |
+
os.environ.get("GOOGLE_CLIENT_ID", ""),
|
302 |
+
)
|
303 |
+
|
304 |
+
GOOGLE_CLIENT_SECRET = PersistentConfig(
|
305 |
+
"GOOGLE_CLIENT_SECRET",
|
306 |
+
"oauth.google.client_secret",
|
307 |
+
os.environ.get("GOOGLE_CLIENT_SECRET", ""),
|
308 |
+
)
|
309 |
+
|
310 |
+
GOOGLE_DRIVE_CLIENT_ID = PersistentConfig(
|
311 |
+
"GOOGLE_DRIVE_CLIENT_ID",
|
312 |
+
"google_drive.client_id",
|
313 |
+
os.environ.get("GOOGLE_DRIVE_CLIENT_ID", ""),
|
314 |
+
)
|
315 |
+
|
316 |
+
GOOGLE_DRIVE_API_KEY = PersistentConfig(
|
317 |
+
"GOOGLE_DRIVE_API_KEY",
|
318 |
+
"google_drive.api_key",
|
319 |
+
os.environ.get("GOOGLE_DRIVE_API_KEY", ""),
|
320 |
+
)
|
321 |
+
|
322 |
+
GOOGLE_OAUTH_SCOPE = PersistentConfig(
|
323 |
+
"GOOGLE_OAUTH_SCOPE",
|
324 |
+
"oauth.google.scope",
|
325 |
+
os.environ.get("GOOGLE_OAUTH_SCOPE", "openid email profile"),
|
326 |
+
)
|
327 |
+
|
328 |
+
GOOGLE_REDIRECT_URI = PersistentConfig(
|
329 |
+
"GOOGLE_REDIRECT_URI",
|
330 |
+
"oauth.google.redirect_uri",
|
331 |
+
os.environ.get("GOOGLE_REDIRECT_URI", ""),
|
332 |
+
)
|
333 |
+
|
334 |
+
MICROSOFT_CLIENT_ID = PersistentConfig(
|
335 |
+
"MICROSOFT_CLIENT_ID",
|
336 |
+
"oauth.microsoft.client_id",
|
337 |
+
os.environ.get("MICROSOFT_CLIENT_ID", ""),
|
338 |
+
)
|
339 |
+
|
340 |
+
MICROSOFT_CLIENT_SECRET = PersistentConfig(
|
341 |
+
"MICROSOFT_CLIENT_SECRET",
|
342 |
+
"oauth.microsoft.client_secret",
|
343 |
+
os.environ.get("MICROSOFT_CLIENT_SECRET", ""),
|
344 |
+
)
|
345 |
+
|
346 |
+
MICROSOFT_CLIENT_TENANT_ID = PersistentConfig(
|
347 |
+
"MICROSOFT_CLIENT_TENANT_ID",
|
348 |
+
"oauth.microsoft.tenant_id",
|
349 |
+
os.environ.get("MICROSOFT_CLIENT_TENANT_ID", ""),
|
350 |
+
)
|
351 |
+
|
352 |
+
MICROSOFT_OAUTH_SCOPE = PersistentConfig(
|
353 |
+
"MICROSOFT_OAUTH_SCOPE",
|
354 |
+
"oauth.microsoft.scope",
|
355 |
+
os.environ.get("MICROSOFT_OAUTH_SCOPE", "openid email profile"),
|
356 |
+
)
|
357 |
+
|
358 |
+
MICROSOFT_REDIRECT_URI = PersistentConfig(
|
359 |
+
"MICROSOFT_REDIRECT_URI",
|
360 |
+
"oauth.microsoft.redirect_uri",
|
361 |
+
os.environ.get("MICROSOFT_REDIRECT_URI", ""),
|
362 |
+
)
|
363 |
+
|
364 |
+
OAUTH_CLIENT_ID = PersistentConfig(
|
365 |
+
"OAUTH_CLIENT_ID",
|
366 |
+
"oauth.oidc.client_id",
|
367 |
+
os.environ.get("OAUTH_CLIENT_ID", ""),
|
368 |
+
)
|
369 |
+
|
370 |
+
OAUTH_CLIENT_SECRET = PersistentConfig(
|
371 |
+
"OAUTH_CLIENT_SECRET",
|
372 |
+
"oauth.oidc.client_secret",
|
373 |
+
os.environ.get("OAUTH_CLIENT_SECRET", ""),
|
374 |
+
)
|
375 |
+
|
376 |
+
OPENID_PROVIDER_URL = PersistentConfig(
|
377 |
+
"OPENID_PROVIDER_URL",
|
378 |
+
"oauth.oidc.provider_url",
|
379 |
+
os.environ.get("OPENID_PROVIDER_URL", ""),
|
380 |
+
)
|
381 |
+
|
382 |
+
OPENID_REDIRECT_URI = PersistentConfig(
|
383 |
+
"OPENID_REDIRECT_URI",
|
384 |
+
"oauth.oidc.redirect_uri",
|
385 |
+
os.environ.get("OPENID_REDIRECT_URI", ""),
|
386 |
+
)
|
387 |
+
|
388 |
+
OAUTH_SCOPES = PersistentConfig(
|
389 |
+
"OAUTH_SCOPES",
|
390 |
+
"oauth.oidc.scopes",
|
391 |
+
os.environ.get("OAUTH_SCOPES", "openid email profile"),
|
392 |
+
)
|
393 |
+
|
394 |
+
OAUTH_PROVIDER_NAME = PersistentConfig(
|
395 |
+
"OAUTH_PROVIDER_NAME",
|
396 |
+
"oauth.oidc.provider_name",
|
397 |
+
os.environ.get("OAUTH_PROVIDER_NAME", "SSO"),
|
398 |
+
)
|
399 |
+
|
400 |
+
OAUTH_USERNAME_CLAIM = PersistentConfig(
|
401 |
+
"OAUTH_USERNAME_CLAIM",
|
402 |
+
"oauth.oidc.username_claim",
|
403 |
+
os.environ.get("OAUTH_USERNAME_CLAIM", "name"),
|
404 |
+
)
|
405 |
+
|
406 |
+
OAUTH_PICTURE_CLAIM = PersistentConfig(
|
407 |
+
"OAUTH_PICTURE_CLAIM",
|
408 |
+
"oauth.oidc.avatar_claim",
|
409 |
+
os.environ.get("OAUTH_PICTURE_CLAIM", "picture"),
|
410 |
+
)
|
411 |
+
|
412 |
+
OAUTH_EMAIL_CLAIM = PersistentConfig(
|
413 |
+
"OAUTH_EMAIL_CLAIM",
|
414 |
+
"oauth.oidc.email_claim",
|
415 |
+
os.environ.get("OAUTH_EMAIL_CLAIM", "email"),
|
416 |
+
)
|
417 |
+
|
418 |
+
OAUTH_GROUPS_CLAIM = PersistentConfig(
|
419 |
+
"OAUTH_GROUPS_CLAIM",
|
420 |
+
"oauth.oidc.group_claim",
|
421 |
+
os.environ.get("OAUTH_GROUP_CLAIM", "groups"),
|
422 |
+
)
|
423 |
+
|
424 |
+
ENABLE_OAUTH_ROLE_MANAGEMENT = PersistentConfig(
|
425 |
+
"ENABLE_OAUTH_ROLE_MANAGEMENT",
|
426 |
+
"oauth.enable_role_mapping",
|
427 |
+
os.environ.get("ENABLE_OAUTH_ROLE_MANAGEMENT", "False").lower() == "true",
|
428 |
+
)
|
429 |
+
|
430 |
+
ENABLE_OAUTH_GROUP_MANAGEMENT = PersistentConfig(
|
431 |
+
"ENABLE_OAUTH_GROUP_MANAGEMENT",
|
432 |
+
"oauth.enable_group_mapping",
|
433 |
+
os.environ.get("ENABLE_OAUTH_GROUP_MANAGEMENT", "False").lower() == "true",
|
434 |
+
)
|
435 |
+
|
436 |
+
OAUTH_ROLES_CLAIM = PersistentConfig(
|
437 |
+
"OAUTH_ROLES_CLAIM",
|
438 |
+
"oauth.roles_claim",
|
439 |
+
os.environ.get("OAUTH_ROLES_CLAIM", "roles"),
|
440 |
+
)
|
441 |
+
|
442 |
+
OAUTH_ALLOWED_ROLES = PersistentConfig(
|
443 |
+
"OAUTH_ALLOWED_ROLES",
|
444 |
+
"oauth.allowed_roles",
|
445 |
+
[
|
446 |
+
role.strip()
|
447 |
+
for role in os.environ.get("OAUTH_ALLOWED_ROLES", "user,admin").split(",")
|
448 |
+
],
|
449 |
+
)
|
450 |
+
|
451 |
+
OAUTH_ADMIN_ROLES = PersistentConfig(
|
452 |
+
"OAUTH_ADMIN_ROLES",
|
453 |
+
"oauth.admin_roles",
|
454 |
+
[role.strip() for role in os.environ.get("OAUTH_ADMIN_ROLES", "admin").split(",")],
|
455 |
+
)
|
456 |
+
|
457 |
+
OAUTH_ALLOWED_DOMAINS = PersistentConfig(
|
458 |
+
"OAUTH_ALLOWED_DOMAINS",
|
459 |
+
"oauth.allowed_domains",
|
460 |
+
[
|
461 |
+
domain.strip()
|
462 |
+
for domain in os.environ.get("OAUTH_ALLOWED_DOMAINS", "*").split(",")
|
463 |
+
],
|
464 |
+
)
|
465 |
+
|
466 |
+
|
467 |
+
def load_oauth_providers():
|
468 |
+
OAUTH_PROVIDERS.clear()
|
469 |
+
if GOOGLE_CLIENT_ID.value and GOOGLE_CLIENT_SECRET.value:
|
470 |
+
OAUTH_PROVIDERS["google"] = {
|
471 |
+
"client_id": GOOGLE_CLIENT_ID.value,
|
472 |
+
"client_secret": GOOGLE_CLIENT_SECRET.value,
|
473 |
+
"server_metadata_url": "https://accounts.google.com/.well-known/openid-configuration",
|
474 |
+
"scope": GOOGLE_OAUTH_SCOPE.value,
|
475 |
+
"redirect_uri": GOOGLE_REDIRECT_URI.value,
|
476 |
+
}
|
477 |
+
|
478 |
+
if (
|
479 |
+
MICROSOFT_CLIENT_ID.value
|
480 |
+
and MICROSOFT_CLIENT_SECRET.value
|
481 |
+
and MICROSOFT_CLIENT_TENANT_ID.value
|
482 |
+
):
|
483 |
+
OAUTH_PROVIDERS["microsoft"] = {
|
484 |
+
"client_id": MICROSOFT_CLIENT_ID.value,
|
485 |
+
"client_secret": MICROSOFT_CLIENT_SECRET.value,
|
486 |
+
"server_metadata_url": f"https://login.microsoftonline.com/{MICROSOFT_CLIENT_TENANT_ID.value}/v2.0/.well-known/openid-configuration",
|
487 |
+
"scope": MICROSOFT_OAUTH_SCOPE.value,
|
488 |
+
"redirect_uri": MICROSOFT_REDIRECT_URI.value,
|
489 |
+
}
|
490 |
+
|
491 |
+
if (
|
492 |
+
OAUTH_CLIENT_ID.value
|
493 |
+
and OAUTH_CLIENT_SECRET.value
|
494 |
+
and OPENID_PROVIDER_URL.value
|
495 |
+
):
|
496 |
+
OAUTH_PROVIDERS["oidc"] = {
|
497 |
+
"client_id": OAUTH_CLIENT_ID.value,
|
498 |
+
"client_secret": OAUTH_CLIENT_SECRET.value,
|
499 |
+
"server_metadata_url": OPENID_PROVIDER_URL.value,
|
500 |
+
"scope": OAUTH_SCOPES.value,
|
501 |
+
"name": OAUTH_PROVIDER_NAME.value,
|
502 |
+
"redirect_uri": OPENID_REDIRECT_URI.value,
|
503 |
+
}
|
504 |
+
|
505 |
+
|
506 |
+
load_oauth_providers()
|
507 |
+
|
508 |
+
####################################
|
509 |
+
# Static DIR
|
510 |
+
####################################
|
511 |
+
|
512 |
+
STATIC_DIR = Path(os.getenv("STATIC_DIR", OPEN_WEBUI_DIR / "static")).resolve()
|
513 |
+
|
514 |
+
frontend_favicon = FRONTEND_BUILD_DIR / "static" / "favicon.png"
|
515 |
+
|
516 |
+
if frontend_favicon.exists():
|
517 |
+
try:
|
518 |
+
shutil.copyfile(frontend_favicon, STATIC_DIR / "favicon.png")
|
519 |
+
except Exception as e:
|
520 |
+
logging.error(f"An error occurred: {e}")
|
521 |
+
else:
|
522 |
+
logging.warning(f"Frontend favicon not found at {frontend_favicon}")
|
523 |
+
|
524 |
+
frontend_splash = FRONTEND_BUILD_DIR / "static" / "splash.png"
|
525 |
+
|
526 |
+
if frontend_splash.exists():
|
527 |
+
try:
|
528 |
+
shutil.copyfile(frontend_splash, STATIC_DIR / "splash.png")
|
529 |
+
except Exception as e:
|
530 |
+
logging.error(f"An error occurred: {e}")
|
531 |
+
else:
|
532 |
+
logging.warning(f"Frontend splash not found at {frontend_splash}")
|
533 |
+
|
534 |
+
|
535 |
+
####################################
|
536 |
+
# CUSTOM_NAME
|
537 |
+
####################################
|
538 |
+
|
539 |
+
CUSTOM_NAME = os.environ.get("CUSTOM_NAME", "")
|
540 |
+
|
541 |
+
if CUSTOM_NAME:
|
542 |
+
try:
|
543 |
+
r = requests.get(f"https://api.openwebui.com/api/v1/custom/{CUSTOM_NAME}")
|
544 |
+
data = r.json()
|
545 |
+
if r.ok:
|
546 |
+
if "logo" in data:
|
547 |
+
WEBUI_FAVICON_URL = url = (
|
548 |
+
f"https://api.openwebui.com{data['logo']}"
|
549 |
+
if data["logo"][0] == "/"
|
550 |
+
else data["logo"]
|
551 |
+
)
|
552 |
+
|
553 |
+
r = requests.get(url, stream=True)
|
554 |
+
if r.status_code == 200:
|
555 |
+
with open(f"{STATIC_DIR}/favicon.png", "wb") as f:
|
556 |
+
r.raw.decode_content = True
|
557 |
+
shutil.copyfileobj(r.raw, f)
|
558 |
+
|
559 |
+
if "splash" in data:
|
560 |
+
url = (
|
561 |
+
f"https://api.openwebui.com{data['splash']}"
|
562 |
+
if data["splash"][0] == "/"
|
563 |
+
else data["splash"]
|
564 |
+
)
|
565 |
+
|
566 |
+
r = requests.get(url, stream=True)
|
567 |
+
if r.status_code == 200:
|
568 |
+
with open(f"{STATIC_DIR}/splash.png", "wb") as f:
|
569 |
+
r.raw.decode_content = True
|
570 |
+
shutil.copyfileobj(r.raw, f)
|
571 |
+
|
572 |
+
WEBUI_NAME = data["name"]
|
573 |
+
except Exception as e:
|
574 |
+
log.exception(e)
|
575 |
+
pass
|
576 |
+
|
577 |
+
|
578 |
+
####################################
|
579 |
+
# STORAGE PROVIDER
|
580 |
+
####################################
|
581 |
+
|
582 |
+
STORAGE_PROVIDER = os.environ.get("STORAGE_PROVIDER", "") # defaults to local, s3
|
583 |
+
|
584 |
+
S3_ACCESS_KEY_ID = os.environ.get("S3_ACCESS_KEY_ID", None)
|
585 |
+
S3_SECRET_ACCESS_KEY = os.environ.get("S3_SECRET_ACCESS_KEY", None)
|
586 |
+
S3_REGION_NAME = os.environ.get("S3_REGION_NAME", None)
|
587 |
+
S3_BUCKET_NAME = os.environ.get("S3_BUCKET_NAME", None)
|
588 |
+
S3_ENDPOINT_URL = os.environ.get("S3_ENDPOINT_URL", None)
|
589 |
+
|
590 |
+
####################################
|
591 |
+
# File Upload DIR
|
592 |
+
####################################
|
593 |
+
|
594 |
+
UPLOAD_DIR = f"{DATA_DIR}/uploads"
|
595 |
+
Path(UPLOAD_DIR).mkdir(parents=True, exist_ok=True)
|
596 |
+
|
597 |
+
|
598 |
+
####################################
|
599 |
+
# Cache DIR
|
600 |
+
####################################
|
601 |
+
|
602 |
+
CACHE_DIR = f"{DATA_DIR}/cache"
|
603 |
+
Path(CACHE_DIR).mkdir(parents=True, exist_ok=True)
|
604 |
+
|
605 |
+
####################################
|
606 |
+
# OLLAMA_BASE_URL
|
607 |
+
####################################
|
608 |
+
|
609 |
+
ENABLE_OLLAMA_API = PersistentConfig(
|
610 |
+
"ENABLE_OLLAMA_API",
|
611 |
+
"ollama.enable",
|
612 |
+
os.environ.get("ENABLE_OLLAMA_API", "True").lower() == "true",
|
613 |
+
)
|
614 |
+
|
615 |
+
OLLAMA_API_BASE_URL = os.environ.get(
|
616 |
+
"OLLAMA_API_BASE_URL", "http://localhost:11434/api"
|
617 |
+
)
|
618 |
+
|
619 |
+
OLLAMA_BASE_URL = os.environ.get("OLLAMA_BASE_URL", "")
|
620 |
+
if OLLAMA_BASE_URL:
|
621 |
+
# Remove trailing slash
|
622 |
+
OLLAMA_BASE_URL = (
|
623 |
+
OLLAMA_BASE_URL[:-1] if OLLAMA_BASE_URL.endswith("/") else OLLAMA_BASE_URL
|
624 |
+
)
|
625 |
+
|
626 |
+
|
627 |
+
K8S_FLAG = os.environ.get("K8S_FLAG", "")
|
628 |
+
USE_OLLAMA_DOCKER = os.environ.get("USE_OLLAMA_DOCKER", "false")
|
629 |
+
|
630 |
+
if OLLAMA_BASE_URL == "" and OLLAMA_API_BASE_URL != "":
|
631 |
+
OLLAMA_BASE_URL = (
|
632 |
+
OLLAMA_API_BASE_URL[:-4]
|
633 |
+
if OLLAMA_API_BASE_URL.endswith("/api")
|
634 |
+
else OLLAMA_API_BASE_URL
|
635 |
+
)
|
636 |
+
|
637 |
+
if ENV == "prod":
|
638 |
+
if OLLAMA_BASE_URL == "/ollama" and not K8S_FLAG:
|
639 |
+
if USE_OLLAMA_DOCKER.lower() == "true":
|
640 |
+
# if you use all-in-one docker container (Open WebUI + Ollama)
|
641 |
+
# with the docker build arg USE_OLLAMA=true (--build-arg="USE_OLLAMA=true") this only works with http://localhost:11434
|
642 |
+
OLLAMA_BASE_URL = "http://localhost:11434"
|
643 |
+
else:
|
644 |
+
OLLAMA_BASE_URL = "http://host.docker.internal:11434"
|
645 |
+
elif K8S_FLAG:
|
646 |
+
OLLAMA_BASE_URL = "http://ollama-service.open-webui.svc.cluster.local:11434"
|
647 |
+
|
648 |
+
|
649 |
+
OLLAMA_BASE_URLS = os.environ.get("OLLAMA_BASE_URLS", "")
|
650 |
+
OLLAMA_BASE_URLS = OLLAMA_BASE_URLS if OLLAMA_BASE_URLS != "" else OLLAMA_BASE_URL
|
651 |
+
|
652 |
+
OLLAMA_BASE_URLS = [url.strip() for url in OLLAMA_BASE_URLS.split(";")]
|
653 |
+
OLLAMA_BASE_URLS = PersistentConfig(
|
654 |
+
"OLLAMA_BASE_URLS", "ollama.base_urls", OLLAMA_BASE_URLS
|
655 |
+
)
|
656 |
+
|
657 |
+
OLLAMA_API_CONFIGS = PersistentConfig(
|
658 |
+
"OLLAMA_API_CONFIGS",
|
659 |
+
"ollama.api_configs",
|
660 |
+
{},
|
661 |
+
)
|
662 |
+
|
663 |
+
####################################
|
664 |
+
# OPENAI_API
|
665 |
+
####################################
|
666 |
+
|
667 |
+
|
668 |
+
ENABLE_OPENAI_API = PersistentConfig(
|
669 |
+
"ENABLE_OPENAI_API",
|
670 |
+
"openai.enable",
|
671 |
+
os.environ.get("ENABLE_OPENAI_API", "True").lower() == "true",
|
672 |
+
)
|
673 |
+
|
674 |
+
|
675 |
+
OPENAI_API_KEY = os.environ.get("OPENAI_API_KEY", "")
|
676 |
+
OPENAI_API_BASE_URL = os.environ.get("OPENAI_API_BASE_URL", "")
|
677 |
+
|
678 |
+
|
679 |
+
if OPENAI_API_BASE_URL == "":
|
680 |
+
OPENAI_API_BASE_URL = "https://api.openai.com/v1"
|
681 |
+
|
682 |
+
OPENAI_API_KEYS = os.environ.get("OPENAI_API_KEYS", "")
|
683 |
+
OPENAI_API_KEYS = OPENAI_API_KEYS if OPENAI_API_KEYS != "" else OPENAI_API_KEY
|
684 |
+
|
685 |
+
OPENAI_API_KEYS = [url.strip() for url in OPENAI_API_KEYS.split(";")]
|
686 |
+
OPENAI_API_KEYS = PersistentConfig(
|
687 |
+
"OPENAI_API_KEYS", "openai.api_keys", OPENAI_API_KEYS
|
688 |
+
)
|
689 |
+
|
690 |
+
OPENAI_API_BASE_URLS = os.environ.get("OPENAI_API_BASE_URLS", "")
|
691 |
+
OPENAI_API_BASE_URLS = (
|
692 |
+
OPENAI_API_BASE_URLS if OPENAI_API_BASE_URLS != "" else OPENAI_API_BASE_URL
|
693 |
+
)
|
694 |
+
|
695 |
+
OPENAI_API_BASE_URLS = [
|
696 |
+
url.strip() if url != "" else "https://api.openai.com/v1"
|
697 |
+
for url in OPENAI_API_BASE_URLS.split(";")
|
698 |
+
]
|
699 |
+
OPENAI_API_BASE_URLS = PersistentConfig(
|
700 |
+
"OPENAI_API_BASE_URLS", "openai.api_base_urls", OPENAI_API_BASE_URLS
|
701 |
+
)
|
702 |
+
|
703 |
+
OPENAI_API_CONFIGS = PersistentConfig(
|
704 |
+
"OPENAI_API_CONFIGS",
|
705 |
+
"openai.api_configs",
|
706 |
+
{},
|
707 |
+
)
|
708 |
+
|
709 |
+
# Get the actual OpenAI API key based on the base URL
|
710 |
+
OPENAI_API_KEY = ""
|
711 |
+
try:
|
712 |
+
OPENAI_API_KEY = OPENAI_API_KEYS.value[
|
713 |
+
OPENAI_API_BASE_URLS.value.index("https://api.openai.com/v1")
|
714 |
+
]
|
715 |
+
except Exception:
|
716 |
+
pass
|
717 |
+
OPENAI_API_BASE_URL = "https://api.openai.com/v1"
|
718 |
+
|
719 |
+
####################################
|
720 |
+
# WEBUI
|
721 |
+
####################################
|
722 |
+
|
723 |
+
|
724 |
+
WEBUI_URL = PersistentConfig(
|
725 |
+
"WEBUI_URL", "webui.url", os.environ.get("WEBUI_URL", "http://localhost:3000")
|
726 |
+
)
|
727 |
+
|
728 |
+
|
729 |
+
ENABLE_SIGNUP = PersistentConfig(
|
730 |
+
"ENABLE_SIGNUP",
|
731 |
+
"ui.enable_signup",
|
732 |
+
(
|
733 |
+
False
|
734 |
+
if not WEBUI_AUTH
|
735 |
+
else os.environ.get("ENABLE_SIGNUP", "True").lower() == "true"
|
736 |
+
),
|
737 |
+
)
|
738 |
+
|
739 |
+
ENABLE_LOGIN_FORM = PersistentConfig(
|
740 |
+
"ENABLE_LOGIN_FORM",
|
741 |
+
"ui.ENABLE_LOGIN_FORM",
|
742 |
+
os.environ.get("ENABLE_LOGIN_FORM", "True").lower() == "true",
|
743 |
+
)
|
744 |
+
|
745 |
+
|
746 |
+
DEFAULT_LOCALE = PersistentConfig(
|
747 |
+
"DEFAULT_LOCALE",
|
748 |
+
"ui.default_locale",
|
749 |
+
os.environ.get("DEFAULT_LOCALE", ""),
|
750 |
+
)
|
751 |
+
|
752 |
+
DEFAULT_MODELS = PersistentConfig(
|
753 |
+
"DEFAULT_MODELS", "ui.default_models", os.environ.get("DEFAULT_MODELS", None)
|
754 |
+
)
|
755 |
+
|
756 |
+
DEFAULT_PROMPT_SUGGESTIONS = PersistentConfig(
|
757 |
+
"DEFAULT_PROMPT_SUGGESTIONS",
|
758 |
+
"ui.prompt_suggestions",
|
759 |
+
[
|
760 |
+
{
|
761 |
+
"title": ["Help me study", "vocabulary for a college entrance exam"],
|
762 |
+
"content": "Help me study vocabulary: write a sentence for me to fill in the blank, and I'll try to pick the correct option.",
|
763 |
+
},
|
764 |
+
{
|
765 |
+
"title": ["Give me ideas", "for what to do with my kids' art"],
|
766 |
+
"content": "What are 5 creative things I could do with my kids' art? I don't want to throw them away, but it's also so much clutter.",
|
767 |
+
},
|
768 |
+
{
|
769 |
+
"title": ["Tell me a fun fact", "about the Roman Empire"],
|
770 |
+
"content": "Tell me a random fun fact about the Roman Empire",
|
771 |
+
},
|
772 |
+
{
|
773 |
+
"title": ["Show me a code snippet", "of a website's sticky header"],
|
774 |
+
"content": "Show me a code snippet of a website's sticky header in CSS and JavaScript.",
|
775 |
+
},
|
776 |
+
{
|
777 |
+
"title": [
|
778 |
+
"Explain options trading",
|
779 |
+
"if I'm familiar with buying and selling stocks",
|
780 |
+
],
|
781 |
+
"content": "Explain options trading in simple terms if I'm familiar with buying and selling stocks.",
|
782 |
+
},
|
783 |
+
{
|
784 |
+
"title": ["Overcome procrastination", "give me tips"],
|
785 |
+
"content": "Could you start by asking me about instances when I procrastinate the most and then give me some suggestions to overcome it?",
|
786 |
+
},
|
787 |
+
],
|
788 |
+
)
|
789 |
+
|
790 |
+
MODEL_ORDER_LIST = PersistentConfig(
|
791 |
+
"MODEL_ORDER_LIST",
|
792 |
+
"ui.model_order_list",
|
793 |
+
[],
|
794 |
+
)
|
795 |
+
|
796 |
+
DEFAULT_USER_ROLE = PersistentConfig(
|
797 |
+
"DEFAULT_USER_ROLE",
|
798 |
+
"ui.default_user_role",
|
799 |
+
os.getenv("DEFAULT_USER_ROLE", "pending"),
|
800 |
+
)
|
801 |
+
|
802 |
+
USER_PERMISSIONS_WORKSPACE_MODELS_ACCESS = (
|
803 |
+
os.environ.get("USER_PERMISSIONS_WORKSPACE_MODELS_ACCESS", "False").lower()
|
804 |
+
== "true"
|
805 |
+
)
|
806 |
+
|
807 |
+
USER_PERMISSIONS_WORKSPACE_KNOWLEDGE_ACCESS = (
|
808 |
+
os.environ.get("USER_PERMISSIONS_WORKSPACE_KNOWLEDGE_ACCESS", "False").lower()
|
809 |
+
== "true"
|
810 |
+
)
|
811 |
+
|
812 |
+
USER_PERMISSIONS_WORKSPACE_PROMPTS_ACCESS = (
|
813 |
+
os.environ.get("USER_PERMISSIONS_WORKSPACE_PROMPTS_ACCESS", "False").lower()
|
814 |
+
== "true"
|
815 |
+
)
|
816 |
+
|
817 |
+
USER_PERMISSIONS_WORKSPACE_TOOLS_ACCESS = (
|
818 |
+
os.environ.get("USER_PERMISSIONS_WORKSPACE_TOOLS_ACCESS", "False").lower() == "true"
|
819 |
+
)
|
820 |
+
|
821 |
+
USER_PERMISSIONS_CHAT_FILE_UPLOAD = (
|
822 |
+
os.environ.get("USER_PERMISSIONS_CHAT_FILE_UPLOAD", "True").lower() == "true"
|
823 |
+
)
|
824 |
+
|
825 |
+
USER_PERMISSIONS_CHAT_DELETE = (
|
826 |
+
os.environ.get("USER_PERMISSIONS_CHAT_DELETE", "True").lower() == "true"
|
827 |
+
)
|
828 |
+
|
829 |
+
USER_PERMISSIONS_CHAT_EDIT = (
|
830 |
+
os.environ.get("USER_PERMISSIONS_CHAT_EDIT", "True").lower() == "true"
|
831 |
+
)
|
832 |
+
|
833 |
+
USER_PERMISSIONS_CHAT_TEMPORARY = (
|
834 |
+
os.environ.get("USER_PERMISSIONS_CHAT_TEMPORARY", "True").lower() == "true"
|
835 |
+
)
|
836 |
+
|
837 |
+
USER_PERMISSIONS = PersistentConfig(
|
838 |
+
"USER_PERMISSIONS",
|
839 |
+
"user.permissions",
|
840 |
+
{
|
841 |
+
"workspace": {
|
842 |
+
"models": USER_PERMISSIONS_WORKSPACE_MODELS_ACCESS,
|
843 |
+
"knowledge": USER_PERMISSIONS_WORKSPACE_KNOWLEDGE_ACCESS,
|
844 |
+
"prompts": USER_PERMISSIONS_WORKSPACE_PROMPTS_ACCESS,
|
845 |
+
"tools": USER_PERMISSIONS_WORKSPACE_TOOLS_ACCESS,
|
846 |
+
},
|
847 |
+
"chat": {
|
848 |
+
"file_upload": USER_PERMISSIONS_CHAT_FILE_UPLOAD,
|
849 |
+
"delete": USER_PERMISSIONS_CHAT_DELETE,
|
850 |
+
"edit": USER_PERMISSIONS_CHAT_EDIT,
|
851 |
+
"temporary": USER_PERMISSIONS_CHAT_TEMPORARY,
|
852 |
+
},
|
853 |
+
},
|
854 |
+
)
|
855 |
+
|
856 |
+
ENABLE_CHANNELS = PersistentConfig(
|
857 |
+
"ENABLE_CHANNELS",
|
858 |
+
"channels.enable",
|
859 |
+
os.environ.get("ENABLE_CHANNELS", "False").lower() == "true",
|
860 |
+
)
|
861 |
+
|
862 |
+
|
863 |
+
ENABLE_EVALUATION_ARENA_MODELS = PersistentConfig(
|
864 |
+
"ENABLE_EVALUATION_ARENA_MODELS",
|
865 |
+
"evaluation.arena.enable",
|
866 |
+
os.environ.get("ENABLE_EVALUATION_ARENA_MODELS", "True").lower() == "true",
|
867 |
+
)
|
868 |
+
EVALUATION_ARENA_MODELS = PersistentConfig(
|
869 |
+
"EVALUATION_ARENA_MODELS",
|
870 |
+
"evaluation.arena.models",
|
871 |
+
[],
|
872 |
+
)
|
873 |
+
|
874 |
+
DEFAULT_ARENA_MODEL = {
|
875 |
+
"id": "arena-model",
|
876 |
+
"name": "Arena Model",
|
877 |
+
"meta": {
|
878 |
+
"profile_image_url": "/favicon.png",
|
879 |
+
"description": "Submit your questions to anonymous AI chatbots and vote on the best response.",
|
880 |
+
"model_ids": None,
|
881 |
+
},
|
882 |
+
}
|
883 |
+
|
884 |
+
WEBHOOK_URL = PersistentConfig(
|
885 |
+
"WEBHOOK_URL", "webhook_url", os.environ.get("WEBHOOK_URL", "")
|
886 |
+
)
|
887 |
+
|
888 |
+
ENABLE_ADMIN_EXPORT = os.environ.get("ENABLE_ADMIN_EXPORT", "True").lower() == "true"
|
889 |
+
|
890 |
+
ENABLE_ADMIN_CHAT_ACCESS = (
|
891 |
+
os.environ.get("ENABLE_ADMIN_CHAT_ACCESS", "True").lower() == "true"
|
892 |
+
)
|
893 |
+
|
894 |
+
ENABLE_COMMUNITY_SHARING = PersistentConfig(
|
895 |
+
"ENABLE_COMMUNITY_SHARING",
|
896 |
+
"ui.enable_community_sharing",
|
897 |
+
os.environ.get("ENABLE_COMMUNITY_SHARING", "True").lower() == "true",
|
898 |
+
)
|
899 |
+
|
900 |
+
ENABLE_MESSAGE_RATING = PersistentConfig(
|
901 |
+
"ENABLE_MESSAGE_RATING",
|
902 |
+
"ui.enable_message_rating",
|
903 |
+
os.environ.get("ENABLE_MESSAGE_RATING", "True").lower() == "true",
|
904 |
+
)
|
905 |
+
|
906 |
+
|
907 |
+
def validate_cors_origins(origins):
|
908 |
+
for origin in origins:
|
909 |
+
if origin != "*":
|
910 |
+
validate_cors_origin(origin)
|
911 |
+
|
912 |
+
|
913 |
+
def validate_cors_origin(origin):
|
914 |
+
parsed_url = urlparse(origin)
|
915 |
+
|
916 |
+
# Check if the scheme is either http or https
|
917 |
+
if parsed_url.scheme not in ["http", "https"]:
|
918 |
+
raise ValueError(
|
919 |
+
f"Invalid scheme in CORS_ALLOW_ORIGIN: '{origin}'. Only 'http' and 'https' are allowed."
|
920 |
+
)
|
921 |
+
|
922 |
+
# Ensure that the netloc (domain + port) is present, indicating it's a valid URL
|
923 |
+
if not parsed_url.netloc:
|
924 |
+
raise ValueError(f"Invalid URL structure in CORS_ALLOW_ORIGIN: '{origin}'.")
|
925 |
+
|
926 |
+
|
927 |
+
# For production, you should only need one host as
|
928 |
+
# fastapi serves the svelte-kit built frontend and backend from the same host and port.
|
929 |
+
# To test CORS_ALLOW_ORIGIN locally, you can set something like
|
930 |
+
# CORS_ALLOW_ORIGIN=http://localhost:5173;http://localhost:8080
|
931 |
+
# in your .env file depending on your frontend port, 5173 in this case.
|
932 |
+
CORS_ALLOW_ORIGIN = os.environ.get("CORS_ALLOW_ORIGIN", "*").split(";")
|
933 |
+
|
934 |
+
if "*" in CORS_ALLOW_ORIGIN:
|
935 |
+
log.warning(
|
936 |
+
"\n\nWARNING: CORS_ALLOW_ORIGIN IS SET TO '*' - NOT RECOMMENDED FOR PRODUCTION DEPLOYMENTS.\n"
|
937 |
+
)
|
938 |
+
|
939 |
+
validate_cors_origins(CORS_ALLOW_ORIGIN)
|
940 |
+
|
941 |
+
|
942 |
+
class BannerModel(BaseModel):
|
943 |
+
id: str
|
944 |
+
type: str
|
945 |
+
title: Optional[str] = None
|
946 |
+
content: str
|
947 |
+
dismissible: bool
|
948 |
+
timestamp: int
|
949 |
+
|
950 |
+
|
951 |
+
try:
|
952 |
+
banners = json.loads(os.environ.get("WEBUI_BANNERS", "[]"))
|
953 |
+
banners = [BannerModel(**banner) for banner in banners]
|
954 |
+
except Exception as e:
|
955 |
+
print(f"Error loading WEBUI_BANNERS: {e}")
|
956 |
+
banners = []
|
957 |
+
|
958 |
+
WEBUI_BANNERS = PersistentConfig("WEBUI_BANNERS", "ui.banners", banners)
|
959 |
+
|
960 |
+
|
961 |
+
SHOW_ADMIN_DETAILS = PersistentConfig(
|
962 |
+
"SHOW_ADMIN_DETAILS",
|
963 |
+
"auth.admin.show",
|
964 |
+
os.environ.get("SHOW_ADMIN_DETAILS", "true").lower() == "true",
|
965 |
+
)
|
966 |
+
|
967 |
+
ADMIN_EMAIL = PersistentConfig(
|
968 |
+
"ADMIN_EMAIL",
|
969 |
+
"auth.admin.email",
|
970 |
+
os.environ.get("ADMIN_EMAIL", None),
|
971 |
+
)
|
972 |
+
|
973 |
+
|
974 |
+
####################################
|
975 |
+
# TASKS
|
976 |
+
####################################
|
977 |
+
|
978 |
+
|
979 |
+
TASK_MODEL = PersistentConfig(
|
980 |
+
"TASK_MODEL",
|
981 |
+
"task.model.default",
|
982 |
+
os.environ.get("TASK_MODEL", ""),
|
983 |
+
)
|
984 |
+
|
985 |
+
TASK_MODEL_EXTERNAL = PersistentConfig(
|
986 |
+
"TASK_MODEL_EXTERNAL",
|
987 |
+
"task.model.external",
|
988 |
+
os.environ.get("TASK_MODEL_EXTERNAL", ""),
|
989 |
+
)
|
990 |
+
|
991 |
+
TITLE_GENERATION_PROMPT_TEMPLATE = PersistentConfig(
|
992 |
+
"TITLE_GENERATION_PROMPT_TEMPLATE",
|
993 |
+
"task.title.prompt_template",
|
994 |
+
os.environ.get("TITLE_GENERATION_PROMPT_TEMPLATE", ""),
|
995 |
+
)
|
996 |
+
|
997 |
+
DEFAULT_TITLE_GENERATION_PROMPT_TEMPLATE = """Create a concise, 3-5 word title with an emoji as a title for the chat history, in the given language. Suitable Emojis for the summary can be used to enhance understanding but avoid quotation marks or special formatting. RESPOND ONLY WITH THE TITLE TEXT.
|
998 |
+
|
999 |
+
Examples of titles:
|
1000 |
+
📉 Stock Market Trends
|
1001 |
+
🍪 Perfect Chocolate Chip Recipe
|
1002 |
+
Evolution of Music Streaming
|
1003 |
+
Remote Work Productivity Tips
|
1004 |
+
Artificial Intelligence in Healthcare
|
1005 |
+
🎮 Video Game Development Insights
|
1006 |
+
|
1007 |
+
<chat_history>
|
1008 |
+
{{MESSAGES:END:2}}
|
1009 |
+
</chat_history>"""
|
1010 |
+
|
1011 |
+
|
1012 |
+
TAGS_GENERATION_PROMPT_TEMPLATE = PersistentConfig(
|
1013 |
+
"TAGS_GENERATION_PROMPT_TEMPLATE",
|
1014 |
+
"task.tags.prompt_template",
|
1015 |
+
os.environ.get("TAGS_GENERATION_PROMPT_TEMPLATE", ""),
|
1016 |
+
)
|
1017 |
+
|
1018 |
+
DEFAULT_TAGS_GENERATION_PROMPT_TEMPLATE = """### Task:
|
1019 |
+
Generate 1-3 broad tags categorizing the main themes of the chat history, along with 1-3 more specific subtopic tags.
|
1020 |
+
|
1021 |
+
### Guidelines:
|
1022 |
+
- Start with high-level domains (e.g. Science, Technology, Philosophy, Arts, Politics, Business, Health, Sports, Entertainment, Education)
|
1023 |
+
- Consider including relevant subfields/subdomains if they are strongly represented throughout the conversation
|
1024 |
+
- If content is too short (less than 3 messages) or too diverse, use only ["General"]
|
1025 |
+
- Use the chat's primary language; default to English if multilingual
|
1026 |
+
- Prioritize accuracy over specificity
|
1027 |
+
|
1028 |
+
### Output:
|
1029 |
+
JSON format: { "tags": ["tag1", "tag2", "tag3"] }
|
1030 |
+
|
1031 |
+
### Chat History:
|
1032 |
+
<chat_history>
|
1033 |
+
{{MESSAGES:END:6}}
|
1034 |
+
</chat_history>"""
|
1035 |
+
|
1036 |
+
ENABLE_TAGS_GENERATION = PersistentConfig(
|
1037 |
+
"ENABLE_TAGS_GENERATION",
|
1038 |
+
"task.tags.enable",
|
1039 |
+
os.environ.get("ENABLE_TAGS_GENERATION", "True").lower() == "true",
|
1040 |
+
)
|
1041 |
+
|
1042 |
+
|
1043 |
+
ENABLE_SEARCH_QUERY_GENERATION = PersistentConfig(
|
1044 |
+
"ENABLE_SEARCH_QUERY_GENERATION",
|
1045 |
+
"task.query.search.enable",
|
1046 |
+
os.environ.get("ENABLE_SEARCH_QUERY_GENERATION", "True").lower() == "true",
|
1047 |
+
)
|
1048 |
+
|
1049 |
+
ENABLE_RETRIEVAL_QUERY_GENERATION = PersistentConfig(
|
1050 |
+
"ENABLE_RETRIEVAL_QUERY_GENERATION",
|
1051 |
+
"task.query.retrieval.enable",
|
1052 |
+
os.environ.get("ENABLE_RETRIEVAL_QUERY_GENERATION", "True").lower() == "true",
|
1053 |
+
)
|
1054 |
+
|
1055 |
+
|
1056 |
+
QUERY_GENERATION_PROMPT_TEMPLATE = PersistentConfig(
|
1057 |
+
"QUERY_GENERATION_PROMPT_TEMPLATE",
|
1058 |
+
"task.query.prompt_template",
|
1059 |
+
os.environ.get("QUERY_GENERATION_PROMPT_TEMPLATE", ""),
|
1060 |
+
)
|
1061 |
+
|
1062 |
+
DEFAULT_QUERY_GENERATION_PROMPT_TEMPLATE = """### Task:
|
1063 |
+
Analyze the chat history to determine the necessity of generating search queries, in the given language. By default, **prioritize generating 1-3 broad and relevant search queries** unless it is absolutely certain that no additional information is required. The aim is to retrieve comprehensive, updated, and valuable information even with minimal uncertainty. If no search is unequivocally needed, return an empty list.
|
1064 |
+
|
1065 |
+
### Guidelines:
|
1066 |
+
- Respond **EXCLUSIVELY** with a JSON object. Any form of extra commentary, explanation, or additional text is strictly prohibited.
|
1067 |
+
- When generating search queries, respond in the format: { "queries": ["query1", "query2"] }, ensuring each query is distinct, concise, and relevant to the topic.
|
1068 |
+
- If and only if it is entirely certain that no useful results can be retrieved by a search, return: { "queries": [] }.
|
1069 |
+
- Err on the side of suggesting search queries if there is **any chance** they might provide useful or updated information.
|
1070 |
+
- Be concise and focused on composing high-quality search queries, avoiding unnecessary elaboration, commentary, or assumptions.
|
1071 |
+
- Today's date is: {{CURRENT_DATE}}.
|
1072 |
+
- Always prioritize providing actionable and broad queries that maximize informational coverage.
|
1073 |
+
|
1074 |
+
### Output:
|
1075 |
+
Strictly return in JSON format:
|
1076 |
+
{
|
1077 |
+
"queries": ["query1", "query2"]
|
1078 |
+
}
|
1079 |
+
|
1080 |
+
### Chat History:
|
1081 |
+
<chat_history>
|
1082 |
+
{{MESSAGES:END:6}}
|
1083 |
+
</chat_history>
|
1084 |
+
"""
|
1085 |
+
|
1086 |
+
ENABLE_AUTOCOMPLETE_GENERATION = PersistentConfig(
|
1087 |
+
"ENABLE_AUTOCOMPLETE_GENERATION",
|
1088 |
+
"task.autocomplete.enable",
|
1089 |
+
os.environ.get("ENABLE_AUTOCOMPLETE_GENERATION", "True").lower() == "true",
|
1090 |
+
)
|
1091 |
+
|
1092 |
+
AUTOCOMPLETE_GENERATION_INPUT_MAX_LENGTH = PersistentConfig(
|
1093 |
+
"AUTOCOMPLETE_GENERATION_INPUT_MAX_LENGTH",
|
1094 |
+
"task.autocomplete.input_max_length",
|
1095 |
+
int(os.environ.get("AUTOCOMPLETE_GENERATION_INPUT_MAX_LENGTH", "-1")),
|
1096 |
+
)
|
1097 |
+
|
1098 |
+
AUTOCOMPLETE_GENERATION_PROMPT_TEMPLATE = PersistentConfig(
|
1099 |
+
"AUTOCOMPLETE_GENERATION_PROMPT_TEMPLATE",
|
1100 |
+
"task.autocomplete.prompt_template",
|
1101 |
+
os.environ.get("AUTOCOMPLETE_GENERATION_PROMPT_TEMPLATE", ""),
|
1102 |
+
)
|
1103 |
+
|
1104 |
+
|
1105 |
+
DEFAULT_AUTOCOMPLETE_GENERATION_PROMPT_TEMPLATE = """### Task:
|
1106 |
+
You are an autocompletion system. Continue the text in `<text>` based on the **completion type** in `<type>` and the given language.
|
1107 |
+
|
1108 |
+
### **Instructions**:
|
1109 |
+
1. Analyze `<text>` for context and meaning.
|
1110 |
+
2. Use `<type>` to guide your output:
|
1111 |
+
- **General**: Provide a natural, concise continuation.
|
1112 |
+
- **Search Query**: Complete as if generating a realistic search query.
|
1113 |
+
3. Start as if you are directly continuing `<text>`. Do **not** repeat, paraphrase, or respond as a model. Simply complete the text.
|
1114 |
+
4. Ensure the continuation:
|
1115 |
+
- Flows naturally from `<text>`.
|
1116 |
+
- Avoids repetition, overexplaining, or unrelated ideas.
|
1117 |
+
5. If unsure, return: `{ "text": "" }`.
|
1118 |
+
|
1119 |
+
### **Output Rules**:
|
1120 |
+
- Respond only in JSON format: `{ "text": "<your_completion>" }`.
|
1121 |
+
|
1122 |
+
### **Examples**:
|
1123 |
+
#### Example 1:
|
1124 |
+
Input:
|
1125 |
+
<type>General</type>
|
1126 |
+
<text>The sun was setting over the horizon, painting the sky</text>
|
1127 |
+
Output:
|
1128 |
+
{ "text": "with vibrant shades of orange and pink." }
|
1129 |
+
|
1130 |
+
#### Example 2:
|
1131 |
+
Input:
|
1132 |
+
<type>Search Query</type>
|
1133 |
+
<text>Top-rated restaurants in</text>
|
1134 |
+
Output:
|
1135 |
+
{ "text": "New York City for Italian cuisine." }
|
1136 |
+
|
1137 |
+
---
|
1138 |
+
### Context:
|
1139 |
+
<chat_history>
|
1140 |
+
{{MESSAGES:END:6}}
|
1141 |
+
</chat_history>
|
1142 |
+
<type>{{TYPE}}</type>
|
1143 |
+
<text>{{PROMPT}}</text>
|
1144 |
+
#### Output:
|
1145 |
+
"""
|
1146 |
+
|
1147 |
+
TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE = PersistentConfig(
|
1148 |
+
"TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE",
|
1149 |
+
"task.tools.prompt_template",
|
1150 |
+
os.environ.get("TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE", ""),
|
1151 |
+
)
|
1152 |
+
|
1153 |
+
|
1154 |
+
DEFAULT_TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE = """Available Tools: {{TOOLS}}\nReturn an empty string if no tools match the query. If a function tool matches, construct and return a JSON object in the format {\"name\": \"functionName\", \"parameters\": {\"requiredFunctionParamKey\": \"requiredFunctionParamValue\"}} using the appropriate tool and its parameters. Only return the object and limit the response to the JSON object without additional text."""
|
1155 |
+
|
1156 |
+
|
1157 |
+
DEFAULT_EMOJI_GENERATION_PROMPT_TEMPLATE = """Your task is to reflect the speaker's likely facial expression through a fitting emoji. Interpret emotions from the message and reflect their facial expression using fitting, diverse emojis (e.g., 😊, 😢, 😡, 😱).
|
1158 |
+
|
1159 |
+
Message: ```{{prompt}}```"""
|
1160 |
+
|
1161 |
+
DEFAULT_MOA_GENERATION_PROMPT_TEMPLATE = """You have been provided with a set of responses from various models to the latest user query: "{{prompt}}"
|
1162 |
+
|
1163 |
+
Your task is to synthesize these responses into a single, high-quality response. It is crucial to critically evaluate the information provided in these responses, recognizing that some of it may be biased or incorrect. Your response should not simply replicate the given answers but should offer a refined, accurate, and comprehensive reply to the instruction. Ensure your response is well-structured, coherent, and adheres to the highest standards of accuracy and reliability.
|
1164 |
+
|
1165 |
+
Responses from models: {{responses}}"""
|
1166 |
+
|
1167 |
+
####################################
|
1168 |
+
# Vector Database
|
1169 |
+
####################################
|
1170 |
+
|
1171 |
+
VECTOR_DB = os.environ.get("VECTOR_DB", "chroma")
|
1172 |
+
|
1173 |
+
# Chroma
|
1174 |
+
CHROMA_DATA_PATH = f"{DATA_DIR}/vector_db"
|
1175 |
+
CHROMA_TENANT = os.environ.get("CHROMA_TENANT", chromadb.DEFAULT_TENANT)
|
1176 |
+
CHROMA_DATABASE = os.environ.get("CHROMA_DATABASE", chromadb.DEFAULT_DATABASE)
|
1177 |
+
CHROMA_HTTP_HOST = os.environ.get("CHROMA_HTTP_HOST", "")
|
1178 |
+
CHROMA_HTTP_PORT = int(os.environ.get("CHROMA_HTTP_PORT", "8000"))
|
1179 |
+
CHROMA_CLIENT_AUTH_PROVIDER = os.environ.get("CHROMA_CLIENT_AUTH_PROVIDER", "")
|
1180 |
+
CHROMA_CLIENT_AUTH_CREDENTIALS = os.environ.get("CHROMA_CLIENT_AUTH_CREDENTIALS", "")
|
1181 |
+
# Comma-separated list of header=value pairs
|
1182 |
+
CHROMA_HTTP_HEADERS = os.environ.get("CHROMA_HTTP_HEADERS", "")
|
1183 |
+
if CHROMA_HTTP_HEADERS:
|
1184 |
+
CHROMA_HTTP_HEADERS = dict(
|
1185 |
+
[pair.split("=") for pair in CHROMA_HTTP_HEADERS.split(",")]
|
1186 |
+
)
|
1187 |
+
else:
|
1188 |
+
CHROMA_HTTP_HEADERS = None
|
1189 |
+
CHROMA_HTTP_SSL = os.environ.get("CHROMA_HTTP_SSL", "false").lower() == "true"
|
1190 |
+
# this uses the model defined in the Dockerfile ENV variable. If you dont use docker or docker based deployments such as k8s, the default embedding model will be used (sentence-transformers/all-MiniLM-L6-v2)
|
1191 |
+
|
1192 |
+
# Milvus
|
1193 |
+
|
1194 |
+
MILVUS_URI = os.environ.get("MILVUS_URI", f"{DATA_DIR}/vector_db/milvus.db")
|
1195 |
+
|
1196 |
+
# Qdrant
|
1197 |
+
QDRANT_URI = os.environ.get("QDRANT_URI", None)
|
1198 |
+
QDRANT_API_KEY = os.environ.get("QDRANT_API_KEY", None)
|
1199 |
+
|
1200 |
+
# OpenSearch
|
1201 |
+
OPENSEARCH_URI = os.environ.get("OPENSEARCH_URI", "https://localhost:9200")
|
1202 |
+
OPENSEARCH_SSL = os.environ.get("OPENSEARCH_SSL", True)
|
1203 |
+
OPENSEARCH_CERT_VERIFY = os.environ.get("OPENSEARCH_CERT_VERIFY", False)
|
1204 |
+
OPENSEARCH_USERNAME = os.environ.get("OPENSEARCH_USERNAME", None)
|
1205 |
+
OPENSEARCH_PASSWORD = os.environ.get("OPENSEARCH_PASSWORD", None)
|
1206 |
+
|
1207 |
+
# Pgvector
|
1208 |
+
PGVECTOR_DB_URL = os.environ.get("PGVECTOR_DB_URL", DATABASE_URL)
|
1209 |
+
if VECTOR_DB == "pgvector" and not PGVECTOR_DB_URL.startswith("postgres"):
|
1210 |
+
raise ValueError(
|
1211 |
+
"Pgvector requires setting PGVECTOR_DB_URL or using Postgres with vector extension as the primary database."
|
1212 |
+
)
|
1213 |
+
|
1214 |
+
####################################
|
1215 |
+
# Information Retrieval (RAG)
|
1216 |
+
####################################
|
1217 |
+
|
1218 |
+
|
1219 |
+
# If configured, Google Drive will be available as an upload option.
|
1220 |
+
ENABLE_GOOGLE_DRIVE_INTEGRATION = PersistentConfig(
|
1221 |
+
"ENABLE_GOOGLE_DRIVE_INTEGRATION",
|
1222 |
+
"google_drive.enable",
|
1223 |
+
os.getenv("ENABLE_GOOGLE_DRIVE_INTEGRATION", "False").lower() == "true",
|
1224 |
+
)
|
1225 |
+
|
1226 |
+
|
1227 |
+
# RAG Content Extraction
|
1228 |
+
CONTENT_EXTRACTION_ENGINE = PersistentConfig(
|
1229 |
+
"CONTENT_EXTRACTION_ENGINE",
|
1230 |
+
"rag.CONTENT_EXTRACTION_ENGINE",
|
1231 |
+
os.environ.get("CONTENT_EXTRACTION_ENGINE", "").lower(),
|
1232 |
+
)
|
1233 |
+
|
1234 |
+
TIKA_SERVER_URL = PersistentConfig(
|
1235 |
+
"TIKA_SERVER_URL",
|
1236 |
+
"rag.tika_server_url",
|
1237 |
+
os.getenv("TIKA_SERVER_URL", "http://tika:9998"), # Default for sidecar deployment
|
1238 |
+
)
|
1239 |
+
|
1240 |
+
RAG_TOP_K = PersistentConfig(
|
1241 |
+
"RAG_TOP_K", "rag.top_k", int(os.environ.get("RAG_TOP_K", "3"))
|
1242 |
+
)
|
1243 |
+
RAG_RELEVANCE_THRESHOLD = PersistentConfig(
|
1244 |
+
"RAG_RELEVANCE_THRESHOLD",
|
1245 |
+
"rag.relevance_threshold",
|
1246 |
+
float(os.environ.get("RAG_RELEVANCE_THRESHOLD", "0.0")),
|
1247 |
+
)
|
1248 |
+
|
1249 |
+
ENABLE_RAG_HYBRID_SEARCH = PersistentConfig(
|
1250 |
+
"ENABLE_RAG_HYBRID_SEARCH",
|
1251 |
+
"rag.enable_hybrid_search",
|
1252 |
+
os.environ.get("ENABLE_RAG_HYBRID_SEARCH", "").lower() == "true",
|
1253 |
+
)
|
1254 |
+
|
1255 |
+
RAG_FILE_MAX_COUNT = PersistentConfig(
|
1256 |
+
"RAG_FILE_MAX_COUNT",
|
1257 |
+
"rag.file.max_count",
|
1258 |
+
(
|
1259 |
+
int(os.environ.get("RAG_FILE_MAX_COUNT"))
|
1260 |
+
if os.environ.get("RAG_FILE_MAX_COUNT")
|
1261 |
+
else None
|
1262 |
+
),
|
1263 |
+
)
|
1264 |
+
|
1265 |
+
RAG_FILE_MAX_SIZE = PersistentConfig(
|
1266 |
+
"RAG_FILE_MAX_SIZE",
|
1267 |
+
"rag.file.max_size",
|
1268 |
+
(
|
1269 |
+
int(os.environ.get("RAG_FILE_MAX_SIZE"))
|
1270 |
+
if os.environ.get("RAG_FILE_MAX_SIZE")
|
1271 |
+
else None
|
1272 |
+
),
|
1273 |
+
)
|
1274 |
+
|
1275 |
+
ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION = PersistentConfig(
|
1276 |
+
"ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION",
|
1277 |
+
"rag.enable_web_loader_ssl_verification",
|
1278 |
+
os.environ.get("ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION", "True").lower() == "true",
|
1279 |
+
)
|
1280 |
+
|
1281 |
+
RAG_EMBEDDING_ENGINE = PersistentConfig(
|
1282 |
+
"RAG_EMBEDDING_ENGINE",
|
1283 |
+
"rag.embedding_engine",
|
1284 |
+
os.environ.get("RAG_EMBEDDING_ENGINE", ""),
|
1285 |
+
)
|
1286 |
+
|
1287 |
+
PDF_EXTRACT_IMAGES = PersistentConfig(
|
1288 |
+
"PDF_EXTRACT_IMAGES",
|
1289 |
+
"rag.pdf_extract_images",
|
1290 |
+
os.environ.get("PDF_EXTRACT_IMAGES", "False").lower() == "true",
|
1291 |
+
)
|
1292 |
+
|
1293 |
+
RAG_EMBEDDING_MODEL = PersistentConfig(
|
1294 |
+
"RAG_EMBEDDING_MODEL",
|
1295 |
+
"rag.embedding_model",
|
1296 |
+
os.environ.get("RAG_EMBEDDING_MODEL", "sentence-transformers/all-MiniLM-L6-v2"),
|
1297 |
+
)
|
1298 |
+
log.info(f"Embedding model set: {RAG_EMBEDDING_MODEL.value}")
|
1299 |
+
|
1300 |
+
RAG_EMBEDDING_MODEL_AUTO_UPDATE = (
|
1301 |
+
not OFFLINE_MODE
|
1302 |
+
and os.environ.get("RAG_EMBEDDING_MODEL_AUTO_UPDATE", "True").lower() == "true"
|
1303 |
+
)
|
1304 |
+
|
1305 |
+
RAG_EMBEDDING_MODEL_TRUST_REMOTE_CODE = (
|
1306 |
+
os.environ.get("RAG_EMBEDDING_MODEL_TRUST_REMOTE_CODE", "True").lower() == "true"
|
1307 |
+
)
|
1308 |
+
|
1309 |
+
RAG_EMBEDDING_BATCH_SIZE = PersistentConfig(
|
1310 |
+
"RAG_EMBEDDING_BATCH_SIZE",
|
1311 |
+
"rag.embedding_batch_size",
|
1312 |
+
int(
|
1313 |
+
os.environ.get("RAG_EMBEDDING_BATCH_SIZE")
|
1314 |
+
or os.environ.get("RAG_EMBEDDING_OPENAI_BATCH_SIZE", "1")
|
1315 |
+
),
|
1316 |
+
)
|
1317 |
+
|
1318 |
+
RAG_RERANKING_MODEL = PersistentConfig(
|
1319 |
+
"RAG_RERANKING_MODEL",
|
1320 |
+
"rag.reranking_model",
|
1321 |
+
os.environ.get("RAG_RERANKING_MODEL", ""),
|
1322 |
+
)
|
1323 |
+
if RAG_RERANKING_MODEL.value != "":
|
1324 |
+
log.info(f"Reranking model set: {RAG_RERANKING_MODEL.value}")
|
1325 |
+
|
1326 |
+
RAG_RERANKING_MODEL_AUTO_UPDATE = (
|
1327 |
+
not OFFLINE_MODE
|
1328 |
+
and os.environ.get("RAG_RERANKING_MODEL_AUTO_UPDATE", "True").lower() == "true"
|
1329 |
+
)
|
1330 |
+
|
1331 |
+
RAG_RERANKING_MODEL_TRUST_REMOTE_CODE = (
|
1332 |
+
os.environ.get("RAG_RERANKING_MODEL_TRUST_REMOTE_CODE", "True").lower() == "true"
|
1333 |
+
)
|
1334 |
+
|
1335 |
+
|
1336 |
+
RAG_TEXT_SPLITTER = PersistentConfig(
|
1337 |
+
"RAG_TEXT_SPLITTER",
|
1338 |
+
"rag.text_splitter",
|
1339 |
+
os.environ.get("RAG_TEXT_SPLITTER", ""),
|
1340 |
+
)
|
1341 |
+
|
1342 |
+
|
1343 |
+
TIKTOKEN_CACHE_DIR = os.environ.get("TIKTOKEN_CACHE_DIR", f"{CACHE_DIR}/tiktoken")
|
1344 |
+
TIKTOKEN_ENCODING_NAME = PersistentConfig(
|
1345 |
+
"TIKTOKEN_ENCODING_NAME",
|
1346 |
+
"rag.tiktoken_encoding_name",
|
1347 |
+
os.environ.get("TIKTOKEN_ENCODING_NAME", "cl100k_base"),
|
1348 |
+
)
|
1349 |
+
|
1350 |
+
|
1351 |
+
CHUNK_SIZE = PersistentConfig(
|
1352 |
+
"CHUNK_SIZE", "rag.chunk_size", int(os.environ.get("CHUNK_SIZE", "1000"))
|
1353 |
+
)
|
1354 |
+
CHUNK_OVERLAP = PersistentConfig(
|
1355 |
+
"CHUNK_OVERLAP",
|
1356 |
+
"rag.chunk_overlap",
|
1357 |
+
int(os.environ.get("CHUNK_OVERLAP", "100")),
|
1358 |
+
)
|
1359 |
+
|
1360 |
+
DEFAULT_RAG_TEMPLATE = """### Task:
|
1361 |
+
Respond to the user query using the provided context, incorporating inline citations in the format [source_id] **only when the <source_id> tag is explicitly provided** in the context.
|
1362 |
+
|
1363 |
+
### Guidelines:
|
1364 |
+
- If you don't know the answer, clearly state that.
|
1365 |
+
- If uncertain, ask the user for clarification.
|
1366 |
+
- Respond in the same language as the user's query.
|
1367 |
+
- If the context is unreadable or of poor quality, inform the user and provide the best possible answer.
|
1368 |
+
- If the answer isn't present in the context but you possess the knowledge, explain this to the user and provide the answer using your own understanding.
|
1369 |
+
- **Only include inline citations using [source_id] when a <source_id> tag is explicitly provided in the context.**
|
1370 |
+
- Do not cite if the <source_id> tag is not provided in the context.
|
1371 |
+
- Do not use XML tags in your response.
|
1372 |
+
- Ensure citations are concise and directly related to the information provided.
|
1373 |
+
|
1374 |
+
### Example of Citation:
|
1375 |
+
If the user asks about a specific topic and the information is found in "whitepaper.pdf" with a provided <source_id>, the response should include the citation like so:
|
1376 |
+
* "According to the study, the proposed method increases efficiency by 20% [whitepaper.pdf]."
|
1377 |
+
If no <source_id> is present, the response should omit the citation.
|
1378 |
+
|
1379 |
+
### Output:
|
1380 |
+
Provide a clear and direct response to the user's query, including inline citations in the format [source_id] only when the <source_id> tag is present in the context.
|
1381 |
+
|
1382 |
+
<context>
|
1383 |
+
{{CONTEXT}}
|
1384 |
+
</context>
|
1385 |
+
|
1386 |
+
<user_query>
|
1387 |
+
{{QUERY}}
|
1388 |
+
</user_query>
|
1389 |
+
"""
|
1390 |
+
|
1391 |
+
RAG_TEMPLATE = PersistentConfig(
|
1392 |
+
"RAG_TEMPLATE",
|
1393 |
+
"rag.template",
|
1394 |
+
os.environ.get("RAG_TEMPLATE", DEFAULT_RAG_TEMPLATE),
|
1395 |
+
)
|
1396 |
+
|
1397 |
+
RAG_OPENAI_API_BASE_URL = PersistentConfig(
|
1398 |
+
"RAG_OPENAI_API_BASE_URL",
|
1399 |
+
"rag.openai_api_base_url",
|
1400 |
+
os.getenv("RAG_OPENAI_API_BASE_URL", OPENAI_API_BASE_URL),
|
1401 |
+
)
|
1402 |
+
RAG_OPENAI_API_KEY = PersistentConfig(
|
1403 |
+
"RAG_OPENAI_API_KEY",
|
1404 |
+
"rag.openai_api_key",
|
1405 |
+
os.getenv("RAG_OPENAI_API_KEY", OPENAI_API_KEY),
|
1406 |
+
)
|
1407 |
+
|
1408 |
+
RAG_OLLAMA_BASE_URL = PersistentConfig(
|
1409 |
+
"RAG_OLLAMA_BASE_URL",
|
1410 |
+
"rag.ollama.url",
|
1411 |
+
os.getenv("RAG_OLLAMA_BASE_URL", OLLAMA_BASE_URL),
|
1412 |
+
)
|
1413 |
+
|
1414 |
+
RAG_OLLAMA_API_KEY = PersistentConfig(
|
1415 |
+
"RAG_OLLAMA_API_KEY",
|
1416 |
+
"rag.ollama.key",
|
1417 |
+
os.getenv("RAG_OLLAMA_API_KEY", ""),
|
1418 |
+
)
|
1419 |
+
|
1420 |
+
|
1421 |
+
ENABLE_RAG_LOCAL_WEB_FETCH = (
|
1422 |
+
os.getenv("ENABLE_RAG_LOCAL_WEB_FETCH", "False").lower() == "true"
|
1423 |
+
)
|
1424 |
+
|
1425 |
+
YOUTUBE_LOADER_LANGUAGE = PersistentConfig(
|
1426 |
+
"YOUTUBE_LOADER_LANGUAGE",
|
1427 |
+
"rag.youtube_loader_language",
|
1428 |
+
os.getenv("YOUTUBE_LOADER_LANGUAGE", "en").split(","),
|
1429 |
+
)
|
1430 |
+
|
1431 |
+
YOUTUBE_LOADER_PROXY_URL = PersistentConfig(
|
1432 |
+
"YOUTUBE_LOADER_PROXY_URL",
|
1433 |
+
"rag.youtube_loader_proxy_url",
|
1434 |
+
os.getenv("YOUTUBE_LOADER_PROXY_URL", ""),
|
1435 |
+
)
|
1436 |
+
|
1437 |
+
|
1438 |
+
ENABLE_RAG_WEB_SEARCH = PersistentConfig(
|
1439 |
+
"ENABLE_RAG_WEB_SEARCH",
|
1440 |
+
"rag.web.search.enable",
|
1441 |
+
os.getenv("ENABLE_RAG_WEB_SEARCH", "False").lower() == "true",
|
1442 |
+
)
|
1443 |
+
|
1444 |
+
RAG_WEB_SEARCH_ENGINE = PersistentConfig(
|
1445 |
+
"RAG_WEB_SEARCH_ENGINE",
|
1446 |
+
"rag.web.search.engine",
|
1447 |
+
os.getenv("RAG_WEB_SEARCH_ENGINE", ""),
|
1448 |
+
)
|
1449 |
+
|
1450 |
+
# You can provide a list of your own websites to filter after performing a web search.
|
1451 |
+
# This ensures the highest level of safety and reliability of the information sources.
|
1452 |
+
RAG_WEB_SEARCH_DOMAIN_FILTER_LIST = PersistentConfig(
|
1453 |
+
"RAG_WEB_SEARCH_DOMAIN_FILTER_LIST",
|
1454 |
+
"rag.rag.web.search.domain.filter_list",
|
1455 |
+
[
|
1456 |
+
# "wikipedia.com",
|
1457 |
+
# "wikimedia.org",
|
1458 |
+
# "wikidata.org",
|
1459 |
+
],
|
1460 |
+
)
|
1461 |
+
|
1462 |
+
|
1463 |
+
SEARXNG_QUERY_URL = PersistentConfig(
|
1464 |
+
"SEARXNG_QUERY_URL",
|
1465 |
+
"rag.web.search.searxng_query_url",
|
1466 |
+
os.getenv("SEARXNG_QUERY_URL", ""),
|
1467 |
+
)
|
1468 |
+
|
1469 |
+
GOOGLE_PSE_API_KEY = PersistentConfig(
|
1470 |
+
"GOOGLE_PSE_API_KEY",
|
1471 |
+
"rag.web.search.google_pse_api_key",
|
1472 |
+
os.getenv("GOOGLE_PSE_API_KEY", ""),
|
1473 |
+
)
|
1474 |
+
|
1475 |
+
GOOGLE_PSE_ENGINE_ID = PersistentConfig(
|
1476 |
+
"GOOGLE_PSE_ENGINE_ID",
|
1477 |
+
"rag.web.search.google_pse_engine_id",
|
1478 |
+
os.getenv("GOOGLE_PSE_ENGINE_ID", ""),
|
1479 |
+
)
|
1480 |
+
|
1481 |
+
BRAVE_SEARCH_API_KEY = PersistentConfig(
|
1482 |
+
"BRAVE_SEARCH_API_KEY",
|
1483 |
+
"rag.web.search.brave_search_api_key",
|
1484 |
+
os.getenv("BRAVE_SEARCH_API_KEY", ""),
|
1485 |
+
)
|
1486 |
+
|
1487 |
+
KAGI_SEARCH_API_KEY = PersistentConfig(
|
1488 |
+
"KAGI_SEARCH_API_KEY",
|
1489 |
+
"rag.web.search.kagi_search_api_key",
|
1490 |
+
os.getenv("KAGI_SEARCH_API_KEY", ""),
|
1491 |
+
)
|
1492 |
+
|
1493 |
+
MOJEEK_SEARCH_API_KEY = PersistentConfig(
|
1494 |
+
"MOJEEK_SEARCH_API_KEY",
|
1495 |
+
"rag.web.search.mojeek_search_api_key",
|
1496 |
+
os.getenv("MOJEEK_SEARCH_API_KEY", ""),
|
1497 |
+
)
|
1498 |
+
|
1499 |
+
SERPSTACK_API_KEY = PersistentConfig(
|
1500 |
+
"SERPSTACK_API_KEY",
|
1501 |
+
"rag.web.search.serpstack_api_key",
|
1502 |
+
os.getenv("SERPSTACK_API_KEY", ""),
|
1503 |
+
)
|
1504 |
+
|
1505 |
+
SERPSTACK_HTTPS = PersistentConfig(
|
1506 |
+
"SERPSTACK_HTTPS",
|
1507 |
+
"rag.web.search.serpstack_https",
|
1508 |
+
os.getenv("SERPSTACK_HTTPS", "True").lower() == "true",
|
1509 |
+
)
|
1510 |
+
|
1511 |
+
SERPER_API_KEY = PersistentConfig(
|
1512 |
+
"SERPER_API_KEY",
|
1513 |
+
"rag.web.search.serper_api_key",
|
1514 |
+
os.getenv("SERPER_API_KEY", ""),
|
1515 |
+
)
|
1516 |
+
|
1517 |
+
SERPLY_API_KEY = PersistentConfig(
|
1518 |
+
"SERPLY_API_KEY",
|
1519 |
+
"rag.web.search.serply_api_key",
|
1520 |
+
os.getenv("SERPLY_API_KEY", ""),
|
1521 |
+
)
|
1522 |
+
|
1523 |
+
TAVILY_API_KEY = PersistentConfig(
|
1524 |
+
"TAVILY_API_KEY",
|
1525 |
+
"rag.web.search.tavily_api_key",
|
1526 |
+
os.getenv("TAVILY_API_KEY", ""),
|
1527 |
+
)
|
1528 |
+
|
1529 |
+
JINA_API_KEY = PersistentConfig(
|
1530 |
+
"JINA_API_KEY",
|
1531 |
+
"rag.web.search.jina_api_key",
|
1532 |
+
os.getenv("JINA_API_KEY", ""),
|
1533 |
+
)
|
1534 |
+
|
1535 |
+
SEARCHAPI_API_KEY = PersistentConfig(
|
1536 |
+
"SEARCHAPI_API_KEY",
|
1537 |
+
"rag.web.search.searchapi_api_key",
|
1538 |
+
os.getenv("SEARCHAPI_API_KEY", ""),
|
1539 |
+
)
|
1540 |
+
|
1541 |
+
SEARCHAPI_ENGINE = PersistentConfig(
|
1542 |
+
"SEARCHAPI_ENGINE",
|
1543 |
+
"rag.web.search.searchapi_engine",
|
1544 |
+
os.getenv("SEARCHAPI_ENGINE", ""),
|
1545 |
+
)
|
1546 |
+
|
1547 |
+
BING_SEARCH_V7_ENDPOINT = PersistentConfig(
|
1548 |
+
"BING_SEARCH_V7_ENDPOINT",
|
1549 |
+
"rag.web.search.bing_search_v7_endpoint",
|
1550 |
+
os.environ.get(
|
1551 |
+
"BING_SEARCH_V7_ENDPOINT", "https://api.bing.microsoft.com/v7.0/search"
|
1552 |
+
),
|
1553 |
+
)
|
1554 |
+
|
1555 |
+
BING_SEARCH_V7_SUBSCRIPTION_KEY = PersistentConfig(
|
1556 |
+
"BING_SEARCH_V7_SUBSCRIPTION_KEY",
|
1557 |
+
"rag.web.search.bing_search_v7_subscription_key",
|
1558 |
+
os.environ.get("BING_SEARCH_V7_SUBSCRIPTION_KEY", ""),
|
1559 |
+
)
|
1560 |
+
|
1561 |
+
|
1562 |
+
RAG_WEB_SEARCH_RESULT_COUNT = PersistentConfig(
|
1563 |
+
"RAG_WEB_SEARCH_RESULT_COUNT",
|
1564 |
+
"rag.web.search.result_count",
|
1565 |
+
int(os.getenv("RAG_WEB_SEARCH_RESULT_COUNT", "3")),
|
1566 |
+
)
|
1567 |
+
|
1568 |
+
RAG_WEB_SEARCH_CONCURRENT_REQUESTS = PersistentConfig(
|
1569 |
+
"RAG_WEB_SEARCH_CONCURRENT_REQUESTS",
|
1570 |
+
"rag.web.search.concurrent_requests",
|
1571 |
+
int(os.getenv("RAG_WEB_SEARCH_CONCURRENT_REQUESTS", "10")),
|
1572 |
+
)
|
1573 |
+
|
1574 |
+
|
1575 |
+
####################################
|
1576 |
+
# Images
|
1577 |
+
####################################
|
1578 |
+
|
1579 |
+
IMAGE_GENERATION_ENGINE = PersistentConfig(
|
1580 |
+
"IMAGE_GENERATION_ENGINE",
|
1581 |
+
"image_generation.engine",
|
1582 |
+
os.getenv("IMAGE_GENERATION_ENGINE", "openai"),
|
1583 |
+
)
|
1584 |
+
|
1585 |
+
ENABLE_IMAGE_GENERATION = PersistentConfig(
|
1586 |
+
"ENABLE_IMAGE_GENERATION",
|
1587 |
+
"image_generation.enable",
|
1588 |
+
os.environ.get("ENABLE_IMAGE_GENERATION", "").lower() == "true",
|
1589 |
+
)
|
1590 |
+
AUTOMATIC1111_BASE_URL = PersistentConfig(
|
1591 |
+
"AUTOMATIC1111_BASE_URL",
|
1592 |
+
"image_generation.automatic1111.base_url",
|
1593 |
+
os.getenv("AUTOMATIC1111_BASE_URL", ""),
|
1594 |
+
)
|
1595 |
+
AUTOMATIC1111_API_AUTH = PersistentConfig(
|
1596 |
+
"AUTOMATIC1111_API_AUTH",
|
1597 |
+
"image_generation.automatic1111.api_auth",
|
1598 |
+
os.getenv("AUTOMATIC1111_API_AUTH", ""),
|
1599 |
+
)
|
1600 |
+
|
1601 |
+
AUTOMATIC1111_CFG_SCALE = PersistentConfig(
|
1602 |
+
"AUTOMATIC1111_CFG_SCALE",
|
1603 |
+
"image_generation.automatic1111.cfg_scale",
|
1604 |
+
(
|
1605 |
+
float(os.environ.get("AUTOMATIC1111_CFG_SCALE"))
|
1606 |
+
if os.environ.get("AUTOMATIC1111_CFG_SCALE")
|
1607 |
+
else None
|
1608 |
+
),
|
1609 |
+
)
|
1610 |
+
|
1611 |
+
|
1612 |
+
AUTOMATIC1111_SAMPLER = PersistentConfig(
|
1613 |
+
"AUTOMATIC1111_SAMPLER",
|
1614 |
+
"image_generation.automatic1111.sampler",
|
1615 |
+
(
|
1616 |
+
os.environ.get("AUTOMATIC1111_SAMPLER")
|
1617 |
+
if os.environ.get("AUTOMATIC1111_SAMPLER")
|
1618 |
+
else None
|
1619 |
+
),
|
1620 |
+
)
|
1621 |
+
|
1622 |
+
AUTOMATIC1111_SCHEDULER = PersistentConfig(
|
1623 |
+
"AUTOMATIC1111_SCHEDULER",
|
1624 |
+
"image_generation.automatic1111.scheduler",
|
1625 |
+
(
|
1626 |
+
os.environ.get("AUTOMATIC1111_SCHEDULER")
|
1627 |
+
if os.environ.get("AUTOMATIC1111_SCHEDULER")
|
1628 |
+
else None
|
1629 |
+
),
|
1630 |
+
)
|
1631 |
+
|
1632 |
+
COMFYUI_BASE_URL = PersistentConfig(
|
1633 |
+
"COMFYUI_BASE_URL",
|
1634 |
+
"image_generation.comfyui.base_url",
|
1635 |
+
os.getenv("COMFYUI_BASE_URL", ""),
|
1636 |
+
)
|
1637 |
+
|
1638 |
+
COMFYUI_API_KEY = PersistentConfig(
|
1639 |
+
"COMFYUI_API_KEY",
|
1640 |
+
"image_generation.comfyui.api_key",
|
1641 |
+
os.getenv("COMFYUI_API_KEY", ""),
|
1642 |
+
)
|
1643 |
+
|
1644 |
+
COMFYUI_DEFAULT_WORKFLOW = """
|
1645 |
+
{
|
1646 |
+
"3": {
|
1647 |
+
"inputs": {
|
1648 |
+
"seed": 0,
|
1649 |
+
"steps": 20,
|
1650 |
+
"cfg": 8,
|
1651 |
+
"sampler_name": "euler",
|
1652 |
+
"scheduler": "normal",
|
1653 |
+
"denoise": 1,
|
1654 |
+
"model": [
|
1655 |
+
"4",
|
1656 |
+
0
|
1657 |
+
],
|
1658 |
+
"positive": [
|
1659 |
+
"6",
|
1660 |
+
0
|
1661 |
+
],
|
1662 |
+
"negative": [
|
1663 |
+
"7",
|
1664 |
+
0
|
1665 |
+
],
|
1666 |
+
"latent_image": [
|
1667 |
+
"5",
|
1668 |
+
0
|
1669 |
+
]
|
1670 |
+
},
|
1671 |
+
"class_type": "KSampler",
|
1672 |
+
"_meta": {
|
1673 |
+
"title": "KSampler"
|
1674 |
+
}
|
1675 |
+
},
|
1676 |
+
"4": {
|
1677 |
+
"inputs": {
|
1678 |
+
"ckpt_name": "model.safetensors"
|
1679 |
+
},
|
1680 |
+
"class_type": "CheckpointLoaderSimple",
|
1681 |
+
"_meta": {
|
1682 |
+
"title": "Load Checkpoint"
|
1683 |
+
}
|
1684 |
+
},
|
1685 |
+
"5": {
|
1686 |
+
"inputs": {
|
1687 |
+
"width": 512,
|
1688 |
+
"height": 512,
|
1689 |
+
"batch_size": 1
|
1690 |
+
},
|
1691 |
+
"class_type": "EmptyLatentImage",
|
1692 |
+
"_meta": {
|
1693 |
+
"title": "Empty Latent Image"
|
1694 |
+
}
|
1695 |
+
},
|
1696 |
+
"6": {
|
1697 |
+
"inputs": {
|
1698 |
+
"text": "Prompt",
|
1699 |
+
"clip": [
|
1700 |
+
"4",
|
1701 |
+
1
|
1702 |
+
]
|
1703 |
+
},
|
1704 |
+
"class_type": "CLIPTextEncode",
|
1705 |
+
"_meta": {
|
1706 |
+
"title": "CLIP Text Encode (Prompt)"
|
1707 |
+
}
|
1708 |
+
},
|
1709 |
+
"7": {
|
1710 |
+
"inputs": {
|
1711 |
+
"text": "",
|
1712 |
+
"clip": [
|
1713 |
+
"4",
|
1714 |
+
1
|
1715 |
+
]
|
1716 |
+
},
|
1717 |
+
"class_type": "CLIPTextEncode",
|
1718 |
+
"_meta": {
|
1719 |
+
"title": "CLIP Text Encode (Prompt)"
|
1720 |
+
}
|
1721 |
+
},
|
1722 |
+
"8": {
|
1723 |
+
"inputs": {
|
1724 |
+
"samples": [
|
1725 |
+
"3",
|
1726 |
+
0
|
1727 |
+
],
|
1728 |
+
"vae": [
|
1729 |
+
"4",
|
1730 |
+
2
|
1731 |
+
]
|
1732 |
+
},
|
1733 |
+
"class_type": "VAEDecode",
|
1734 |
+
"_meta": {
|
1735 |
+
"title": "VAE Decode"
|
1736 |
+
}
|
1737 |
+
},
|
1738 |
+
"9": {
|
1739 |
+
"inputs": {
|
1740 |
+
"filename_prefix": "ComfyUI",
|
1741 |
+
"images": [
|
1742 |
+
"8",
|
1743 |
+
0
|
1744 |
+
]
|
1745 |
+
},
|
1746 |
+
"class_type": "SaveImage",
|
1747 |
+
"_meta": {
|
1748 |
+
"title": "Save Image"
|
1749 |
+
}
|
1750 |
+
}
|
1751 |
+
}
|
1752 |
+
"""
|
1753 |
+
|
1754 |
+
|
1755 |
+
COMFYUI_WORKFLOW = PersistentConfig(
|
1756 |
+
"COMFYUI_WORKFLOW",
|
1757 |
+
"image_generation.comfyui.workflow",
|
1758 |
+
os.getenv("COMFYUI_WORKFLOW", COMFYUI_DEFAULT_WORKFLOW),
|
1759 |
+
)
|
1760 |
+
|
1761 |
+
COMFYUI_WORKFLOW_NODES = PersistentConfig(
|
1762 |
+
"COMFYUI_WORKFLOW",
|
1763 |
+
"image_generation.comfyui.nodes",
|
1764 |
+
[],
|
1765 |
+
)
|
1766 |
+
|
1767 |
+
IMAGES_OPENAI_API_BASE_URL = PersistentConfig(
|
1768 |
+
"IMAGES_OPENAI_API_BASE_URL",
|
1769 |
+
"image_generation.openai.api_base_url",
|
1770 |
+
os.getenv("IMAGES_OPENAI_API_BASE_URL", OPENAI_API_BASE_URL),
|
1771 |
+
)
|
1772 |
+
IMAGES_OPENAI_API_KEY = PersistentConfig(
|
1773 |
+
"IMAGES_OPENAI_API_KEY",
|
1774 |
+
"image_generation.openai.api_key",
|
1775 |
+
os.getenv("IMAGES_OPENAI_API_KEY", OPENAI_API_KEY),
|
1776 |
+
)
|
1777 |
+
|
1778 |
+
IMAGE_SIZE = PersistentConfig(
|
1779 |
+
"IMAGE_SIZE", "image_generation.size", os.getenv("IMAGE_SIZE", "512x512")
|
1780 |
+
)
|
1781 |
+
|
1782 |
+
IMAGE_STEPS = PersistentConfig(
|
1783 |
+
"IMAGE_STEPS", "image_generation.steps", int(os.getenv("IMAGE_STEPS", 50))
|
1784 |
+
)
|
1785 |
+
|
1786 |
+
IMAGE_GENERATION_MODEL = PersistentConfig(
|
1787 |
+
"IMAGE_GENERATION_MODEL",
|
1788 |
+
"image_generation.model",
|
1789 |
+
os.getenv("IMAGE_GENERATION_MODEL", ""),
|
1790 |
+
)
|
1791 |
+
|
1792 |
+
####################################
|
1793 |
+
# Audio
|
1794 |
+
####################################
|
1795 |
+
|
1796 |
+
# Transcription
|
1797 |
+
WHISPER_MODEL = PersistentConfig(
|
1798 |
+
"WHISPER_MODEL",
|
1799 |
+
"audio.stt.whisper_model",
|
1800 |
+
os.getenv("WHISPER_MODEL", "base"),
|
1801 |
+
)
|
1802 |
+
|
1803 |
+
WHISPER_MODEL_DIR = os.getenv("WHISPER_MODEL_DIR", f"{CACHE_DIR}/whisper/models")
|
1804 |
+
WHISPER_MODEL_AUTO_UPDATE = (
|
1805 |
+
not OFFLINE_MODE
|
1806 |
+
and os.environ.get("WHISPER_MODEL_AUTO_UPDATE", "").lower() == "true"
|
1807 |
+
)
|
1808 |
+
|
1809 |
+
|
1810 |
+
AUDIO_STT_OPENAI_API_BASE_URL = PersistentConfig(
|
1811 |
+
"AUDIO_STT_OPENAI_API_BASE_URL",
|
1812 |
+
"audio.stt.openai.api_base_url",
|
1813 |
+
os.getenv("AUDIO_STT_OPENAI_API_BASE_URL", OPENAI_API_BASE_URL),
|
1814 |
+
)
|
1815 |
+
|
1816 |
+
AUDIO_STT_OPENAI_API_KEY = PersistentConfig(
|
1817 |
+
"AUDIO_STT_OPENAI_API_KEY",
|
1818 |
+
"audio.stt.openai.api_key",
|
1819 |
+
os.getenv("AUDIO_STT_OPENAI_API_KEY", OPENAI_API_KEY),
|
1820 |
+
)
|
1821 |
+
|
1822 |
+
AUDIO_STT_ENGINE = PersistentConfig(
|
1823 |
+
"AUDIO_STT_ENGINE",
|
1824 |
+
"audio.stt.engine",
|
1825 |
+
os.getenv("AUDIO_STT_ENGINE", ""),
|
1826 |
+
)
|
1827 |
+
|
1828 |
+
AUDIO_STT_MODEL = PersistentConfig(
|
1829 |
+
"AUDIO_STT_MODEL",
|
1830 |
+
"audio.stt.model",
|
1831 |
+
os.getenv("AUDIO_STT_MODEL", ""),
|
1832 |
+
)
|
1833 |
+
|
1834 |
+
AUDIO_TTS_OPENAI_API_BASE_URL = PersistentConfig(
|
1835 |
+
"AUDIO_TTS_OPENAI_API_BASE_URL",
|
1836 |
+
"audio.tts.openai.api_base_url",
|
1837 |
+
os.getenv("AUDIO_TTS_OPENAI_API_BASE_URL", OPENAI_API_BASE_URL),
|
1838 |
+
)
|
1839 |
+
AUDIO_TTS_OPENAI_API_KEY = PersistentConfig(
|
1840 |
+
"AUDIO_TTS_OPENAI_API_KEY",
|
1841 |
+
"audio.tts.openai.api_key",
|
1842 |
+
os.getenv("AUDIO_TTS_OPENAI_API_KEY", OPENAI_API_KEY),
|
1843 |
+
)
|
1844 |
+
|
1845 |
+
AUDIO_TTS_API_KEY = PersistentConfig(
|
1846 |
+
"AUDIO_TTS_API_KEY",
|
1847 |
+
"audio.tts.api_key",
|
1848 |
+
os.getenv("AUDIO_TTS_API_KEY", ""),
|
1849 |
+
)
|
1850 |
+
|
1851 |
+
AUDIO_TTS_ENGINE = PersistentConfig(
|
1852 |
+
"AUDIO_TTS_ENGINE",
|
1853 |
+
"audio.tts.engine",
|
1854 |
+
os.getenv("AUDIO_TTS_ENGINE", ""),
|
1855 |
+
)
|
1856 |
+
|
1857 |
+
|
1858 |
+
AUDIO_TTS_MODEL = PersistentConfig(
|
1859 |
+
"AUDIO_TTS_MODEL",
|
1860 |
+
"audio.tts.model",
|
1861 |
+
os.getenv("AUDIO_TTS_MODEL", "tts-1"), # OpenAI default model
|
1862 |
+
)
|
1863 |
+
|
1864 |
+
AUDIO_TTS_VOICE = PersistentConfig(
|
1865 |
+
"AUDIO_TTS_VOICE",
|
1866 |
+
"audio.tts.voice",
|
1867 |
+
os.getenv("AUDIO_TTS_VOICE", "alloy"), # OpenAI default voice
|
1868 |
+
)
|
1869 |
+
|
1870 |
+
AUDIO_TTS_SPLIT_ON = PersistentConfig(
|
1871 |
+
"AUDIO_TTS_SPLIT_ON",
|
1872 |
+
"audio.tts.split_on",
|
1873 |
+
os.getenv("AUDIO_TTS_SPLIT_ON", "punctuation"),
|
1874 |
+
)
|
1875 |
+
|
1876 |
+
AUDIO_TTS_AZURE_SPEECH_REGION = PersistentConfig(
|
1877 |
+
"AUDIO_TTS_AZURE_SPEECH_REGION",
|
1878 |
+
"audio.tts.azure.speech_region",
|
1879 |
+
os.getenv("AUDIO_TTS_AZURE_SPEECH_REGION", "eastus"),
|
1880 |
+
)
|
1881 |
+
|
1882 |
+
AUDIO_TTS_AZURE_SPEECH_OUTPUT_FORMAT = PersistentConfig(
|
1883 |
+
"AUDIO_TTS_AZURE_SPEECH_OUTPUT_FORMAT",
|
1884 |
+
"audio.tts.azure.speech_output_format",
|
1885 |
+
os.getenv(
|
1886 |
+
"AUDIO_TTS_AZURE_SPEECH_OUTPUT_FORMAT", "audio-24khz-160kbitrate-mono-mp3"
|
1887 |
+
),
|
1888 |
+
)
|
1889 |
+
|
1890 |
+
|
1891 |
+
####################################
|
1892 |
+
# LDAP
|
1893 |
+
####################################
|
1894 |
+
|
1895 |
+
ENABLE_LDAP = PersistentConfig(
|
1896 |
+
"ENABLE_LDAP",
|
1897 |
+
"ldap.enable",
|
1898 |
+
os.environ.get("ENABLE_LDAP", "false").lower() == "true",
|
1899 |
+
)
|
1900 |
+
|
1901 |
+
LDAP_SERVER_LABEL = PersistentConfig(
|
1902 |
+
"LDAP_SERVER_LABEL",
|
1903 |
+
"ldap.server.label",
|
1904 |
+
os.environ.get("LDAP_SERVER_LABEL", "LDAP Server"),
|
1905 |
+
)
|
1906 |
+
|
1907 |
+
LDAP_SERVER_HOST = PersistentConfig(
|
1908 |
+
"LDAP_SERVER_HOST",
|
1909 |
+
"ldap.server.host",
|
1910 |
+
os.environ.get("LDAP_SERVER_HOST", "localhost"),
|
1911 |
+
)
|
1912 |
+
|
1913 |
+
LDAP_SERVER_PORT = PersistentConfig(
|
1914 |
+
"LDAP_SERVER_PORT",
|
1915 |
+
"ldap.server.port",
|
1916 |
+
int(os.environ.get("LDAP_SERVER_PORT", "389")),
|
1917 |
+
)
|
1918 |
+
|
1919 |
+
LDAP_ATTRIBUTE_FOR_USERNAME = PersistentConfig(
|
1920 |
+
"LDAP_ATTRIBUTE_FOR_USERNAME",
|
1921 |
+
"ldap.server.attribute_for_username",
|
1922 |
+
os.environ.get("LDAP_ATTRIBUTE_FOR_USERNAME", "uid"),
|
1923 |
+
)
|
1924 |
+
|
1925 |
+
LDAP_APP_DN = PersistentConfig(
|
1926 |
+
"LDAP_APP_DN", "ldap.server.app_dn", os.environ.get("LDAP_APP_DN", "")
|
1927 |
+
)
|
1928 |
+
|
1929 |
+
LDAP_APP_PASSWORD = PersistentConfig(
|
1930 |
+
"LDAP_APP_PASSWORD",
|
1931 |
+
"ldap.server.app_password",
|
1932 |
+
os.environ.get("LDAP_APP_PASSWORD", ""),
|
1933 |
+
)
|
1934 |
+
|
1935 |
+
LDAP_SEARCH_BASE = PersistentConfig(
|
1936 |
+
"LDAP_SEARCH_BASE", "ldap.server.users_dn", os.environ.get("LDAP_SEARCH_BASE", "")
|
1937 |
+
)
|
1938 |
+
|
1939 |
+
LDAP_SEARCH_FILTERS = PersistentConfig(
|
1940 |
+
"LDAP_SEARCH_FILTER",
|
1941 |
+
"ldap.server.search_filter",
|
1942 |
+
os.environ.get("LDAP_SEARCH_FILTER", ""),
|
1943 |
+
)
|
1944 |
+
|
1945 |
+
LDAP_USE_TLS = PersistentConfig(
|
1946 |
+
"LDAP_USE_TLS",
|
1947 |
+
"ldap.server.use_tls",
|
1948 |
+
os.environ.get("LDAP_USE_TLS", "True").lower() == "true",
|
1949 |
+
)
|
1950 |
+
|
1951 |
+
LDAP_CA_CERT_FILE = PersistentConfig(
|
1952 |
+
"LDAP_CA_CERT_FILE",
|
1953 |
+
"ldap.server.ca_cert_file",
|
1954 |
+
os.environ.get("LDAP_CA_CERT_FILE", ""),
|
1955 |
+
)
|
1956 |
+
|
1957 |
+
LDAP_CIPHERS = PersistentConfig(
|
1958 |
+
"LDAP_CIPHERS", "ldap.server.ciphers", os.environ.get("LDAP_CIPHERS", "ALL")
|
1959 |
+
)
|
backend/open_webui/constants.py
ADDED
@@ -0,0 +1,118 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from enum import Enum
|
2 |
+
|
3 |
+
|
4 |
+
class MESSAGES(str, Enum):
|
5 |
+
DEFAULT = lambda msg="": f"{msg if msg else ''}"
|
6 |
+
MODEL_ADDED = lambda model="": f"The model '{model}' has been added successfully."
|
7 |
+
MODEL_DELETED = (
|
8 |
+
lambda model="": f"The model '{model}' has been deleted successfully."
|
9 |
+
)
|
10 |
+
|
11 |
+
|
12 |
+
class WEBHOOK_MESSAGES(str, Enum):
|
13 |
+
DEFAULT = lambda msg="": f"{msg if msg else ''}"
|
14 |
+
USER_SIGNUP = lambda username="": (
|
15 |
+
f"New user signed up: {username}" if username else "New user signed up"
|
16 |
+
)
|
17 |
+
|
18 |
+
|
19 |
+
class ERROR_MESSAGES(str, Enum):
|
20 |
+
def __str__(self) -> str:
|
21 |
+
return super().__str__()
|
22 |
+
|
23 |
+
DEFAULT = (
|
24 |
+
lambda err="": f'{"Something went wrong :/" if err == "" else "[ERROR: " + str(err) + "]"}'
|
25 |
+
)
|
26 |
+
ENV_VAR_NOT_FOUND = "Required environment variable not found. Terminating now."
|
27 |
+
CREATE_USER_ERROR = "Oops! Something went wrong while creating your account. Please try again later. If the issue persists, contact support for assistance."
|
28 |
+
DELETE_USER_ERROR = "Oops! Something went wrong. We encountered an issue while trying to delete the user. Please give it another shot."
|
29 |
+
EMAIL_MISMATCH = "Uh-oh! This email does not match the email your provider is registered with. Please check your email and try again."
|
30 |
+
EMAIL_TAKEN = "Uh-oh! This email is already registered. Sign in with your existing account or choose another email to start anew."
|
31 |
+
USERNAME_TAKEN = (
|
32 |
+
"Uh-oh! This username is already registered. Please choose another username."
|
33 |
+
)
|
34 |
+
COMMAND_TAKEN = "Uh-oh! This command is already registered. Please choose another command string."
|
35 |
+
FILE_EXISTS = "Uh-oh! This file is already registered. Please choose another file."
|
36 |
+
|
37 |
+
ID_TAKEN = "Uh-oh! This id is already registered. Please choose another id string."
|
38 |
+
MODEL_ID_TAKEN = "Uh-oh! This model id is already registered. Please choose another model id string."
|
39 |
+
NAME_TAG_TAKEN = "Uh-oh! This name tag is already registered. Please choose another name tag string."
|
40 |
+
|
41 |
+
INVALID_TOKEN = (
|
42 |
+
"Your session has expired or the token is invalid. Please sign in again."
|
43 |
+
)
|
44 |
+
INVALID_CRED = "The email or password provided is incorrect. Please check for typos and try logging in again."
|
45 |
+
INVALID_EMAIL_FORMAT = "The email format you entered is invalid. Please double-check and make sure you're using a valid email address (e.g., yourname@example.com)."
|
46 |
+
INVALID_PASSWORD = (
|
47 |
+
"The password provided is incorrect. Please check for typos and try again."
|
48 |
+
)
|
49 |
+
INVALID_TRUSTED_HEADER = "Your provider has not provided a trusted header. Please contact your administrator for assistance."
|
50 |
+
|
51 |
+
EXISTING_USERS = "You can't turn off authentication because there are existing users. If you want to disable WEBUI_AUTH, make sure your web interface doesn't have any existing users and is a fresh installation."
|
52 |
+
|
53 |
+
UNAUTHORIZED = "401 Unauthorized"
|
54 |
+
ACCESS_PROHIBITED = "You do not have permission to access this resource. Please contact your administrator for assistance."
|
55 |
+
ACTION_PROHIBITED = (
|
56 |
+
"The requested action has been restricted as a security measure."
|
57 |
+
)
|
58 |
+
|
59 |
+
FILE_NOT_SENT = "FILE_NOT_SENT"
|
60 |
+
FILE_NOT_SUPPORTED = "Oops! It seems like the file format you're trying to upload is not supported. Please upload a file with a supported format (e.g., JPG, PNG, PDF, TXT) and try again."
|
61 |
+
|
62 |
+
NOT_FOUND = "We could not find what you're looking for :/"
|
63 |
+
USER_NOT_FOUND = "We could not find what you're looking for :/"
|
64 |
+
API_KEY_NOT_FOUND = "Oops! It looks like there's a hiccup. The API key is missing. Please make sure to provide a valid API key to access this feature."
|
65 |
+
API_KEY_NOT_ALLOWED = "Use of API key is not enabled in the environment."
|
66 |
+
|
67 |
+
MALICIOUS = "Unusual activities detected, please try again in a few minutes."
|
68 |
+
|
69 |
+
PANDOC_NOT_INSTALLED = "Pandoc is not installed on the server. Please contact your administrator for assistance."
|
70 |
+
INCORRECT_FORMAT = (
|
71 |
+
lambda err="": f"Invalid format. Please use the correct format{err}"
|
72 |
+
)
|
73 |
+
RATE_LIMIT_EXCEEDED = "API rate limit exceeded"
|
74 |
+
|
75 |
+
MODEL_NOT_FOUND = lambda name="": f"Model '{name}' was not found"
|
76 |
+
OPENAI_NOT_FOUND = lambda name="": "OpenAI API was not found"
|
77 |
+
OLLAMA_NOT_FOUND = "WebUI could not connect to Ollama"
|
78 |
+
CREATE_API_KEY_ERROR = "Oops! Something went wrong while creating your API key. Please try again later. If the issue persists, contact support for assistance."
|
79 |
+
API_KEY_CREATION_NOT_ALLOWED = "API key creation is not allowed in the environment."
|
80 |
+
|
81 |
+
EMPTY_CONTENT = "The content provided is empty. Please ensure that there is text or data present before proceeding."
|
82 |
+
|
83 |
+
DB_NOT_SQLITE = "This feature is only available when running with SQLite databases."
|
84 |
+
|
85 |
+
INVALID_URL = (
|
86 |
+
"Oops! The URL you provided is invalid. Please double-check and try again."
|
87 |
+
)
|
88 |
+
|
89 |
+
WEB_SEARCH_ERROR = (
|
90 |
+
lambda err="": f"{err if err else 'Oops! Something went wrong while searching the web.'}"
|
91 |
+
)
|
92 |
+
|
93 |
+
OLLAMA_API_DISABLED = (
|
94 |
+
"The Ollama API is disabled. Please enable it to use this feature."
|
95 |
+
)
|
96 |
+
|
97 |
+
FILE_TOO_LARGE = (
|
98 |
+
lambda size="": f"Oops! The file you're trying to upload is too large. Please upload a file that is less than {size}."
|
99 |
+
)
|
100 |
+
|
101 |
+
DUPLICATE_CONTENT = (
|
102 |
+
"Duplicate content detected. Please provide unique content to proceed."
|
103 |
+
)
|
104 |
+
FILE_NOT_PROCESSED = "Extracted content is not available for this file. Please ensure that the file is processed before proceeding."
|
105 |
+
|
106 |
+
|
107 |
+
class TASKS(str, Enum):
|
108 |
+
def __str__(self) -> str:
|
109 |
+
return super().__str__()
|
110 |
+
|
111 |
+
DEFAULT = lambda task="": f"{task if task else 'generation'}"
|
112 |
+
TITLE_GENERATION = "title_generation"
|
113 |
+
TAGS_GENERATION = "tags_generation"
|
114 |
+
EMOJI_GENERATION = "emoji_generation"
|
115 |
+
QUERY_GENERATION = "query_generation"
|
116 |
+
AUTOCOMPLETE_GENERATION = "autocomplete_generation"
|
117 |
+
FUNCTION_CALLING = "function_calling"
|
118 |
+
MOA_RESPONSE_GENERATION = "moa_response_generation"
|
backend/open_webui/env.py
ADDED
@@ -0,0 +1,394 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import importlib.metadata
|
2 |
+
import json
|
3 |
+
import logging
|
4 |
+
import os
|
5 |
+
import pkgutil
|
6 |
+
import sys
|
7 |
+
import shutil
|
8 |
+
from pathlib import Path
|
9 |
+
|
10 |
+
import markdown
|
11 |
+
from bs4 import BeautifulSoup
|
12 |
+
from open_webui.constants import ERROR_MESSAGES
|
13 |
+
|
14 |
+
####################################
|
15 |
+
# Load .env file
|
16 |
+
####################################
|
17 |
+
|
18 |
+
OPEN_WEBUI_DIR = Path(__file__).parent # the path containing this file
|
19 |
+
print(OPEN_WEBUI_DIR)
|
20 |
+
|
21 |
+
BACKEND_DIR = OPEN_WEBUI_DIR.parent # the path containing this file
|
22 |
+
BASE_DIR = BACKEND_DIR.parent # the path containing the backend/
|
23 |
+
|
24 |
+
print(BACKEND_DIR)
|
25 |
+
print(BASE_DIR)
|
26 |
+
|
27 |
+
try:
|
28 |
+
from dotenv import find_dotenv, load_dotenv
|
29 |
+
|
30 |
+
load_dotenv(find_dotenv(str(BASE_DIR / ".env")))
|
31 |
+
except ImportError:
|
32 |
+
print("dotenv not installed, skipping...")
|
33 |
+
|
34 |
+
DOCKER = os.environ.get("DOCKER", "False").lower() == "true"
|
35 |
+
|
36 |
+
# device type embedding models - "cpu" (default), "cuda" (nvidia gpu required) or "mps" (apple silicon) - choosing this right can lead to better performance
|
37 |
+
USE_CUDA = os.environ.get("USE_CUDA_DOCKER", "false")
|
38 |
+
|
39 |
+
if USE_CUDA.lower() == "true":
|
40 |
+
try:
|
41 |
+
import torch
|
42 |
+
|
43 |
+
assert torch.cuda.is_available(), "CUDA not available"
|
44 |
+
DEVICE_TYPE = "cuda"
|
45 |
+
except Exception as e:
|
46 |
+
cuda_error = (
|
47 |
+
"Error when testing CUDA but USE_CUDA_DOCKER is true. "
|
48 |
+
f"Resetting USE_CUDA_DOCKER to false: {e}"
|
49 |
+
)
|
50 |
+
os.environ["USE_CUDA_DOCKER"] = "false"
|
51 |
+
USE_CUDA = "false"
|
52 |
+
DEVICE_TYPE = "cpu"
|
53 |
+
else:
|
54 |
+
DEVICE_TYPE = "cpu"
|
55 |
+
|
56 |
+
|
57 |
+
####################################
|
58 |
+
# LOGGING
|
59 |
+
####################################
|
60 |
+
|
61 |
+
log_levels = ["CRITICAL", "ERROR", "WARNING", "INFO", "DEBUG"]
|
62 |
+
|
63 |
+
GLOBAL_LOG_LEVEL = os.environ.get("GLOBAL_LOG_LEVEL", "").upper()
|
64 |
+
if GLOBAL_LOG_LEVEL in log_levels:
|
65 |
+
logging.basicConfig(stream=sys.stdout, level=GLOBAL_LOG_LEVEL, force=True)
|
66 |
+
else:
|
67 |
+
GLOBAL_LOG_LEVEL = "INFO"
|
68 |
+
|
69 |
+
log = logging.getLogger(__name__)
|
70 |
+
log.info(f"GLOBAL_LOG_LEVEL: {GLOBAL_LOG_LEVEL}")
|
71 |
+
|
72 |
+
if "cuda_error" in locals():
|
73 |
+
log.exception(cuda_error)
|
74 |
+
|
75 |
+
log_sources = [
|
76 |
+
"AUDIO",
|
77 |
+
"COMFYUI",
|
78 |
+
"CONFIG",
|
79 |
+
"DB",
|
80 |
+
"IMAGES",
|
81 |
+
"MAIN",
|
82 |
+
"MODELS",
|
83 |
+
"OLLAMA",
|
84 |
+
"OPENAI",
|
85 |
+
"RAG",
|
86 |
+
"WEBHOOK",
|
87 |
+
"SOCKET",
|
88 |
+
]
|
89 |
+
|
90 |
+
SRC_LOG_LEVELS = {}
|
91 |
+
|
92 |
+
for source in log_sources:
|
93 |
+
log_env_var = source + "_LOG_LEVEL"
|
94 |
+
SRC_LOG_LEVELS[source] = os.environ.get(log_env_var, "").upper()
|
95 |
+
if SRC_LOG_LEVELS[source] not in log_levels:
|
96 |
+
SRC_LOG_LEVELS[source] = GLOBAL_LOG_LEVEL
|
97 |
+
log.info(f"{log_env_var}: {SRC_LOG_LEVELS[source]}")
|
98 |
+
|
99 |
+
log.setLevel(SRC_LOG_LEVELS["CONFIG"])
|
100 |
+
|
101 |
+
|
102 |
+
WEBUI_NAME = os.environ.get("WEBUI_NAME", "Open WebUI")
|
103 |
+
if WEBUI_NAME != "Open WebUI":
|
104 |
+
WEBUI_NAME += " (Open WebUI)"
|
105 |
+
|
106 |
+
WEBUI_FAVICON_URL = "https://openwebui.com/favicon.png"
|
107 |
+
|
108 |
+
|
109 |
+
####################################
|
110 |
+
# ENV (dev,test,prod)
|
111 |
+
####################################
|
112 |
+
|
113 |
+
ENV = os.environ.get("ENV", "dev")
|
114 |
+
|
115 |
+
FROM_INIT_PY = os.environ.get("FROM_INIT_PY", "False").lower() == "true"
|
116 |
+
|
117 |
+
if FROM_INIT_PY:
|
118 |
+
PACKAGE_DATA = {"version": importlib.metadata.version("open-webui")}
|
119 |
+
else:
|
120 |
+
try:
|
121 |
+
PACKAGE_DATA = json.loads((BASE_DIR / "package.json").read_text())
|
122 |
+
except Exception:
|
123 |
+
PACKAGE_DATA = {"version": "0.0.0"}
|
124 |
+
|
125 |
+
|
126 |
+
VERSION = PACKAGE_DATA["version"]
|
127 |
+
|
128 |
+
|
129 |
+
# Function to parse each section
|
130 |
+
def parse_section(section):
|
131 |
+
items = []
|
132 |
+
for li in section.find_all("li"):
|
133 |
+
# Extract raw HTML string
|
134 |
+
raw_html = str(li)
|
135 |
+
|
136 |
+
# Extract text without HTML tags
|
137 |
+
text = li.get_text(separator=" ", strip=True)
|
138 |
+
|
139 |
+
# Split into title and content
|
140 |
+
parts = text.split(": ", 1)
|
141 |
+
title = parts[0].strip() if len(parts) > 1 else ""
|
142 |
+
content = parts[1].strip() if len(parts) > 1 else text
|
143 |
+
|
144 |
+
items.append({"title": title, "content": content, "raw": raw_html})
|
145 |
+
return items
|
146 |
+
|
147 |
+
|
148 |
+
try:
|
149 |
+
changelog_path = BASE_DIR / "CHANGELOG.md"
|
150 |
+
with open(str(changelog_path.absolute()), "r", encoding="utf8") as file:
|
151 |
+
changelog_content = file.read()
|
152 |
+
|
153 |
+
except Exception:
|
154 |
+
changelog_content = (pkgutil.get_data("open_webui", "CHANGELOG.md") or b"").decode()
|
155 |
+
|
156 |
+
|
157 |
+
# Convert markdown content to HTML
|
158 |
+
html_content = markdown.markdown(changelog_content)
|
159 |
+
|
160 |
+
# Parse the HTML content
|
161 |
+
soup = BeautifulSoup(html_content, "html.parser")
|
162 |
+
|
163 |
+
# Initialize JSON structure
|
164 |
+
changelog_json = {}
|
165 |
+
|
166 |
+
# Iterate over each version
|
167 |
+
for version in soup.find_all("h2"):
|
168 |
+
version_number = version.get_text().strip().split(" - ")[0][1:-1] # Remove brackets
|
169 |
+
date = version.get_text().strip().split(" - ")[1]
|
170 |
+
|
171 |
+
version_data = {"date": date}
|
172 |
+
|
173 |
+
# Find the next sibling that is a h3 tag (section title)
|
174 |
+
current = version.find_next_sibling()
|
175 |
+
|
176 |
+
while current and current.name != "h2":
|
177 |
+
if current.name == "h3":
|
178 |
+
section_title = current.get_text().lower() # e.g., "added", "fixed"
|
179 |
+
section_items = parse_section(current.find_next_sibling("ul"))
|
180 |
+
version_data[section_title] = section_items
|
181 |
+
|
182 |
+
# Move to the next element
|
183 |
+
current = current.find_next_sibling()
|
184 |
+
|
185 |
+
changelog_json[version_number] = version_data
|
186 |
+
|
187 |
+
|
188 |
+
CHANGELOG = changelog_json
|
189 |
+
|
190 |
+
####################################
|
191 |
+
# SAFE_MODE
|
192 |
+
####################################
|
193 |
+
|
194 |
+
SAFE_MODE = os.environ.get("SAFE_MODE", "false").lower() == "true"
|
195 |
+
|
196 |
+
####################################
|
197 |
+
# ENABLE_FORWARD_USER_INFO_HEADERS
|
198 |
+
####################################
|
199 |
+
|
200 |
+
ENABLE_FORWARD_USER_INFO_HEADERS = (
|
201 |
+
os.environ.get("ENABLE_FORWARD_USER_INFO_HEADERS", "False").lower() == "true"
|
202 |
+
)
|
203 |
+
|
204 |
+
|
205 |
+
####################################
|
206 |
+
# WEBUI_BUILD_HASH
|
207 |
+
####################################
|
208 |
+
|
209 |
+
WEBUI_BUILD_HASH = os.environ.get("WEBUI_BUILD_HASH", "dev-build")
|
210 |
+
|
211 |
+
####################################
|
212 |
+
# DATA/FRONTEND BUILD DIR
|
213 |
+
####################################
|
214 |
+
|
215 |
+
DATA_DIR = Path(os.getenv("DATA_DIR", BACKEND_DIR / "data")).resolve()
|
216 |
+
|
217 |
+
if FROM_INIT_PY:
|
218 |
+
NEW_DATA_DIR = Path(os.getenv("DATA_DIR", OPEN_WEBUI_DIR / "data")).resolve()
|
219 |
+
NEW_DATA_DIR.mkdir(parents=True, exist_ok=True)
|
220 |
+
|
221 |
+
# Check if the data directory exists in the package directory
|
222 |
+
if DATA_DIR.exists() and DATA_DIR != NEW_DATA_DIR:
|
223 |
+
log.info(f"Moving {DATA_DIR} to {NEW_DATA_DIR}")
|
224 |
+
for item in DATA_DIR.iterdir():
|
225 |
+
dest = NEW_DATA_DIR / item.name
|
226 |
+
if item.is_dir():
|
227 |
+
shutil.copytree(item, dest, dirs_exist_ok=True)
|
228 |
+
else:
|
229 |
+
shutil.copy2(item, dest)
|
230 |
+
|
231 |
+
# Zip the data directory
|
232 |
+
shutil.make_archive(DATA_DIR.parent / "open_webui_data", "zip", DATA_DIR)
|
233 |
+
|
234 |
+
# Remove the old data directory
|
235 |
+
shutil.rmtree(DATA_DIR)
|
236 |
+
|
237 |
+
DATA_DIR = Path(os.getenv("DATA_DIR", OPEN_WEBUI_DIR / "data"))
|
238 |
+
|
239 |
+
|
240 |
+
STATIC_DIR = Path(os.getenv("STATIC_DIR", OPEN_WEBUI_DIR / "static"))
|
241 |
+
|
242 |
+
FONTS_DIR = Path(os.getenv("FONTS_DIR", OPEN_WEBUI_DIR / "static" / "fonts"))
|
243 |
+
|
244 |
+
FRONTEND_BUILD_DIR = Path(os.getenv("FRONTEND_BUILD_DIR", BASE_DIR / "build")).resolve()
|
245 |
+
|
246 |
+
if FROM_INIT_PY:
|
247 |
+
FRONTEND_BUILD_DIR = Path(
|
248 |
+
os.getenv("FRONTEND_BUILD_DIR", OPEN_WEBUI_DIR / "frontend")
|
249 |
+
).resolve()
|
250 |
+
|
251 |
+
|
252 |
+
####################################
|
253 |
+
# Database
|
254 |
+
####################################
|
255 |
+
|
256 |
+
# Check if the file exists
|
257 |
+
if os.path.exists(f"{DATA_DIR}/ollama.db"):
|
258 |
+
# Rename the file
|
259 |
+
os.rename(f"{DATA_DIR}/ollama.db", f"{DATA_DIR}/webui.db")
|
260 |
+
log.info("Database migrated from Ollama-WebUI successfully.")
|
261 |
+
else:
|
262 |
+
pass
|
263 |
+
|
264 |
+
DATABASE_URL = os.environ.get("DATABASE_URL", f"sqlite:///{DATA_DIR}/webui.db")
|
265 |
+
|
266 |
+
# Replace the postgres:// with postgresql://
|
267 |
+
if "postgres://" in DATABASE_URL:
|
268 |
+
DATABASE_URL = DATABASE_URL.replace("postgres://", "postgresql://")
|
269 |
+
|
270 |
+
DATABASE_POOL_SIZE = os.environ.get("DATABASE_POOL_SIZE", 0)
|
271 |
+
|
272 |
+
if DATABASE_POOL_SIZE == "":
|
273 |
+
DATABASE_POOL_SIZE = 0
|
274 |
+
else:
|
275 |
+
try:
|
276 |
+
DATABASE_POOL_SIZE = int(DATABASE_POOL_SIZE)
|
277 |
+
except Exception:
|
278 |
+
DATABASE_POOL_SIZE = 0
|
279 |
+
|
280 |
+
DATABASE_POOL_MAX_OVERFLOW = os.environ.get("DATABASE_POOL_MAX_OVERFLOW", 0)
|
281 |
+
|
282 |
+
if DATABASE_POOL_MAX_OVERFLOW == "":
|
283 |
+
DATABASE_POOL_MAX_OVERFLOW = 0
|
284 |
+
else:
|
285 |
+
try:
|
286 |
+
DATABASE_POOL_MAX_OVERFLOW = int(DATABASE_POOL_MAX_OVERFLOW)
|
287 |
+
except Exception:
|
288 |
+
DATABASE_POOL_MAX_OVERFLOW = 0
|
289 |
+
|
290 |
+
DATABASE_POOL_TIMEOUT = os.environ.get("DATABASE_POOL_TIMEOUT", 30)
|
291 |
+
|
292 |
+
if DATABASE_POOL_TIMEOUT == "":
|
293 |
+
DATABASE_POOL_TIMEOUT = 30
|
294 |
+
else:
|
295 |
+
try:
|
296 |
+
DATABASE_POOL_TIMEOUT = int(DATABASE_POOL_TIMEOUT)
|
297 |
+
except Exception:
|
298 |
+
DATABASE_POOL_TIMEOUT = 30
|
299 |
+
|
300 |
+
DATABASE_POOL_RECYCLE = os.environ.get("DATABASE_POOL_RECYCLE", 3600)
|
301 |
+
|
302 |
+
if DATABASE_POOL_RECYCLE == "":
|
303 |
+
DATABASE_POOL_RECYCLE = 3600
|
304 |
+
else:
|
305 |
+
try:
|
306 |
+
DATABASE_POOL_RECYCLE = int(DATABASE_POOL_RECYCLE)
|
307 |
+
except Exception:
|
308 |
+
DATABASE_POOL_RECYCLE = 3600
|
309 |
+
|
310 |
+
RESET_CONFIG_ON_START = (
|
311 |
+
os.environ.get("RESET_CONFIG_ON_START", "False").lower() == "true"
|
312 |
+
)
|
313 |
+
|
314 |
+
####################################
|
315 |
+
# REDIS
|
316 |
+
####################################
|
317 |
+
|
318 |
+
REDIS_URL = os.environ.get("REDIS_URL", "redis://localhost:6379/0")
|
319 |
+
|
320 |
+
####################################
|
321 |
+
# WEBUI_AUTH (Required for security)
|
322 |
+
####################################
|
323 |
+
|
324 |
+
WEBUI_AUTH = os.environ.get("WEBUI_AUTH", "True").lower() == "true"
|
325 |
+
WEBUI_AUTH_TRUSTED_EMAIL_HEADER = os.environ.get(
|
326 |
+
"WEBUI_AUTH_TRUSTED_EMAIL_HEADER", None
|
327 |
+
)
|
328 |
+
WEBUI_AUTH_TRUSTED_NAME_HEADER = os.environ.get("WEBUI_AUTH_TRUSTED_NAME_HEADER", None)
|
329 |
+
|
330 |
+
BYPASS_MODEL_ACCESS_CONTROL = (
|
331 |
+
os.environ.get("BYPASS_MODEL_ACCESS_CONTROL", "False").lower() == "true"
|
332 |
+
)
|
333 |
+
|
334 |
+
####################################
|
335 |
+
# WEBUI_SECRET_KEY
|
336 |
+
####################################
|
337 |
+
|
338 |
+
WEBUI_SECRET_KEY = os.environ.get(
|
339 |
+
"WEBUI_SECRET_KEY",
|
340 |
+
os.environ.get(
|
341 |
+
"WEBUI_JWT_SECRET_KEY", "t0p-s3cr3t"
|
342 |
+
), # DEPRECATED: remove at next major version
|
343 |
+
)
|
344 |
+
|
345 |
+
WEBUI_SESSION_COOKIE_SAME_SITE = os.environ.get(
|
346 |
+
"WEBUI_SESSION_COOKIE_SAME_SITE",
|
347 |
+
os.environ.get("WEBUI_SESSION_COOKIE_SAME_SITE", "lax"),
|
348 |
+
)
|
349 |
+
|
350 |
+
WEBUI_SESSION_COOKIE_SECURE = os.environ.get(
|
351 |
+
"WEBUI_SESSION_COOKIE_SECURE",
|
352 |
+
os.environ.get("WEBUI_SESSION_COOKIE_SECURE", "false").lower() == "true",
|
353 |
+
)
|
354 |
+
|
355 |
+
if WEBUI_AUTH and WEBUI_SECRET_KEY == "":
|
356 |
+
raise ValueError(ERROR_MESSAGES.ENV_VAR_NOT_FOUND)
|
357 |
+
|
358 |
+
ENABLE_WEBSOCKET_SUPPORT = (
|
359 |
+
os.environ.get("ENABLE_WEBSOCKET_SUPPORT", "True").lower() == "true"
|
360 |
+
)
|
361 |
+
|
362 |
+
WEBSOCKET_MANAGER = os.environ.get("WEBSOCKET_MANAGER", "")
|
363 |
+
|
364 |
+
WEBSOCKET_REDIS_URL = os.environ.get("WEBSOCKET_REDIS_URL", REDIS_URL)
|
365 |
+
|
366 |
+
AIOHTTP_CLIENT_TIMEOUT = os.environ.get("AIOHTTP_CLIENT_TIMEOUT", "")
|
367 |
+
|
368 |
+
if AIOHTTP_CLIENT_TIMEOUT == "":
|
369 |
+
AIOHTTP_CLIENT_TIMEOUT = None
|
370 |
+
else:
|
371 |
+
try:
|
372 |
+
AIOHTTP_CLIENT_TIMEOUT = int(AIOHTTP_CLIENT_TIMEOUT)
|
373 |
+
except Exception:
|
374 |
+
AIOHTTP_CLIENT_TIMEOUT = 300
|
375 |
+
|
376 |
+
AIOHTTP_CLIENT_TIMEOUT_OPENAI_MODEL_LIST = os.environ.get(
|
377 |
+
"AIOHTTP_CLIENT_TIMEOUT_OPENAI_MODEL_LIST", ""
|
378 |
+
)
|
379 |
+
|
380 |
+
if AIOHTTP_CLIENT_TIMEOUT_OPENAI_MODEL_LIST == "":
|
381 |
+
AIOHTTP_CLIENT_TIMEOUT_OPENAI_MODEL_LIST = None
|
382 |
+
else:
|
383 |
+
try:
|
384 |
+
AIOHTTP_CLIENT_TIMEOUT_OPENAI_MODEL_LIST = int(
|
385 |
+
AIOHTTP_CLIENT_TIMEOUT_OPENAI_MODEL_LIST
|
386 |
+
)
|
387 |
+
except Exception:
|
388 |
+
AIOHTTP_CLIENT_TIMEOUT_OPENAI_MODEL_LIST = 5
|
389 |
+
|
390 |
+
####################################
|
391 |
+
# OFFLINE_MODE
|
392 |
+
####################################
|
393 |
+
|
394 |
+
OFFLINE_MODE = os.environ.get("OFFLINE_MODE", "false").lower() == "true"
|
backend/open_webui/functions.py
ADDED
@@ -0,0 +1,316 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import logging
|
2 |
+
import sys
|
3 |
+
import inspect
|
4 |
+
import json
|
5 |
+
|
6 |
+
from pydantic import BaseModel
|
7 |
+
from typing import AsyncGenerator, Generator, Iterator
|
8 |
+
from fastapi import (
|
9 |
+
Depends,
|
10 |
+
FastAPI,
|
11 |
+
File,
|
12 |
+
Form,
|
13 |
+
HTTPException,
|
14 |
+
Request,
|
15 |
+
UploadFile,
|
16 |
+
status,
|
17 |
+
)
|
18 |
+
from starlette.responses import Response, StreamingResponse
|
19 |
+
|
20 |
+
|
21 |
+
from open_webui.socket.main import (
|
22 |
+
get_event_call,
|
23 |
+
get_event_emitter,
|
24 |
+
)
|
25 |
+
|
26 |
+
|
27 |
+
from open_webui.models.functions import Functions
|
28 |
+
from open_webui.models.models import Models
|
29 |
+
|
30 |
+
from open_webui.utils.plugin import load_function_module_by_id
|
31 |
+
from open_webui.utils.tools import get_tools
|
32 |
+
from open_webui.utils.access_control import has_access
|
33 |
+
|
34 |
+
from open_webui.env import SRC_LOG_LEVELS, GLOBAL_LOG_LEVEL
|
35 |
+
|
36 |
+
from open_webui.utils.misc import (
|
37 |
+
add_or_update_system_message,
|
38 |
+
get_last_user_message,
|
39 |
+
prepend_to_first_user_message_content,
|
40 |
+
openai_chat_chunk_message_template,
|
41 |
+
openai_chat_completion_message_template,
|
42 |
+
)
|
43 |
+
from open_webui.utils.payload import (
|
44 |
+
apply_model_params_to_body_openai,
|
45 |
+
apply_model_system_prompt_to_body,
|
46 |
+
)
|
47 |
+
|
48 |
+
|
49 |
+
logging.basicConfig(stream=sys.stdout, level=GLOBAL_LOG_LEVEL)
|
50 |
+
log = logging.getLogger(__name__)
|
51 |
+
log.setLevel(SRC_LOG_LEVELS["MAIN"])
|
52 |
+
|
53 |
+
|
54 |
+
def get_function_module_by_id(request: Request, pipe_id: str):
|
55 |
+
# Check if function is already loaded
|
56 |
+
if pipe_id not in request.app.state.FUNCTIONS:
|
57 |
+
function_module, _, _ = load_function_module_by_id(pipe_id)
|
58 |
+
request.app.state.FUNCTIONS[pipe_id] = function_module
|
59 |
+
else:
|
60 |
+
function_module = request.app.state.FUNCTIONS[pipe_id]
|
61 |
+
|
62 |
+
if hasattr(function_module, "valves") and hasattr(function_module, "Valves"):
|
63 |
+
valves = Functions.get_function_valves_by_id(pipe_id)
|
64 |
+
function_module.valves = function_module.Valves(**(valves if valves else {}))
|
65 |
+
return function_module
|
66 |
+
|
67 |
+
|
68 |
+
async def get_function_models(request):
|
69 |
+
pipes = Functions.get_functions_by_type("pipe", active_only=True)
|
70 |
+
pipe_models = []
|
71 |
+
|
72 |
+
for pipe in pipes:
|
73 |
+
function_module = get_function_module_by_id(request, pipe.id)
|
74 |
+
|
75 |
+
# Check if function is a manifold
|
76 |
+
if hasattr(function_module, "pipes"):
|
77 |
+
sub_pipes = []
|
78 |
+
|
79 |
+
# Check if pipes is a function or a list
|
80 |
+
|
81 |
+
try:
|
82 |
+
if callable(function_module.pipes):
|
83 |
+
sub_pipes = function_module.pipes()
|
84 |
+
else:
|
85 |
+
sub_pipes = function_module.pipes
|
86 |
+
except Exception as e:
|
87 |
+
log.exception(e)
|
88 |
+
sub_pipes = []
|
89 |
+
|
90 |
+
log.debug(
|
91 |
+
f"get_function_models: function '{pipe.id}' is a manifold of {sub_pipes}"
|
92 |
+
)
|
93 |
+
|
94 |
+
for p in sub_pipes:
|
95 |
+
sub_pipe_id = f'{pipe.id}.{p["id"]}'
|
96 |
+
sub_pipe_name = p["name"]
|
97 |
+
|
98 |
+
if hasattr(function_module, "name"):
|
99 |
+
sub_pipe_name = f"{function_module.name}{sub_pipe_name}"
|
100 |
+
|
101 |
+
pipe_flag = {"type": pipe.type}
|
102 |
+
|
103 |
+
pipe_models.append(
|
104 |
+
{
|
105 |
+
"id": sub_pipe_id,
|
106 |
+
"name": sub_pipe_name,
|
107 |
+
"object": "model",
|
108 |
+
"created": pipe.created_at,
|
109 |
+
"owned_by": "openai",
|
110 |
+
"pipe": pipe_flag,
|
111 |
+
}
|
112 |
+
)
|
113 |
+
else:
|
114 |
+
pipe_flag = {"type": "pipe"}
|
115 |
+
|
116 |
+
log.debug(
|
117 |
+
f"get_function_models: function '{pipe.id}' is a single pipe {{ 'id': {pipe.id}, 'name': {pipe.name} }}"
|
118 |
+
)
|
119 |
+
|
120 |
+
pipe_models.append(
|
121 |
+
{
|
122 |
+
"id": pipe.id,
|
123 |
+
"name": pipe.name,
|
124 |
+
"object": "model",
|
125 |
+
"created": pipe.created_at,
|
126 |
+
"owned_by": "openai",
|
127 |
+
"pipe": pipe_flag,
|
128 |
+
}
|
129 |
+
)
|
130 |
+
|
131 |
+
return pipe_models
|
132 |
+
|
133 |
+
|
134 |
+
async def generate_function_chat_completion(
|
135 |
+
request, form_data, user, models: dict = {}
|
136 |
+
):
|
137 |
+
async def execute_pipe(pipe, params):
|
138 |
+
if inspect.iscoroutinefunction(pipe):
|
139 |
+
return await pipe(**params)
|
140 |
+
else:
|
141 |
+
return pipe(**params)
|
142 |
+
|
143 |
+
async def get_message_content(res: str | Generator | AsyncGenerator) -> str:
|
144 |
+
if isinstance(res, str):
|
145 |
+
return res
|
146 |
+
if isinstance(res, Generator):
|
147 |
+
return "".join(map(str, res))
|
148 |
+
if isinstance(res, AsyncGenerator):
|
149 |
+
return "".join([str(stream) async for stream in res])
|
150 |
+
|
151 |
+
def process_line(form_data: dict, line):
|
152 |
+
if isinstance(line, BaseModel):
|
153 |
+
line = line.model_dump_json()
|
154 |
+
line = f"data: {line}"
|
155 |
+
if isinstance(line, dict):
|
156 |
+
line = f"data: {json.dumps(line)}"
|
157 |
+
|
158 |
+
try:
|
159 |
+
line = line.decode("utf-8")
|
160 |
+
except Exception:
|
161 |
+
pass
|
162 |
+
|
163 |
+
if line.startswith("data:"):
|
164 |
+
return f"{line}\n\n"
|
165 |
+
else:
|
166 |
+
line = openai_chat_chunk_message_template(form_data["model"], line)
|
167 |
+
return f"data: {json.dumps(line)}\n\n"
|
168 |
+
|
169 |
+
def get_pipe_id(form_data: dict) -> str:
|
170 |
+
pipe_id = form_data["model"]
|
171 |
+
if "." in pipe_id:
|
172 |
+
pipe_id, _ = pipe_id.split(".", 1)
|
173 |
+
return pipe_id
|
174 |
+
|
175 |
+
def get_function_params(function_module, form_data, user, extra_params=None):
|
176 |
+
if extra_params is None:
|
177 |
+
extra_params = {}
|
178 |
+
|
179 |
+
pipe_id = get_pipe_id(form_data)
|
180 |
+
|
181 |
+
# Get the signature of the function
|
182 |
+
sig = inspect.signature(function_module.pipe)
|
183 |
+
params = {"body": form_data} | {
|
184 |
+
k: v for k, v in extra_params.items() if k in sig.parameters
|
185 |
+
}
|
186 |
+
|
187 |
+
if "__user__" in params and hasattr(function_module, "UserValves"):
|
188 |
+
user_valves = Functions.get_user_valves_by_id_and_user_id(pipe_id, user.id)
|
189 |
+
try:
|
190 |
+
params["__user__"]["valves"] = function_module.UserValves(**user_valves)
|
191 |
+
except Exception as e:
|
192 |
+
log.exception(e)
|
193 |
+
params["__user__"]["valves"] = function_module.UserValves()
|
194 |
+
|
195 |
+
return params
|
196 |
+
|
197 |
+
model_id = form_data.get("model")
|
198 |
+
model_info = Models.get_model_by_id(model_id)
|
199 |
+
|
200 |
+
metadata = form_data.pop("metadata", {})
|
201 |
+
|
202 |
+
files = metadata.get("files", [])
|
203 |
+
tool_ids = metadata.get("tool_ids", [])
|
204 |
+
# Check if tool_ids is None
|
205 |
+
if tool_ids is None:
|
206 |
+
tool_ids = []
|
207 |
+
|
208 |
+
__event_emitter__ = None
|
209 |
+
__event_call__ = None
|
210 |
+
__task__ = None
|
211 |
+
__task_body__ = None
|
212 |
+
|
213 |
+
if metadata:
|
214 |
+
if all(k in metadata for k in ("session_id", "chat_id", "message_id")):
|
215 |
+
__event_emitter__ = get_event_emitter(metadata)
|
216 |
+
__event_call__ = get_event_call(metadata)
|
217 |
+
__task__ = metadata.get("task", None)
|
218 |
+
__task_body__ = metadata.get("task_body", None)
|
219 |
+
|
220 |
+
extra_params = {
|
221 |
+
"__event_emitter__": __event_emitter__,
|
222 |
+
"__event_call__": __event_call__,
|
223 |
+
"__task__": __task__,
|
224 |
+
"__task_body__": __task_body__,
|
225 |
+
"__files__": files,
|
226 |
+
"__user__": {
|
227 |
+
"id": user.id,
|
228 |
+
"email": user.email,
|
229 |
+
"name": user.name,
|
230 |
+
"role": user.role,
|
231 |
+
},
|
232 |
+
"__metadata__": metadata,
|
233 |
+
"__request__": request,
|
234 |
+
}
|
235 |
+
extra_params["__tools__"] = get_tools(
|
236 |
+
request,
|
237 |
+
tool_ids,
|
238 |
+
user,
|
239 |
+
{
|
240 |
+
**extra_params,
|
241 |
+
"__model__": models.get(form_data["model"], None),
|
242 |
+
"__messages__": form_data["messages"],
|
243 |
+
"__files__": files,
|
244 |
+
},
|
245 |
+
)
|
246 |
+
|
247 |
+
if model_info:
|
248 |
+
if model_info.base_model_id:
|
249 |
+
form_data["model"] = model_info.base_model_id
|
250 |
+
|
251 |
+
params = model_info.params.model_dump()
|
252 |
+
form_data = apply_model_params_to_body_openai(params, form_data)
|
253 |
+
form_data = apply_model_system_prompt_to_body(params, form_data, user)
|
254 |
+
|
255 |
+
pipe_id = get_pipe_id(form_data)
|
256 |
+
function_module = get_function_module_by_id(request, pipe_id)
|
257 |
+
|
258 |
+
pipe = function_module.pipe
|
259 |
+
params = get_function_params(function_module, form_data, user, extra_params)
|
260 |
+
|
261 |
+
if form_data.get("stream", False):
|
262 |
+
|
263 |
+
async def stream_content():
|
264 |
+
try:
|
265 |
+
res = await execute_pipe(pipe, params)
|
266 |
+
|
267 |
+
# Directly return if the response is a StreamingResponse
|
268 |
+
if isinstance(res, StreamingResponse):
|
269 |
+
async for data in res.body_iterator:
|
270 |
+
yield data
|
271 |
+
return
|
272 |
+
if isinstance(res, dict):
|
273 |
+
yield f"data: {json.dumps(res)}\n\n"
|
274 |
+
return
|
275 |
+
|
276 |
+
except Exception as e:
|
277 |
+
log.error(f"Error: {e}")
|
278 |
+
yield f"data: {json.dumps({'error': {'detail':str(e)}})}\n\n"
|
279 |
+
return
|
280 |
+
|
281 |
+
if isinstance(res, str):
|
282 |
+
message = openai_chat_chunk_message_template(form_data["model"], res)
|
283 |
+
yield f"data: {json.dumps(message)}\n\n"
|
284 |
+
|
285 |
+
if isinstance(res, Iterator):
|
286 |
+
for line in res:
|
287 |
+
yield process_line(form_data, line)
|
288 |
+
|
289 |
+
if isinstance(res, AsyncGenerator):
|
290 |
+
async for line in res:
|
291 |
+
yield process_line(form_data, line)
|
292 |
+
|
293 |
+
if isinstance(res, str) or isinstance(res, Generator):
|
294 |
+
finish_message = openai_chat_chunk_message_template(
|
295 |
+
form_data["model"], ""
|
296 |
+
)
|
297 |
+
finish_message["choices"][0]["finish_reason"] = "stop"
|
298 |
+
yield f"data: {json.dumps(finish_message)}\n\n"
|
299 |
+
yield "data: [DONE]"
|
300 |
+
|
301 |
+
return StreamingResponse(stream_content(), media_type="text/event-stream")
|
302 |
+
else:
|
303 |
+
try:
|
304 |
+
res = await execute_pipe(pipe, params)
|
305 |
+
|
306 |
+
except Exception as e:
|
307 |
+
log.error(f"Error: {e}")
|
308 |
+
return {"error": {"detail": str(e)}}
|
309 |
+
|
310 |
+
if isinstance(res, StreamingResponse) or isinstance(res, dict):
|
311 |
+
return res
|
312 |
+
if isinstance(res, BaseModel):
|
313 |
+
return res.model_dump()
|
314 |
+
|
315 |
+
message = await get_message_content(res)
|
316 |
+
return openai_chat_completion_message_template(form_data["model"], message)
|
backend/open_webui/internal/db.py
ADDED
@@ -0,0 +1,114 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import json
|
2 |
+
import logging
|
3 |
+
from contextlib import contextmanager
|
4 |
+
from typing import Any, Optional
|
5 |
+
|
6 |
+
from open_webui.internal.wrappers import register_connection
|
7 |
+
from open_webui.env import (
|
8 |
+
OPEN_WEBUI_DIR,
|
9 |
+
DATABASE_URL,
|
10 |
+
SRC_LOG_LEVELS,
|
11 |
+
DATABASE_POOL_MAX_OVERFLOW,
|
12 |
+
DATABASE_POOL_RECYCLE,
|
13 |
+
DATABASE_POOL_SIZE,
|
14 |
+
DATABASE_POOL_TIMEOUT,
|
15 |
+
)
|
16 |
+
from peewee_migrate import Router
|
17 |
+
from sqlalchemy import Dialect, create_engine, types
|
18 |
+
from sqlalchemy.ext.declarative import declarative_base
|
19 |
+
from sqlalchemy.orm import scoped_session, sessionmaker
|
20 |
+
from sqlalchemy.pool import QueuePool, NullPool
|
21 |
+
from sqlalchemy.sql.type_api import _T
|
22 |
+
from typing_extensions import Self
|
23 |
+
|
24 |
+
log = logging.getLogger(__name__)
|
25 |
+
log.setLevel(SRC_LOG_LEVELS["DB"])
|
26 |
+
|
27 |
+
|
28 |
+
class JSONField(types.TypeDecorator):
|
29 |
+
impl = types.Text
|
30 |
+
cache_ok = True
|
31 |
+
|
32 |
+
def process_bind_param(self, value: Optional[_T], dialect: Dialect) -> Any:
|
33 |
+
return json.dumps(value)
|
34 |
+
|
35 |
+
def process_result_value(self, value: Optional[_T], dialect: Dialect) -> Any:
|
36 |
+
if value is not None:
|
37 |
+
return json.loads(value)
|
38 |
+
|
39 |
+
def copy(self, **kw: Any) -> Self:
|
40 |
+
return JSONField(self.impl.length)
|
41 |
+
|
42 |
+
def db_value(self, value):
|
43 |
+
return json.dumps(value)
|
44 |
+
|
45 |
+
def python_value(self, value):
|
46 |
+
if value is not None:
|
47 |
+
return json.loads(value)
|
48 |
+
|
49 |
+
|
50 |
+
# Workaround to handle the peewee migration
|
51 |
+
# This is required to ensure the peewee migration is handled before the alembic migration
|
52 |
+
def handle_peewee_migration(DATABASE_URL):
|
53 |
+
# db = None
|
54 |
+
try:
|
55 |
+
# Replace the postgresql:// with postgres:// to handle the peewee migration
|
56 |
+
db = register_connection(DATABASE_URL.replace("postgresql://", "postgres://"))
|
57 |
+
migrate_dir = OPEN_WEBUI_DIR / "internal" / "migrations"
|
58 |
+
router = Router(db, logger=log, migrate_dir=migrate_dir)
|
59 |
+
router.run()
|
60 |
+
db.close()
|
61 |
+
|
62 |
+
except Exception as e:
|
63 |
+
log.error(f"Failed to initialize the database connection: {e}")
|
64 |
+
raise
|
65 |
+
finally:
|
66 |
+
# Properly closing the database connection
|
67 |
+
if db and not db.is_closed():
|
68 |
+
db.close()
|
69 |
+
|
70 |
+
# Assert if db connection has been closed
|
71 |
+
assert db.is_closed(), "Database connection is still open."
|
72 |
+
|
73 |
+
|
74 |
+
handle_peewee_migration(DATABASE_URL)
|
75 |
+
|
76 |
+
|
77 |
+
SQLALCHEMY_DATABASE_URL = DATABASE_URL
|
78 |
+
if "sqlite" in SQLALCHEMY_DATABASE_URL:
|
79 |
+
engine = create_engine(
|
80 |
+
SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False}
|
81 |
+
)
|
82 |
+
else:
|
83 |
+
if DATABASE_POOL_SIZE > 0:
|
84 |
+
engine = create_engine(
|
85 |
+
SQLALCHEMY_DATABASE_URL,
|
86 |
+
pool_size=DATABASE_POOL_SIZE,
|
87 |
+
max_overflow=DATABASE_POOL_MAX_OVERFLOW,
|
88 |
+
pool_timeout=DATABASE_POOL_TIMEOUT,
|
89 |
+
pool_recycle=DATABASE_POOL_RECYCLE,
|
90 |
+
pool_pre_ping=True,
|
91 |
+
poolclass=QueuePool,
|
92 |
+
)
|
93 |
+
else:
|
94 |
+
engine = create_engine(
|
95 |
+
SQLALCHEMY_DATABASE_URL, pool_pre_ping=True, poolclass=NullPool
|
96 |
+
)
|
97 |
+
|
98 |
+
|
99 |
+
SessionLocal = sessionmaker(
|
100 |
+
autocommit=False, autoflush=False, bind=engine, expire_on_commit=False
|
101 |
+
)
|
102 |
+
Base = declarative_base()
|
103 |
+
Session = scoped_session(SessionLocal)
|
104 |
+
|
105 |
+
|
106 |
+
def get_session():
|
107 |
+
db = SessionLocal()
|
108 |
+
try:
|
109 |
+
yield db
|
110 |
+
finally:
|
111 |
+
db.close()
|
112 |
+
|
113 |
+
|
114 |
+
get_db = contextmanager(get_session)
|
backend/open_webui/internal/migrations/001_initial_schema.py
ADDED
@@ -0,0 +1,254 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"""Peewee migrations -- 001_initial_schema.py.
|
2 |
+
|
3 |
+
Some examples (model - class or model name)::
|
4 |
+
|
5 |
+
> Model = migrator.orm['table_name'] # Return model in current state by name
|
6 |
+
> Model = migrator.ModelClass # Return model in current state by name
|
7 |
+
|
8 |
+
> migrator.sql(sql) # Run custom SQL
|
9 |
+
> migrator.run(func, *args, **kwargs) # Run python function with the given args
|
10 |
+
> migrator.create_model(Model) # Create a model (could be used as decorator)
|
11 |
+
> migrator.remove_model(model, cascade=True) # Remove a model
|
12 |
+
> migrator.add_fields(model, **fields) # Add fields to a model
|
13 |
+
> migrator.change_fields(model, **fields) # Change fields
|
14 |
+
> migrator.remove_fields(model, *field_names, cascade=True)
|
15 |
+
> migrator.rename_field(model, old_field_name, new_field_name)
|
16 |
+
> migrator.rename_table(model, new_table_name)
|
17 |
+
> migrator.add_index(model, *col_names, unique=False)
|
18 |
+
> migrator.add_not_null(model, *field_names)
|
19 |
+
> migrator.add_default(model, field_name, default)
|
20 |
+
> migrator.add_constraint(model, name, sql)
|
21 |
+
> migrator.drop_index(model, *col_names)
|
22 |
+
> migrator.drop_not_null(model, *field_names)
|
23 |
+
> migrator.drop_constraints(model, *constraints)
|
24 |
+
|
25 |
+
"""
|
26 |
+
|
27 |
+
from contextlib import suppress
|
28 |
+
|
29 |
+
import peewee as pw
|
30 |
+
from peewee_migrate import Migrator
|
31 |
+
|
32 |
+
|
33 |
+
with suppress(ImportError):
|
34 |
+
import playhouse.postgres_ext as pw_pext
|
35 |
+
|
36 |
+
|
37 |
+
def migrate(migrator: Migrator, database: pw.Database, *, fake=False):
|
38 |
+
"""Write your migrations here."""
|
39 |
+
|
40 |
+
# We perform different migrations for SQLite and other databases
|
41 |
+
# This is because SQLite is very loose with enforcing its schema, and trying to migrate other databases like SQLite
|
42 |
+
# will require per-database SQL queries.
|
43 |
+
# Instead, we assume that because external DB support was added at a later date, it is safe to assume a newer base
|
44 |
+
# schema instead of trying to migrate from an older schema.
|
45 |
+
if isinstance(database, pw.SqliteDatabase):
|
46 |
+
migrate_sqlite(migrator, database, fake=fake)
|
47 |
+
else:
|
48 |
+
migrate_external(migrator, database, fake=fake)
|
49 |
+
|
50 |
+
|
51 |
+
def migrate_sqlite(migrator: Migrator, database: pw.Database, *, fake=False):
|
52 |
+
@migrator.create_model
|
53 |
+
class Auth(pw.Model):
|
54 |
+
id = pw.CharField(max_length=255, unique=True)
|
55 |
+
email = pw.CharField(max_length=255)
|
56 |
+
password = pw.CharField(max_length=255)
|
57 |
+
active = pw.BooleanField()
|
58 |
+
|
59 |
+
class Meta:
|
60 |
+
table_name = "auth"
|
61 |
+
|
62 |
+
@migrator.create_model
|
63 |
+
class Chat(pw.Model):
|
64 |
+
id = pw.CharField(max_length=255, unique=True)
|
65 |
+
user_id = pw.CharField(max_length=255)
|
66 |
+
title = pw.CharField()
|
67 |
+
chat = pw.TextField()
|
68 |
+
timestamp = pw.BigIntegerField()
|
69 |
+
|
70 |
+
class Meta:
|
71 |
+
table_name = "chat"
|
72 |
+
|
73 |
+
@migrator.create_model
|
74 |
+
class ChatIdTag(pw.Model):
|
75 |
+
id = pw.CharField(max_length=255, unique=True)
|
76 |
+
tag_name = pw.CharField(max_length=255)
|
77 |
+
chat_id = pw.CharField(max_length=255)
|
78 |
+
user_id = pw.CharField(max_length=255)
|
79 |
+
timestamp = pw.BigIntegerField()
|
80 |
+
|
81 |
+
class Meta:
|
82 |
+
table_name = "chatidtag"
|
83 |
+
|
84 |
+
@migrator.create_model
|
85 |
+
class Document(pw.Model):
|
86 |
+
id = pw.AutoField()
|
87 |
+
collection_name = pw.CharField(max_length=255, unique=True)
|
88 |
+
name = pw.CharField(max_length=255, unique=True)
|
89 |
+
title = pw.CharField()
|
90 |
+
filename = pw.CharField()
|
91 |
+
content = pw.TextField(null=True)
|
92 |
+
user_id = pw.CharField(max_length=255)
|
93 |
+
timestamp = pw.BigIntegerField()
|
94 |
+
|
95 |
+
class Meta:
|
96 |
+
table_name = "document"
|
97 |
+
|
98 |
+
@migrator.create_model
|
99 |
+
class Modelfile(pw.Model):
|
100 |
+
id = pw.AutoField()
|
101 |
+
tag_name = pw.CharField(max_length=255, unique=True)
|
102 |
+
user_id = pw.CharField(max_length=255)
|
103 |
+
modelfile = pw.TextField()
|
104 |
+
timestamp = pw.BigIntegerField()
|
105 |
+
|
106 |
+
class Meta:
|
107 |
+
table_name = "modelfile"
|
108 |
+
|
109 |
+
@migrator.create_model
|
110 |
+
class Prompt(pw.Model):
|
111 |
+
id = pw.AutoField()
|
112 |
+
command = pw.CharField(max_length=255, unique=True)
|
113 |
+
user_id = pw.CharField(max_length=255)
|
114 |
+
title = pw.CharField()
|
115 |
+
content = pw.TextField()
|
116 |
+
timestamp = pw.BigIntegerField()
|
117 |
+
|
118 |
+
class Meta:
|
119 |
+
table_name = "prompt"
|
120 |
+
|
121 |
+
@migrator.create_model
|
122 |
+
class Tag(pw.Model):
|
123 |
+
id = pw.CharField(max_length=255, unique=True)
|
124 |
+
name = pw.CharField(max_length=255)
|
125 |
+
user_id = pw.CharField(max_length=255)
|
126 |
+
data = pw.TextField(null=True)
|
127 |
+
|
128 |
+
class Meta:
|
129 |
+
table_name = "tag"
|
130 |
+
|
131 |
+
@migrator.create_model
|
132 |
+
class User(pw.Model):
|
133 |
+
id = pw.CharField(max_length=255, unique=True)
|
134 |
+
name = pw.CharField(max_length=255)
|
135 |
+
email = pw.CharField(max_length=255)
|
136 |
+
role = pw.CharField(max_length=255)
|
137 |
+
profile_image_url = pw.CharField(max_length=255)
|
138 |
+
timestamp = pw.BigIntegerField()
|
139 |
+
|
140 |
+
class Meta:
|
141 |
+
table_name = "user"
|
142 |
+
|
143 |
+
|
144 |
+
def migrate_external(migrator: Migrator, database: pw.Database, *, fake=False):
|
145 |
+
@migrator.create_model
|
146 |
+
class Auth(pw.Model):
|
147 |
+
id = pw.CharField(max_length=255, unique=True)
|
148 |
+
email = pw.CharField(max_length=255)
|
149 |
+
password = pw.TextField()
|
150 |
+
active = pw.BooleanField()
|
151 |
+
|
152 |
+
class Meta:
|
153 |
+
table_name = "auth"
|
154 |
+
|
155 |
+
@migrator.create_model
|
156 |
+
class Chat(pw.Model):
|
157 |
+
id = pw.CharField(max_length=255, unique=True)
|
158 |
+
user_id = pw.CharField(max_length=255)
|
159 |
+
title = pw.TextField()
|
160 |
+
chat = pw.TextField()
|
161 |
+
timestamp = pw.BigIntegerField()
|
162 |
+
|
163 |
+
class Meta:
|
164 |
+
table_name = "chat"
|
165 |
+
|
166 |
+
@migrator.create_model
|
167 |
+
class ChatIdTag(pw.Model):
|
168 |
+
id = pw.CharField(max_length=255, unique=True)
|
169 |
+
tag_name = pw.CharField(max_length=255)
|
170 |
+
chat_id = pw.CharField(max_length=255)
|
171 |
+
user_id = pw.CharField(max_length=255)
|
172 |
+
timestamp = pw.BigIntegerField()
|
173 |
+
|
174 |
+
class Meta:
|
175 |
+
table_name = "chatidtag"
|
176 |
+
|
177 |
+
@migrator.create_model
|
178 |
+
class Document(pw.Model):
|
179 |
+
id = pw.AutoField()
|
180 |
+
collection_name = pw.CharField(max_length=255, unique=True)
|
181 |
+
name = pw.CharField(max_length=255, unique=True)
|
182 |
+
title = pw.TextField()
|
183 |
+
filename = pw.TextField()
|
184 |
+
content = pw.TextField(null=True)
|
185 |
+
user_id = pw.CharField(max_length=255)
|
186 |
+
timestamp = pw.BigIntegerField()
|
187 |
+
|
188 |
+
class Meta:
|
189 |
+
table_name = "document"
|
190 |
+
|
191 |
+
@migrator.create_model
|
192 |
+
class Modelfile(pw.Model):
|
193 |
+
id = pw.AutoField()
|
194 |
+
tag_name = pw.CharField(max_length=255, unique=True)
|
195 |
+
user_id = pw.CharField(max_length=255)
|
196 |
+
modelfile = pw.TextField()
|
197 |
+
timestamp = pw.BigIntegerField()
|
198 |
+
|
199 |
+
class Meta:
|
200 |
+
table_name = "modelfile"
|
201 |
+
|
202 |
+
@migrator.create_model
|
203 |
+
class Prompt(pw.Model):
|
204 |
+
id = pw.AutoField()
|
205 |
+
command = pw.CharField(max_length=255, unique=True)
|
206 |
+
user_id = pw.CharField(max_length=255)
|
207 |
+
title = pw.TextField()
|
208 |
+
content = pw.TextField()
|
209 |
+
timestamp = pw.BigIntegerField()
|
210 |
+
|
211 |
+
class Meta:
|
212 |
+
table_name = "prompt"
|
213 |
+
|
214 |
+
@migrator.create_model
|
215 |
+
class Tag(pw.Model):
|
216 |
+
id = pw.CharField(max_length=255, unique=True)
|
217 |
+
name = pw.CharField(max_length=255)
|
218 |
+
user_id = pw.CharField(max_length=255)
|
219 |
+
data = pw.TextField(null=True)
|
220 |
+
|
221 |
+
class Meta:
|
222 |
+
table_name = "tag"
|
223 |
+
|
224 |
+
@migrator.create_model
|
225 |
+
class User(pw.Model):
|
226 |
+
id = pw.CharField(max_length=255, unique=True)
|
227 |
+
name = pw.CharField(max_length=255)
|
228 |
+
email = pw.CharField(max_length=255)
|
229 |
+
role = pw.CharField(max_length=255)
|
230 |
+
profile_image_url = pw.TextField()
|
231 |
+
timestamp = pw.BigIntegerField()
|
232 |
+
|
233 |
+
class Meta:
|
234 |
+
table_name = "user"
|
235 |
+
|
236 |
+
|
237 |
+
def rollback(migrator: Migrator, database: pw.Database, *, fake=False):
|
238 |
+
"""Write your rollback migrations here."""
|
239 |
+
|
240 |
+
migrator.remove_model("user")
|
241 |
+
|
242 |
+
migrator.remove_model("tag")
|
243 |
+
|
244 |
+
migrator.remove_model("prompt")
|
245 |
+
|
246 |
+
migrator.remove_model("modelfile")
|
247 |
+
|
248 |
+
migrator.remove_model("document")
|
249 |
+
|
250 |
+
migrator.remove_model("chatidtag")
|
251 |
+
|
252 |
+
migrator.remove_model("chat")
|
253 |
+
|
254 |
+
migrator.remove_model("auth")
|
backend/open_webui/internal/migrations/002_add_local_sharing.py
ADDED
@@ -0,0 +1,48 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"""Peewee migrations -- 002_add_local_sharing.py.
|
2 |
+
|
3 |
+
Some examples (model - class or model name)::
|
4 |
+
|
5 |
+
> Model = migrator.orm['table_name'] # Return model in current state by name
|
6 |
+
> Model = migrator.ModelClass # Return model in current state by name
|
7 |
+
|
8 |
+
> migrator.sql(sql) # Run custom SQL
|
9 |
+
> migrator.run(func, *args, **kwargs) # Run python function with the given args
|
10 |
+
> migrator.create_model(Model) # Create a model (could be used as decorator)
|
11 |
+
> migrator.remove_model(model, cascade=True) # Remove a model
|
12 |
+
> migrator.add_fields(model, **fields) # Add fields to a model
|
13 |
+
> migrator.change_fields(model, **fields) # Change fields
|
14 |
+
> migrator.remove_fields(model, *field_names, cascade=True)
|
15 |
+
> migrator.rename_field(model, old_field_name, new_field_name)
|
16 |
+
> migrator.rename_table(model, new_table_name)
|
17 |
+
> migrator.add_index(model, *col_names, unique=False)
|
18 |
+
> migrator.add_not_null(model, *field_names)
|
19 |
+
> migrator.add_default(model, field_name, default)
|
20 |
+
> migrator.add_constraint(model, name, sql)
|
21 |
+
> migrator.drop_index(model, *col_names)
|
22 |
+
> migrator.drop_not_null(model, *field_names)
|
23 |
+
> migrator.drop_constraints(model, *constraints)
|
24 |
+
|
25 |
+
"""
|
26 |
+
|
27 |
+
from contextlib import suppress
|
28 |
+
|
29 |
+
import peewee as pw
|
30 |
+
from peewee_migrate import Migrator
|
31 |
+
|
32 |
+
|
33 |
+
with suppress(ImportError):
|
34 |
+
import playhouse.postgres_ext as pw_pext
|
35 |
+
|
36 |
+
|
37 |
+
def migrate(migrator: Migrator, database: pw.Database, *, fake=False):
|
38 |
+
"""Write your migrations here."""
|
39 |
+
|
40 |
+
migrator.add_fields(
|
41 |
+
"chat", share_id=pw.CharField(max_length=255, null=True, unique=True)
|
42 |
+
)
|
43 |
+
|
44 |
+
|
45 |
+
def rollback(migrator: Migrator, database: pw.Database, *, fake=False):
|
46 |
+
"""Write your rollback migrations here."""
|
47 |
+
|
48 |
+
migrator.remove_fields("chat", "share_id")
|
backend/open_webui/internal/migrations/003_add_auth_api_key.py
ADDED
@@ -0,0 +1,48 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"""Peewee migrations -- 002_add_local_sharing.py.
|
2 |
+
|
3 |
+
Some examples (model - class or model name)::
|
4 |
+
|
5 |
+
> Model = migrator.orm['table_name'] # Return model in current state by name
|
6 |
+
> Model = migrator.ModelClass # Return model in current state by name
|
7 |
+
|
8 |
+
> migrator.sql(sql) # Run custom SQL
|
9 |
+
> migrator.run(func, *args, **kwargs) # Run python function with the given args
|
10 |
+
> migrator.create_model(Model) # Create a model (could be used as decorator)
|
11 |
+
> migrator.remove_model(model, cascade=True) # Remove a model
|
12 |
+
> migrator.add_fields(model, **fields) # Add fields to a model
|
13 |
+
> migrator.change_fields(model, **fields) # Change fields
|
14 |
+
> migrator.remove_fields(model, *field_names, cascade=True)
|
15 |
+
> migrator.rename_field(model, old_field_name, new_field_name)
|
16 |
+
> migrator.rename_table(model, new_table_name)
|
17 |
+
> migrator.add_index(model, *col_names, unique=False)
|
18 |
+
> migrator.add_not_null(model, *field_names)
|
19 |
+
> migrator.add_default(model, field_name, default)
|
20 |
+
> migrator.add_constraint(model, name, sql)
|
21 |
+
> migrator.drop_index(model, *col_names)
|
22 |
+
> migrator.drop_not_null(model, *field_names)
|
23 |
+
> migrator.drop_constraints(model, *constraints)
|
24 |
+
|
25 |
+
"""
|
26 |
+
|
27 |
+
from contextlib import suppress
|
28 |
+
|
29 |
+
import peewee as pw
|
30 |
+
from peewee_migrate import Migrator
|
31 |
+
|
32 |
+
|
33 |
+
with suppress(ImportError):
|
34 |
+
import playhouse.postgres_ext as pw_pext
|
35 |
+
|
36 |
+
|
37 |
+
def migrate(migrator: Migrator, database: pw.Database, *, fake=False):
|
38 |
+
"""Write your migrations here."""
|
39 |
+
|
40 |
+
migrator.add_fields(
|
41 |
+
"user", api_key=pw.CharField(max_length=255, null=True, unique=True)
|
42 |
+
)
|
43 |
+
|
44 |
+
|
45 |
+
def rollback(migrator: Migrator, database: pw.Database, *, fake=False):
|
46 |
+
"""Write your rollback migrations here."""
|
47 |
+
|
48 |
+
migrator.remove_fields("user", "api_key")
|
backend/open_webui/internal/migrations/004_add_archived.py
ADDED
@@ -0,0 +1,46 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"""Peewee migrations -- 002_add_local_sharing.py.
|
2 |
+
|
3 |
+
Some examples (model - class or model name)::
|
4 |
+
|
5 |
+
> Model = migrator.orm['table_name'] # Return model in current state by name
|
6 |
+
> Model = migrator.ModelClass # Return model in current state by name
|
7 |
+
|
8 |
+
> migrator.sql(sql) # Run custom SQL
|
9 |
+
> migrator.run(func, *args, **kwargs) # Run python function with the given args
|
10 |
+
> migrator.create_model(Model) # Create a model (could be used as decorator)
|
11 |
+
> migrator.remove_model(model, cascade=True) # Remove a model
|
12 |
+
> migrator.add_fields(model, **fields) # Add fields to a model
|
13 |
+
> migrator.change_fields(model, **fields) # Change fields
|
14 |
+
> migrator.remove_fields(model, *field_names, cascade=True)
|
15 |
+
> migrator.rename_field(model, old_field_name, new_field_name)
|
16 |
+
> migrator.rename_table(model, new_table_name)
|
17 |
+
> migrator.add_index(model, *col_names, unique=False)
|
18 |
+
> migrator.add_not_null(model, *field_names)
|
19 |
+
> migrator.add_default(model, field_name, default)
|
20 |
+
> migrator.add_constraint(model, name, sql)
|
21 |
+
> migrator.drop_index(model, *col_names)
|
22 |
+
> migrator.drop_not_null(model, *field_names)
|
23 |
+
> migrator.drop_constraints(model, *constraints)
|
24 |
+
|
25 |
+
"""
|
26 |
+
|
27 |
+
from contextlib import suppress
|
28 |
+
|
29 |
+
import peewee as pw
|
30 |
+
from peewee_migrate import Migrator
|
31 |
+
|
32 |
+
|
33 |
+
with suppress(ImportError):
|
34 |
+
import playhouse.postgres_ext as pw_pext
|
35 |
+
|
36 |
+
|
37 |
+
def migrate(migrator: Migrator, database: pw.Database, *, fake=False):
|
38 |
+
"""Write your migrations here."""
|
39 |
+
|
40 |
+
migrator.add_fields("chat", archived=pw.BooleanField(default=False))
|
41 |
+
|
42 |
+
|
43 |
+
def rollback(migrator: Migrator, database: pw.Database, *, fake=False):
|
44 |
+
"""Write your rollback migrations here."""
|
45 |
+
|
46 |
+
migrator.remove_fields("chat", "archived")
|
backend/open_webui/internal/migrations/005_add_updated_at.py
ADDED
@@ -0,0 +1,130 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"""Peewee migrations -- 002_add_local_sharing.py.
|
2 |
+
|
3 |
+
Some examples (model - class or model name)::
|
4 |
+
|
5 |
+
> Model = migrator.orm['table_name'] # Return model in current state by name
|
6 |
+
> Model = migrator.ModelClass # Return model in current state by name
|
7 |
+
|
8 |
+
> migrator.sql(sql) # Run custom SQL
|
9 |
+
> migrator.run(func, *args, **kwargs) # Run python function with the given args
|
10 |
+
> migrator.create_model(Model) # Create a model (could be used as decorator)
|
11 |
+
> migrator.remove_model(model, cascade=True) # Remove a model
|
12 |
+
> migrator.add_fields(model, **fields) # Add fields to a model
|
13 |
+
> migrator.change_fields(model, **fields) # Change fields
|
14 |
+
> migrator.remove_fields(model, *field_names, cascade=True)
|
15 |
+
> migrator.rename_field(model, old_field_name, new_field_name)
|
16 |
+
> migrator.rename_table(model, new_table_name)
|
17 |
+
> migrator.add_index(model, *col_names, unique=False)
|
18 |
+
> migrator.add_not_null(model, *field_names)
|
19 |
+
> migrator.add_default(model, field_name, default)
|
20 |
+
> migrator.add_constraint(model, name, sql)
|
21 |
+
> migrator.drop_index(model, *col_names)
|
22 |
+
> migrator.drop_not_null(model, *field_names)
|
23 |
+
> migrator.drop_constraints(model, *constraints)
|
24 |
+
|
25 |
+
"""
|
26 |
+
|
27 |
+
from contextlib import suppress
|
28 |
+
|
29 |
+
import peewee as pw
|
30 |
+
from peewee_migrate import Migrator
|
31 |
+
|
32 |
+
|
33 |
+
with suppress(ImportError):
|
34 |
+
import playhouse.postgres_ext as pw_pext
|
35 |
+
|
36 |
+
|
37 |
+
def migrate(migrator: Migrator, database: pw.Database, *, fake=False):
|
38 |
+
"""Write your migrations here."""
|
39 |
+
|
40 |
+
if isinstance(database, pw.SqliteDatabase):
|
41 |
+
migrate_sqlite(migrator, database, fake=fake)
|
42 |
+
else:
|
43 |
+
migrate_external(migrator, database, fake=fake)
|
44 |
+
|
45 |
+
|
46 |
+
def migrate_sqlite(migrator: Migrator, database: pw.Database, *, fake=False):
|
47 |
+
# Adding fields created_at and updated_at to the 'chat' table
|
48 |
+
migrator.add_fields(
|
49 |
+
"chat",
|
50 |
+
created_at=pw.DateTimeField(null=True), # Allow null for transition
|
51 |
+
updated_at=pw.DateTimeField(null=True), # Allow null for transition
|
52 |
+
)
|
53 |
+
|
54 |
+
# Populate the new fields from an existing 'timestamp' field
|
55 |
+
migrator.sql(
|
56 |
+
"UPDATE chat SET created_at = timestamp, updated_at = timestamp WHERE timestamp IS NOT NULL"
|
57 |
+
)
|
58 |
+
|
59 |
+
# Now that the data has been copied, remove the original 'timestamp' field
|
60 |
+
migrator.remove_fields("chat", "timestamp")
|
61 |
+
|
62 |
+
# Update the fields to be not null now that they are populated
|
63 |
+
migrator.change_fields(
|
64 |
+
"chat",
|
65 |
+
created_at=pw.DateTimeField(null=False),
|
66 |
+
updated_at=pw.DateTimeField(null=False),
|
67 |
+
)
|
68 |
+
|
69 |
+
|
70 |
+
def migrate_external(migrator: Migrator, database: pw.Database, *, fake=False):
|
71 |
+
# Adding fields created_at and updated_at to the 'chat' table
|
72 |
+
migrator.add_fields(
|
73 |
+
"chat",
|
74 |
+
created_at=pw.BigIntegerField(null=True), # Allow null for transition
|
75 |
+
updated_at=pw.BigIntegerField(null=True), # Allow null for transition
|
76 |
+
)
|
77 |
+
|
78 |
+
# Populate the new fields from an existing 'timestamp' field
|
79 |
+
migrator.sql(
|
80 |
+
"UPDATE chat SET created_at = timestamp, updated_at = timestamp WHERE timestamp IS NOT NULL"
|
81 |
+
)
|
82 |
+
|
83 |
+
# Now that the data has been copied, remove the original 'timestamp' field
|
84 |
+
migrator.remove_fields("chat", "timestamp")
|
85 |
+
|
86 |
+
# Update the fields to be not null now that they are populated
|
87 |
+
migrator.change_fields(
|
88 |
+
"chat",
|
89 |
+
created_at=pw.BigIntegerField(null=False),
|
90 |
+
updated_at=pw.BigIntegerField(null=False),
|
91 |
+
)
|
92 |
+
|
93 |
+
|
94 |
+
def rollback(migrator: Migrator, database: pw.Database, *, fake=False):
|
95 |
+
"""Write your rollback migrations here."""
|
96 |
+
|
97 |
+
if isinstance(database, pw.SqliteDatabase):
|
98 |
+
rollback_sqlite(migrator, database, fake=fake)
|
99 |
+
else:
|
100 |
+
rollback_external(migrator, database, fake=fake)
|
101 |
+
|
102 |
+
|
103 |
+
def rollback_sqlite(migrator: Migrator, database: pw.Database, *, fake=False):
|
104 |
+
# Recreate the timestamp field initially allowing null values for safe transition
|
105 |
+
migrator.add_fields("chat", timestamp=pw.DateTimeField(null=True))
|
106 |
+
|
107 |
+
# Copy the earliest created_at date back into the new timestamp field
|
108 |
+
# This assumes created_at was originally a copy of timestamp
|
109 |
+
migrator.sql("UPDATE chat SET timestamp = created_at")
|
110 |
+
|
111 |
+
# Remove the created_at and updated_at fields
|
112 |
+
migrator.remove_fields("chat", "created_at", "updated_at")
|
113 |
+
|
114 |
+
# Finally, alter the timestamp field to not allow nulls if that was the original setting
|
115 |
+
migrator.change_fields("chat", timestamp=pw.DateTimeField(null=False))
|
116 |
+
|
117 |
+
|
118 |
+
def rollback_external(migrator: Migrator, database: pw.Database, *, fake=False):
|
119 |
+
# Recreate the timestamp field initially allowing null values for safe transition
|
120 |
+
migrator.add_fields("chat", timestamp=pw.BigIntegerField(null=True))
|
121 |
+
|
122 |
+
# Copy the earliest created_at date back into the new timestamp field
|
123 |
+
# This assumes created_at was originally a copy of timestamp
|
124 |
+
migrator.sql("UPDATE chat SET timestamp = created_at")
|
125 |
+
|
126 |
+
# Remove the created_at and updated_at fields
|
127 |
+
migrator.remove_fields("chat", "created_at", "updated_at")
|
128 |
+
|
129 |
+
# Finally, alter the timestamp field to not allow nulls if that was the original setting
|
130 |
+
migrator.change_fields("chat", timestamp=pw.BigIntegerField(null=False))
|
backend/open_webui/internal/migrations/006_migrate_timestamps_and_charfields.py
ADDED
@@ -0,0 +1,130 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"""Peewee migrations -- 006_migrate_timestamps_and_charfields.py.
|
2 |
+
|
3 |
+
Some examples (model - class or model name)::
|
4 |
+
|
5 |
+
> Model = migrator.orm['table_name'] # Return model in current state by name
|
6 |
+
> Model = migrator.ModelClass # Return model in current state by name
|
7 |
+
|
8 |
+
> migrator.sql(sql) # Run custom SQL
|
9 |
+
> migrator.run(func, *args, **kwargs) # Run python function with the given args
|
10 |
+
> migrator.create_model(Model) # Create a model (could be used as decorator)
|
11 |
+
> migrator.remove_model(model, cascade=True) # Remove a model
|
12 |
+
> migrator.add_fields(model, **fields) # Add fields to a model
|
13 |
+
> migrator.change_fields(model, **fields) # Change fields
|
14 |
+
> migrator.remove_fields(model, *field_names, cascade=True)
|
15 |
+
> migrator.rename_field(model, old_field_name, new_field_name)
|
16 |
+
> migrator.rename_table(model, new_table_name)
|
17 |
+
> migrator.add_index(model, *col_names, unique=False)
|
18 |
+
> migrator.add_not_null(model, *field_names)
|
19 |
+
> migrator.add_default(model, field_name, default)
|
20 |
+
> migrator.add_constraint(model, name, sql)
|
21 |
+
> migrator.drop_index(model, *col_names)
|
22 |
+
> migrator.drop_not_null(model, *field_names)
|
23 |
+
> migrator.drop_constraints(model, *constraints)
|
24 |
+
|
25 |
+
"""
|
26 |
+
|
27 |
+
from contextlib import suppress
|
28 |
+
|
29 |
+
import peewee as pw
|
30 |
+
from peewee_migrate import Migrator
|
31 |
+
|
32 |
+
|
33 |
+
with suppress(ImportError):
|
34 |
+
import playhouse.postgres_ext as pw_pext
|
35 |
+
|
36 |
+
|
37 |
+
def migrate(migrator: Migrator, database: pw.Database, *, fake=False):
|
38 |
+
"""Write your migrations here."""
|
39 |
+
|
40 |
+
# Alter the tables with timestamps
|
41 |
+
migrator.change_fields(
|
42 |
+
"chatidtag",
|
43 |
+
timestamp=pw.BigIntegerField(),
|
44 |
+
)
|
45 |
+
migrator.change_fields(
|
46 |
+
"document",
|
47 |
+
timestamp=pw.BigIntegerField(),
|
48 |
+
)
|
49 |
+
migrator.change_fields(
|
50 |
+
"modelfile",
|
51 |
+
timestamp=pw.BigIntegerField(),
|
52 |
+
)
|
53 |
+
migrator.change_fields(
|
54 |
+
"prompt",
|
55 |
+
timestamp=pw.BigIntegerField(),
|
56 |
+
)
|
57 |
+
migrator.change_fields(
|
58 |
+
"user",
|
59 |
+
timestamp=pw.BigIntegerField(),
|
60 |
+
)
|
61 |
+
# Alter the tables with varchar to text where necessary
|
62 |
+
migrator.change_fields(
|
63 |
+
"auth",
|
64 |
+
password=pw.TextField(),
|
65 |
+
)
|
66 |
+
migrator.change_fields(
|
67 |
+
"chat",
|
68 |
+
title=pw.TextField(),
|
69 |
+
)
|
70 |
+
migrator.change_fields(
|
71 |
+
"document",
|
72 |
+
title=pw.TextField(),
|
73 |
+
filename=pw.TextField(),
|
74 |
+
)
|
75 |
+
migrator.change_fields(
|
76 |
+
"prompt",
|
77 |
+
title=pw.TextField(),
|
78 |
+
)
|
79 |
+
migrator.change_fields(
|
80 |
+
"user",
|
81 |
+
profile_image_url=pw.TextField(),
|
82 |
+
)
|
83 |
+
|
84 |
+
|
85 |
+
def rollback(migrator: Migrator, database: pw.Database, *, fake=False):
|
86 |
+
"""Write your rollback migrations here."""
|
87 |
+
|
88 |
+
if isinstance(database, pw.SqliteDatabase):
|
89 |
+
# Alter the tables with timestamps
|
90 |
+
migrator.change_fields(
|
91 |
+
"chatidtag",
|
92 |
+
timestamp=pw.DateField(),
|
93 |
+
)
|
94 |
+
migrator.change_fields(
|
95 |
+
"document",
|
96 |
+
timestamp=pw.DateField(),
|
97 |
+
)
|
98 |
+
migrator.change_fields(
|
99 |
+
"modelfile",
|
100 |
+
timestamp=pw.DateField(),
|
101 |
+
)
|
102 |
+
migrator.change_fields(
|
103 |
+
"prompt",
|
104 |
+
timestamp=pw.DateField(),
|
105 |
+
)
|
106 |
+
migrator.change_fields(
|
107 |
+
"user",
|
108 |
+
timestamp=pw.DateField(),
|
109 |
+
)
|
110 |
+
migrator.change_fields(
|
111 |
+
"auth",
|
112 |
+
password=pw.CharField(max_length=255),
|
113 |
+
)
|
114 |
+
migrator.change_fields(
|
115 |
+
"chat",
|
116 |
+
title=pw.CharField(),
|
117 |
+
)
|
118 |
+
migrator.change_fields(
|
119 |
+
"document",
|
120 |
+
title=pw.CharField(),
|
121 |
+
filename=pw.CharField(),
|
122 |
+
)
|
123 |
+
migrator.change_fields(
|
124 |
+
"prompt",
|
125 |
+
title=pw.CharField(),
|
126 |
+
)
|
127 |
+
migrator.change_fields(
|
128 |
+
"user",
|
129 |
+
profile_image_url=pw.CharField(),
|
130 |
+
)
|
backend/open_webui/internal/migrations/007_add_user_last_active_at.py
ADDED
@@ -0,0 +1,79 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"""Peewee migrations -- 002_add_local_sharing.py.
|
2 |
+
|
3 |
+
Some examples (model - class or model name)::
|
4 |
+
|
5 |
+
> Model = migrator.orm['table_name'] # Return model in current state by name
|
6 |
+
> Model = migrator.ModelClass # Return model in current state by name
|
7 |
+
|
8 |
+
> migrator.sql(sql) # Run custom SQL
|
9 |
+
> migrator.run(func, *args, **kwargs) # Run python function with the given args
|
10 |
+
> migrator.create_model(Model) # Create a model (could be used as decorator)
|
11 |
+
> migrator.remove_model(model, cascade=True) # Remove a model
|
12 |
+
> migrator.add_fields(model, **fields) # Add fields to a model
|
13 |
+
> migrator.change_fields(model, **fields) # Change fields
|
14 |
+
> migrator.remove_fields(model, *field_names, cascade=True)
|
15 |
+
> migrator.rename_field(model, old_field_name, new_field_name)
|
16 |
+
> migrator.rename_table(model, new_table_name)
|
17 |
+
> migrator.add_index(model, *col_names, unique=False)
|
18 |
+
> migrator.add_not_null(model, *field_names)
|
19 |
+
> migrator.add_default(model, field_name, default)
|
20 |
+
> migrator.add_constraint(model, name, sql)
|
21 |
+
> migrator.drop_index(model, *col_names)
|
22 |
+
> migrator.drop_not_null(model, *field_names)
|
23 |
+
> migrator.drop_constraints(model, *constraints)
|
24 |
+
|
25 |
+
"""
|
26 |
+
|
27 |
+
from contextlib import suppress
|
28 |
+
|
29 |
+
import peewee as pw
|
30 |
+
from peewee_migrate import Migrator
|
31 |
+
|
32 |
+
|
33 |
+
with suppress(ImportError):
|
34 |
+
import playhouse.postgres_ext as pw_pext
|
35 |
+
|
36 |
+
|
37 |
+
def migrate(migrator: Migrator, database: pw.Database, *, fake=False):
|
38 |
+
"""Write your migrations here."""
|
39 |
+
|
40 |
+
# Adding fields created_at and updated_at to the 'user' table
|
41 |
+
migrator.add_fields(
|
42 |
+
"user",
|
43 |
+
created_at=pw.BigIntegerField(null=True), # Allow null for transition
|
44 |
+
updated_at=pw.BigIntegerField(null=True), # Allow null for transition
|
45 |
+
last_active_at=pw.BigIntegerField(null=True), # Allow null for transition
|
46 |
+
)
|
47 |
+
|
48 |
+
# Populate the new fields from an existing 'timestamp' field
|
49 |
+
migrator.sql(
|
50 |
+
'UPDATE "user" SET created_at = timestamp, updated_at = timestamp, last_active_at = timestamp WHERE timestamp IS NOT NULL'
|
51 |
+
)
|
52 |
+
|
53 |
+
# Now that the data has been copied, remove the original 'timestamp' field
|
54 |
+
migrator.remove_fields("user", "timestamp")
|
55 |
+
|
56 |
+
# Update the fields to be not null now that they are populated
|
57 |
+
migrator.change_fields(
|
58 |
+
"user",
|
59 |
+
created_at=pw.BigIntegerField(null=False),
|
60 |
+
updated_at=pw.BigIntegerField(null=False),
|
61 |
+
last_active_at=pw.BigIntegerField(null=False),
|
62 |
+
)
|
63 |
+
|
64 |
+
|
65 |
+
def rollback(migrator: Migrator, database: pw.Database, *, fake=False):
|
66 |
+
"""Write your rollback migrations here."""
|
67 |
+
|
68 |
+
# Recreate the timestamp field initially allowing null values for safe transition
|
69 |
+
migrator.add_fields("user", timestamp=pw.BigIntegerField(null=True))
|
70 |
+
|
71 |
+
# Copy the earliest created_at date back into the new timestamp field
|
72 |
+
# This assumes created_at was originally a copy of timestamp
|
73 |
+
migrator.sql('UPDATE "user" SET timestamp = created_at')
|
74 |
+
|
75 |
+
# Remove the created_at and updated_at fields
|
76 |
+
migrator.remove_fields("user", "created_at", "updated_at", "last_active_at")
|
77 |
+
|
78 |
+
# Finally, alter the timestamp field to not allow nulls if that was the original setting
|
79 |
+
migrator.change_fields("user", timestamp=pw.BigIntegerField(null=False))
|