Commit
•
c9e5647
1
Parent(s):
5ce79f0
Upload folder using huggingface_hub
Browse filesThis view is limited to 50 files because it contains too many changes.
See raw diff
- .dockerignore +18 -0
- .env.example +17 -0
- .eslintignore +13 -0
- .eslintrc.cjs +31 -0
- .gitattributes +4 -35
- .github/FUNDING.yml +1 -0
- .github/ISSUE_TEMPLATE/bug_report.md +63 -0
- .github/ISSUE_TEMPLATE/feature_request.md +19 -0
- .github/dependabot.disabled +11 -0
- .github/pull_request_template.md +72 -0
- .github/workflows/build-release.yml +70 -0
- .github/workflows/deploy-to-hf-spaces.yml +59 -0
- .github/workflows/docker-build.yaml +414 -0
- .github/workflows/format-backend.yaml +39 -0
- .github/workflows/format-build-frontend.yaml +57 -0
- .github/workflows/integration-test.yml +199 -0
- .github/workflows/lint-backend.disabled +27 -0
- .github/workflows/lint-frontend.disabled +21 -0
- .github/workflows/release-pypi.yml +32 -0
- .gitignore +308 -0
- .npmrc +1 -0
- .prettierignore +316 -0
- .prettierrc +9 -0
- CHANGELOG.md +424 -0
- Caddyfile.localhost +64 -0
- Dockerfile +158 -0
- INSTALLATION.md +35 -0
- LICENSE +21 -0
- Makefile +33 -0
- README.md +244 -0
- TROUBLESHOOTING.md +32 -0
- backend/.dockerignore +14 -0
- backend/.gitignore +16 -0
- backend/apps/audio/main.py +226 -0
- backend/apps/images/main.py +527 -0
- backend/apps/images/utils/comfyui.py +234 -0
- backend/apps/litellm/main.py +379 -0
- backend/apps/ollama/main.py +1447 -0
- backend/apps/openai/main.py +394 -0
- backend/apps/rag/main.py +971 -0
- backend/apps/rag/utils.py +522 -0
- backend/apps/web/internal/db.py +25 -0
- backend/apps/web/internal/migrations/001_initial_schema.py +254 -0
- backend/apps/web/internal/migrations/002_add_local_sharing.py +48 -0
- backend/apps/web/internal/migrations/003_add_auth_api_key.py +48 -0
- backend/apps/web/internal/migrations/004_add_archived.py +46 -0
- backend/apps/web/internal/migrations/005_add_updated_at.py +130 -0
- backend/apps/web/internal/migrations/006_migrate_timestamps_and_charfields.py +130 -0
- backend/apps/web/internal/migrations/007_add_user_last_active_at.py +79 -0
- backend/apps/web/internal/migrations/008_add_memory.py +53 -0
.dockerignore
ADDED
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 |
+
.env
|
14 |
+
_old
|
15 |
+
uploads
|
16 |
+
.ipynb_checkpoints
|
17 |
+
**/*.db
|
18 |
+
_test
|
.env.example
ADDED
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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
|
14 |
+
|
15 |
+
# Use locally bundled version of the LiteLLM cost map json
|
16 |
+
# to avoid repetitive startup connections
|
17 |
+
LITELLM_LOCAL_MODEL_COST_MAP="True"
|
.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
CHANGED
@@ -1,35 +1,4 @@
|
|
1 |
-
*.
|
2 |
-
|
3 |
-
|
4 |
-
|
5 |
-
*.ckpt filter=lfs diff=lfs merge=lfs -text
|
6 |
-
*.ftz filter=lfs diff=lfs merge=lfs -text
|
7 |
-
*.gz filter=lfs diff=lfs merge=lfs -text
|
8 |
-
*.h5 filter=lfs diff=lfs merge=lfs -text
|
9 |
-
*.joblib filter=lfs diff=lfs merge=lfs -text
|
10 |
-
*.lfs.* filter=lfs diff=lfs merge=lfs -text
|
11 |
-
*.mlmodel filter=lfs diff=lfs merge=lfs -text
|
12 |
-
*.model filter=lfs diff=lfs merge=lfs -text
|
13 |
-
*.msgpack filter=lfs diff=lfs merge=lfs -text
|
14 |
-
*.npy filter=lfs diff=lfs merge=lfs -text
|
15 |
-
*.npz filter=lfs diff=lfs merge=lfs -text
|
16 |
-
*.onnx filter=lfs diff=lfs merge=lfs -text
|
17 |
-
*.ot filter=lfs diff=lfs merge=lfs -text
|
18 |
-
*.parquet filter=lfs diff=lfs merge=lfs -text
|
19 |
-
*.pb filter=lfs diff=lfs merge=lfs -text
|
20 |
-
*.pickle filter=lfs diff=lfs merge=lfs -text
|
21 |
-
*.pkl filter=lfs diff=lfs merge=lfs -text
|
22 |
-
*.pt filter=lfs diff=lfs merge=lfs -text
|
23 |
-
*.pth filter=lfs diff=lfs merge=lfs -text
|
24 |
-
*.rar filter=lfs diff=lfs merge=lfs -text
|
25 |
-
*.safetensors filter=lfs diff=lfs merge=lfs -text
|
26 |
-
saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
27 |
-
*.tar.* filter=lfs diff=lfs merge=lfs -text
|
28 |
-
*.tar filter=lfs diff=lfs merge=lfs -text
|
29 |
-
*.tflite filter=lfs diff=lfs merge=lfs -text
|
30 |
-
*.tgz filter=lfs diff=lfs merge=lfs -text
|
31 |
-
*.wasm filter=lfs diff=lfs merge=lfs -text
|
32 |
-
*.xz filter=lfs diff=lfs merge=lfs -text
|
33 |
-
*.zip filter=lfs diff=lfs merge=lfs -text
|
34 |
-
*.zst filter=lfs diff=lfs merge=lfs -text
|
35 |
-
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
|
|
1 |
+
*.sh text eol=lfbackend/static/fonts/NotoSansJP-Regular.ttf filter=lfs diff=lfs merge=lfs -text
|
2 |
+
backend/static/fonts/NotoSansKR-Regular.ttf filter=lfs diff=lfs merge=lfs -text
|
3 |
+
demo.gif filter=lfs diff=lfs merge=lfs -text
|
4 |
+
test/test_files/image_gen/sd-empty.pt 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,63 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 |
+
## Description
|
12 |
+
|
13 |
+
**Bug Summary:**
|
14 |
+
[Provide a brief but clear summary of the bug]
|
15 |
+
|
16 |
+
**Steps to Reproduce:**
|
17 |
+
[Outline the steps to reproduce the bug. Be as detailed as possible.]
|
18 |
+
|
19 |
+
**Expected Behavior:**
|
20 |
+
[Describe what you expected to happen.]
|
21 |
+
|
22 |
+
**Actual Behavior:**
|
23 |
+
[Describe what actually happened.]
|
24 |
+
|
25 |
+
## Environment
|
26 |
+
|
27 |
+
- **Open WebUI Version:** [e.g., 0.1.120]
|
28 |
+
- **Ollama (if applicable):** [e.g., 0.1.30, 0.1.32-rc1]
|
29 |
+
|
30 |
+
- **Operating System:** [e.g., Windows 10, macOS Big Sur, Ubuntu 20.04]
|
31 |
+
- **Browser (if applicable):** [e.g., Chrome 100.0, Firefox 98.0]
|
32 |
+
|
33 |
+
## Reproduction Details
|
34 |
+
|
35 |
+
**Confirmation:**
|
36 |
+
|
37 |
+
- [ ] I have read and followed all the instructions provided in the README.md.
|
38 |
+
- [ ] I am on the latest version of both Open WebUI and Ollama.
|
39 |
+
- [ ] I have included the browser console logs.
|
40 |
+
- [ ] I have included the Docker container logs.
|
41 |
+
|
42 |
+
## Logs and Screenshots
|
43 |
+
|
44 |
+
**Browser Console Logs:**
|
45 |
+
[Include relevant browser console logs, if applicable]
|
46 |
+
|
47 |
+
**Docker Container Logs:**
|
48 |
+
[Include relevant Docker container logs, if applicable]
|
49 |
+
|
50 |
+
**Screenshots (if applicable):**
|
51 |
+
[Attach any relevant screenshots to help illustrate the issue]
|
52 |
+
|
53 |
+
## Installation Method
|
54 |
+
|
55 |
+
[Describe the method you used to install the project, e.g., manual installation, Docker, package manager, etc.]
|
56 |
+
|
57 |
+
## Additional Information
|
58 |
+
|
59 |
+
[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.]
|
60 |
+
|
61 |
+
## Note
|
62 |
+
|
63 |
+
If the bug report is incomplete or does not follow the provided instructions, it may not be addressed. Please ensure that you have followed the steps outlined in the README.md and troubleshooting.md documents, and provide all necessary information for us to reproduce and address the issue. Thank you!
|
.github/ISSUE_TEMPLATE/feature_request.md
ADDED
@@ -0,0 +1,19 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
---
|
2 |
+
name: Feature request
|
3 |
+
about: Suggest an idea for this project
|
4 |
+
title: ''
|
5 |
+
labels: ''
|
6 |
+
assignees: ''
|
7 |
+
---
|
8 |
+
|
9 |
+
**Is your feature request related to a problem? Please describe.**
|
10 |
+
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
11 |
+
|
12 |
+
**Describe the solution you'd like**
|
13 |
+
A clear and concise description of what you want to happen.
|
14 |
+
|
15 |
+
**Describe alternatives you've considered**
|
16 |
+
A clear and concise description of any alternative solutions or features you've considered.
|
17 |
+
|
18 |
+
**Additional context**
|
19 |
+
Add any other context or screenshots about the feature request here.
|
.github/dependabot.disabled
ADDED
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
version: 2
|
2 |
+
updates:
|
3 |
+
- package-ecosystem: pip
|
4 |
+
directory: '/backend'
|
5 |
+
schedule:
|
6 |
+
interval: weekly
|
7 |
+
- package-ecosystem: 'github-actions'
|
8 |
+
directory: '/'
|
9 |
+
schedule:
|
10 |
+
# Check for updates to GitHub Actions every week
|
11 |
+
interval: 'weekly'
|
.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 |
+
- [ ] **Label:** To cleary categorize this pull request, assign a relevant label to 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,70 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 |
+
env:
|
59 |
+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
60 |
+
|
61 |
+
- name: Trigger Docker build workflow
|
62 |
+
uses: actions/github-script@v7
|
63 |
+
with:
|
64 |
+
script: |
|
65 |
+
github.rest.actions.createWorkflowDispatch({
|
66 |
+
owner: context.repo.owner,
|
67 |
+
repo: context.repo.repo,
|
68 |
+
workflow_id: 'docker-build.yaml',
|
69 |
+
ref: 'v${{ steps.get_version.outputs.version }}',
|
70 |
+
})
|
.github/workflows/deploy-to-hf-spaces.yml
ADDED
@@ -0,0 +1,59 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
name: Deploy to HuggingFace Spaces
|
2 |
+
|
3 |
+
on:
|
4 |
+
push:
|
5 |
+
branches:
|
6 |
+
- dev
|
7 |
+
- main
|
8 |
+
workflow_dispatch:
|
9 |
+
|
10 |
+
jobs:
|
11 |
+
check-secret:
|
12 |
+
runs-on: ubuntu-latest
|
13 |
+
outputs:
|
14 |
+
token-set: ${{ steps.check-key.outputs.defined }}
|
15 |
+
steps:
|
16 |
+
- id: check-key
|
17 |
+
env:
|
18 |
+
HF_TOKEN: ${{ secrets.HF_TOKEN }}
|
19 |
+
if: "${{ env.HF_TOKEN != '' }}"
|
20 |
+
run: echo "defined=true" >> $GITHUB_OUTPUT
|
21 |
+
|
22 |
+
deploy:
|
23 |
+
runs-on: ubuntu-latest
|
24 |
+
needs: [check-secret]
|
25 |
+
if: needs.check-secret.outputs.token-set == 'true'
|
26 |
+
env:
|
27 |
+
HF_TOKEN: ${{ secrets.HF_TOKEN }}
|
28 |
+
steps:
|
29 |
+
- name: Checkout repository
|
30 |
+
uses: actions/checkout@v4
|
31 |
+
|
32 |
+
- name: Remove git history
|
33 |
+
run: rm -rf .git
|
34 |
+
|
35 |
+
- name: Prepend YAML front matter to README.md
|
36 |
+
run: |
|
37 |
+
echo "---" > temp_readme.md
|
38 |
+
echo "title: Open WebUI" >> temp_readme.md
|
39 |
+
echo "emoji: 🐳" >> temp_readme.md
|
40 |
+
echo "colorFrom: purple" >> temp_readme.md
|
41 |
+
echo "colorTo: gray" >> temp_readme.md
|
42 |
+
echo "sdk: docker" >> temp_readme.md
|
43 |
+
echo "app_port: 8080" >> temp_readme.md
|
44 |
+
echo "---" >> temp_readme.md
|
45 |
+
cat README.md >> temp_readme.md
|
46 |
+
mv temp_readme.md README.md
|
47 |
+
|
48 |
+
- name: Configure git
|
49 |
+
run: |
|
50 |
+
git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
51 |
+
git config --global user.name "github-actions[bot]"
|
52 |
+
- name: Set up Git and push to Space
|
53 |
+
run: |
|
54 |
+
git init --initial-branch=main
|
55 |
+
git lfs track "*.ttf"
|
56 |
+
rm demo.gif
|
57 |
+
git add .
|
58 |
+
git commit -m "GitHub deploy: ${{ github.sha }}"
|
59 |
+
git push --force https://open-webui:${HF_TOKEN}@huggingface.co/spaces/open-webui/open-webui main
|
.github/workflows/docker-build.yaml
ADDED
@@ -0,0 +1,414 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 |
+
IMAGE_NAME: ${{ github.repository }}
|
15 |
+
FULL_IMAGE_NAME: ghcr.io/${{ github.repository }}
|
16 |
+
|
17 |
+
jobs:
|
18 |
+
build-main-image:
|
19 |
+
runs-on: ubuntu-latest
|
20 |
+
permissions:
|
21 |
+
contents: read
|
22 |
+
packages: write
|
23 |
+
strategy:
|
24 |
+
fail-fast: false
|
25 |
+
matrix:
|
26 |
+
platform:
|
27 |
+
- linux/amd64
|
28 |
+
- linux/arm64
|
29 |
+
|
30 |
+
steps:
|
31 |
+
- name: Prepare
|
32 |
+
run: |
|
33 |
+
platform=${{ matrix.platform }}
|
34 |
+
echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV
|
35 |
+
|
36 |
+
- name: Checkout repository
|
37 |
+
uses: actions/checkout@v4
|
38 |
+
|
39 |
+
- name: Set up QEMU
|
40 |
+
uses: docker/setup-qemu-action@v3
|
41 |
+
|
42 |
+
- name: Set up Docker Buildx
|
43 |
+
uses: docker/setup-buildx-action@v3
|
44 |
+
|
45 |
+
- name: Log in to the Container registry
|
46 |
+
uses: docker/login-action@v3
|
47 |
+
with:
|
48 |
+
registry: ${{ env.REGISTRY }}
|
49 |
+
username: ${{ github.actor }}
|
50 |
+
password: ${{ secrets.GITHUB_TOKEN }}
|
51 |
+
|
52 |
+
- name: Extract metadata for Docker images (default latest tag)
|
53 |
+
id: meta
|
54 |
+
uses: docker/metadata-action@v5
|
55 |
+
with:
|
56 |
+
images: ${{ env.FULL_IMAGE_NAME }}
|
57 |
+
tags: |
|
58 |
+
type=ref,event=branch
|
59 |
+
type=ref,event=tag
|
60 |
+
type=sha,prefix=git-
|
61 |
+
type=semver,pattern={{version}}
|
62 |
+
type=semver,pattern={{major}}.{{minor}}
|
63 |
+
flavor: |
|
64 |
+
latest=${{ github.ref == 'refs/heads/main' }}
|
65 |
+
|
66 |
+
- name: Extract metadata for Docker cache
|
67 |
+
id: cache-meta
|
68 |
+
uses: docker/metadata-action@v5
|
69 |
+
with:
|
70 |
+
images: ${{ env.FULL_IMAGE_NAME }}
|
71 |
+
tags: |
|
72 |
+
type=ref,event=branch
|
73 |
+
flavor: |
|
74 |
+
prefix=cache-${{ matrix.platform }}-
|
75 |
+
|
76 |
+
- name: Build Docker image (latest)
|
77 |
+
uses: docker/build-push-action@v5
|
78 |
+
id: build
|
79 |
+
with:
|
80 |
+
context: .
|
81 |
+
push: true
|
82 |
+
platforms: ${{ matrix.platform }}
|
83 |
+
labels: ${{ steps.meta.outputs.labels }}
|
84 |
+
outputs: type=image,name=${{ env.FULL_IMAGE_NAME }},push-by-digest=true,name-canonical=true,push=true
|
85 |
+
cache-from: type=registry,ref=${{ steps.cache-meta.outputs.tags }}
|
86 |
+
cache-to: type=registry,ref=${{ steps.cache-meta.outputs.tags }},mode=max
|
87 |
+
|
88 |
+
- name: Export digest
|
89 |
+
run: |
|
90 |
+
mkdir -p /tmp/digests
|
91 |
+
digest="${{ steps.build.outputs.digest }}"
|
92 |
+
touch "/tmp/digests/${digest#sha256:}"
|
93 |
+
|
94 |
+
- name: Upload digest
|
95 |
+
uses: actions/upload-artifact@v4
|
96 |
+
with:
|
97 |
+
name: digests-main-${{ env.PLATFORM_PAIR }}
|
98 |
+
path: /tmp/digests/*
|
99 |
+
if-no-files-found: error
|
100 |
+
retention-days: 1
|
101 |
+
|
102 |
+
build-cuda-image:
|
103 |
+
runs-on: ubuntu-latest
|
104 |
+
permissions:
|
105 |
+
contents: read
|
106 |
+
packages: write
|
107 |
+
strategy:
|
108 |
+
fail-fast: false
|
109 |
+
matrix:
|
110 |
+
platform:
|
111 |
+
- linux/amd64
|
112 |
+
- linux/arm64
|
113 |
+
|
114 |
+
steps:
|
115 |
+
- name: Prepare
|
116 |
+
run: |
|
117 |
+
platform=${{ matrix.platform }}
|
118 |
+
echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV
|
119 |
+
|
120 |
+
- name: Checkout repository
|
121 |
+
uses: actions/checkout@v4
|
122 |
+
|
123 |
+
- name: Set up QEMU
|
124 |
+
uses: docker/setup-qemu-action@v3
|
125 |
+
|
126 |
+
- name: Set up Docker Buildx
|
127 |
+
uses: docker/setup-buildx-action@v3
|
128 |
+
|
129 |
+
- name: Log in to the Container registry
|
130 |
+
uses: docker/login-action@v3
|
131 |
+
with:
|
132 |
+
registry: ${{ env.REGISTRY }}
|
133 |
+
username: ${{ github.actor }}
|
134 |
+
password: ${{ secrets.GITHUB_TOKEN }}
|
135 |
+
|
136 |
+
- name: Extract metadata for Docker images (cuda tag)
|
137 |
+
id: meta
|
138 |
+
uses: docker/metadata-action@v5
|
139 |
+
with:
|
140 |
+
images: ${{ env.FULL_IMAGE_NAME }}
|
141 |
+
tags: |
|
142 |
+
type=ref,event=branch
|
143 |
+
type=ref,event=tag
|
144 |
+
type=sha,prefix=git-
|
145 |
+
type=semver,pattern={{version}}
|
146 |
+
type=semver,pattern={{major}}.{{minor}}
|
147 |
+
type=raw,enable=${{ github.ref == 'refs/heads/main' }},prefix=,suffix=,value=cuda
|
148 |
+
flavor: |
|
149 |
+
latest=${{ github.ref == 'refs/heads/main' }}
|
150 |
+
suffix=-cuda,onlatest=true
|
151 |
+
|
152 |
+
- name: Extract metadata for Docker cache
|
153 |
+
id: cache-meta
|
154 |
+
uses: docker/metadata-action@v5
|
155 |
+
with:
|
156 |
+
images: ${{ env.FULL_IMAGE_NAME }}
|
157 |
+
tags: |
|
158 |
+
type=ref,event=branch
|
159 |
+
flavor: |
|
160 |
+
prefix=cache-cuda-${{ matrix.platform }}-
|
161 |
+
|
162 |
+
- name: Build Docker image (cuda)
|
163 |
+
uses: docker/build-push-action@v5
|
164 |
+
id: build
|
165 |
+
with:
|
166 |
+
context: .
|
167 |
+
push: true
|
168 |
+
platforms: ${{ matrix.platform }}
|
169 |
+
labels: ${{ steps.meta.outputs.labels }}
|
170 |
+
outputs: type=image,name=${{ env.FULL_IMAGE_NAME }},push-by-digest=true,name-canonical=true,push=true
|
171 |
+
cache-from: type=registry,ref=${{ steps.cache-meta.outputs.tags }}
|
172 |
+
cache-to: type=registry,ref=${{ steps.cache-meta.outputs.tags }},mode=max
|
173 |
+
build-args: USE_CUDA=true
|
174 |
+
|
175 |
+
- name: Export digest
|
176 |
+
run: |
|
177 |
+
mkdir -p /tmp/digests
|
178 |
+
digest="${{ steps.build.outputs.digest }}"
|
179 |
+
touch "/tmp/digests/${digest#sha256:}"
|
180 |
+
|
181 |
+
- name: Upload digest
|
182 |
+
uses: actions/upload-artifact@v4
|
183 |
+
with:
|
184 |
+
name: digests-cuda-${{ env.PLATFORM_PAIR }}
|
185 |
+
path: /tmp/digests/*
|
186 |
+
if-no-files-found: error
|
187 |
+
retention-days: 1
|
188 |
+
|
189 |
+
build-ollama-image:
|
190 |
+
runs-on: ubuntu-latest
|
191 |
+
permissions:
|
192 |
+
contents: read
|
193 |
+
packages: write
|
194 |
+
strategy:
|
195 |
+
fail-fast: false
|
196 |
+
matrix:
|
197 |
+
platform:
|
198 |
+
- linux/amd64
|
199 |
+
- linux/arm64
|
200 |
+
|
201 |
+
steps:
|
202 |
+
- name: Prepare
|
203 |
+
run: |
|
204 |
+
platform=${{ matrix.platform }}
|
205 |
+
echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV
|
206 |
+
|
207 |
+
- name: Checkout repository
|
208 |
+
uses: actions/checkout@v4
|
209 |
+
|
210 |
+
- name: Set up QEMU
|
211 |
+
uses: docker/setup-qemu-action@v3
|
212 |
+
|
213 |
+
- name: Set up Docker Buildx
|
214 |
+
uses: docker/setup-buildx-action@v3
|
215 |
+
|
216 |
+
- name: Log in to the Container registry
|
217 |
+
uses: docker/login-action@v3
|
218 |
+
with:
|
219 |
+
registry: ${{ env.REGISTRY }}
|
220 |
+
username: ${{ github.actor }}
|
221 |
+
password: ${{ secrets.GITHUB_TOKEN }}
|
222 |
+
|
223 |
+
- name: Extract metadata for Docker images (ollama tag)
|
224 |
+
id: meta
|
225 |
+
uses: docker/metadata-action@v5
|
226 |
+
with:
|
227 |
+
images: ${{ env.FULL_IMAGE_NAME }}
|
228 |
+
tags: |
|
229 |
+
type=ref,event=branch
|
230 |
+
type=ref,event=tag
|
231 |
+
type=sha,prefix=git-
|
232 |
+
type=semver,pattern={{version}}
|
233 |
+
type=semver,pattern={{major}}.{{minor}}
|
234 |
+
type=raw,enable=${{ github.ref == 'refs/heads/main' }},prefix=,suffix=,value=ollama
|
235 |
+
flavor: |
|
236 |
+
latest=${{ github.ref == 'refs/heads/main' }}
|
237 |
+
suffix=-ollama,onlatest=true
|
238 |
+
|
239 |
+
- name: Extract metadata for Docker cache
|
240 |
+
id: cache-meta
|
241 |
+
uses: docker/metadata-action@v5
|
242 |
+
with:
|
243 |
+
images: ${{ env.FULL_IMAGE_NAME }}
|
244 |
+
tags: |
|
245 |
+
type=ref,event=branch
|
246 |
+
flavor: |
|
247 |
+
prefix=cache-ollama-${{ matrix.platform }}-
|
248 |
+
|
249 |
+
- name: Build Docker image (ollama)
|
250 |
+
uses: docker/build-push-action@v5
|
251 |
+
id: build
|
252 |
+
with:
|
253 |
+
context: .
|
254 |
+
push: true
|
255 |
+
platforms: ${{ matrix.platform }}
|
256 |
+
labels: ${{ steps.meta.outputs.labels }}
|
257 |
+
outputs: type=image,name=${{ env.FULL_IMAGE_NAME }},push-by-digest=true,name-canonical=true,push=true
|
258 |
+
cache-from: type=registry,ref=${{ steps.cache-meta.outputs.tags }}
|
259 |
+
cache-to: type=registry,ref=${{ steps.cache-meta.outputs.tags }},mode=max
|
260 |
+
build-args: USE_OLLAMA=true
|
261 |
+
|
262 |
+
- name: Export digest
|
263 |
+
run: |
|
264 |
+
mkdir -p /tmp/digests
|
265 |
+
digest="${{ steps.build.outputs.digest }}"
|
266 |
+
touch "/tmp/digests/${digest#sha256:}"
|
267 |
+
|
268 |
+
- name: Upload digest
|
269 |
+
uses: actions/upload-artifact@v4
|
270 |
+
with:
|
271 |
+
name: digests-ollama-${{ env.PLATFORM_PAIR }}
|
272 |
+
path: /tmp/digests/*
|
273 |
+
if-no-files-found: error
|
274 |
+
retention-days: 1
|
275 |
+
|
276 |
+
merge-main-images:
|
277 |
+
runs-on: ubuntu-latest
|
278 |
+
needs: [ build-main-image ]
|
279 |
+
steps:
|
280 |
+
- name: Download digests
|
281 |
+
uses: actions/download-artifact@v4
|
282 |
+
with:
|
283 |
+
pattern: digests-main-*
|
284 |
+
path: /tmp/digests
|
285 |
+
merge-multiple: true
|
286 |
+
|
287 |
+
- name: Set up Docker Buildx
|
288 |
+
uses: docker/setup-buildx-action@v3
|
289 |
+
|
290 |
+
- name: Log in to the Container registry
|
291 |
+
uses: docker/login-action@v3
|
292 |
+
with:
|
293 |
+
registry: ${{ env.REGISTRY }}
|
294 |
+
username: ${{ github.actor }}
|
295 |
+
password: ${{ secrets.GITHUB_TOKEN }}
|
296 |
+
|
297 |
+
- name: Extract metadata for Docker images (default latest tag)
|
298 |
+
id: meta
|
299 |
+
uses: docker/metadata-action@v5
|
300 |
+
with:
|
301 |
+
images: ${{ env.FULL_IMAGE_NAME }}
|
302 |
+
tags: |
|
303 |
+
type=ref,event=branch
|
304 |
+
type=ref,event=tag
|
305 |
+
type=sha,prefix=git-
|
306 |
+
type=semver,pattern={{version}}
|
307 |
+
type=semver,pattern={{major}}.{{minor}}
|
308 |
+
flavor: |
|
309 |
+
latest=${{ github.ref == 'refs/heads/main' }}
|
310 |
+
|
311 |
+
- name: Create manifest list and push
|
312 |
+
working-directory: /tmp/digests
|
313 |
+
run: |
|
314 |
+
docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
|
315 |
+
$(printf '${{ env.FULL_IMAGE_NAME }}@sha256:%s ' *)
|
316 |
+
|
317 |
+
- name: Inspect image
|
318 |
+
run: |
|
319 |
+
docker buildx imagetools inspect ${{ env.FULL_IMAGE_NAME }}:${{ steps.meta.outputs.version }}
|
320 |
+
|
321 |
+
|
322 |
+
merge-cuda-images:
|
323 |
+
runs-on: ubuntu-latest
|
324 |
+
needs: [ build-cuda-image ]
|
325 |
+
steps:
|
326 |
+
- name: Download digests
|
327 |
+
uses: actions/download-artifact@v4
|
328 |
+
with:
|
329 |
+
pattern: digests-cuda-*
|
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 |
+
type=raw,enable=${{ github.ref == 'refs/heads/main' }},prefix=,suffix=,value=cuda
|
355 |
+
flavor: |
|
356 |
+
latest=${{ github.ref == 'refs/heads/main' }}
|
357 |
+
suffix=-cuda,onlatest=true
|
358 |
+
|
359 |
+
- name: Create manifest list and push
|
360 |
+
working-directory: /tmp/digests
|
361 |
+
run: |
|
362 |
+
docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
|
363 |
+
$(printf '${{ env.FULL_IMAGE_NAME }}@sha256:%s ' *)
|
364 |
+
|
365 |
+
- name: Inspect image
|
366 |
+
run: |
|
367 |
+
docker buildx imagetools inspect ${{ env.FULL_IMAGE_NAME }}:${{ steps.meta.outputs.version }}
|
368 |
+
|
369 |
+
merge-ollama-images:
|
370 |
+
runs-on: ubuntu-latest
|
371 |
+
needs: [ build-ollama-image ]
|
372 |
+
steps:
|
373 |
+
- name: Download digests
|
374 |
+
uses: actions/download-artifact@v4
|
375 |
+
with:
|
376 |
+
pattern: digests-ollama-*
|
377 |
+
path: /tmp/digests
|
378 |
+
merge-multiple: true
|
379 |
+
|
380 |
+
- name: Set up Docker Buildx
|
381 |
+
uses: docker/setup-buildx-action@v3
|
382 |
+
|
383 |
+
- name: Log in to the Container registry
|
384 |
+
uses: docker/login-action@v3
|
385 |
+
with:
|
386 |
+
registry: ${{ env.REGISTRY }}
|
387 |
+
username: ${{ github.actor }}
|
388 |
+
password: ${{ secrets.GITHUB_TOKEN }}
|
389 |
+
|
390 |
+
- name: Extract metadata for Docker images (default ollama tag)
|
391 |
+
id: meta
|
392 |
+
uses: docker/metadata-action@v5
|
393 |
+
with:
|
394 |
+
images: ${{ env.FULL_IMAGE_NAME }}
|
395 |
+
tags: |
|
396 |
+
type=ref,event=branch
|
397 |
+
type=ref,event=tag
|
398 |
+
type=sha,prefix=git-
|
399 |
+
type=semver,pattern={{version}}
|
400 |
+
type=semver,pattern={{major}}.{{minor}}
|
401 |
+
type=raw,enable=${{ github.ref == 'refs/heads/main' }},prefix=,suffix=,value=ollama
|
402 |
+
flavor: |
|
403 |
+
latest=${{ github.ref == 'refs/heads/main' }}
|
404 |
+
suffix=-ollama,onlatest=true
|
405 |
+
|
406 |
+
- name: Create manifest list and push
|
407 |
+
working-directory: /tmp/digests
|
408 |
+
run: |
|
409 |
+
docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
|
410 |
+
$(printf '${{ env.FULL_IMAGE_NAME }}@sha256:%s ' *)
|
411 |
+
|
412 |
+
- name: Inspect image
|
413 |
+
run: |
|
414 |
+
docker buildx imagetools inspect ${{ env.FULL_IMAGE_NAME }}:${{ steps.meta.outputs.version }}
|
.github/workflows/format-backend.yaml
ADDED
@@ -0,0 +1,39 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
name: Python CI
|
2 |
+
|
3 |
+
on:
|
4 |
+
push:
|
5 |
+
branches:
|
6 |
+
- main
|
7 |
+
- dev
|
8 |
+
pull_request:
|
9 |
+
branches:
|
10 |
+
- main
|
11 |
+
- dev
|
12 |
+
|
13 |
+
jobs:
|
14 |
+
build:
|
15 |
+
name: 'Format Backend'
|
16 |
+
runs-on: ubuntu-latest
|
17 |
+
|
18 |
+
strategy:
|
19 |
+
matrix:
|
20 |
+
python-version: [3.11]
|
21 |
+
|
22 |
+
steps:
|
23 |
+
- uses: actions/checkout@v4
|
24 |
+
|
25 |
+
- name: Set up Python
|
26 |
+
uses: actions/setup-python@v4
|
27 |
+
with:
|
28 |
+
python-version: ${{ matrix.python-version }}
|
29 |
+
|
30 |
+
- name: Install dependencies
|
31 |
+
run: |
|
32 |
+
python -m pip install --upgrade pip
|
33 |
+
pip install black
|
34 |
+
|
35 |
+
- name: Format backend
|
36 |
+
run: npm run format:backend
|
37 |
+
|
38 |
+
- name: Check for changes after format
|
39 |
+
run: git diff --exit-code
|
.github/workflows/format-build-frontend.yaml
ADDED
@@ -0,0 +1,57 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
name: Frontend Build
|
2 |
+
|
3 |
+
on:
|
4 |
+
push:
|
5 |
+
branches:
|
6 |
+
- main
|
7 |
+
- dev
|
8 |
+
pull_request:
|
9 |
+
branches:
|
10 |
+
- main
|
11 |
+
- dev
|
12 |
+
|
13 |
+
jobs:
|
14 |
+
build:
|
15 |
+
name: 'Format & Build Frontend'
|
16 |
+
runs-on: ubuntu-latest
|
17 |
+
steps:
|
18 |
+
- name: Checkout Repository
|
19 |
+
uses: actions/checkout@v4
|
20 |
+
|
21 |
+
- name: Setup Node.js
|
22 |
+
uses: actions/setup-node@v4
|
23 |
+
with:
|
24 |
+
node-version: '20' # Or specify any other version you want to use
|
25 |
+
|
26 |
+
- name: Install Dependencies
|
27 |
+
run: npm install
|
28 |
+
|
29 |
+
- name: Format Frontend
|
30 |
+
run: npm run format
|
31 |
+
|
32 |
+
- name: Run i18next
|
33 |
+
run: npm run i18n:parse
|
34 |
+
|
35 |
+
- name: Check for Changes After Format
|
36 |
+
run: git diff --exit-code
|
37 |
+
|
38 |
+
- name: Build Frontend
|
39 |
+
run: npm run build
|
40 |
+
|
41 |
+
test-frontend:
|
42 |
+
name: 'Frontend Unit Tests'
|
43 |
+
runs-on: ubuntu-latest
|
44 |
+
steps:
|
45 |
+
- name: Checkout Repository
|
46 |
+
uses: actions/checkout@v4
|
47 |
+
|
48 |
+
- name: Setup Node.js
|
49 |
+
uses: actions/setup-node@v4
|
50 |
+
with:
|
51 |
+
node-version: '20'
|
52 |
+
|
53 |
+
- name: Install Dependencies
|
54 |
+
run: npm ci
|
55 |
+
|
56 |
+
- name: Run vitest
|
57 |
+
run: npm run test:frontend
|
.github/workflows/integration-test.yml
ADDED
@@ -0,0 +1,199 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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: Checkout Repository
|
19 |
+
uses: actions/checkout@v4
|
20 |
+
|
21 |
+
- name: Build and run Compose Stack
|
22 |
+
run: |
|
23 |
+
docker compose \
|
24 |
+
--file docker-compose.yaml \
|
25 |
+
--file docker-compose.api.yaml \
|
26 |
+
--file docker-compose.a1111-test.yaml \
|
27 |
+
up --detach --build
|
28 |
+
|
29 |
+
- name: Wait for Ollama to be up
|
30 |
+
timeout-minutes: 5
|
31 |
+
run: |
|
32 |
+
until curl --output /dev/null --silent --fail http://localhost:11434; do
|
33 |
+
printf '.'
|
34 |
+
sleep 1
|
35 |
+
done
|
36 |
+
echo "Service is up!"
|
37 |
+
|
38 |
+
- name: Preload Ollama model
|
39 |
+
run: |
|
40 |
+
docker exec ollama ollama pull qwen:0.5b-chat-v1.5-q2_K
|
41 |
+
|
42 |
+
- name: Cypress run
|
43 |
+
uses: cypress-io/github-action@v6
|
44 |
+
with:
|
45 |
+
browser: chrome
|
46 |
+
wait-on: 'http://localhost:3000'
|
47 |
+
config: baseUrl=http://localhost:3000
|
48 |
+
|
49 |
+
- uses: actions/upload-artifact@v4
|
50 |
+
if: always()
|
51 |
+
name: Upload Cypress videos
|
52 |
+
with:
|
53 |
+
name: cypress-videos
|
54 |
+
path: cypress/videos
|
55 |
+
if-no-files-found: ignore
|
56 |
+
|
57 |
+
- name: Extract Compose logs
|
58 |
+
if: always()
|
59 |
+
run: |
|
60 |
+
docker compose logs > compose-logs.txt
|
61 |
+
|
62 |
+
- uses: actions/upload-artifact@v4
|
63 |
+
if: always()
|
64 |
+
name: Upload Compose logs
|
65 |
+
with:
|
66 |
+
name: compose-logs
|
67 |
+
path: compose-logs.txt
|
68 |
+
if-no-files-found: ignore
|
69 |
+
|
70 |
+
migration_test:
|
71 |
+
name: Run Migration Tests
|
72 |
+
runs-on: ubuntu-latest
|
73 |
+
services:
|
74 |
+
postgres:
|
75 |
+
image: postgres
|
76 |
+
env:
|
77 |
+
POSTGRES_PASSWORD: postgres
|
78 |
+
options: >-
|
79 |
+
--health-cmd pg_isready
|
80 |
+
--health-interval 10s
|
81 |
+
--health-timeout 5s
|
82 |
+
--health-retries 5
|
83 |
+
ports:
|
84 |
+
- 5432:5432
|
85 |
+
# mysql:
|
86 |
+
# image: mysql
|
87 |
+
# env:
|
88 |
+
# MYSQL_ROOT_PASSWORD: mysql
|
89 |
+
# MYSQL_DATABASE: mysql
|
90 |
+
# options: >-
|
91 |
+
# --health-cmd "mysqladmin ping -h localhost"
|
92 |
+
# --health-interval 10s
|
93 |
+
# --health-timeout 5s
|
94 |
+
# --health-retries 5
|
95 |
+
# ports:
|
96 |
+
# - 3306:3306
|
97 |
+
steps:
|
98 |
+
- name: Checkout Repository
|
99 |
+
uses: actions/checkout@v4
|
100 |
+
|
101 |
+
- name: Set up Python
|
102 |
+
uses: actions/setup-python@v5
|
103 |
+
with:
|
104 |
+
python-version: ${{ matrix.python-version }}
|
105 |
+
|
106 |
+
- name: Set up uv
|
107 |
+
uses: yezz123/setup-uv@v4
|
108 |
+
with:
|
109 |
+
uv-venv: venv
|
110 |
+
|
111 |
+
- name: Activate virtualenv
|
112 |
+
run: |
|
113 |
+
. venv/bin/activate
|
114 |
+
echo PATH=$PATH >> $GITHUB_ENV
|
115 |
+
|
116 |
+
- name: Install dependencies
|
117 |
+
run: |
|
118 |
+
uv pip install -r backend/requirements.txt
|
119 |
+
|
120 |
+
- name: Test backend with SQLite
|
121 |
+
id: sqlite
|
122 |
+
env:
|
123 |
+
WEBUI_SECRET_KEY: secret-key
|
124 |
+
GLOBAL_LOG_LEVEL: debug
|
125 |
+
run: |
|
126 |
+
cd backend
|
127 |
+
uvicorn main:app --port "8080" --forwarded-allow-ips '*' &
|
128 |
+
UVICORN_PID=$!
|
129 |
+
# Wait up to 20 seconds for the server to start
|
130 |
+
for i in {1..20}; do
|
131 |
+
curl -s http://localhost:8080/api/config > /dev/null && break
|
132 |
+
sleep 1
|
133 |
+
if [ $i -eq 20 ]; then
|
134 |
+
echo "Server failed to start"
|
135 |
+
kill -9 $UVICORN_PID
|
136 |
+
exit 1
|
137 |
+
fi
|
138 |
+
done
|
139 |
+
# Check that the server is still running after 5 seconds
|
140 |
+
sleep 5
|
141 |
+
if ! kill -0 $UVICORN_PID; then
|
142 |
+
echo "Server has stopped"
|
143 |
+
exit 1
|
144 |
+
fi
|
145 |
+
|
146 |
+
|
147 |
+
- name: Test backend with Postgres
|
148 |
+
if: success() || steps.sqlite.conclusion == 'failure'
|
149 |
+
env:
|
150 |
+
WEBUI_SECRET_KEY: secret-key
|
151 |
+
GLOBAL_LOG_LEVEL: debug
|
152 |
+
DATABASE_URL: postgresql://postgres:postgres@localhost:5432/postgres
|
153 |
+
run: |
|
154 |
+
cd backend
|
155 |
+
uvicorn main:app --port "8081" --forwarded-allow-ips '*' &
|
156 |
+
UVICORN_PID=$!
|
157 |
+
# Wait up to 20 seconds for the server to start
|
158 |
+
for i in {1..20}; do
|
159 |
+
curl -s http://localhost:8081/api/config > /dev/null && break
|
160 |
+
sleep 1
|
161 |
+
if [ $i -eq 20 ]; then
|
162 |
+
echo "Server failed to start"
|
163 |
+
kill -9 $UVICORN_PID
|
164 |
+
exit 1
|
165 |
+
fi
|
166 |
+
done
|
167 |
+
# Check that the server is still running after 5 seconds
|
168 |
+
sleep 5
|
169 |
+
if ! kill -0 $UVICORN_PID; then
|
170 |
+
echo "Server has stopped"
|
171 |
+
exit 1
|
172 |
+
fi
|
173 |
+
|
174 |
+
# - name: Test backend with MySQL
|
175 |
+
# if: success() || steps.sqlite.conclusion == 'failure' || steps.postgres.conclusion == 'failure'
|
176 |
+
# env:
|
177 |
+
# WEBUI_SECRET_KEY: secret-key
|
178 |
+
# GLOBAL_LOG_LEVEL: debug
|
179 |
+
# DATABASE_URL: mysql://root:mysql@localhost:3306/mysql
|
180 |
+
# run: |
|
181 |
+
# cd backend
|
182 |
+
# uvicorn main:app --port "8083" --forwarded-allow-ips '*' &
|
183 |
+
# UVICORN_PID=$!
|
184 |
+
# # Wait up to 20 seconds for the server to start
|
185 |
+
# for i in {1..20}; do
|
186 |
+
# curl -s http://localhost:8083/api/config > /dev/null && break
|
187 |
+
# sleep 1
|
188 |
+
# if [ $i -eq 20 ]; then
|
189 |
+
# echo "Server failed to start"
|
190 |
+
# kill -9 $UVICORN_PID
|
191 |
+
# exit 1
|
192 |
+
# fi
|
193 |
+
# done
|
194 |
+
# # Check that the server is still running after 5 seconds
|
195 |
+
# sleep 5
|
196 |
+
# if ! kill -0 $UVICORN_PID; then
|
197 |
+
# echo "Server has stopped"
|
198 |
+
# exit 1
|
199 |
+
# fi
|
.github/workflows/lint-backend.disabled
ADDED
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
name: Python CI
|
2 |
+
on:
|
3 |
+
push:
|
4 |
+
branches: ['main']
|
5 |
+
pull_request:
|
6 |
+
jobs:
|
7 |
+
build:
|
8 |
+
name: 'Lint Backend'
|
9 |
+
env:
|
10 |
+
PUBLIC_API_BASE_URL: ''
|
11 |
+
runs-on: ubuntu-latest
|
12 |
+
strategy:
|
13 |
+
matrix:
|
14 |
+
node-version:
|
15 |
+
- latest
|
16 |
+
steps:
|
17 |
+
- uses: actions/checkout@v4
|
18 |
+
- name: Use Python
|
19 |
+
uses: actions/setup-python@v4
|
20 |
+
- name: Use Bun
|
21 |
+
uses: oven-sh/setup-bun@v1
|
22 |
+
- name: Install dependencies
|
23 |
+
run: |
|
24 |
+
python -m pip install --upgrade pip
|
25 |
+
pip install pylint
|
26 |
+
- name: Lint backend
|
27 |
+
run: bun run lint:backend
|
.github/workflows/lint-frontend.disabled
ADDED
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
name: Bun CI
|
2 |
+
on:
|
3 |
+
push:
|
4 |
+
branches: ['main']
|
5 |
+
pull_request:
|
6 |
+
jobs:
|
7 |
+
build:
|
8 |
+
name: 'Lint Frontend'
|
9 |
+
env:
|
10 |
+
PUBLIC_API_BASE_URL: ''
|
11 |
+
runs-on: ubuntu-latest
|
12 |
+
steps:
|
13 |
+
- uses: actions/checkout@v4
|
14 |
+
- name: Use Bun
|
15 |
+
uses: oven-sh/setup-bun@v1
|
16 |
+
- run: bun --version
|
17 |
+
- name: Install frontend dependencies
|
18 |
+
run: bun install --frozen-lockfile
|
19 |
+
- run: bun run lint:frontend
|
20 |
+
- run: bun run lint:types
|
21 |
+
if: success() || failure()
|
.github/workflows/release-pypi.yml
ADDED
@@ -0,0 +1,32 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
name: Release to PyPI
|
2 |
+
|
3 |
+
on:
|
4 |
+
push:
|
5 |
+
branches:
|
6 |
+
- main # or whatever branch you want to use
|
7 |
+
- dev
|
8 |
+
|
9 |
+
jobs:
|
10 |
+
release:
|
11 |
+
runs-on: ubuntu-latest
|
12 |
+
environment:
|
13 |
+
name: pypi
|
14 |
+
url: https://pypi.org/p/open-webui
|
15 |
+
permissions:
|
16 |
+
id-token: write
|
17 |
+
steps:
|
18 |
+
- name: Checkout repository
|
19 |
+
uses: actions/checkout@v4
|
20 |
+
- uses: actions/setup-node@v4
|
21 |
+
with:
|
22 |
+
node-version: 18
|
23 |
+
- uses: actions/setup-python@v5
|
24 |
+
with:
|
25 |
+
python-version: 3.11
|
26 |
+
- name: Build
|
27 |
+
run: |
|
28 |
+
python -m pip install --upgrade pip
|
29 |
+
pip install build
|
30 |
+
python -m build .
|
31 |
+
- name: Publish package distributions to PyPI
|
32 |
+
uses: pypa/gh-action-pypi-publish@release/v1
|
.gitignore
ADDED
@@ -0,0 +1,308 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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
|
.npmrc
ADDED
@@ -0,0 +1 @@
|
|
|
|
|
1 |
+
engine-strict=true
|
.prettierignore
ADDED
@@ -0,0 +1,316 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Ignore files for PNPM, NPM and YARN
|
2 |
+
pnpm-lock.yaml
|
3 |
+
package-lock.json
|
4 |
+
yarn.lock
|
5 |
+
|
6 |
+
kubernetes/
|
7 |
+
|
8 |
+
# Copy of .gitignore
|
9 |
+
.DS_Store
|
10 |
+
node_modules
|
11 |
+
/build
|
12 |
+
/.svelte-kit
|
13 |
+
/package
|
14 |
+
.env
|
15 |
+
.env.*
|
16 |
+
!.env.example
|
17 |
+
vite.config.js.timestamp-*
|
18 |
+
vite.config.ts.timestamp-*
|
19 |
+
# Byte-compiled / optimized / DLL files
|
20 |
+
__pycache__/
|
21 |
+
*.py[cod]
|
22 |
+
*$py.class
|
23 |
+
|
24 |
+
# C extensions
|
25 |
+
*.so
|
26 |
+
|
27 |
+
# Distribution / packaging
|
28 |
+
.Python
|
29 |
+
build/
|
30 |
+
develop-eggs/
|
31 |
+
dist/
|
32 |
+
downloads/
|
33 |
+
eggs/
|
34 |
+
.eggs/
|
35 |
+
lib64/
|
36 |
+
parts/
|
37 |
+
sdist/
|
38 |
+
var/
|
39 |
+
wheels/
|
40 |
+
share/python-wheels/
|
41 |
+
*.egg-info/
|
42 |
+
.installed.cfg
|
43 |
+
*.egg
|
44 |
+
MANIFEST
|
45 |
+
|
46 |
+
# PyInstaller
|
47 |
+
# Usually these files are written by a python script from a template
|
48 |
+
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
49 |
+
*.manifest
|
50 |
+
*.spec
|
51 |
+
|
52 |
+
# Installer logs
|
53 |
+
pip-log.txt
|
54 |
+
pip-delete-this-directory.txt
|
55 |
+
|
56 |
+
# Unit test / coverage reports
|
57 |
+
htmlcov/
|
58 |
+
.tox/
|
59 |
+
.nox/
|
60 |
+
.coverage
|
61 |
+
.coverage.*
|
62 |
+
.cache
|
63 |
+
nosetests.xml
|
64 |
+
coverage.xml
|
65 |
+
*.cover
|
66 |
+
*.py,cover
|
67 |
+
.hypothesis/
|
68 |
+
.pytest_cache/
|
69 |
+
cover/
|
70 |
+
|
71 |
+
# Translations
|
72 |
+
*.mo
|
73 |
+
*.pot
|
74 |
+
|
75 |
+
# Django stuff:
|
76 |
+
*.log
|
77 |
+
local_settings.py
|
78 |
+
db.sqlite3
|
79 |
+
db.sqlite3-journal
|
80 |
+
|
81 |
+
# Flask stuff:
|
82 |
+
instance/
|
83 |
+
.webassets-cache
|
84 |
+
|
85 |
+
# Scrapy stuff:
|
86 |
+
.scrapy
|
87 |
+
|
88 |
+
# Sphinx documentation
|
89 |
+
docs/_build/
|
90 |
+
|
91 |
+
# PyBuilder
|
92 |
+
.pybuilder/
|
93 |
+
target/
|
94 |
+
|
95 |
+
# Jupyter Notebook
|
96 |
+
.ipynb_checkpoints
|
97 |
+
|
98 |
+
# IPython
|
99 |
+
profile_default/
|
100 |
+
ipython_config.py
|
101 |
+
|
102 |
+
# pyenv
|
103 |
+
# For a library or package, you might want to ignore these files since the code is
|
104 |
+
# intended to run in multiple environments; otherwise, check them in:
|
105 |
+
# .python-version
|
106 |
+
|
107 |
+
# pipenv
|
108 |
+
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
109 |
+
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
110 |
+
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
111 |
+
# install all needed dependencies.
|
112 |
+
#Pipfile.lock
|
113 |
+
|
114 |
+
# poetry
|
115 |
+
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
116 |
+
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
117 |
+
# commonly ignored for libraries.
|
118 |
+
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
119 |
+
#poetry.lock
|
120 |
+
|
121 |
+
# pdm
|
122 |
+
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
123 |
+
#pdm.lock
|
124 |
+
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
|
125 |
+
# in version control.
|
126 |
+
# https://pdm.fming.dev/#use-with-ide
|
127 |
+
.pdm.toml
|
128 |
+
|
129 |
+
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
130 |
+
__pypackages__/
|
131 |
+
|
132 |
+
# Celery stuff
|
133 |
+
celerybeat-schedule
|
134 |
+
celerybeat.pid
|
135 |
+
|
136 |
+
# SageMath parsed files
|
137 |
+
*.sage.py
|
138 |
+
|
139 |
+
# Environments
|
140 |
+
.env
|
141 |
+
.venv
|
142 |
+
env/
|
143 |
+
venv/
|
144 |
+
ENV/
|
145 |
+
env.bak/
|
146 |
+
venv.bak/
|
147 |
+
|
148 |
+
# Spyder project settings
|
149 |
+
.spyderproject
|
150 |
+
.spyproject
|
151 |
+
|
152 |
+
# Rope project settings
|
153 |
+
.ropeproject
|
154 |
+
|
155 |
+
# mkdocs documentation
|
156 |
+
/site
|
157 |
+
|
158 |
+
# mypy
|
159 |
+
.mypy_cache/
|
160 |
+
.dmypy.json
|
161 |
+
dmypy.json
|
162 |
+
|
163 |
+
# Pyre type checker
|
164 |
+
.pyre/
|
165 |
+
|
166 |
+
# pytype static type analyzer
|
167 |
+
.pytype/
|
168 |
+
|
169 |
+
# Cython debug symbols
|
170 |
+
cython_debug/
|
171 |
+
|
172 |
+
# PyCharm
|
173 |
+
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
174 |
+
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
175 |
+
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
176 |
+
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
177 |
+
.idea/
|
178 |
+
|
179 |
+
# Logs
|
180 |
+
logs
|
181 |
+
*.log
|
182 |
+
npm-debug.log*
|
183 |
+
yarn-debug.log*
|
184 |
+
yarn-error.log*
|
185 |
+
lerna-debug.log*
|
186 |
+
.pnpm-debug.log*
|
187 |
+
|
188 |
+
# Diagnostic reports (https://nodejs.org/api/report.html)
|
189 |
+
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
190 |
+
|
191 |
+
# Runtime data
|
192 |
+
pids
|
193 |
+
*.pid
|
194 |
+
*.seed
|
195 |
+
*.pid.lock
|
196 |
+
|
197 |
+
# Directory for instrumented libs generated by jscoverage/JSCover
|
198 |
+
lib-cov
|
199 |
+
|
200 |
+
# Coverage directory used by tools like istanbul
|
201 |
+
coverage
|
202 |
+
*.lcov
|
203 |
+
|
204 |
+
# nyc test coverage
|
205 |
+
.nyc_output
|
206 |
+
|
207 |
+
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
|
208 |
+
.grunt
|
209 |
+
|
210 |
+
# Bower dependency directory (https://bower.io/)
|
211 |
+
bower_components
|
212 |
+
|
213 |
+
# node-waf configuration
|
214 |
+
.lock-wscript
|
215 |
+
|
216 |
+
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
217 |
+
build/Release
|
218 |
+
|
219 |
+
# Dependency directories
|
220 |
+
node_modules/
|
221 |
+
jspm_packages/
|
222 |
+
|
223 |
+
# Snowpack dependency directory (https://snowpack.dev/)
|
224 |
+
web_modules/
|
225 |
+
|
226 |
+
# TypeScript cache
|
227 |
+
*.tsbuildinfo
|
228 |
+
|
229 |
+
# Optional npm cache directory
|
230 |
+
.npm
|
231 |
+
|
232 |
+
# Optional eslint cache
|
233 |
+
.eslintcache
|
234 |
+
|
235 |
+
# Optional stylelint cache
|
236 |
+
.stylelintcache
|
237 |
+
|
238 |
+
# Microbundle cache
|
239 |
+
.rpt2_cache/
|
240 |
+
.rts2_cache_cjs/
|
241 |
+
.rts2_cache_es/
|
242 |
+
.rts2_cache_umd/
|
243 |
+
|
244 |
+
# Optional REPL history
|
245 |
+
.node_repl_history
|
246 |
+
|
247 |
+
# Output of 'npm pack'
|
248 |
+
*.tgz
|
249 |
+
|
250 |
+
# Yarn Integrity file
|
251 |
+
.yarn-integrity
|
252 |
+
|
253 |
+
# dotenv environment variable files
|
254 |
+
.env
|
255 |
+
.env.development.local
|
256 |
+
.env.test.local
|
257 |
+
.env.production.local
|
258 |
+
.env.local
|
259 |
+
|
260 |
+
# parcel-bundler cache (https://parceljs.org/)
|
261 |
+
.cache
|
262 |
+
.parcel-cache
|
263 |
+
|
264 |
+
# Next.js build output
|
265 |
+
.next
|
266 |
+
out
|
267 |
+
|
268 |
+
# Nuxt.js build / generate output
|
269 |
+
.nuxt
|
270 |
+
dist
|
271 |
+
|
272 |
+
# Gatsby files
|
273 |
+
.cache/
|
274 |
+
# Comment in the public line in if your project uses Gatsby and not Next.js
|
275 |
+
# https://nextjs.org/blog/next-9-1#public-directory-support
|
276 |
+
# public
|
277 |
+
|
278 |
+
# vuepress build output
|
279 |
+
.vuepress/dist
|
280 |
+
|
281 |
+
# vuepress v2.x temp and cache directory
|
282 |
+
.temp
|
283 |
+
.cache
|
284 |
+
|
285 |
+
# Docusaurus cache and generated files
|
286 |
+
.docusaurus
|
287 |
+
|
288 |
+
# Serverless directories
|
289 |
+
.serverless/
|
290 |
+
|
291 |
+
# FuseBox cache
|
292 |
+
.fusebox/
|
293 |
+
|
294 |
+
# DynamoDB Local files
|
295 |
+
.dynamodb/
|
296 |
+
|
297 |
+
# TernJS port file
|
298 |
+
.tern-port
|
299 |
+
|
300 |
+
# Stores VSCode versions used for testing VSCode extensions
|
301 |
+
.vscode-test
|
302 |
+
|
303 |
+
# yarn v2
|
304 |
+
.yarn/cache
|
305 |
+
.yarn/unplugged
|
306 |
+
.yarn/build-state.yml
|
307 |
+
.yarn/install-state.gz
|
308 |
+
.pnp.*
|
309 |
+
|
310 |
+
# cypress artifacts
|
311 |
+
cypress/videos
|
312 |
+
cypress/screenshots
|
313 |
+
|
314 |
+
|
315 |
+
|
316 |
+
/static/*
|
.prettierrc
ADDED
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
{
|
2 |
+
"useTabs": true,
|
3 |
+
"singleQuote": true,
|
4 |
+
"trailingComma": "none",
|
5 |
+
"printWidth": 100,
|
6 |
+
"plugins": ["prettier-plugin-svelte"],
|
7 |
+
"pluginSearchDirs": ["."],
|
8 |
+
"overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }]
|
9 |
+
}
|
CHANGELOG.md
ADDED
@@ -0,0 +1,424 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Changelog
|
2 |
+
|
3 |
+
All notable changes to this project will be documented in this file.
|
4 |
+
|
5 |
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
6 |
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
7 |
+
|
8 |
+
## [0.1.125] - 2024-05-19
|
9 |
+
|
10 |
+
### Added
|
11 |
+
|
12 |
+
- **🔄 Updated UI**: Chat interface revamped with chat bubbles. Easily switch back to the old style via settings > interface > chat bubble UI.
|
13 |
+
- **📂 Enhanced Sidebar UI**: Model files, documents, prompts, and playground merged into Workspace for streamlined access.
|
14 |
+
- **🚀 Improved Many Model Interaction**: All responses now displayed simultaneously for a smoother experience.
|
15 |
+
- **🐍 Python Code Execution**: Execute Python code locally in the browser with libraries like 'requests', 'beautifulsoup4', 'numpy', 'pandas', 'seaborn', 'matplotlib', 'scikit-learn', 'scipy', 'regex'.
|
16 |
+
- **🧠 Experimental Memory Feature**: Manually input personal information you want LLMs to remember via settings > personalization > memory.
|
17 |
+
- **💾 Persistent Settings**: Settings now saved as config.json for convenience.
|
18 |
+
- **🩺 Health Check Endpoint**: Added for Docker deployment.
|
19 |
+
- **↕️ RTL Support**: Toggle chat direction via settings > interface > chat direction.
|
20 |
+
- **🖥️ PowerPoint Support**: RAG pipeline now supports PowerPoint documents.
|
21 |
+
- **🌐 Language Updates**: Ukrainian, Turkish, Arabic, Chinese, Serbian, Vietnamese updated; Punjabi added.
|
22 |
+
|
23 |
+
### Changed
|
24 |
+
|
25 |
+
- **👤 Shared Chat Update**: Shared chat now includes creator user information.
|
26 |
+
|
27 |
+
## [0.1.124] - 2024-05-08
|
28 |
+
|
29 |
+
### Added
|
30 |
+
|
31 |
+
- **🖼️ Improved Chat Sidebar**: Now conveniently displays time ranges and organizes chats by today, yesterday, and more.
|
32 |
+
- **📜 Citations in RAG Feature**: Easily track the context fed to the LLM with added citations in the RAG feature.
|
33 |
+
- **🔒 Auth Disable Option**: Introducing the ability to disable authentication. Set 'WEBUI_AUTH' to False to disable authentication. Note: Only applicable for fresh installations without existing users.
|
34 |
+
- **📹 Enhanced YouTube RAG Pipeline**: Now supports non-English videos for an enriched experience.
|
35 |
+
- **🔊 Specify OpenAI TTS Models**: Customize your TTS experience by specifying OpenAI TTS models.
|
36 |
+
- **🔧 Additional Environment Variables**: Discover more environment variables in our comprehensive documentation at Open WebUI Documentation (https://docs.openwebui.com).
|
37 |
+
- **🌐 Language Support**: Arabic, Finnish, and Hindi added; Improved support for German, Vietnamese, and Chinese.
|
38 |
+
|
39 |
+
### Fixed
|
40 |
+
|
41 |
+
- **🛠️ Model Selector Styling**: Addressed styling issues for improved user experience.
|
42 |
+
- **⚠️ Warning Messages**: Resolved backend warning messages.
|
43 |
+
|
44 |
+
### Changed
|
45 |
+
|
46 |
+
- **📝 Title Generation**: Limited output to 50 tokens.
|
47 |
+
- **📦 Helm Charts**: Removed Helm charts, now available in a separate repository (https://github.com/open-webui/helm-charts).
|
48 |
+
|
49 |
+
## [0.1.123] - 2024-05-02
|
50 |
+
|
51 |
+
### Added
|
52 |
+
|
53 |
+
- **🎨 New Landing Page Design**: Refreshed design for a more modern look and optimized use of screen space.
|
54 |
+
- **📹 Youtube RAG Pipeline**: Introduces dedicated RAG pipeline for Youtube videos, enabling interaction with video transcriptions directly.
|
55 |
+
- **🔧 Enhanced Admin Panel**: Streamlined user management with options to add users directly or in bulk via CSV import.
|
56 |
+
- **👥 '@' Model Integration**: Easily switch to specific models during conversations; old collaborative chat feature phased out.
|
57 |
+
- **🌐 Language Enhancements**: Swedish translation added, plus improvements to German, Spanish, and the addition of Doge translation.
|
58 |
+
|
59 |
+
### Fixed
|
60 |
+
|
61 |
+
- **🗑️ Delete Chat Shortcut**: Addressed issue where shortcut wasn't functioning.
|
62 |
+
- **🖼️ Modal Closing Bug**: Resolved unexpected closure of modal when dragging from within.
|
63 |
+
- **✏️ Edit Button Styling**: Fixed styling inconsistency with edit buttons.
|
64 |
+
- **🌐 Image Generation Compatibility Issue**: Rectified image generation compatibility issue with third-party APIs.
|
65 |
+
- **📱 iOS PWA Icon Fix**: Corrected iOS PWA home screen icon shape.
|
66 |
+
- **🔍 Scroll Gesture Bug**: Adjusted gesture sensitivity to prevent accidental activation when scrolling through code on mobile; now requires scrolling from the leftmost side to open the sidebar.
|
67 |
+
|
68 |
+
### Changed
|
69 |
+
|
70 |
+
- **🔄 Unlimited Context Length**: Advanced settings now allow unlimited max context length (previously limited to 16000).
|
71 |
+
- **👑 Super Admin Assignment**: The first signup is automatically assigned a super admin role, unchangeable by other admins.
|
72 |
+
- **🛡️ Admin User Restrictions**: User action buttons from the admin panel are now disabled for users with admin roles.
|
73 |
+
- **🔝 Default Model Selector**: Set as default model option now exclusively available on the landing page.
|
74 |
+
|
75 |
+
## [0.1.122] - 2024-04-27
|
76 |
+
|
77 |
+
### Added
|
78 |
+
|
79 |
+
- **🌟 Enhanced RAG Pipeline**: Now with hybrid searching via 'BM25', reranking powered by 'CrossEncoder', and configurable relevance score thresholds.
|
80 |
+
- **🛢️ External Database Support**: Seamlessly connect to custom SQLite or Postgres databases using the 'DATABASE_URL' environment variable.
|
81 |
+
- **🌐 Remote ChromaDB Support**: Introducing the capability to connect to remote ChromaDB servers.
|
82 |
+
- **👨💼 Improved Admin Panel**: Admins can now conveniently check users' chat lists and last active status directly from the admin panel.
|
83 |
+
- **🎨 Splash Screen**: Introducing a loading splash screen for a smoother user experience.
|
84 |
+
- **🌍 Language Support Expansion**: Added support for Bangla (bn-BD), along with enhancements to Chinese, Spanish, and Ukrainian translations.
|
85 |
+
- **💻 Improved LaTeX Rendering Performance**: Enjoy faster rendering times for LaTeX equations.
|
86 |
+
- **🔧 More Environment Variables**: Explore additional environment variables in our documentation (https://docs.openwebui.com), including the 'ENABLE_LITELLM' option to manage memory usage.
|
87 |
+
|
88 |
+
### Fixed
|
89 |
+
|
90 |
+
- **🔧 Ollama Compatibility**: Resolved errors occurring when Ollama server version isn't an integer, such as SHA builds or RCs.
|
91 |
+
- **🐛 Various OpenAI API Issues**: Addressed several issues related to the OpenAI API.
|
92 |
+
- **🛑 Stop Sequence Issue**: Fixed the problem where the stop sequence with a backslash '\' was not functioning.
|
93 |
+
- **🔤 Font Fallback**: Corrected font fallback issue.
|
94 |
+
|
95 |
+
### Changed
|
96 |
+
|
97 |
+
- **⌨️ Prompt Input Behavior on Mobile**: Enter key prompt submission disabled on mobile devices for improved user experience.
|
98 |
+
|
99 |
+
## [0.1.121] - 2024-04-24
|
100 |
+
|
101 |
+
### Fixed
|
102 |
+
|
103 |
+
- **🔧 Translation Issues**: Addressed various translation discrepancies.
|
104 |
+
- **🔒 LiteLLM Security Fix**: Updated LiteLLM version to resolve a security vulnerability.
|
105 |
+
- **🖥️ HTML Tag Display**: Rectified the issue where the '< br >' tag wasn't displaying correctly.
|
106 |
+
- **🔗 WebSocket Connection**: Resolved the failure of WebSocket connection under HTTPS security for ComfyUI server.
|
107 |
+
- **📜 FileReader Optimization**: Implemented FileReader initialization per image in multi-file drag & drop to ensure reusability.
|
108 |
+
- **🏷️ Tag Display**: Corrected tag display inconsistencies.
|
109 |
+
- **📦 Archived Chat Styling**: Fixed styling issues in archived chat.
|
110 |
+
- **🔖 Safari Copy Button Bug**: Addressed the bug where the copy button failed to copy links in Safari.
|
111 |
+
|
112 |
+
## [0.1.120] - 2024-04-20
|
113 |
+
|
114 |
+
### Added
|
115 |
+
|
116 |
+
- **📦 Archive Chat Feature**: Easily archive chats with a new sidebar button, and access archived chats via the profile button > archived chats.
|
117 |
+
- **🔊 Configurable Text-to-Speech Endpoint**: Customize your Text-to-Speech experience with configurable OpenAI endpoints.
|
118 |
+
- **🛠️ Improved Error Handling**: Enhanced error message handling for connection failures.
|
119 |
+
- **⌨️ Enhanced Shortcut**: When editing messages, use ctrl/cmd+enter to save and submit, and esc to close.
|
120 |
+
- **🌐 Language Support**: Added support for Georgian and enhanced translations for Portuguese and Vietnamese.
|
121 |
+
|
122 |
+
### Fixed
|
123 |
+
|
124 |
+
- **🔧 Model Selector**: Resolved issue where default model selection was not saving.
|
125 |
+
- **🔗 Share Link Copy Button**: Fixed bug where the copy button wasn't copying links in Safari.
|
126 |
+
- **🎨 Light Theme Styling**: Addressed styling issue with the light theme.
|
127 |
+
|
128 |
+
## [0.1.119] - 2024-04-16
|
129 |
+
|
130 |
+
### Added
|
131 |
+
|
132 |
+
- **🌟 Enhanced RAG Embedding Support**: Ollama, and OpenAI models can now be used for RAG embedding model.
|
133 |
+
- **🔄 Seamless Integration**: Copy 'ollama run <model name>' directly from Ollama page to easily select and pull models.
|
134 |
+
- **🏷️ Tagging Feature**: Add tags to chats directly via the sidebar chat menu.
|
135 |
+
- **📱 Mobile Accessibility**: Swipe left and right on mobile to effortlessly open and close the sidebar.
|
136 |
+
- **🔍 Improved Navigation**: Admin panel now supports pagination for user list.
|
137 |
+
- **🌍 Additional Language Support**: Added Polish language support.
|
138 |
+
|
139 |
+
### Fixed
|
140 |
+
|
141 |
+
- **🌍 Language Enhancements**: Vietnamese and Spanish translations have been improved.
|
142 |
+
- **🔧 Helm Fixes**: Resolved issues with Helm trailing slash and manifest.json.
|
143 |
+
|
144 |
+
### Changed
|
145 |
+
|
146 |
+
- **🐳 Docker Optimization**: Updated docker image build process to utilize 'uv' for significantly faster builds compared to 'pip3'.
|
147 |
+
|
148 |
+
## [0.1.118] - 2024-04-10
|
149 |
+
|
150 |
+
### Added
|
151 |
+
|
152 |
+
- **🦙 Ollama and CUDA Images**: Added support for ':ollama' and ':cuda' tagged images.
|
153 |
+
- **👍 Enhanced Response Rating**: Now you can annotate your ratings for better feedback.
|
154 |
+
- **👤 User Initials Profile Photo**: User initials are now the default profile photo.
|
155 |
+
- **🔍 Update RAG Embedding Model**: Customize RAG embedding model directly in document settings.
|
156 |
+
- **🌍 Additional Language Support**: Added Turkish language support.
|
157 |
+
|
158 |
+
### Fixed
|
159 |
+
|
160 |
+
- **🔒 Share Chat Permission**: Resolved issue with chat sharing permissions.
|
161 |
+
- **🛠 Modal Close**: Modals can now be closed using the Esc key.
|
162 |
+
|
163 |
+
### Changed
|
164 |
+
|
165 |
+
- **🎨 Admin Panel Styling**: Refreshed styling for the admin panel.
|
166 |
+
- **🐳 Docker Image Build**: Updated docker image build process for improved efficiency.
|
167 |
+
|
168 |
+
## [0.1.117] - 2024-04-03
|
169 |
+
|
170 |
+
### Added
|
171 |
+
|
172 |
+
- 🗨️ **Local Chat Sharing**: Share chat links seamlessly between users.
|
173 |
+
- 🔑 **API Key Generation Support**: Generate secret keys to leverage Open WebUI with OpenAI libraries.
|
174 |
+
- 📄 **Chat Download as PDF**: Easily download chats in PDF format.
|
175 |
+
- 📝 **Improved Logging**: Enhancements to logging functionality.
|
176 |
+
- 📧 **Trusted Email Authentication**: Authenticate using a trusted email header.
|
177 |
+
|
178 |
+
### Fixed
|
179 |
+
|
180 |
+
- 🌷 **Enhanced Dutch Translation**: Improved translation for Dutch users.
|
181 |
+
- ⚪ **White Theme Styling**: Resolved styling issue with the white theme.
|
182 |
+
- 📜 **LaTeX Chat Screen Overflow**: Fixed screen overflow issue with LaTeX rendering.
|
183 |
+
- 🔒 **Security Patches**: Applied necessary security patches.
|
184 |
+
|
185 |
+
## [0.1.116] - 2024-03-31
|
186 |
+
|
187 |
+
### Added
|
188 |
+
|
189 |
+
- **🔄 Enhanced UI**: Model selector now conveniently located in the navbar, enabling seamless switching between multiple models during conversations.
|
190 |
+
- **🔍 Improved Model Selector**: Directly pull a model from the selector/Models now display detailed information for better understanding.
|
191 |
+
- **💬 Webhook Support**: Now compatible with Google Chat and Microsoft Teams.
|
192 |
+
- **🌐 Localization**: Korean translation (I18n) now available.
|
193 |
+
- **🌑 Dark Theme**: OLED dark theme introduced for reduced strain during prolonged usage.
|
194 |
+
- **🏷️ Tag Autocomplete**: Dropdown feature added for effortless chat tagging.
|
195 |
+
|
196 |
+
### Fixed
|
197 |
+
|
198 |
+
- **🔽 Auto-Scrolling**: Addressed OpenAI auto-scrolling issue.
|
199 |
+
- **🏷️ Tag Validation**: Implemented tag validation to prevent empty string tags.
|
200 |
+
- **🚫 Model Whitelisting**: Resolved LiteLLM model whitelisting issue.
|
201 |
+
- **✅ Spelling**: Corrected various spelling issues for improved readability.
|
202 |
+
|
203 |
+
## [0.1.115] - 2024-03-24
|
204 |
+
|
205 |
+
### Added
|
206 |
+
|
207 |
+
- **🔍 Custom Model Selector**: Easily find and select custom models with the new search filter feature.
|
208 |
+
- **🛑 Cancel Model Download**: Added the ability to cancel model downloads.
|
209 |
+
- **🎨 Image Generation ComfyUI**: Image generation now supports ComfyUI.
|
210 |
+
- **🌟 Updated Light Theme**: Updated the light theme for a fresh look.
|
211 |
+
- **🌍 Additional Language Support**: Now supporting Bulgarian, Italian, Portuguese, Japanese, and Dutch.
|
212 |
+
|
213 |
+
### Fixed
|
214 |
+
|
215 |
+
- **🔧 Fixed Broken Experimental GGUF Upload**: Resolved issues with experimental GGUF upload functionality.
|
216 |
+
|
217 |
+
### Changed
|
218 |
+
|
219 |
+
- **🔄 Vector Storage Reset Button**: Moved the reset vector storage button to document settings.
|
220 |
+
|
221 |
+
## [0.1.114] - 2024-03-20
|
222 |
+
|
223 |
+
### Added
|
224 |
+
|
225 |
+
- **🔗 Webhook Integration**: Now you can subscribe to new user sign-up events via webhook. Simply navigate to the admin panel > admin settings > webhook URL.
|
226 |
+
- **🛡️ Enhanced Model Filtering**: Alongside Ollama, OpenAI proxy model whitelisting, we've added model filtering functionality for LiteLLM proxy.
|
227 |
+
- **🌍 Expanded Language Support**: Spanish, Catalan, and Vietnamese languages are now available, with improvements made to others.
|
228 |
+
|
229 |
+
### Fixed
|
230 |
+
|
231 |
+
- **🔧 Input Field Spelling**: Resolved issue with spelling mistakes in input fields.
|
232 |
+
- **🖊️ Light Mode Styling**: Fixed styling issue with light mode in document adding.
|
233 |
+
|
234 |
+
### Changed
|
235 |
+
|
236 |
+
- **🔄 Language Sorting**: Languages are now sorted alphabetically by their code for improved organization.
|
237 |
+
|
238 |
+
## [0.1.113] - 2024-03-18
|
239 |
+
|
240 |
+
### Added
|
241 |
+
|
242 |
+
- 🌍 **Localization**: You can now change the UI language in Settings > General. We support Ukrainian, German, Farsi (Persian), Traditional and Simplified Chinese and French translations. You can help us to translate the UI into your language! More info in our [CONTRIBUTION.md](https://github.com/open-webui/open-webui/blob/main/docs/CONTRIBUTING.md#-translations-and-internationalization).
|
243 |
+
- 🎨 **System-wide Theme**: Introducing a new system-wide theme for enhanced visual experience.
|
244 |
+
|
245 |
+
### Fixed
|
246 |
+
|
247 |
+
- 🌑 **Dark Background on Select Fields**: Improved readability by adding a dark background to select fields, addressing issues on certain browsers/devices.
|
248 |
+
- **Multiple OPENAI_API_BASE_URLS Issue**: Resolved issue where multiple base URLs caused conflicts when one wasn't functioning.
|
249 |
+
- **RAG Encoding Issue**: Fixed encoding problem in RAG.
|
250 |
+
- **npm Audit Fix**: Addressed npm audit findings.
|
251 |
+
- **Reduced Scroll Threshold**: Improved auto-scroll experience by reducing the scroll threshold from 50px to 5px.
|
252 |
+
|
253 |
+
### Changed
|
254 |
+
|
255 |
+
- 🔄 **Sidebar UI Update**: Updated sidebar UI to feature a chat menu dropdown, replacing two icons for improved navigation.
|
256 |
+
|
257 |
+
## [0.1.112] - 2024-03-15
|
258 |
+
|
259 |
+
### Fixed
|
260 |
+
|
261 |
+
- 🗨️ Resolved chat malfunction after image generation.
|
262 |
+
- 🎨 Fixed various RAG issues.
|
263 |
+
- 🧪 Rectified experimental broken GGUF upload logic.
|
264 |
+
|
265 |
+
## [0.1.111] - 2024-03-10
|
266 |
+
|
267 |
+
### Added
|
268 |
+
|
269 |
+
- 🛡️ **Model Whitelisting**: Admins now have the ability to whitelist models for users with the 'user' role.
|
270 |
+
- 🔄 **Update All Models**: Added a convenient button to update all models at once.
|
271 |
+
- 📄 **Toggle PDF OCR**: Users can now toggle PDF OCR option for improved parsing performance.
|
272 |
+
- 🎨 **DALL-E Integration**: Introduced DALL-E integration for image generation alongside automatic1111.
|
273 |
+
- 🛠️ **RAG API Refactoring**: Refactored RAG logic and exposed its API, with additional documentation to follow.
|
274 |
+
|
275 |
+
### Fixed
|
276 |
+
|
277 |
+
- 🔒 **Max Token Settings**: Added max token settings for anthropic/claude-3-sonnet-20240229 (Issue #1094).
|
278 |
+
- 🔧 **Misalignment Issue**: Corrected misalignment of Edit and Delete Icons when Chat Title is Empty (Issue #1104).
|
279 |
+
- 🔄 **Context Loss Fix**: Resolved RAG losing context on model response regeneration with Groq models via API key (Issue #1105).
|
280 |
+
- 📁 **File Handling Bug**: Addressed File Not Found Notification when Dropping a Conversation Element (Issue #1098).
|
281 |
+
- 🖱️ **Dragged File Styling**: Fixed dragged file layover styling issue.
|
282 |
+
|
283 |
+
## [0.1.110] - 2024-03-06
|
284 |
+
|
285 |
+
### Added
|
286 |
+
|
287 |
+
- **🌐 Multiple OpenAI Servers Support**: Enjoy seamless integration with multiple OpenAI-compatible APIs, now supported natively.
|
288 |
+
|
289 |
+
### Fixed
|
290 |
+
|
291 |
+
- **🔍 OCR Issue**: Resolved PDF parsing issue caused by OCR malfunction.
|
292 |
+
- **🚫 RAG Issue**: Fixed the RAG functionality, ensuring it operates smoothly.
|
293 |
+
- **📄 "Add Docs" Model Button**: Addressed the non-functional behavior of the "Add Docs" model button.
|
294 |
+
|
295 |
+
## [0.1.109] - 2024-03-06
|
296 |
+
|
297 |
+
### Added
|
298 |
+
|
299 |
+
- **🔄 Multiple Ollama Servers Support**: Enjoy enhanced scalability and performance with support for multiple Ollama servers in a single WebUI. Load balancing features are now available, providing improved efficiency (#788, #278).
|
300 |
+
- **🔧 Support for Claude 3 and Gemini**: Responding to user requests, we've expanded our toolset to include Claude 3 and Gemini, offering a wider range of functionalities within our platform (#1064).
|
301 |
+
- **🔍 OCR Functionality for PDF Loader**: We've augmented our PDF loader with Optical Character Recognition (OCR) capabilities. Now, extract text from scanned documents and images within PDFs, broadening the scope of content processing (#1050).
|
302 |
+
|
303 |
+
### Fixed
|
304 |
+
|
305 |
+
- **🛠️ RAG Collection**: Implemented a dynamic mechanism to recreate RAG collections, ensuring users have up-to-date and accurate data (#1031).
|
306 |
+
- **📝 User Agent Headers**: Fixed issue of RAG web requests being sent with empty user_agent headers, reducing rejections from certain websites. Realistic headers are now utilized for these requests (#1024).
|
307 |
+
- **⏹️ Playground Cancel Functionality**: Introducing a new "Cancel" option for stopping Ollama generation in the Playground, enhancing user control and usability (#1006).
|
308 |
+
- **🔤 Typographical Error in 'ASSISTANT' Field**: Corrected a typographical error in the 'ASSISTANT' field within the GGUF model upload template for accuracy and consistency (#1061).
|
309 |
+
|
310 |
+
### Changed
|
311 |
+
|
312 |
+
- **🔄 Refactored Message Deletion Logic**: Streamlined message deletion process for improved efficiency and user experience, simplifying interactions within the platform (#1004).
|
313 |
+
- **⚠️ Deprecation of `OLLAMA_API_BASE_URL`**: Deprecated `OLLAMA_API_BASE_URL` environment variable; recommend using `OLLAMA_BASE_URL` instead. Refer to our documentation for further details.
|
314 |
+
|
315 |
+
## [0.1.108] - 2024-03-02
|
316 |
+
|
317 |
+
### Added
|
318 |
+
|
319 |
+
- **🎮 Playground Feature (Beta)**: Explore the full potential of the raw API through an intuitive UI with our new playground feature, accessible to admins. Simply click on the bottom name area of the sidebar to access it. The playground feature offers two modes text completion (notebook) and chat completion. As it's in beta, please report any issues you encounter.
|
320 |
+
- **🛠️ Direct Database Download for Admins**: Admins can now download the database directly from the WebUI via the admin settings.
|
321 |
+
- **🎨 Additional RAG Settings**: Customize your RAG process with the ability to edit the TOP K value. Navigate to Documents > Settings > General to make changes.
|
322 |
+
- **🖥️ UI Improvements**: Tooltips now available in the input area and sidebar handle. More tooltips will be added across other parts of the UI.
|
323 |
+
|
324 |
+
### Fixed
|
325 |
+
|
326 |
+
- Resolved input autofocus issue on mobile when the sidebar is open, making it easier to use.
|
327 |
+
- Corrected numbered list display issue in Safari (#963).
|
328 |
+
- Restricted user ability to delete chats without proper permissions (#993).
|
329 |
+
|
330 |
+
### Changed
|
331 |
+
|
332 |
+
- **Simplified Ollama Settings**: Ollama settings now don't require the `/api` suffix. You can now utilize the Ollama base URL directly, e.g., `http://localhost:11434`. Also, an `OLLAMA_BASE_URL` environment variable has been added.
|
333 |
+
- **Database Renaming**: Starting from this release, `ollama.db` will be automatically renamed to `webui.db`.
|
334 |
+
|
335 |
+
## [0.1.107] - 2024-03-01
|
336 |
+
|
337 |
+
### Added
|
338 |
+
|
339 |
+
- **🚀 Makefile and LLM Update Script**: Included Makefile and a script for LLM updates in the repository.
|
340 |
+
|
341 |
+
### Fixed
|
342 |
+
|
343 |
+
- Corrected issue where links in the settings modal didn't appear clickable (#960).
|
344 |
+
- Fixed problem with web UI port not taking effect due to incorrect environment variable name in run-compose.sh (#996).
|
345 |
+
- Enhanced user experience by displaying chat in browser title and enabling automatic scrolling to the bottom (#992).
|
346 |
+
|
347 |
+
### Changed
|
348 |
+
|
349 |
+
- Upgraded toast library from `svelte-french-toast` to `svelte-sonner` for a more polished UI.
|
350 |
+
- Enhanced accessibility with the addition of dark mode on the authentication page.
|
351 |
+
|
352 |
+
## [0.1.106] - 2024-02-27
|
353 |
+
|
354 |
+
### Added
|
355 |
+
|
356 |
+
- **🎯 Auto-focus Feature**: The input area now automatically focuses when initiating or opening a chat conversation.
|
357 |
+
|
358 |
+
### Fixed
|
359 |
+
|
360 |
+
- Corrected typo from "HuggingFace" to "Hugging Face" (Issue #924).
|
361 |
+
- Resolved bug causing errors in chat completion API calls to OpenAI due to missing "num_ctx" parameter (Issue #927).
|
362 |
+
- Fixed issues preventing text editing, selection, and cursor retention in the input field (Issue #940).
|
363 |
+
- Fixed a bug where defining an OpenAI-compatible API server using 'OPENAI_API_BASE_URL' containing 'openai' string resulted in hiding models not containing 'gpt' string from the model menu. (Issue #930)
|
364 |
+
|
365 |
+
## [0.1.105] - 2024-02-25
|
366 |
+
|
367 |
+
### Added
|
368 |
+
|
369 |
+
- **📄 Document Selection**: Now you can select and delete multiple documents at once for easier management.
|
370 |
+
|
371 |
+
### Changed
|
372 |
+
|
373 |
+
- **🏷️ Document Pre-tagging**: Simply click the "+" button at the top, enter tag names in the popup window, or select from a list of existing tags. Then, upload files with the added tags for streamlined organization.
|
374 |
+
|
375 |
+
## [0.1.104] - 2024-02-25
|
376 |
+
|
377 |
+
### Added
|
378 |
+
|
379 |
+
- **🔄 Check for Updates**: Keep your system current by checking for updates conveniently located in Settings > About.
|
380 |
+
- **🗑️ Automatic Tag Deletion**: Unused tags on the sidebar will now be deleted automatically with just a click.
|
381 |
+
|
382 |
+
### Changed
|
383 |
+
|
384 |
+
- **🎨 Modernized Styling**: Enjoy a refreshed look with updated styling for a more contemporary experience.
|
385 |
+
|
386 |
+
## [0.1.103] - 2024-02-25
|
387 |
+
|
388 |
+
### Added
|
389 |
+
|
390 |
+
- **🔗 Built-in LiteLLM Proxy**: Now includes LiteLLM proxy within Open WebUI for enhanced functionality.
|
391 |
+
|
392 |
+
- Easily integrate existing LiteLLM configurations using `-v /path/to/config.yaml:/app/backend/data/litellm/config.yaml` flag.
|
393 |
+
- When utilizing Docker container to run Open WebUI, ensure connections to localhost use `host.docker.internal`.
|
394 |
+
|
395 |
+
- **🖼️ Image Generation Enhancements**: Introducing Advanced Settings with Image Preview Feature.
|
396 |
+
- Customize image generation by setting the number of steps; defaults to A1111 value.
|
397 |
+
|
398 |
+
### Fixed
|
399 |
+
|
400 |
+
- Resolved issue with RAG scan halting document loading upon encountering unsupported MIME types or exceptions (Issue #866).
|
401 |
+
|
402 |
+
### Changed
|
403 |
+
|
404 |
+
- Ollama is no longer required to run Open WebUI.
|
405 |
+
- Access our comprehensive documentation at [Open WebUI Documentation](https://docs.openwebui.com/).
|
406 |
+
|
407 |
+
## [0.1.102] - 2024-02-22
|
408 |
+
|
409 |
+
### Added
|
410 |
+
|
411 |
+
- **🖼️ Image Generation**: Generate Images using the AUTOMATIC1111/stable-diffusion-webui API. You can set this up in Settings > Images.
|
412 |
+
- **📝 Change title generation prompt**: Change the prompt used to generate titles for your chats. You can set this up in the Settings > Interface.
|
413 |
+
- **🤖 Change embedding model**: Change the embedding model used to generate embeddings for your chats in the Dockerfile. Use any sentence transformer model from huggingface.co.
|
414 |
+
- **📢 CHANGELOG.md/Popup**: This popup will show you the latest changes.
|
415 |
+
|
416 |
+
## [0.1.101] - 2024-02-22
|
417 |
+
|
418 |
+
### Fixed
|
419 |
+
|
420 |
+
- LaTex output formatting issue (#828)
|
421 |
+
|
422 |
+
### Changed
|
423 |
+
|
424 |
+
- Instead of having the previous 1.0.0-alpha.101, we switched to semantic versioning as a way to respect global conventions.
|
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,158 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# syntax=docker/dockerfile:1
|
2 |
+
# Initialize device type args
|
3 |
+
# use build args in the docker build commmand with --build-arg="BUILDARG=true"
|
4 |
+
ARG USE_CUDA=false
|
5 |
+
ARG USE_OLLAMA=false
|
6 |
+
# Tested with cu117 for CUDA 11 and cu121 for CUDA 12 (default)
|
7 |
+
ARG USE_CUDA_VER=cu121
|
8 |
+
# any sentence transformer model; models to use can be found at https://huggingface.co/models?library=sentence-transformers
|
9 |
+
# Leaderboard: https://huggingface.co/spaces/mteb/leaderboard
|
10 |
+
# for better performance and multilangauge support use "intfloat/multilingual-e5-large" (~2.5GB) or "intfloat/multilingual-e5-base" (~1.5GB)
|
11 |
+
# IMPORTANT: If you change the embedding model (sentence-transformers/all-MiniLM-L6-v2) and vice versa, you aren't able to use RAG Chat with your previous documents loaded in the WebUI! You need to re-embed them.
|
12 |
+
ARG USE_EMBEDDING_MODEL=sentence-transformers/all-MiniLM-L6-v2
|
13 |
+
ARG USE_RERANKING_MODEL=""
|
14 |
+
# Override at your own risk - non-root configurations are untested
|
15 |
+
ARG UID=0
|
16 |
+
ARG GID=0
|
17 |
+
|
18 |
+
######## WebUI frontend ########
|
19 |
+
FROM --platform=$BUILDPLATFORM node:21-alpine3.19 as build
|
20 |
+
|
21 |
+
WORKDIR /app
|
22 |
+
|
23 |
+
COPY package.json package-lock.json ./
|
24 |
+
RUN npm ci
|
25 |
+
|
26 |
+
COPY . .
|
27 |
+
RUN npm run build
|
28 |
+
|
29 |
+
######## WebUI backend ########
|
30 |
+
FROM python:3.11-slim-bookworm as base
|
31 |
+
|
32 |
+
# Use args
|
33 |
+
ARG USE_CUDA
|
34 |
+
ARG USE_OLLAMA
|
35 |
+
ARG USE_CUDA_VER
|
36 |
+
ARG USE_EMBEDDING_MODEL
|
37 |
+
ARG USE_RERANKING_MODEL
|
38 |
+
ARG UID
|
39 |
+
ARG GID
|
40 |
+
|
41 |
+
## Basis ##
|
42 |
+
ENV ENV=prod \
|
43 |
+
PORT=8080 \
|
44 |
+
# pass build args to the build
|
45 |
+
USE_OLLAMA_DOCKER=${USE_OLLAMA} \
|
46 |
+
USE_CUDA_DOCKER=${USE_CUDA} \
|
47 |
+
USE_CUDA_DOCKER_VER=${USE_CUDA_VER} \
|
48 |
+
USE_EMBEDDING_MODEL_DOCKER=${USE_EMBEDDING_MODEL} \
|
49 |
+
USE_RERANKING_MODEL_DOCKER=${USE_RERANKING_MODEL}
|
50 |
+
|
51 |
+
## Basis URL Config ##
|
52 |
+
ENV OLLAMA_BASE_URL="/ollama" \
|
53 |
+
OPENAI_API_BASE_URL=""
|
54 |
+
|
55 |
+
## API Key and Security Config ##
|
56 |
+
ENV OPENAI_API_KEY="" \
|
57 |
+
WEBUI_SECRET_KEY="" \
|
58 |
+
SCARF_NO_ANALYTICS=true \
|
59 |
+
DO_NOT_TRACK=true \
|
60 |
+
ANONYMIZED_TELEMETRY=false
|
61 |
+
|
62 |
+
# Use locally bundled version of the LiteLLM cost map json
|
63 |
+
# to avoid repetitive startup connections
|
64 |
+
ENV LITELLM_LOCAL_MODEL_COST_MAP="True"
|
65 |
+
|
66 |
+
|
67 |
+
#### Other models #########################################################
|
68 |
+
## whisper TTS model settings ##
|
69 |
+
ENV WHISPER_MODEL="base" \
|
70 |
+
WHISPER_MODEL_DIR="/app/backend/data/cache/whisper/models"
|
71 |
+
|
72 |
+
## RAG Embedding model settings ##
|
73 |
+
ENV RAG_EMBEDDING_MODEL="$USE_EMBEDDING_MODEL_DOCKER" \
|
74 |
+
RAG_RERANKING_MODEL="$USE_RERANKING_MODEL_DOCKER" \
|
75 |
+
SENTENCE_TRANSFORMERS_HOME="/app/backend/data/cache/embedding/models"
|
76 |
+
|
77 |
+
## Hugging Face download cache ##
|
78 |
+
ENV HF_HOME="/app/backend/data/cache/embedding/models"
|
79 |
+
#### Other models ##########################################################
|
80 |
+
|
81 |
+
WORKDIR /app/backend
|
82 |
+
|
83 |
+
ENV HOME /root
|
84 |
+
# Create user and group if not root
|
85 |
+
RUN if [ $UID -ne 0 ]; then \
|
86 |
+
if [ $GID -ne 0 ]; then \
|
87 |
+
addgroup --gid $GID app; \
|
88 |
+
fi; \
|
89 |
+
adduser --uid $UID --gid $GID --home $HOME --disabled-password --no-create-home app; \
|
90 |
+
fi
|
91 |
+
|
92 |
+
RUN mkdir -p $HOME/.cache/chroma
|
93 |
+
RUN echo -n 00000000-0000-0000-0000-000000000000 > $HOME/.cache/chroma/telemetry_user_id
|
94 |
+
|
95 |
+
# Make sure the user has access to the app and root directory
|
96 |
+
RUN chown -R $UID:$GID /app $HOME
|
97 |
+
|
98 |
+
RUN if [ "$USE_OLLAMA" = "true" ]; then \
|
99 |
+
apt-get update && \
|
100 |
+
# Install pandoc and netcat
|
101 |
+
apt-get install -y --no-install-recommends pandoc netcat-openbsd curl && \
|
102 |
+
# for RAG OCR
|
103 |
+
apt-get install -y --no-install-recommends ffmpeg libsm6 libxext6 && \
|
104 |
+
# install helper tools
|
105 |
+
apt-get install -y --no-install-recommends curl jq && \
|
106 |
+
# install ollama
|
107 |
+
curl -fsSL https://ollama.com/install.sh | sh && \
|
108 |
+
# cleanup
|
109 |
+
rm -rf /var/lib/apt/lists/*; \
|
110 |
+
else \
|
111 |
+
apt-get update && \
|
112 |
+
# Install pandoc and netcat
|
113 |
+
apt-get install -y --no-install-recommends pandoc netcat-openbsd curl jq && \
|
114 |
+
# for RAG OCR
|
115 |
+
apt-get install -y --no-install-recommends ffmpeg libsm6 libxext6 && \
|
116 |
+
# cleanup
|
117 |
+
rm -rf /var/lib/apt/lists/*; \
|
118 |
+
fi
|
119 |
+
|
120 |
+
# install python dependencies
|
121 |
+
COPY --chown=$UID:$GID ./backend/requirements.txt ./requirements.txt
|
122 |
+
|
123 |
+
RUN pip3 install uv && \
|
124 |
+
if [ "$USE_CUDA" = "true" ]; then \
|
125 |
+
# If you use CUDA the whisper and embedding model will be downloaded on first use
|
126 |
+
pip3 install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/$USE_CUDA_DOCKER_VER --no-cache-dir && \
|
127 |
+
uv pip install --system -r requirements.txt --no-cache-dir && \
|
128 |
+
python -c "import os; from sentence_transformers import SentenceTransformer; SentenceTransformer(os.environ['RAG_EMBEDDING_MODEL'], device='cpu')" && \
|
129 |
+
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'])"; \
|
130 |
+
else \
|
131 |
+
pip3 install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cpu --no-cache-dir && \
|
132 |
+
uv pip install --system -r requirements.txt --no-cache-dir && \
|
133 |
+
python -c "import os; from sentence_transformers import SentenceTransformer; SentenceTransformer(os.environ['RAG_EMBEDDING_MODEL'], device='cpu')" && \
|
134 |
+
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'])"; \
|
135 |
+
fi; \
|
136 |
+
chown -R $UID:$GID /app/backend/data/
|
137 |
+
|
138 |
+
|
139 |
+
|
140 |
+
# copy embedding weight from build
|
141 |
+
# RUN mkdir -p /root/.cache/chroma/onnx_models/all-MiniLM-L6-v2
|
142 |
+
# COPY --from=build /app/onnx /root/.cache/chroma/onnx_models/all-MiniLM-L6-v2/onnx
|
143 |
+
|
144 |
+
# copy built frontend files
|
145 |
+
COPY --chown=$UID:$GID --from=build /app/build /app/build
|
146 |
+
COPY --chown=$UID:$GID --from=build /app/CHANGELOG.md /app/CHANGELOG.md
|
147 |
+
COPY --chown=$UID:$GID --from=build /app/package.json /app/package.json
|
148 |
+
|
149 |
+
# copy backend files
|
150 |
+
COPY --chown=$UID:$GID ./backend .
|
151 |
+
|
152 |
+
EXPOSE 8080
|
153 |
+
|
154 |
+
HEALTHCHECK CMD curl --silent --fail http://localhost:8080/health | jq -e '.status == true' || exit 1
|
155 |
+
|
156 |
+
USER $UID:$GID
|
157 |
+
|
158 |
+
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,244 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Open WebUI (Formerly Ollama WebUI) 👋
|
2 |
+
|
3 |
+
![GitHub stars](https://img.shields.io/github/stars/open-webui/open-webui?style=social)
|
4 |
+
![GitHub forks](https://img.shields.io/github/forks/open-webui/open-webui?style=social)
|
5 |
+
![GitHub watchers](https://img.shields.io/github/watchers/open-webui/open-webui?style=social)
|
6 |
+
![GitHub repo size](https://img.shields.io/github/repo-size/open-webui/open-webui)
|
7 |
+
![GitHub language count](https://img.shields.io/github/languages/count/open-webui/open-webui)
|
8 |
+
![GitHub top language](https://img.shields.io/github/languages/top/open-webui/open-webui)
|
9 |
+
![GitHub last commit](https://img.shields.io/github/last-commit/open-webui/open-webui?color=red)
|
10 |
+
![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)
|
11 |
+
[![Discord](https://img.shields.io/badge/Discord-Open_WebUI-blue?logo=discord&logoColor=white)](https://discord.gg/5rJgQTnV4s)
|
12 |
+
[![](https://img.shields.io/static/v1?label=Sponsor&message=%E2%9D%A4&logo=GitHub&color=%23fe8e86)](https://github.com/sponsors/tjbck)
|
13 |
+
|
14 |
+
Open WebUI is an extensible, 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/).
|
15 |
+
|
16 |
+
![Open WebUI Demo](./demo.gif)
|
17 |
+
|
18 |
+
## Features ⭐
|
19 |
+
|
20 |
+
- 🖥️ **Intuitive Interface**: Our chat interface takes inspiration from ChatGPT, ensuring a user-friendly experience.
|
21 |
+
|
22 |
+
- 📱 **Responsive Design**: Enjoy a seamless experience on both desktop and mobile devices.
|
23 |
+
|
24 |
+
- ⚡ **Swift Responsiveness**: Enjoy fast and responsive performance.
|
25 |
+
|
26 |
+
- 🚀 **Effortless Setup**: Install seamlessly using Docker or Kubernetes (kubectl, kustomize or helm) for a hassle-free experience.
|
27 |
+
|
28 |
+
- 🌈 **Theme Customization**: Choose from a variety of themes to personalize your Open WebUI experience.
|
29 |
+
|
30 |
+
- 💻 **Code Syntax Highlighting**: Enjoy enhanced code readability with our syntax highlighting feature.
|
31 |
+
|
32 |
+
- ✒️🔢 **Full Markdown and LaTeX Support**: Elevate your LLM experience with comprehensive Markdown and LaTeX capabilities for enriched interaction.
|
33 |
+
|
34 |
+
- 📚 **Local RAG Integration**: Dive into the future of chat interactions with the 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 `#` command in the prompt. In its alpha phase, occasional issues may arise as we actively refine and enhance this feature to ensure optimal performance and reliability.
|
35 |
+
|
36 |
+
- 🔍 **RAG Embedding Support**: Change the RAG embedding model directly in document settings, enhancing document processing. This feature supports Ollama and OpenAI models.
|
37 |
+
|
38 |
+
- 🌐 **Web Browsing Capability**: Seamlessly integrate websites into your chat experience using the `#` command followed by the URL. This feature allows you to incorporate web content directly into your conversations, enhancing the richness and depth of your interactions.
|
39 |
+
|
40 |
+
- 📜 **Prompt Preset Support**: Instantly access preset prompts using the `/` command in the chat input. Load predefined conversation starters effortlessly and expedite your interactions. Effortlessly import prompts through [Open WebUI Community](https://openwebui.com/) integration.
|
41 |
+
|
42 |
+
- 👍👎 **RLHF Annotation**: Empower your messages by rating them with thumbs up and thumbs down, followed by the option to provide textual feedback, facilitating the creation of datasets for Reinforcement Learning from Human Feedback (RLHF). Utilize your messages to train or fine-tune models, all while ensuring the confidentiality of locally saved data.
|
43 |
+
|
44 |
+
- 🏷️ **Conversation Tagging**: Effortlessly categorize and locate specific chats for quick reference and streamlined data collection.
|
45 |
+
|
46 |
+
- 📥🗑️ **Download/Delete Models**: Easily download or remove models directly from the web UI.
|
47 |
+
|
48 |
+
- 🔄 **Update All Ollama Models**: Easily update locally installed models all at once with a convenient button, streamlining model management.
|
49 |
+
|
50 |
+
- ⬆️ **GGUF File Model Creation**: Effortlessly create Ollama models by uploading GGUF files directly from the web UI. Streamlined process with options to upload from your machine or download GGUF files from Hugging Face.
|
51 |
+
|
52 |
+
- 🤖 **Multiple Model Support**: Seamlessly switch between different chat models for diverse interactions.
|
53 |
+
|
54 |
+
- 🔄 **Multi-Modal Support**: Seamlessly engage with models that support multimodal interactions, including images (e.g., LLava).
|
55 |
+
|
56 |
+
- 🧩 **Modelfile Builder**: Easily create Ollama modelfiles via the web UI. Create and add characters/agents, customize chat elements, and import modelfiles effortlessly through [Open WebUI Community](https://openwebui.com/) integration.
|
57 |
+
|
58 |
+
- ⚙️ **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.
|
59 |
+
|
60 |
+
- 💬 **Collaborative Chat**: Harness the collective intelligence of multiple models by seamlessly orchestrating group conversations. Use the `@` command to specify the model, enabling dynamic and diverse dialogues within your chat interface. Immerse yourself in the collective intelligence woven into your chat environment.
|
61 |
+
|
62 |
+
- 🗨️ **Local Chat Sharing**: Generate and share chat links seamlessly between users, enhancing collaboration and communication.
|
63 |
+
|
64 |
+
- 🔄 **Regeneration History Access**: Easily revisit and explore your entire regeneration history.
|
65 |
+
|
66 |
+
- 📜 **Chat History**: Effortlessly access and manage your conversation history.
|
67 |
+
|
68 |
+
- 📬 **Archive Chats**: Effortlessly store away completed conversations with LLMs for future reference, maintaining a tidy and clutter-free chat interface while allowing for easy retrieval and reference.
|
69 |
+
|
70 |
+
- 📤📥 **Import/Export Chat History**: Seamlessly move your chat data in and out of the platform.
|
71 |
+
|
72 |
+
- 🗣️ **Voice Input Support**: Engage with your model through voice interactions; enjoy the convenience of talking to your model directly. Additionally, explore the option for sending voice input automatically after 3 seconds of silence for a streamlined experience.
|
73 |
+
|
74 |
+
- 🔊 **Configurable Text-to-Speech Endpoint**: Customize your Text-to-Speech experience with configurable OpenAI endpoints.
|
75 |
+
|
76 |
+
- ⚙️ **Fine-Tuned Control with Advanced Parameters**: Gain a deeper level of control by adjusting parameters such as temperature and defining your system prompts to tailor the conversation to your specific preferences and needs.
|
77 |
+
|
78 |
+
- 🎨🤖 **Image Generation Integration**: Seamlessly incorporate image generation capabilities using options such as AUTOMATIC1111 API (local), ComfyUI (local), and DALL-E, enriching your chat experience with dynamic visual content.
|
79 |
+
|
80 |
+
- 🤝 **OpenAI API Integration**: Effortlessly integrate OpenAI-compatible API for versatile conversations alongside Ollama models. Customize the API Base URL to link with **LMStudio, Mistral, OpenRouter, and more**.
|
81 |
+
|
82 |
+
- ✨ **Multiple OpenAI-Compatible API Support**: Seamlessly integrate and customize various OpenAI-compatible APIs, enhancing the versatility of your chat interactions.
|
83 |
+
|
84 |
+
- 🔑 **API Key Generation Support**: Generate secret keys to leverage Open WebUI with OpenAI libraries, simplifying integration and development.
|
85 |
+
|
86 |
+
- 🔗 **External Ollama Server Connection**: Seamlessly link to an external Ollama server hosted on a different address by configuring the environment variable.
|
87 |
+
|
88 |
+
- 🔀 **Multiple Ollama Instance Load Balancing**: Effortlessly distribute chat requests across multiple Ollama instances for enhanced performance and reliability.
|
89 |
+
|
90 |
+
- 👥 **Multi-User Management**: Easily oversee and administer users via our intuitive admin panel, streamlining user management processes.
|
91 |
+
|
92 |
+
- 🔗 **Webhook Integration**: Subscribe to new user sign-up events via webhook (compatible with Google Chat and Microsoft Teams), providing real-time notifications and automation capabilities.
|
93 |
+
|
94 |
+
- 🛡️ **Model Whitelisting**: Admins can whitelist models for users with the 'user' role, enhancing security and access control.
|
95 |
+
|
96 |
+
- 📧 **Trusted Email Authentication**: Authenticate using a trusted email header, adding an additional layer of security and authentication.
|
97 |
+
|
98 |
+
- 🔐 **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.
|
99 |
+
|
100 |
+
- 🔒 **Backend Reverse Proxy Support**: Bolster security through direct communication between Open WebUI backend and Ollama. This key feature eliminates the need to expose Ollama over LAN. Requests made to the '/ollama/api' route from the web UI are seamlessly redirected to Ollama from the backend, enhancing overall system security.
|
101 |
+
|
102 |
+
- 🌐🌍 **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!
|
103 |
+
|
104 |
+
- 🌟 **Continuous Updates**: We are committed to improving Open WebUI with regular updates and new features.
|
105 |
+
|
106 |
+
## 🔗 Also Check Out Open WebUI Community!
|
107 |
+
|
108 |
+
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! 🚀
|
109 |
+
|
110 |
+
## How to Install 🚀
|
111 |
+
|
112 |
+
> [!NOTE]
|
113 |
+
> 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.
|
114 |
+
|
115 |
+
### Quick Start with Docker 🐳
|
116 |
+
|
117 |
+
> [!WARNING]
|
118 |
+
> 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.
|
119 |
+
|
120 |
+
> [!TIP]
|
121 |
+
> 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.
|
122 |
+
|
123 |
+
### Installation with Default Configuration
|
124 |
+
|
125 |
+
- **If Ollama is on your computer**, use this command:
|
126 |
+
|
127 |
+
```bash
|
128 |
+
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
|
129 |
+
```
|
130 |
+
|
131 |
+
- **If Ollama is on a Different Server**, use this command:
|
132 |
+
|
133 |
+
To connect to Ollama on another server, change the `OLLAMA_BASE_URL` to the server's URL:
|
134 |
+
|
135 |
+
```bash
|
136 |
+
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
|
137 |
+
```
|
138 |
+
|
139 |
+
- **To run Open WebUI with Nvidia GPU support**, use this command:
|
140 |
+
|
141 |
+
```bash
|
142 |
+
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
|
143 |
+
```
|
144 |
+
|
145 |
+
### Installation for OpenAI API Usage Only
|
146 |
+
|
147 |
+
- **If you're only using OpenAI API**, use this command:
|
148 |
+
|
149 |
+
```bash
|
150 |
+
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
|
151 |
+
```
|
152 |
+
|
153 |
+
### Installing Open WebUI with Bundled Ollama Support
|
154 |
+
|
155 |
+
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:
|
156 |
+
|
157 |
+
- **With GPU Support**:
|
158 |
+
Utilize GPU resources by running the following command:
|
159 |
+
|
160 |
+
```bash
|
161 |
+
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
|
162 |
+
```
|
163 |
+
|
164 |
+
- **For CPU Only**:
|
165 |
+
If you're not using a GPU, use this command instead:
|
166 |
+
|
167 |
+
```bash
|
168 |
+
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
|
169 |
+
```
|
170 |
+
|
171 |
+
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.
|
172 |
+
|
173 |
+
After installation, you can access Open WebUI at [http://localhost:3000](http://localhost:3000). Enjoy! 😄
|
174 |
+
|
175 |
+
### Other Installation Methods
|
176 |
+
|
177 |
+
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.
|
178 |
+
|
179 |
+
### Troubleshooting
|
180 |
+
|
181 |
+
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).
|
182 |
+
|
183 |
+
#### Open WebUI: Server Connection Error
|
184 |
+
|
185 |
+
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`.
|
186 |
+
|
187 |
+
**Example Docker Command**:
|
188 |
+
|
189 |
+
```bash
|
190 |
+
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
|
191 |
+
```
|
192 |
+
|
193 |
+
### Keeping Your Docker Installation Up-to-Date
|
194 |
+
|
195 |
+
In case you want to update your local Docker installation to the latest version, you can do it with [Watchtower](https://containrrr.dev/watchtower/):
|
196 |
+
|
197 |
+
```bash
|
198 |
+
docker run --rm --volume /var/run/docker.sock:/var/run/docker.sock containrrr/watchtower --run-once open-webui
|
199 |
+
```
|
200 |
+
|
201 |
+
In the last part of the command, replace `open-webui` with your container name if it is different.
|
202 |
+
|
203 |
+
### Moving from Ollama WebUI to Open WebUI
|
204 |
+
|
205 |
+
Check our Migration Guide available in our [Open WebUI Documentation](https://docs.openwebui.com/migration/).
|
206 |
+
|
207 |
+
## What's Next? 🌟
|
208 |
+
|
209 |
+
Discover upcoming features on our roadmap in the [Open WebUI Documentation](https://docs.openwebui.com/roadmap/).
|
210 |
+
|
211 |
+
## Supporters ✨
|
212 |
+
|
213 |
+
A big shoutout to our amazing supporters who's helping to make this project possible! 🙏
|
214 |
+
|
215 |
+
### Platinum Sponsors 🤍
|
216 |
+
|
217 |
+
- We're looking for Sponsors!
|
218 |
+
|
219 |
+
### Acknowledgments
|
220 |
+
|
221 |
+
Special thanks to [Prof. Lawrence Kim](https://www.lhkim.com/) and [Prof. Nick Vincent](https://www.nickmvincent.com/) for their invaluable support and guidance in shaping this project into a research endeavor. Grateful for your mentorship throughout the journey! 🙌
|
222 |
+
|
223 |
+
## License 📜
|
224 |
+
|
225 |
+
This project is licensed under the [MIT License](LICENSE) - see the [LICENSE](LICENSE) file for details. 📄
|
226 |
+
|
227 |
+
## Support 💬
|
228 |
+
|
229 |
+
If you have any questions, suggestions, or need assistance, please open an issue or join our
|
230 |
+
[Open WebUI Discord community](https://discord.gg/5rJgQTnV4s) to connect with us! 🤝
|
231 |
+
|
232 |
+
## Star History
|
233 |
+
|
234 |
+
<a href="https://star-history.com/#open-webui/open-webui&Date">
|
235 |
+
<picture>
|
236 |
+
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=open-webui/open-webui&type=Date&theme=dark" />
|
237 |
+
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=open-webui/open-webui&type=Date" />
|
238 |
+
<img alt="Star History Chart" src="https://api.star-history.com/svg?repos=open-webui/open-webui&type=Date" />
|
239 |
+
</picture>
|
240 |
+
</a>
|
241 |
+
|
242 |
+
---
|
243 |
+
|
244 |
+
Created by [Timothy J. Baek](https://github.com/tjbck) - Let's make Open WebUI even more amazing together! 💪
|
TROUBLESHOOTING.md
ADDED
@@ -0,0 +1,32 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 |
+
### General Connection Errors
|
22 |
+
|
23 |
+
**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.
|
24 |
+
|
25 |
+
**Troubleshooting Steps**:
|
26 |
+
|
27 |
+
1. **Verify Ollama URL Format**:
|
28 |
+
- 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).
|
29 |
+
- In the Open WebUI, navigate to "Settings" > "General".
|
30 |
+
- Confirm that the Ollama Server URL is correctly set to `[OLLAMA URL]` (e.g., `http://localhost:11434`).
|
31 |
+
|
32 |
+
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,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
__pycache__
|
2 |
+
.env
|
3 |
+
_old
|
4 |
+
uploads
|
5 |
+
.ipynb_checkpoints
|
6 |
+
*.db
|
7 |
+
_test
|
8 |
+
Pipfile
|
9 |
+
!/data
|
10 |
+
/data/*
|
11 |
+
!/data/litellm
|
12 |
+
/data/litellm/*
|
13 |
+
!data/litellm/config.yaml
|
14 |
+
|
15 |
+
!data/config.json
|
16 |
+
.webui_secret_key
|
backend/apps/audio/main.py
ADDED
@@ -0,0 +1,226 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import os
|
2 |
+
import logging
|
3 |
+
from fastapi import (
|
4 |
+
FastAPI,
|
5 |
+
Request,
|
6 |
+
Depends,
|
7 |
+
HTTPException,
|
8 |
+
status,
|
9 |
+
UploadFile,
|
10 |
+
File,
|
11 |
+
Form,
|
12 |
+
)
|
13 |
+
|
14 |
+
from fastapi.responses import StreamingResponse, JSONResponse, FileResponse
|
15 |
+
|
16 |
+
from fastapi.middleware.cors import CORSMiddleware
|
17 |
+
from faster_whisper import WhisperModel
|
18 |
+
from pydantic import BaseModel
|
19 |
+
|
20 |
+
|
21 |
+
import requests
|
22 |
+
import hashlib
|
23 |
+
from pathlib import Path
|
24 |
+
import json
|
25 |
+
|
26 |
+
|
27 |
+
from constants import ERROR_MESSAGES
|
28 |
+
from utils.utils import (
|
29 |
+
decode_token,
|
30 |
+
get_current_user,
|
31 |
+
get_verified_user,
|
32 |
+
get_admin_user,
|
33 |
+
)
|
34 |
+
from utils.misc import calculate_sha256
|
35 |
+
|
36 |
+
from config import (
|
37 |
+
SRC_LOG_LEVELS,
|
38 |
+
CACHE_DIR,
|
39 |
+
UPLOAD_DIR,
|
40 |
+
WHISPER_MODEL,
|
41 |
+
WHISPER_MODEL_DIR,
|
42 |
+
WHISPER_MODEL_AUTO_UPDATE,
|
43 |
+
DEVICE_TYPE,
|
44 |
+
AUDIO_OPENAI_API_BASE_URL,
|
45 |
+
AUDIO_OPENAI_API_KEY,
|
46 |
+
AUDIO_OPENAI_API_MODEL,
|
47 |
+
AUDIO_OPENAI_API_VOICE,
|
48 |
+
AppConfig,
|
49 |
+
)
|
50 |
+
|
51 |
+
log = logging.getLogger(__name__)
|
52 |
+
log.setLevel(SRC_LOG_LEVELS["AUDIO"])
|
53 |
+
|
54 |
+
app = FastAPI()
|
55 |
+
app.add_middleware(
|
56 |
+
CORSMiddleware,
|
57 |
+
allow_origins=["*"],
|
58 |
+
allow_credentials=True,
|
59 |
+
allow_methods=["*"],
|
60 |
+
allow_headers=["*"],
|
61 |
+
)
|
62 |
+
|
63 |
+
app.state.config = AppConfig()
|
64 |
+
app.state.config.OPENAI_API_BASE_URL = AUDIO_OPENAI_API_BASE_URL
|
65 |
+
app.state.config.OPENAI_API_KEY = AUDIO_OPENAI_API_KEY
|
66 |
+
app.state.config.OPENAI_API_MODEL = AUDIO_OPENAI_API_MODEL
|
67 |
+
app.state.config.OPENAI_API_VOICE = AUDIO_OPENAI_API_VOICE
|
68 |
+
|
69 |
+
# setting device type for whisper model
|
70 |
+
whisper_device_type = DEVICE_TYPE if DEVICE_TYPE and DEVICE_TYPE == "cuda" else "cpu"
|
71 |
+
log.info(f"whisper_device_type: {whisper_device_type}")
|
72 |
+
|
73 |
+
SPEECH_CACHE_DIR = Path(CACHE_DIR).joinpath("./audio/speech/")
|
74 |
+
SPEECH_CACHE_DIR.mkdir(parents=True, exist_ok=True)
|
75 |
+
|
76 |
+
|
77 |
+
class OpenAIConfigUpdateForm(BaseModel):
|
78 |
+
url: str
|
79 |
+
key: str
|
80 |
+
model: str
|
81 |
+
speaker: str
|
82 |
+
|
83 |
+
|
84 |
+
@app.get("/config")
|
85 |
+
async def get_openai_config(user=Depends(get_admin_user)):
|
86 |
+
return {
|
87 |
+
"OPENAI_API_BASE_URL": app.state.config.OPENAI_API_BASE_URL,
|
88 |
+
"OPENAI_API_KEY": app.state.config.OPENAI_API_KEY,
|
89 |
+
"OPENAI_API_MODEL": app.state.config.OPENAI_API_MODEL,
|
90 |
+
"OPENAI_API_VOICE": app.state.config.OPENAI_API_VOICE,
|
91 |
+
}
|
92 |
+
|
93 |
+
|
94 |
+
@app.post("/config/update")
|
95 |
+
async def update_openai_config(
|
96 |
+
form_data: OpenAIConfigUpdateForm, user=Depends(get_admin_user)
|
97 |
+
):
|
98 |
+
if form_data.key == "":
|
99 |
+
raise HTTPException(status_code=400, detail=ERROR_MESSAGES.API_KEY_NOT_FOUND)
|
100 |
+
|
101 |
+
app.state.config.OPENAI_API_BASE_URL = form_data.url
|
102 |
+
app.state.config.OPENAI_API_KEY = form_data.key
|
103 |
+
app.state.config.OPENAI_API_MODEL = form_data.model
|
104 |
+
app.state.config.OPENAI_API_VOICE = form_data.speaker
|
105 |
+
|
106 |
+
return {
|
107 |
+
"status": True,
|
108 |
+
"OPENAI_API_BASE_URL": app.state.config.OPENAI_API_BASE_URL,
|
109 |
+
"OPENAI_API_KEY": app.state.config.OPENAI_API_KEY,
|
110 |
+
"OPENAI_API_MODEL": app.state.config.OPENAI_API_MODEL,
|
111 |
+
"OPENAI_API_VOICE": app.state.config.OPENAI_API_VOICE,
|
112 |
+
}
|
113 |
+
|
114 |
+
|
115 |
+
@app.post("/speech")
|
116 |
+
async def speech(request: Request, user=Depends(get_verified_user)):
|
117 |
+
body = await request.body()
|
118 |
+
name = hashlib.sha256(body).hexdigest()
|
119 |
+
|
120 |
+
file_path = SPEECH_CACHE_DIR.joinpath(f"{name}.mp3")
|
121 |
+
file_body_path = SPEECH_CACHE_DIR.joinpath(f"{name}.json")
|
122 |
+
|
123 |
+
# Check if the file already exists in the cache
|
124 |
+
if file_path.is_file():
|
125 |
+
return FileResponse(file_path)
|
126 |
+
|
127 |
+
headers = {}
|
128 |
+
headers["Authorization"] = f"Bearer {app.state.config.OPENAI_API_KEY}"
|
129 |
+
headers["Content-Type"] = "application/json"
|
130 |
+
|
131 |
+
r = None
|
132 |
+
try:
|
133 |
+
r = requests.post(
|
134 |
+
url=f"{app.state.config.OPENAI_API_BASE_URL}/audio/speech",
|
135 |
+
data=body,
|
136 |
+
headers=headers,
|
137 |
+
stream=True,
|
138 |
+
)
|
139 |
+
|
140 |
+
r.raise_for_status()
|
141 |
+
|
142 |
+
# Save the streaming content to a file
|
143 |
+
with open(file_path, "wb") as f:
|
144 |
+
for chunk in r.iter_content(chunk_size=8192):
|
145 |
+
f.write(chunk)
|
146 |
+
|
147 |
+
with open(file_body_path, "w") as f:
|
148 |
+
json.dump(json.loads(body.decode("utf-8")), f)
|
149 |
+
|
150 |
+
# Return the saved file
|
151 |
+
return FileResponse(file_path)
|
152 |
+
|
153 |
+
except Exception as e:
|
154 |
+
log.exception(e)
|
155 |
+
error_detail = "Open WebUI: Server Connection Error"
|
156 |
+
if r is not None:
|
157 |
+
try:
|
158 |
+
res = r.json()
|
159 |
+
if "error" in res:
|
160 |
+
error_detail = f"External: {res['error']['message']}"
|
161 |
+
except:
|
162 |
+
error_detail = f"External: {e}"
|
163 |
+
|
164 |
+
raise HTTPException(
|
165 |
+
status_code=r.status_code if r != None else 500,
|
166 |
+
detail=error_detail,
|
167 |
+
)
|
168 |
+
|
169 |
+
|
170 |
+
@app.post("/transcriptions")
|
171 |
+
def transcribe(
|
172 |
+
file: UploadFile = File(...),
|
173 |
+
user=Depends(get_current_user),
|
174 |
+
):
|
175 |
+
log.info(f"file.content_type: {file.content_type}")
|
176 |
+
|
177 |
+
if file.content_type not in ["audio/mpeg", "audio/wav"]:
|
178 |
+
raise HTTPException(
|
179 |
+
status_code=status.HTTP_400_BAD_REQUEST,
|
180 |
+
detail=ERROR_MESSAGES.FILE_NOT_SUPPORTED,
|
181 |
+
)
|
182 |
+
|
183 |
+
try:
|
184 |
+
filename = file.filename
|
185 |
+
file_path = f"{UPLOAD_DIR}/{filename}"
|
186 |
+
contents = file.file.read()
|
187 |
+
with open(file_path, "wb") as f:
|
188 |
+
f.write(contents)
|
189 |
+
f.close()
|
190 |
+
|
191 |
+
whisper_kwargs = {
|
192 |
+
"model_size_or_path": WHISPER_MODEL,
|
193 |
+
"device": whisper_device_type,
|
194 |
+
"compute_type": "int8",
|
195 |
+
"download_root": WHISPER_MODEL_DIR,
|
196 |
+
"local_files_only": not WHISPER_MODEL_AUTO_UPDATE,
|
197 |
+
}
|
198 |
+
|
199 |
+
log.debug(f"whisper_kwargs: {whisper_kwargs}")
|
200 |
+
|
201 |
+
try:
|
202 |
+
model = WhisperModel(**whisper_kwargs)
|
203 |
+
except:
|
204 |
+
log.warning(
|
205 |
+
"WhisperModel initialization failed, attempting download with local_files_only=False"
|
206 |
+
)
|
207 |
+
whisper_kwargs["local_files_only"] = False
|
208 |
+
model = WhisperModel(**whisper_kwargs)
|
209 |
+
|
210 |
+
segments, info = model.transcribe(file_path, beam_size=5)
|
211 |
+
log.info(
|
212 |
+
"Detected language '%s' with probability %f"
|
213 |
+
% (info.language, info.language_probability)
|
214 |
+
)
|
215 |
+
|
216 |
+
transcript = "".join([segment.text for segment in list(segments)])
|
217 |
+
|
218 |
+
return {"text": transcript.strip()}
|
219 |
+
|
220 |
+
except Exception as e:
|
221 |
+
log.exception(e)
|
222 |
+
|
223 |
+
raise HTTPException(
|
224 |
+
status_code=status.HTTP_400_BAD_REQUEST,
|
225 |
+
detail=ERROR_MESSAGES.DEFAULT(e),
|
226 |
+
)
|
backend/apps/images/main.py
ADDED
@@ -0,0 +1,527 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import re
|
2 |
+
import requests
|
3 |
+
from fastapi import (
|
4 |
+
FastAPI,
|
5 |
+
Request,
|
6 |
+
Depends,
|
7 |
+
HTTPException,
|
8 |
+
status,
|
9 |
+
UploadFile,
|
10 |
+
File,
|
11 |
+
Form,
|
12 |
+
)
|
13 |
+
from fastapi.middleware.cors import CORSMiddleware
|
14 |
+
from faster_whisper import WhisperModel
|
15 |
+
|
16 |
+
from constants import ERROR_MESSAGES
|
17 |
+
from utils.utils import (
|
18 |
+
get_current_user,
|
19 |
+
get_admin_user,
|
20 |
+
)
|
21 |
+
|
22 |
+
from apps.images.utils.comfyui import ImageGenerationPayload, comfyui_generate_image
|
23 |
+
from utils.misc import calculate_sha256
|
24 |
+
from typing import Optional
|
25 |
+
from pydantic import BaseModel
|
26 |
+
from pathlib import Path
|
27 |
+
import mimetypes
|
28 |
+
import uuid
|
29 |
+
import base64
|
30 |
+
import json
|
31 |
+
import logging
|
32 |
+
|
33 |
+
from config import (
|
34 |
+
SRC_LOG_LEVELS,
|
35 |
+
CACHE_DIR,
|
36 |
+
IMAGE_GENERATION_ENGINE,
|
37 |
+
ENABLE_IMAGE_GENERATION,
|
38 |
+
AUTOMATIC1111_BASE_URL,
|
39 |
+
COMFYUI_BASE_URL,
|
40 |
+
IMAGES_OPENAI_API_BASE_URL,
|
41 |
+
IMAGES_OPENAI_API_KEY,
|
42 |
+
IMAGE_GENERATION_MODEL,
|
43 |
+
IMAGE_SIZE,
|
44 |
+
IMAGE_STEPS,
|
45 |
+
AppConfig,
|
46 |
+
)
|
47 |
+
|
48 |
+
|
49 |
+
log = logging.getLogger(__name__)
|
50 |
+
log.setLevel(SRC_LOG_LEVELS["IMAGES"])
|
51 |
+
|
52 |
+
IMAGE_CACHE_DIR = Path(CACHE_DIR).joinpath("./image/generations/")
|
53 |
+
IMAGE_CACHE_DIR.mkdir(parents=True, exist_ok=True)
|
54 |
+
|
55 |
+
app = FastAPI()
|
56 |
+
app.add_middleware(
|
57 |
+
CORSMiddleware,
|
58 |
+
allow_origins=["*"],
|
59 |
+
allow_credentials=True,
|
60 |
+
allow_methods=["*"],
|
61 |
+
allow_headers=["*"],
|
62 |
+
)
|
63 |
+
|
64 |
+
app.state.config = AppConfig()
|
65 |
+
|
66 |
+
app.state.config.ENGINE = IMAGE_GENERATION_ENGINE
|
67 |
+
app.state.config.ENABLED = ENABLE_IMAGE_GENERATION
|
68 |
+
|
69 |
+
app.state.config.OPENAI_API_BASE_URL = IMAGES_OPENAI_API_BASE_URL
|
70 |
+
app.state.config.OPENAI_API_KEY = IMAGES_OPENAI_API_KEY
|
71 |
+
|
72 |
+
app.state.config.MODEL = IMAGE_GENERATION_MODEL
|
73 |
+
|
74 |
+
|
75 |
+
app.state.config.AUTOMATIC1111_BASE_URL = AUTOMATIC1111_BASE_URL
|
76 |
+
app.state.config.COMFYUI_BASE_URL = COMFYUI_BASE_URL
|
77 |
+
|
78 |
+
|
79 |
+
app.state.config.IMAGE_SIZE = IMAGE_SIZE
|
80 |
+
app.state.config.IMAGE_STEPS = IMAGE_STEPS
|
81 |
+
|
82 |
+
|
83 |
+
@app.get("/config")
|
84 |
+
async def get_config(request: Request, user=Depends(get_admin_user)):
|
85 |
+
return {
|
86 |
+
"engine": app.state.config.ENGINE,
|
87 |
+
"enabled": app.state.config.ENABLED,
|
88 |
+
}
|
89 |
+
|
90 |
+
|
91 |
+
class ConfigUpdateForm(BaseModel):
|
92 |
+
engine: str
|
93 |
+
enabled: bool
|
94 |
+
|
95 |
+
|
96 |
+
@app.post("/config/update")
|
97 |
+
async def update_config(form_data: ConfigUpdateForm, user=Depends(get_admin_user)):
|
98 |
+
app.state.config.ENGINE = form_data.engine
|
99 |
+
app.state.config.ENABLED = form_data.enabled
|
100 |
+
return {
|
101 |
+
"engine": app.state.config.ENGINE,
|
102 |
+
"enabled": app.state.config.ENABLED,
|
103 |
+
}
|
104 |
+
|
105 |
+
|
106 |
+
class EngineUrlUpdateForm(BaseModel):
|
107 |
+
AUTOMATIC1111_BASE_URL: Optional[str] = None
|
108 |
+
COMFYUI_BASE_URL: Optional[str] = None
|
109 |
+
|
110 |
+
|
111 |
+
@app.get("/url")
|
112 |
+
async def get_engine_url(user=Depends(get_admin_user)):
|
113 |
+
return {
|
114 |
+
"AUTOMATIC1111_BASE_URL": app.state.config.AUTOMATIC1111_BASE_URL,
|
115 |
+
"COMFYUI_BASE_URL": app.state.config.COMFYUI_BASE_URL,
|
116 |
+
}
|
117 |
+
|
118 |
+
|
119 |
+
@app.post("/url/update")
|
120 |
+
async def update_engine_url(
|
121 |
+
form_data: EngineUrlUpdateForm, user=Depends(get_admin_user)
|
122 |
+
):
|
123 |
+
|
124 |
+
if form_data.AUTOMATIC1111_BASE_URL == None:
|
125 |
+
app.state.config.AUTOMATIC1111_BASE_URL = AUTOMATIC1111_BASE_URL
|
126 |
+
else:
|
127 |
+
url = form_data.AUTOMATIC1111_BASE_URL.strip("/")
|
128 |
+
try:
|
129 |
+
r = requests.head(url)
|
130 |
+
app.state.config.AUTOMATIC1111_BASE_URL = url
|
131 |
+
except Exception as e:
|
132 |
+
raise HTTPException(status_code=400, detail=ERROR_MESSAGES.DEFAULT(e))
|
133 |
+
|
134 |
+
if form_data.COMFYUI_BASE_URL == None:
|
135 |
+
app.state.config.COMFYUI_BASE_URL = COMFYUI_BASE_URL
|
136 |
+
else:
|
137 |
+
url = form_data.COMFYUI_BASE_URL.strip("/")
|
138 |
+
|
139 |
+
try:
|
140 |
+
r = requests.head(url)
|
141 |
+
app.state.config.COMFYUI_BASE_URL = url
|
142 |
+
except Exception as e:
|
143 |
+
raise HTTPException(status_code=400, detail=ERROR_MESSAGES.DEFAULT(e))
|
144 |
+
|
145 |
+
return {
|
146 |
+
"AUTOMATIC1111_BASE_URL": app.state.config.AUTOMATIC1111_BASE_URL,
|
147 |
+
"COMFYUI_BASE_URL": app.state.config.COMFYUI_BASE_URL,
|
148 |
+
"status": True,
|
149 |
+
}
|
150 |
+
|
151 |
+
|
152 |
+
class OpenAIConfigUpdateForm(BaseModel):
|
153 |
+
url: str
|
154 |
+
key: str
|
155 |
+
|
156 |
+
|
157 |
+
@app.get("/openai/config")
|
158 |
+
async def get_openai_config(user=Depends(get_admin_user)):
|
159 |
+
return {
|
160 |
+
"OPENAI_API_BASE_URL": app.state.config.OPENAI_API_BASE_URL,
|
161 |
+
"OPENAI_API_KEY": app.state.config.OPENAI_API_KEY,
|
162 |
+
}
|
163 |
+
|
164 |
+
|
165 |
+
@app.post("/openai/config/update")
|
166 |
+
async def update_openai_config(
|
167 |
+
form_data: OpenAIConfigUpdateForm, user=Depends(get_admin_user)
|
168 |
+
):
|
169 |
+
if form_data.key == "":
|
170 |
+
raise HTTPException(status_code=400, detail=ERROR_MESSAGES.API_KEY_NOT_FOUND)
|
171 |
+
|
172 |
+
app.state.config.OPENAI_API_BASE_URL = form_data.url
|
173 |
+
app.state.config.OPENAI_API_KEY = form_data.key
|
174 |
+
|
175 |
+
return {
|
176 |
+
"status": True,
|
177 |
+
"OPENAI_API_BASE_URL": app.state.config.OPENAI_API_BASE_URL,
|
178 |
+
"OPENAI_API_KEY": app.state.config.OPENAI_API_KEY,
|
179 |
+
}
|
180 |
+
|
181 |
+
|
182 |
+
class ImageSizeUpdateForm(BaseModel):
|
183 |
+
size: str
|
184 |
+
|
185 |
+
|
186 |
+
@app.get("/size")
|
187 |
+
async def get_image_size(user=Depends(get_admin_user)):
|
188 |
+
return {"IMAGE_SIZE": app.state.config.IMAGE_SIZE}
|
189 |
+
|
190 |
+
|
191 |
+
@app.post("/size/update")
|
192 |
+
async def update_image_size(
|
193 |
+
form_data: ImageSizeUpdateForm, user=Depends(get_admin_user)
|
194 |
+
):
|
195 |
+
pattern = r"^\d+x\d+$" # Regular expression pattern
|
196 |
+
if re.match(pattern, form_data.size):
|
197 |
+
app.state.config.IMAGE_SIZE = form_data.size
|
198 |
+
return {
|
199 |
+
"IMAGE_SIZE": app.state.config.IMAGE_SIZE,
|
200 |
+
"status": True,
|
201 |
+
}
|
202 |
+
else:
|
203 |
+
raise HTTPException(
|
204 |
+
status_code=400,
|
205 |
+
detail=ERROR_MESSAGES.INCORRECT_FORMAT(" (e.g., 512x512)."),
|
206 |
+
)
|
207 |
+
|
208 |
+
|
209 |
+
class ImageStepsUpdateForm(BaseModel):
|
210 |
+
steps: int
|
211 |
+
|
212 |
+
|
213 |
+
@app.get("/steps")
|
214 |
+
async def get_image_size(user=Depends(get_admin_user)):
|
215 |
+
return {"IMAGE_STEPS": app.state.config.IMAGE_STEPS}
|
216 |
+
|
217 |
+
|
218 |
+
@app.post("/steps/update")
|
219 |
+
async def update_image_size(
|
220 |
+
form_data: ImageStepsUpdateForm, user=Depends(get_admin_user)
|
221 |
+
):
|
222 |
+
if form_data.steps >= 0:
|
223 |
+
app.state.config.IMAGE_STEPS = form_data.steps
|
224 |
+
return {
|
225 |
+
"IMAGE_STEPS": app.state.config.IMAGE_STEPS,
|
226 |
+
"status": True,
|
227 |
+
}
|
228 |
+
else:
|
229 |
+
raise HTTPException(
|
230 |
+
status_code=400,
|
231 |
+
detail=ERROR_MESSAGES.INCORRECT_FORMAT(" (e.g., 50)."),
|
232 |
+
)
|
233 |
+
|
234 |
+
|
235 |
+
@app.get("/models")
|
236 |
+
def get_models(user=Depends(get_current_user)):
|
237 |
+
try:
|
238 |
+
if app.state.config.ENGINE == "openai":
|
239 |
+
return [
|
240 |
+
{"id": "dall-e-2", "name": "DALL·E 2"},
|
241 |
+
{"id": "dall-e-3", "name": "DALL·E 3"},
|
242 |
+
]
|
243 |
+
elif app.state.config.ENGINE == "comfyui":
|
244 |
+
|
245 |
+
r = requests.get(url=f"{app.state.config.COMFYUI_BASE_URL}/object_info")
|
246 |
+
info = r.json()
|
247 |
+
|
248 |
+
return list(
|
249 |
+
map(
|
250 |
+
lambda model: {"id": model, "name": model},
|
251 |
+
info["CheckpointLoaderSimple"]["input"]["required"]["ckpt_name"][0],
|
252 |
+
)
|
253 |
+
)
|
254 |
+
|
255 |
+
else:
|
256 |
+
r = requests.get(
|
257 |
+
url=f"{app.state.config.AUTOMATIC1111_BASE_URL}/sdapi/v1/sd-models"
|
258 |
+
)
|
259 |
+
models = r.json()
|
260 |
+
return list(
|
261 |
+
map(
|
262 |
+
lambda model: {"id": model["title"], "name": model["model_name"]},
|
263 |
+
models,
|
264 |
+
)
|
265 |
+
)
|
266 |
+
except Exception as e:
|
267 |
+
app.state.config.ENABLED = False
|
268 |
+
raise HTTPException(status_code=400, detail=ERROR_MESSAGES.DEFAULT(e))
|
269 |
+
|
270 |
+
|
271 |
+
@app.get("/models/default")
|
272 |
+
async def get_default_model(user=Depends(get_admin_user)):
|
273 |
+
try:
|
274 |
+
if app.state.config.ENGINE == "openai":
|
275 |
+
return {
|
276 |
+
"model": (
|
277 |
+
app.state.config.MODEL if app.state.config.MODEL else "dall-e-2"
|
278 |
+
)
|
279 |
+
}
|
280 |
+
elif app.state.config.ENGINE == "comfyui":
|
281 |
+
return {"model": (app.state.config.MODEL if app.state.config.MODEL else "")}
|
282 |
+
else:
|
283 |
+
r = requests.get(
|
284 |
+
url=f"{app.state.config.AUTOMATIC1111_BASE_URL}/sdapi/v1/options"
|
285 |
+
)
|
286 |
+
options = r.json()
|
287 |
+
return {"model": options["sd_model_checkpoint"]}
|
288 |
+
except Exception as e:
|
289 |
+
app.state.config.ENABLED = False
|
290 |
+
raise HTTPException(status_code=400, detail=ERROR_MESSAGES.DEFAULT(e))
|
291 |
+
|
292 |
+
|
293 |
+
class UpdateModelForm(BaseModel):
|
294 |
+
model: str
|
295 |
+
|
296 |
+
|
297 |
+
def set_model_handler(model: str):
|
298 |
+
if app.state.config.ENGINE in ["openai", "comfyui"]:
|
299 |
+
app.state.config.MODEL = model
|
300 |
+
return app.state.config.MODEL
|
301 |
+
else:
|
302 |
+
r = requests.get(
|
303 |
+
url=f"{app.state.config.AUTOMATIC1111_BASE_URL}/sdapi/v1/options"
|
304 |
+
)
|
305 |
+
options = r.json()
|
306 |
+
|
307 |
+
if model != options["sd_model_checkpoint"]:
|
308 |
+
options["sd_model_checkpoint"] = model
|
309 |
+
r = requests.post(
|
310 |
+
url=f"{app.state.config.AUTOMATIC1111_BASE_URL}/sdapi/v1/options",
|
311 |
+
json=options,
|
312 |
+
)
|
313 |
+
|
314 |
+
return options
|
315 |
+
|
316 |
+
|
317 |
+
@app.post("/models/default/update")
|
318 |
+
def update_default_model(
|
319 |
+
form_data: UpdateModelForm,
|
320 |
+
user=Depends(get_current_user),
|
321 |
+
):
|
322 |
+
return set_model_handler(form_data.model)
|
323 |
+
|
324 |
+
|
325 |
+
class GenerateImageForm(BaseModel):
|
326 |
+
model: Optional[str] = None
|
327 |
+
prompt: str
|
328 |
+
n: int = 1
|
329 |
+
size: Optional[str] = None
|
330 |
+
negative_prompt: Optional[str] = None
|
331 |
+
|
332 |
+
|
333 |
+
def save_b64_image(b64_str):
|
334 |
+
try:
|
335 |
+
image_id = str(uuid.uuid4())
|
336 |
+
|
337 |
+
if "," in b64_str:
|
338 |
+
header, encoded = b64_str.split(",", 1)
|
339 |
+
mime_type = header.split(";")[0]
|
340 |
+
|
341 |
+
img_data = base64.b64decode(encoded)
|
342 |
+
image_format = mimetypes.guess_extension(mime_type)
|
343 |
+
|
344 |
+
image_filename = f"{image_id}{image_format}"
|
345 |
+
file_path = IMAGE_CACHE_DIR / f"{image_filename}"
|
346 |
+
with open(file_path, "wb") as f:
|
347 |
+
f.write(img_data)
|
348 |
+
return image_filename
|
349 |
+
else:
|
350 |
+
image_filename = f"{image_id}.png"
|
351 |
+
file_path = IMAGE_CACHE_DIR.joinpath(image_filename)
|
352 |
+
|
353 |
+
img_data = base64.b64decode(b64_str)
|
354 |
+
|
355 |
+
# Write the image data to a file
|
356 |
+
with open(file_path, "wb") as f:
|
357 |
+
f.write(img_data)
|
358 |
+
return image_filename
|
359 |
+
|
360 |
+
except Exception as e:
|
361 |
+
log.exception(f"Error saving image: {e}")
|
362 |
+
return None
|
363 |
+
|
364 |
+
|
365 |
+
def save_url_image(url):
|
366 |
+
image_id = str(uuid.uuid4())
|
367 |
+
try:
|
368 |
+
r = requests.get(url)
|
369 |
+
r.raise_for_status()
|
370 |
+
if r.headers["content-type"].split("/")[0] == "image":
|
371 |
+
|
372 |
+
mime_type = r.headers["content-type"]
|
373 |
+
image_format = mimetypes.guess_extension(mime_type)
|
374 |
+
|
375 |
+
if not image_format:
|
376 |
+
raise ValueError("Could not determine image type from MIME type")
|
377 |
+
|
378 |
+
image_filename = f"{image_id}{image_format}"
|
379 |
+
|
380 |
+
file_path = IMAGE_CACHE_DIR.joinpath(f"{image_filename}")
|
381 |
+
with open(file_path, "wb") as image_file:
|
382 |
+
for chunk in r.iter_content(chunk_size=8192):
|
383 |
+
image_file.write(chunk)
|
384 |
+
return image_filename
|
385 |
+
else:
|
386 |
+
log.error(f"Url does not point to an image.")
|
387 |
+
return None
|
388 |
+
|
389 |
+
except Exception as e:
|
390 |
+
log.exception(f"Error saving image: {e}")
|
391 |
+
return None
|
392 |
+
|
393 |
+
|
394 |
+
@app.post("/generations")
|
395 |
+
def generate_image(
|
396 |
+
form_data: GenerateImageForm,
|
397 |
+
user=Depends(get_current_user),
|
398 |
+
):
|
399 |
+
|
400 |
+
width, height = tuple(map(int, app.state.config.IMAGE_SIZE.split("x")))
|
401 |
+
|
402 |
+
r = None
|
403 |
+
try:
|
404 |
+
if app.state.config.ENGINE == "openai":
|
405 |
+
|
406 |
+
headers = {}
|
407 |
+
headers["Authorization"] = f"Bearer {app.state.config.OPENAI_API_KEY}"
|
408 |
+
headers["Content-Type"] = "application/json"
|
409 |
+
|
410 |
+
data = {
|
411 |
+
"model": (
|
412 |
+
app.state.config.MODEL
|
413 |
+
if app.state.config.MODEL != ""
|
414 |
+
else "dall-e-2"
|
415 |
+
),
|
416 |
+
"prompt": form_data.prompt,
|
417 |
+
"n": form_data.n,
|
418 |
+
"size": (
|
419 |
+
form_data.size if form_data.size else app.state.config.IMAGE_SIZE
|
420 |
+
),
|
421 |
+
"response_format": "b64_json",
|
422 |
+
}
|
423 |
+
|
424 |
+
r = requests.post(
|
425 |
+
url=f"{app.state.config.OPENAI_API_BASE_URL}/images/generations",
|
426 |
+
json=data,
|
427 |
+
headers=headers,
|
428 |
+
)
|
429 |
+
|
430 |
+
r.raise_for_status()
|
431 |
+
res = r.json()
|
432 |
+
|
433 |
+
images = []
|
434 |
+
|
435 |
+
for image in res["data"]:
|
436 |
+
image_filename = save_b64_image(image["b64_json"])
|
437 |
+
images.append({"url": f"/cache/image/generations/{image_filename}"})
|
438 |
+
file_body_path = IMAGE_CACHE_DIR.joinpath(f"{image_filename}.json")
|
439 |
+
|
440 |
+
with open(file_body_path, "w") as f:
|
441 |
+
json.dump(data, f)
|
442 |
+
|
443 |
+
return images
|
444 |
+
|
445 |
+
elif app.state.config.ENGINE == "comfyui":
|
446 |
+
|
447 |
+
data = {
|
448 |
+
"prompt": form_data.prompt,
|
449 |
+
"width": width,
|
450 |
+
"height": height,
|
451 |
+
"n": form_data.n,
|
452 |
+
}
|
453 |
+
|
454 |
+
if app.state.config.IMAGE_STEPS is not None:
|
455 |
+
data["steps"] = app.state.config.IMAGE_STEPS
|
456 |
+
|
457 |
+
if form_data.negative_prompt is not None:
|
458 |
+
data["negative_prompt"] = form_data.negative_prompt
|
459 |
+
|
460 |
+
data = ImageGenerationPayload(**data)
|
461 |
+
|
462 |
+
res = comfyui_generate_image(
|
463 |
+
app.state.config.MODEL,
|
464 |
+
data,
|
465 |
+
user.id,
|
466 |
+
app.state.config.COMFYUI_BASE_URL,
|
467 |
+
)
|
468 |
+
log.debug(f"res: {res}")
|
469 |
+
|
470 |
+
images = []
|
471 |
+
|
472 |
+
for image in res["data"]:
|
473 |
+
image_filename = save_url_image(image["url"])
|
474 |
+
images.append({"url": f"/cache/image/generations/{image_filename}"})
|
475 |
+
file_body_path = IMAGE_CACHE_DIR.joinpath(f"{image_filename}.json")
|
476 |
+
|
477 |
+
with open(file_body_path, "w") as f:
|
478 |
+
json.dump(data.model_dump(exclude_none=True), f)
|
479 |
+
|
480 |
+
log.debug(f"images: {images}")
|
481 |
+
return images
|
482 |
+
else:
|
483 |
+
if form_data.model:
|
484 |
+
set_model_handler(form_data.model)
|
485 |
+
|
486 |
+
data = {
|
487 |
+
"prompt": form_data.prompt,
|
488 |
+
"batch_size": form_data.n,
|
489 |
+
"width": width,
|
490 |
+
"height": height,
|
491 |
+
}
|
492 |
+
|
493 |
+
if app.state.config.IMAGE_STEPS is not None:
|
494 |
+
data["steps"] = app.state.config.IMAGE_STEPS
|
495 |
+
|
496 |
+
if form_data.negative_prompt is not None:
|
497 |
+
data["negative_prompt"] = form_data.negative_prompt
|
498 |
+
|
499 |
+
r = requests.post(
|
500 |
+
url=f"{app.state.config.AUTOMATIC1111_BASE_URL}/sdapi/v1/txt2img",
|
501 |
+
json=data,
|
502 |
+
)
|
503 |
+
|
504 |
+
res = r.json()
|
505 |
+
|
506 |
+
log.debug(f"res: {res}")
|
507 |
+
|
508 |
+
images = []
|
509 |
+
|
510 |
+
for image in res["images"]:
|
511 |
+
image_filename = save_b64_image(image)
|
512 |
+
images.append({"url": f"/cache/image/generations/{image_filename}"})
|
513 |
+
file_body_path = IMAGE_CACHE_DIR.joinpath(f"{image_filename}.json")
|
514 |
+
|
515 |
+
with open(file_body_path, "w") as f:
|
516 |
+
json.dump({**data, "info": res["info"]}, f)
|
517 |
+
|
518 |
+
return images
|
519 |
+
|
520 |
+
except Exception as e:
|
521 |
+
error = e
|
522 |
+
|
523 |
+
if r != None:
|
524 |
+
data = r.json()
|
525 |
+
if "error" in data:
|
526 |
+
error = data["error"]["message"]
|
527 |
+
raise HTTPException(status_code=400, detail=ERROR_MESSAGES.DEFAULT(error))
|
backend/apps/images/utils/comfyui.py
ADDED
@@ -0,0 +1,234 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import websocket # NOTE: websocket-client (https://github.com/websocket-client/websocket-client)
|
2 |
+
import uuid
|
3 |
+
import json
|
4 |
+
import urllib.request
|
5 |
+
import urllib.parse
|
6 |
+
import random
|
7 |
+
import logging
|
8 |
+
|
9 |
+
from config import SRC_LOG_LEVELS
|
10 |
+
|
11 |
+
log = logging.getLogger(__name__)
|
12 |
+
log.setLevel(SRC_LOG_LEVELS["COMFYUI"])
|
13 |
+
|
14 |
+
from pydantic import BaseModel
|
15 |
+
|
16 |
+
from typing import Optional
|
17 |
+
|
18 |
+
COMFYUI_DEFAULT_PROMPT = """
|
19 |
+
{
|
20 |
+
"3": {
|
21 |
+
"inputs": {
|
22 |
+
"seed": 0,
|
23 |
+
"steps": 20,
|
24 |
+
"cfg": 8,
|
25 |
+
"sampler_name": "euler",
|
26 |
+
"scheduler": "normal",
|
27 |
+
"denoise": 1,
|
28 |
+
"model": [
|
29 |
+
"4",
|
30 |
+
0
|
31 |
+
],
|
32 |
+
"positive": [
|
33 |
+
"6",
|
34 |
+
0
|
35 |
+
],
|
36 |
+
"negative": [
|
37 |
+
"7",
|
38 |
+
0
|
39 |
+
],
|
40 |
+
"latent_image": [
|
41 |
+
"5",
|
42 |
+
0
|
43 |
+
]
|
44 |
+
},
|
45 |
+
"class_type": "KSampler",
|
46 |
+
"_meta": {
|
47 |
+
"title": "KSampler"
|
48 |
+
}
|
49 |
+
},
|
50 |
+
"4": {
|
51 |
+
"inputs": {
|
52 |
+
"ckpt_name": "model.safetensors"
|
53 |
+
},
|
54 |
+
"class_type": "CheckpointLoaderSimple",
|
55 |
+
"_meta": {
|
56 |
+
"title": "Load Checkpoint"
|
57 |
+
}
|
58 |
+
},
|
59 |
+
"5": {
|
60 |
+
"inputs": {
|
61 |
+
"width": 512,
|
62 |
+
"height": 512,
|
63 |
+
"batch_size": 1
|
64 |
+
},
|
65 |
+
"class_type": "EmptyLatentImage",
|
66 |
+
"_meta": {
|
67 |
+
"title": "Empty Latent Image"
|
68 |
+
}
|
69 |
+
},
|
70 |
+
"6": {
|
71 |
+
"inputs": {
|
72 |
+
"text": "Prompt",
|
73 |
+
"clip": [
|
74 |
+
"4",
|
75 |
+
1
|
76 |
+
]
|
77 |
+
},
|
78 |
+
"class_type": "CLIPTextEncode",
|
79 |
+
"_meta": {
|
80 |
+
"title": "CLIP Text Encode (Prompt)"
|
81 |
+
}
|
82 |
+
},
|
83 |
+
"7": {
|
84 |
+
"inputs": {
|
85 |
+
"text": "Negative Prompt",
|
86 |
+
"clip": [
|
87 |
+
"4",
|
88 |
+
1
|
89 |
+
]
|
90 |
+
},
|
91 |
+
"class_type": "CLIPTextEncode",
|
92 |
+
"_meta": {
|
93 |
+
"title": "CLIP Text Encode (Prompt)"
|
94 |
+
}
|
95 |
+
},
|
96 |
+
"8": {
|
97 |
+
"inputs": {
|
98 |
+
"samples": [
|
99 |
+
"3",
|
100 |
+
0
|
101 |
+
],
|
102 |
+
"vae": [
|
103 |
+
"4",
|
104 |
+
2
|
105 |
+
]
|
106 |
+
},
|
107 |
+
"class_type": "VAEDecode",
|
108 |
+
"_meta": {
|
109 |
+
"title": "VAE Decode"
|
110 |
+
}
|
111 |
+
},
|
112 |
+
"9": {
|
113 |
+
"inputs": {
|
114 |
+
"filename_prefix": "ComfyUI",
|
115 |
+
"images": [
|
116 |
+
"8",
|
117 |
+
0
|
118 |
+
]
|
119 |
+
},
|
120 |
+
"class_type": "SaveImage",
|
121 |
+
"_meta": {
|
122 |
+
"title": "Save Image"
|
123 |
+
}
|
124 |
+
}
|
125 |
+
}
|
126 |
+
"""
|
127 |
+
|
128 |
+
|
129 |
+
def queue_prompt(prompt, client_id, base_url):
|
130 |
+
log.info("queue_prompt")
|
131 |
+
p = {"prompt": prompt, "client_id": client_id}
|
132 |
+
data = json.dumps(p).encode("utf-8")
|
133 |
+
req = urllib.request.Request(f"{base_url}/prompt", data=data)
|
134 |
+
return json.loads(urllib.request.urlopen(req).read())
|
135 |
+
|
136 |
+
|
137 |
+
def get_image(filename, subfolder, folder_type, base_url):
|
138 |
+
log.info("get_image")
|
139 |
+
data = {"filename": filename, "subfolder": subfolder, "type": folder_type}
|
140 |
+
url_values = urllib.parse.urlencode(data)
|
141 |
+
with urllib.request.urlopen(f"{base_url}/view?{url_values}") as response:
|
142 |
+
return response.read()
|
143 |
+
|
144 |
+
|
145 |
+
def get_image_url(filename, subfolder, folder_type, base_url):
|
146 |
+
log.info("get_image")
|
147 |
+
data = {"filename": filename, "subfolder": subfolder, "type": folder_type}
|
148 |
+
url_values = urllib.parse.urlencode(data)
|
149 |
+
return f"{base_url}/view?{url_values}"
|
150 |
+
|
151 |
+
|
152 |
+
def get_history(prompt_id, base_url):
|
153 |
+
log.info("get_history")
|
154 |
+
with urllib.request.urlopen(f"{base_url}/history/{prompt_id}") as response:
|
155 |
+
return json.loads(response.read())
|
156 |
+
|
157 |
+
|
158 |
+
def get_images(ws, prompt, client_id, base_url):
|
159 |
+
prompt_id = queue_prompt(prompt, client_id, base_url)["prompt_id"]
|
160 |
+
output_images = []
|
161 |
+
while True:
|
162 |
+
out = ws.recv()
|
163 |
+
if isinstance(out, str):
|
164 |
+
message = json.loads(out)
|
165 |
+
if message["type"] == "executing":
|
166 |
+
data = message["data"]
|
167 |
+
if data["node"] is None and data["prompt_id"] == prompt_id:
|
168 |
+
break # Execution is done
|
169 |
+
else:
|
170 |
+
continue # previews are binary data
|
171 |
+
|
172 |
+
history = get_history(prompt_id, base_url)[prompt_id]
|
173 |
+
for o in history["outputs"]:
|
174 |
+
for node_id in history["outputs"]:
|
175 |
+
node_output = history["outputs"][node_id]
|
176 |
+
if "images" in node_output:
|
177 |
+
for image in node_output["images"]:
|
178 |
+
url = get_image_url(
|
179 |
+
image["filename"], image["subfolder"], image["type"], base_url
|
180 |
+
)
|
181 |
+
output_images.append({"url": url})
|
182 |
+
return {"data": output_images}
|
183 |
+
|
184 |
+
|
185 |
+
class ImageGenerationPayload(BaseModel):
|
186 |
+
prompt: str
|
187 |
+
negative_prompt: Optional[str] = ""
|
188 |
+
steps: Optional[int] = None
|
189 |
+
seed: Optional[int] = None
|
190 |
+
width: int
|
191 |
+
height: int
|
192 |
+
n: int = 1
|
193 |
+
|
194 |
+
|
195 |
+
def comfyui_generate_image(
|
196 |
+
model: str, payload: ImageGenerationPayload, client_id, base_url
|
197 |
+
):
|
198 |
+
ws_url = base_url.replace("http://", "ws://").replace("https://", "wss://")
|
199 |
+
|
200 |
+
comfyui_prompt = json.loads(COMFYUI_DEFAULT_PROMPT)
|
201 |
+
|
202 |
+
comfyui_prompt["4"]["inputs"]["ckpt_name"] = model
|
203 |
+
comfyui_prompt["5"]["inputs"]["batch_size"] = payload.n
|
204 |
+
comfyui_prompt["5"]["inputs"]["width"] = payload.width
|
205 |
+
comfyui_prompt["5"]["inputs"]["height"] = payload.height
|
206 |
+
|
207 |
+
# set the text prompt for our positive CLIPTextEncode
|
208 |
+
comfyui_prompt["6"]["inputs"]["text"] = payload.prompt
|
209 |
+
comfyui_prompt["7"]["inputs"]["text"] = payload.negative_prompt
|
210 |
+
|
211 |
+
if payload.steps:
|
212 |
+
comfyui_prompt["3"]["inputs"]["steps"] = payload.steps
|
213 |
+
|
214 |
+
comfyui_prompt["3"]["inputs"]["seed"] = (
|
215 |
+
payload.seed if payload.seed else random.randint(0, 18446744073709551614)
|
216 |
+
)
|
217 |
+
|
218 |
+
try:
|
219 |
+
ws = websocket.WebSocket()
|
220 |
+
ws.connect(f"{ws_url}/ws?clientId={client_id}")
|
221 |
+
log.info("WebSocket connection established.")
|
222 |
+
except Exception as e:
|
223 |
+
log.exception(f"Failed to connect to WebSocket server: {e}")
|
224 |
+
return None
|
225 |
+
|
226 |
+
try:
|
227 |
+
images = get_images(ws, comfyui_prompt, client_id, base_url)
|
228 |
+
except Exception as e:
|
229 |
+
log.exception(f"Error while receiving images: {e}")
|
230 |
+
images = None
|
231 |
+
|
232 |
+
ws.close()
|
233 |
+
|
234 |
+
return images
|
backend/apps/litellm/main.py
ADDED
@@ -0,0 +1,379 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import sys
|
2 |
+
from contextlib import asynccontextmanager
|
3 |
+
|
4 |
+
from fastapi import FastAPI, Depends, HTTPException
|
5 |
+
from fastapi.routing import APIRoute
|
6 |
+
from fastapi.middleware.cors import CORSMiddleware
|
7 |
+
|
8 |
+
import logging
|
9 |
+
from fastapi import FastAPI, Request, Depends, status, Response
|
10 |
+
from fastapi.responses import JSONResponse
|
11 |
+
|
12 |
+
from starlette.middleware.base import BaseHTTPMiddleware, RequestResponseEndpoint
|
13 |
+
from starlette.responses import StreamingResponse
|
14 |
+
import json
|
15 |
+
import time
|
16 |
+
import requests
|
17 |
+
|
18 |
+
from pydantic import BaseModel, ConfigDict
|
19 |
+
from typing import Optional, List
|
20 |
+
|
21 |
+
from utils.utils import get_verified_user, get_current_user, get_admin_user
|
22 |
+
from config import SRC_LOG_LEVELS, ENV
|
23 |
+
from constants import MESSAGES
|
24 |
+
|
25 |
+
import os
|
26 |
+
|
27 |
+
log = logging.getLogger(__name__)
|
28 |
+
log.setLevel(SRC_LOG_LEVELS["LITELLM"])
|
29 |
+
|
30 |
+
|
31 |
+
from config import (
|
32 |
+
ENABLE_LITELLM,
|
33 |
+
ENABLE_MODEL_FILTER,
|
34 |
+
MODEL_FILTER_LIST,
|
35 |
+
DATA_DIR,
|
36 |
+
LITELLM_PROXY_PORT,
|
37 |
+
LITELLM_PROXY_HOST,
|
38 |
+
)
|
39 |
+
|
40 |
+
import warnings
|
41 |
+
|
42 |
+
warnings.simplefilter("ignore")
|
43 |
+
|
44 |
+
from litellm.utils import get_llm_provider
|
45 |
+
|
46 |
+
import asyncio
|
47 |
+
import subprocess
|
48 |
+
import yaml
|
49 |
+
|
50 |
+
|
51 |
+
@asynccontextmanager
|
52 |
+
async def lifespan(app: FastAPI):
|
53 |
+
log.info("startup_event")
|
54 |
+
# TODO: Check config.yaml file and create one
|
55 |
+
asyncio.create_task(start_litellm_background())
|
56 |
+
yield
|
57 |
+
|
58 |
+
|
59 |
+
app = FastAPI(lifespan=lifespan)
|
60 |
+
|
61 |
+
origins = ["*"]
|
62 |
+
|
63 |
+
app.add_middleware(
|
64 |
+
CORSMiddleware,
|
65 |
+
allow_origins=origins,
|
66 |
+
allow_credentials=True,
|
67 |
+
allow_methods=["*"],
|
68 |
+
allow_headers=["*"],
|
69 |
+
)
|
70 |
+
|
71 |
+
|
72 |
+
LITELLM_CONFIG_DIR = f"{DATA_DIR}/litellm/config.yaml"
|
73 |
+
|
74 |
+
with open(LITELLM_CONFIG_DIR, "r") as file:
|
75 |
+
litellm_config = yaml.safe_load(file)
|
76 |
+
|
77 |
+
|
78 |
+
app.state.ENABLE_MODEL_FILTER = ENABLE_MODEL_FILTER.value
|
79 |
+
app.state.MODEL_FILTER_LIST = MODEL_FILTER_LIST.value
|
80 |
+
|
81 |
+
|
82 |
+
app.state.ENABLE = ENABLE_LITELLM
|
83 |
+
app.state.CONFIG = litellm_config
|
84 |
+
|
85 |
+
# Global variable to store the subprocess reference
|
86 |
+
background_process = None
|
87 |
+
|
88 |
+
CONFLICT_ENV_VARS = [
|
89 |
+
# Uvicorn uses PORT, so LiteLLM might use it as well
|
90 |
+
"PORT",
|
91 |
+
# LiteLLM uses DATABASE_URL for Prisma connections
|
92 |
+
"DATABASE_URL",
|
93 |
+
]
|
94 |
+
|
95 |
+
|
96 |
+
async def run_background_process(command):
|
97 |
+
global background_process
|
98 |
+
log.info("run_background_process")
|
99 |
+
|
100 |
+
try:
|
101 |
+
# Log the command to be executed
|
102 |
+
log.info(f"Executing command: {command}")
|
103 |
+
# Filter environment variables known to conflict with litellm
|
104 |
+
env = {k: v for k, v in os.environ.items() if k not in CONFLICT_ENV_VARS}
|
105 |
+
# Execute the command and create a subprocess
|
106 |
+
process = await asyncio.create_subprocess_exec(
|
107 |
+
*command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, env=env
|
108 |
+
)
|
109 |
+
background_process = process
|
110 |
+
log.info("Subprocess started successfully.")
|
111 |
+
|
112 |
+
# Capture STDERR for debugging purposes
|
113 |
+
stderr_output = await process.stderr.read()
|
114 |
+
stderr_text = stderr_output.decode().strip()
|
115 |
+
if stderr_text:
|
116 |
+
log.info(f"Subprocess STDERR: {stderr_text}")
|
117 |
+
|
118 |
+
# log.info output line by line
|
119 |
+
async for line in process.stdout:
|
120 |
+
log.info(line.decode().strip())
|
121 |
+
|
122 |
+
# Wait for the process to finish
|
123 |
+
returncode = await process.wait()
|
124 |
+
log.info(f"Subprocess exited with return code {returncode}")
|
125 |
+
except Exception as e:
|
126 |
+
log.error(f"Failed to start subprocess: {e}")
|
127 |
+
raise # Optionally re-raise the exception if you want it to propagate
|
128 |
+
|
129 |
+
|
130 |
+
async def start_litellm_background():
|
131 |
+
log.info("start_litellm_background")
|
132 |
+
# Command to run in the background
|
133 |
+
command = [
|
134 |
+
"litellm",
|
135 |
+
"--port",
|
136 |
+
str(LITELLM_PROXY_PORT),
|
137 |
+
"--host",
|
138 |
+
LITELLM_PROXY_HOST,
|
139 |
+
"--telemetry",
|
140 |
+
"False",
|
141 |
+
"--config",
|
142 |
+
LITELLM_CONFIG_DIR,
|
143 |
+
]
|
144 |
+
|
145 |
+
await run_background_process(command)
|
146 |
+
|
147 |
+
|
148 |
+
async def shutdown_litellm_background():
|
149 |
+
log.info("shutdown_litellm_background")
|
150 |
+
global background_process
|
151 |
+
if background_process:
|
152 |
+
background_process.terminate()
|
153 |
+
await background_process.wait() # Ensure the process has terminated
|
154 |
+
log.info("Subprocess terminated")
|
155 |
+
background_process = None
|
156 |
+
|
157 |
+
|
158 |
+
@app.get("/")
|
159 |
+
async def get_status():
|
160 |
+
return {"status": True}
|
161 |
+
|
162 |
+
|
163 |
+
async def restart_litellm():
|
164 |
+
"""
|
165 |
+
Endpoint to restart the litellm background service.
|
166 |
+
"""
|
167 |
+
log.info("Requested restart of litellm service.")
|
168 |
+
try:
|
169 |
+
# Shut down the existing process if it is running
|
170 |
+
await shutdown_litellm_background()
|
171 |
+
log.info("litellm service shutdown complete.")
|
172 |
+
|
173 |
+
# Restart the background service
|
174 |
+
|
175 |
+
asyncio.create_task(start_litellm_background())
|
176 |
+
log.info("litellm service restart complete.")
|
177 |
+
|
178 |
+
return {
|
179 |
+
"status": "success",
|
180 |
+
"message": "litellm service restarted successfully.",
|
181 |
+
}
|
182 |
+
except Exception as e:
|
183 |
+
log.info(f"Error restarting litellm service: {e}")
|
184 |
+
raise HTTPException(
|
185 |
+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=str(e)
|
186 |
+
)
|
187 |
+
|
188 |
+
|
189 |
+
@app.get("/restart")
|
190 |
+
async def restart_litellm_handler(user=Depends(get_admin_user)):
|
191 |
+
return await restart_litellm()
|
192 |
+
|
193 |
+
|
194 |
+
@app.get("/config")
|
195 |
+
async def get_config(user=Depends(get_admin_user)):
|
196 |
+
return app.state.CONFIG
|
197 |
+
|
198 |
+
|
199 |
+
class LiteLLMConfigForm(BaseModel):
|
200 |
+
general_settings: Optional[dict] = None
|
201 |
+
litellm_settings: Optional[dict] = None
|
202 |
+
model_list: Optional[List[dict]] = None
|
203 |
+
router_settings: Optional[dict] = None
|
204 |
+
|
205 |
+
model_config = ConfigDict(protected_namespaces=())
|
206 |
+
|
207 |
+
|
208 |
+
@app.post("/config/update")
|
209 |
+
async def update_config(form_data: LiteLLMConfigForm, user=Depends(get_admin_user)):
|
210 |
+
app.state.CONFIG = form_data.model_dump(exclude_none=True)
|
211 |
+
|
212 |
+
with open(LITELLM_CONFIG_DIR, "w") as file:
|
213 |
+
yaml.dump(app.state.CONFIG, file)
|
214 |
+
|
215 |
+
await restart_litellm()
|
216 |
+
return app.state.CONFIG
|
217 |
+
|
218 |
+
|
219 |
+
@app.get("/models")
|
220 |
+
@app.get("/v1/models")
|
221 |
+
async def get_models(user=Depends(get_current_user)):
|
222 |
+
|
223 |
+
if app.state.ENABLE:
|
224 |
+
while not background_process:
|
225 |
+
await asyncio.sleep(0.1)
|
226 |
+
|
227 |
+
url = f"http://localhost:{LITELLM_PROXY_PORT}/v1"
|
228 |
+
r = None
|
229 |
+
try:
|
230 |
+
r = requests.request(method="GET", url=f"{url}/models")
|
231 |
+
r.raise_for_status()
|
232 |
+
|
233 |
+
data = r.json()
|
234 |
+
|
235 |
+
if app.state.ENABLE_MODEL_FILTER:
|
236 |
+
if user and user.role == "user":
|
237 |
+
data["data"] = list(
|
238 |
+
filter(
|
239 |
+
lambda model: model["id"] in app.state.MODEL_FILTER_LIST,
|
240 |
+
data["data"],
|
241 |
+
)
|
242 |
+
)
|
243 |
+
|
244 |
+
return data
|
245 |
+
except Exception as e:
|
246 |
+
|
247 |
+
log.exception(e)
|
248 |
+
error_detail = "Open WebUI: Server Connection Error"
|
249 |
+
if r is not None:
|
250 |
+
try:
|
251 |
+
res = r.json()
|
252 |
+
if "error" in res:
|
253 |
+
error_detail = f"External: {res['error']}"
|
254 |
+
except:
|
255 |
+
error_detail = f"External: {e}"
|
256 |
+
|
257 |
+
return {
|
258 |
+
"data": [
|
259 |
+
{
|
260 |
+
"id": model["model_name"],
|
261 |
+
"object": "model",
|
262 |
+
"created": int(time.time()),
|
263 |
+
"owned_by": "openai",
|
264 |
+
}
|
265 |
+
for model in app.state.CONFIG["model_list"]
|
266 |
+
],
|
267 |
+
"object": "list",
|
268 |
+
}
|
269 |
+
else:
|
270 |
+
return {
|
271 |
+
"data": [],
|
272 |
+
"object": "list",
|
273 |
+
}
|
274 |
+
|
275 |
+
|
276 |
+
@app.get("/model/info")
|
277 |
+
async def get_model_list(user=Depends(get_admin_user)):
|
278 |
+
return {"data": app.state.CONFIG["model_list"]}
|
279 |
+
|
280 |
+
|
281 |
+
class AddLiteLLMModelForm(BaseModel):
|
282 |
+
model_name: str
|
283 |
+
litellm_params: dict
|
284 |
+
|
285 |
+
model_config = ConfigDict(protected_namespaces=())
|
286 |
+
|
287 |
+
|
288 |
+
@app.post("/model/new")
|
289 |
+
async def add_model_to_config(
|
290 |
+
form_data: AddLiteLLMModelForm, user=Depends(get_admin_user)
|
291 |
+
):
|
292 |
+
try:
|
293 |
+
get_llm_provider(model=form_data.model_name)
|
294 |
+
app.state.CONFIG["model_list"].append(form_data.model_dump())
|
295 |
+
|
296 |
+
with open(LITELLM_CONFIG_DIR, "w") as file:
|
297 |
+
yaml.dump(app.state.CONFIG, file)
|
298 |
+
|
299 |
+
await restart_litellm()
|
300 |
+
|
301 |
+
return {"message": MESSAGES.MODEL_ADDED(form_data.model_name)}
|
302 |
+
except Exception as e:
|
303 |
+
print(e)
|
304 |
+
raise HTTPException(
|
305 |
+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=str(e)
|
306 |
+
)
|
307 |
+
|
308 |
+
|
309 |
+
class DeleteLiteLLMModelForm(BaseModel):
|
310 |
+
id: str
|
311 |
+
|
312 |
+
|
313 |
+
@app.post("/model/delete")
|
314 |
+
async def delete_model_from_config(
|
315 |
+
form_data: DeleteLiteLLMModelForm, user=Depends(get_admin_user)
|
316 |
+
):
|
317 |
+
app.state.CONFIG["model_list"] = [
|
318 |
+
model
|
319 |
+
for model in app.state.CONFIG["model_list"]
|
320 |
+
if model["model_name"] != form_data.id
|
321 |
+
]
|
322 |
+
|
323 |
+
with open(LITELLM_CONFIG_DIR, "w") as file:
|
324 |
+
yaml.dump(app.state.CONFIG, file)
|
325 |
+
|
326 |
+
await restart_litellm()
|
327 |
+
|
328 |
+
return {"message": MESSAGES.MODEL_DELETED(form_data.id)}
|
329 |
+
|
330 |
+
|
331 |
+
@app.api_route("/{path:path}", methods=["GET", "POST", "PUT", "DELETE"])
|
332 |
+
async def proxy(path: str, request: Request, user=Depends(get_verified_user)):
|
333 |
+
body = await request.body()
|
334 |
+
|
335 |
+
url = f"http://localhost:{LITELLM_PROXY_PORT}"
|
336 |
+
|
337 |
+
target_url = f"{url}/{path}"
|
338 |
+
|
339 |
+
headers = {}
|
340 |
+
# headers["Authorization"] = f"Bearer {key}"
|
341 |
+
headers["Content-Type"] = "application/json"
|
342 |
+
|
343 |
+
r = None
|
344 |
+
|
345 |
+
try:
|
346 |
+
r = requests.request(
|
347 |
+
method=request.method,
|
348 |
+
url=target_url,
|
349 |
+
data=body,
|
350 |
+
headers=headers,
|
351 |
+
stream=True,
|
352 |
+
)
|
353 |
+
|
354 |
+
r.raise_for_status()
|
355 |
+
|
356 |
+
# Check if response is SSE
|
357 |
+
if "text/event-stream" in r.headers.get("Content-Type", ""):
|
358 |
+
return StreamingResponse(
|
359 |
+
r.iter_content(chunk_size=8192),
|
360 |
+
status_code=r.status_code,
|
361 |
+
headers=dict(r.headers),
|
362 |
+
)
|
363 |
+
else:
|
364 |
+
response_data = r.json()
|
365 |
+
return response_data
|
366 |
+
except Exception as e:
|
367 |
+
log.exception(e)
|
368 |
+
error_detail = "Open WebUI: Server Connection Error"
|
369 |
+
if r is not None:
|
370 |
+
try:
|
371 |
+
res = r.json()
|
372 |
+
if "error" in res:
|
373 |
+
error_detail = f"External: {res['error']['message'] if 'message' in res['error'] else res['error']}"
|
374 |
+
except:
|
375 |
+
error_detail = f"External: {e}"
|
376 |
+
|
377 |
+
raise HTTPException(
|
378 |
+
status_code=r.status_code if r else 500, detail=error_detail
|
379 |
+
)
|
backend/apps/ollama/main.py
ADDED
@@ -0,0 +1,1447 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from fastapi import (
|
2 |
+
FastAPI,
|
3 |
+
Request,
|
4 |
+
Response,
|
5 |
+
HTTPException,
|
6 |
+
Depends,
|
7 |
+
status,
|
8 |
+
UploadFile,
|
9 |
+
File,
|
10 |
+
BackgroundTasks,
|
11 |
+
)
|
12 |
+
from fastapi.middleware.cors import CORSMiddleware
|
13 |
+
from fastapi.responses import StreamingResponse
|
14 |
+
from fastapi.concurrency import run_in_threadpool
|
15 |
+
|
16 |
+
from pydantic import BaseModel, ConfigDict
|
17 |
+
|
18 |
+
import os
|
19 |
+
import re
|
20 |
+
import copy
|
21 |
+
import random
|
22 |
+
import requests
|
23 |
+
import json
|
24 |
+
import uuid
|
25 |
+
import aiohttp
|
26 |
+
import asyncio
|
27 |
+
import logging
|
28 |
+
import time
|
29 |
+
from urllib.parse import urlparse
|
30 |
+
from typing import Optional, List, Union
|
31 |
+
|
32 |
+
|
33 |
+
from apps.web.models.users import Users
|
34 |
+
from constants import ERROR_MESSAGES
|
35 |
+
from utils.utils import (
|
36 |
+
decode_token,
|
37 |
+
get_current_user,
|
38 |
+
get_verified_user,
|
39 |
+
get_admin_user,
|
40 |
+
)
|
41 |
+
|
42 |
+
|
43 |
+
from config import (
|
44 |
+
SRC_LOG_LEVELS,
|
45 |
+
OLLAMA_BASE_URLS,
|
46 |
+
ENABLE_OLLAMA_API,
|
47 |
+
ENABLE_MODEL_FILTER,
|
48 |
+
MODEL_FILTER_LIST,
|
49 |
+
UPLOAD_DIR,
|
50 |
+
AppConfig,
|
51 |
+
)
|
52 |
+
from utils.misc import calculate_sha256
|
53 |
+
|
54 |
+
log = logging.getLogger(__name__)
|
55 |
+
log.setLevel(SRC_LOG_LEVELS["OLLAMA"])
|
56 |
+
|
57 |
+
app = FastAPI()
|
58 |
+
app.add_middleware(
|
59 |
+
CORSMiddleware,
|
60 |
+
allow_origins=["*"],
|
61 |
+
allow_credentials=True,
|
62 |
+
allow_methods=["*"],
|
63 |
+
allow_headers=["*"],
|
64 |
+
)
|
65 |
+
|
66 |
+
app.state.config = AppConfig()
|
67 |
+
|
68 |
+
app.state.config.ENABLE_MODEL_FILTER = ENABLE_MODEL_FILTER
|
69 |
+
app.state.config.MODEL_FILTER_LIST = MODEL_FILTER_LIST
|
70 |
+
|
71 |
+
|
72 |
+
app.state.config.ENABLE_OLLAMA_API = ENABLE_OLLAMA_API
|
73 |
+
app.state.config.OLLAMA_BASE_URLS = OLLAMA_BASE_URLS
|
74 |
+
app.state.MODELS = {}
|
75 |
+
|
76 |
+
|
77 |
+
REQUEST_POOL = []
|
78 |
+
|
79 |
+
|
80 |
+
# TODO: Implement a more intelligent load balancing mechanism for distributing requests among multiple backend instances.
|
81 |
+
# Current implementation uses a simple round-robin approach (random.choice). Consider incorporating algorithms like weighted round-robin,
|
82 |
+
# least connections, or least response time for better resource utilization and performance optimization.
|
83 |
+
|
84 |
+
|
85 |
+
@app.middleware("http")
|
86 |
+
async def check_url(request: Request, call_next):
|
87 |
+
if len(app.state.MODELS) == 0:
|
88 |
+
await get_all_models()
|
89 |
+
else:
|
90 |
+
pass
|
91 |
+
|
92 |
+
response = await call_next(request)
|
93 |
+
return response
|
94 |
+
|
95 |
+
|
96 |
+
@app.head("/")
|
97 |
+
@app.get("/")
|
98 |
+
async def get_status():
|
99 |
+
return {"status": True}
|
100 |
+
|
101 |
+
|
102 |
+
@app.get("/config")
|
103 |
+
async def get_config(user=Depends(get_admin_user)):
|
104 |
+
return {"ENABLE_OLLAMA_API": app.state.config.ENABLE_OLLAMA_API}
|
105 |
+
|
106 |
+
|
107 |
+
class OllamaConfigForm(BaseModel):
|
108 |
+
enable_ollama_api: Optional[bool] = None
|
109 |
+
|
110 |
+
|
111 |
+
@app.post("/config/update")
|
112 |
+
async def update_config(form_data: OllamaConfigForm, user=Depends(get_admin_user)):
|
113 |
+
app.state.config.ENABLE_OLLAMA_API = form_data.enable_ollama_api
|
114 |
+
return {"ENABLE_OLLAMA_API": app.state.config.ENABLE_OLLAMA_API}
|
115 |
+
|
116 |
+
|
117 |
+
@app.get("/urls")
|
118 |
+
async def get_ollama_api_urls(user=Depends(get_admin_user)):
|
119 |
+
return {"OLLAMA_BASE_URLS": app.state.config.OLLAMA_BASE_URLS}
|
120 |
+
|
121 |
+
|
122 |
+
class UrlUpdateForm(BaseModel):
|
123 |
+
urls: List[str]
|
124 |
+
|
125 |
+
|
126 |
+
@app.post("/urls/update")
|
127 |
+
async def update_ollama_api_url(form_data: UrlUpdateForm, user=Depends(get_admin_user)):
|
128 |
+
app.state.config.OLLAMA_BASE_URLS = form_data.urls
|
129 |
+
|
130 |
+
log.info(f"app.state.config.OLLAMA_BASE_URLS: {app.state.config.OLLAMA_BASE_URLS}")
|
131 |
+
return {"OLLAMA_BASE_URLS": app.state.config.OLLAMA_BASE_URLS}
|
132 |
+
|
133 |
+
|
134 |
+
@app.get("/cancel/{request_id}")
|
135 |
+
async def cancel_ollama_request(request_id: str, user=Depends(get_current_user)):
|
136 |
+
if user:
|
137 |
+
if request_id in REQUEST_POOL:
|
138 |
+
REQUEST_POOL.remove(request_id)
|
139 |
+
return True
|
140 |
+
else:
|
141 |
+
raise HTTPException(status_code=401, detail=ERROR_MESSAGES.ACCESS_PROHIBITED)
|
142 |
+
|
143 |
+
|
144 |
+
async def fetch_url(url):
|
145 |
+
timeout = aiohttp.ClientTimeout(total=5)
|
146 |
+
try:
|
147 |
+
async with aiohttp.ClientSession(timeout=timeout) as session:
|
148 |
+
async with session.get(url) as response:
|
149 |
+
return await response.json()
|
150 |
+
except Exception as e:
|
151 |
+
# Handle connection error here
|
152 |
+
log.error(f"Connection error: {e}")
|
153 |
+
return None
|
154 |
+
|
155 |
+
|
156 |
+
def merge_models_lists(model_lists):
|
157 |
+
merged_models = {}
|
158 |
+
|
159 |
+
for idx, model_list in enumerate(model_lists):
|
160 |
+
if model_list is not None:
|
161 |
+
for model in model_list:
|
162 |
+
digest = model["digest"]
|
163 |
+
if digest not in merged_models:
|
164 |
+
model["urls"] = [idx]
|
165 |
+
merged_models[digest] = model
|
166 |
+
else:
|
167 |
+
merged_models[digest]["urls"].append(idx)
|
168 |
+
|
169 |
+
return list(merged_models.values())
|
170 |
+
|
171 |
+
|
172 |
+
# user=Depends(get_current_user)
|
173 |
+
|
174 |
+
|
175 |
+
async def get_all_models():
|
176 |
+
log.info("get_all_models()")
|
177 |
+
|
178 |
+
if app.state.config.ENABLE_OLLAMA_API:
|
179 |
+
tasks = [
|
180 |
+
fetch_url(f"{url}/api/tags") for url in app.state.config.OLLAMA_BASE_URLS
|
181 |
+
]
|
182 |
+
responses = await asyncio.gather(*tasks)
|
183 |
+
|
184 |
+
models = {
|
185 |
+
"models": merge_models_lists(
|
186 |
+
map(
|
187 |
+
lambda response: response["models"] if response else None, responses
|
188 |
+
)
|
189 |
+
)
|
190 |
+
}
|
191 |
+
|
192 |
+
else:
|
193 |
+
models = {"models": []}
|
194 |
+
|
195 |
+
app.state.MODELS = {model["model"]: model for model in models["models"]}
|
196 |
+
|
197 |
+
return models
|
198 |
+
|
199 |
+
|
200 |
+
@app.get("/api/tags")
|
201 |
+
@app.get("/api/tags/{url_idx}")
|
202 |
+
async def get_ollama_tags(
|
203 |
+
url_idx: Optional[int] = None, user=Depends(get_verified_user)
|
204 |
+
):
|
205 |
+
if url_idx == None:
|
206 |
+
models = await get_all_models()
|
207 |
+
|
208 |
+
if app.state.config.ENABLE_MODEL_FILTER:
|
209 |
+
if user.role == "user":
|
210 |
+
models["models"] = list(
|
211 |
+
filter(
|
212 |
+
lambda model: model["name"]
|
213 |
+
in app.state.config.MODEL_FILTER_LIST,
|
214 |
+
models["models"],
|
215 |
+
)
|
216 |
+
)
|
217 |
+
return models
|
218 |
+
return models
|
219 |
+
else:
|
220 |
+
url = app.state.config.OLLAMA_BASE_URLS[url_idx]
|
221 |
+
try:
|
222 |
+
r = requests.request(method="GET", url=f"{url}/api/tags")
|
223 |
+
r.raise_for_status()
|
224 |
+
|
225 |
+
return r.json()
|
226 |
+
except Exception as e:
|
227 |
+
log.exception(e)
|
228 |
+
error_detail = "Open WebUI: Server Connection Error"
|
229 |
+
if r is not None:
|
230 |
+
try:
|
231 |
+
res = r.json()
|
232 |
+
if "error" in res:
|
233 |
+
error_detail = f"Ollama: {res['error']}"
|
234 |
+
except:
|
235 |
+
error_detail = f"Ollama: {e}"
|
236 |
+
|
237 |
+
raise HTTPException(
|
238 |
+
status_code=r.status_code if r else 500,
|
239 |
+
detail=error_detail,
|
240 |
+
)
|
241 |
+
|
242 |
+
|
243 |
+
@app.get("/api/version")
|
244 |
+
@app.get("/api/version/{url_idx}")
|
245 |
+
async def get_ollama_versions(url_idx: Optional[int] = None):
|
246 |
+
|
247 |
+
if url_idx == None:
|
248 |
+
|
249 |
+
# returns lowest version
|
250 |
+
tasks = [
|
251 |
+
fetch_url(f"{url}/api/version") for url in app.state.config.OLLAMA_BASE_URLS
|
252 |
+
]
|
253 |
+
responses = await asyncio.gather(*tasks)
|
254 |
+
responses = list(filter(lambda x: x is not None, responses))
|
255 |
+
|
256 |
+
if len(responses) > 0:
|
257 |
+
lowest_version = min(
|
258 |
+
responses,
|
259 |
+
key=lambda x: tuple(
|
260 |
+
map(int, re.sub(r"^v|-.*", "", x["version"]).split("."))
|
261 |
+
),
|
262 |
+
)
|
263 |
+
|
264 |
+
return {"version": lowest_version["version"]}
|
265 |
+
else:
|
266 |
+
raise HTTPException(
|
267 |
+
status_code=500,
|
268 |
+
detail=ERROR_MESSAGES.OLLAMA_NOT_FOUND,
|
269 |
+
)
|
270 |
+
else:
|
271 |
+
url = app.state.config.OLLAMA_BASE_URLS[url_idx]
|
272 |
+
try:
|
273 |
+
r = requests.request(method="GET", url=f"{url}/api/version")
|
274 |
+
r.raise_for_status()
|
275 |
+
|
276 |
+
return r.json()
|
277 |
+
except Exception as e:
|
278 |
+
log.exception(e)
|
279 |
+
error_detail = "Open WebUI: Server Connection Error"
|
280 |
+
if r is not None:
|
281 |
+
try:
|
282 |
+
res = r.json()
|
283 |
+
if "error" in res:
|
284 |
+
error_detail = f"Ollama: {res['error']}"
|
285 |
+
except:
|
286 |
+
error_detail = f"Ollama: {e}"
|
287 |
+
|
288 |
+
raise HTTPException(
|
289 |
+
status_code=r.status_code if r else 500,
|
290 |
+
detail=error_detail,
|
291 |
+
)
|
292 |
+
|
293 |
+
|
294 |
+
class ModelNameForm(BaseModel):
|
295 |
+
name: str
|
296 |
+
|
297 |
+
|
298 |
+
@app.post("/api/pull")
|
299 |
+
@app.post("/api/pull/{url_idx}")
|
300 |
+
async def pull_model(
|
301 |
+
form_data: ModelNameForm, url_idx: int = 0, user=Depends(get_admin_user)
|
302 |
+
):
|
303 |
+
url = app.state.config.OLLAMA_BASE_URLS[url_idx]
|
304 |
+
log.info(f"url: {url}")
|
305 |
+
|
306 |
+
r = None
|
307 |
+
|
308 |
+
def get_request():
|
309 |
+
nonlocal url
|
310 |
+
nonlocal r
|
311 |
+
|
312 |
+
request_id = str(uuid.uuid4())
|
313 |
+
try:
|
314 |
+
REQUEST_POOL.append(request_id)
|
315 |
+
|
316 |
+
def stream_content():
|
317 |
+
try:
|
318 |
+
yield json.dumps({"id": request_id, "done": False}) + "\n"
|
319 |
+
|
320 |
+
for chunk in r.iter_content(chunk_size=8192):
|
321 |
+
if request_id in REQUEST_POOL:
|
322 |
+
yield chunk
|
323 |
+
else:
|
324 |
+
log.warning("User: canceled request")
|
325 |
+
break
|
326 |
+
finally:
|
327 |
+
if hasattr(r, "close"):
|
328 |
+
r.close()
|
329 |
+
if request_id in REQUEST_POOL:
|
330 |
+
REQUEST_POOL.remove(request_id)
|
331 |
+
|
332 |
+
r = requests.request(
|
333 |
+
method="POST",
|
334 |
+
url=f"{url}/api/pull",
|
335 |
+
data=form_data.model_dump_json(exclude_none=True).encode(),
|
336 |
+
stream=True,
|
337 |
+
)
|
338 |
+
|
339 |
+
r.raise_for_status()
|
340 |
+
|
341 |
+
return StreamingResponse(
|
342 |
+
stream_content(),
|
343 |
+
status_code=r.status_code,
|
344 |
+
headers=dict(r.headers),
|
345 |
+
)
|
346 |
+
except Exception as e:
|
347 |
+
raise e
|
348 |
+
|
349 |
+
try:
|
350 |
+
return await run_in_threadpool(get_request)
|
351 |
+
|
352 |
+
except Exception as e:
|
353 |
+
log.exception(e)
|
354 |
+
error_detail = "Open WebUI: Server Connection Error"
|
355 |
+
if r is not None:
|
356 |
+
try:
|
357 |
+
res = r.json()
|
358 |
+
if "error" in res:
|
359 |
+
error_detail = f"Ollama: {res['error']}"
|
360 |
+
except:
|
361 |
+
error_detail = f"Ollama: {e}"
|
362 |
+
|
363 |
+
raise HTTPException(
|
364 |
+
status_code=r.status_code if r else 500,
|
365 |
+
detail=error_detail,
|
366 |
+
)
|
367 |
+
|
368 |
+
|
369 |
+
class PushModelForm(BaseModel):
|
370 |
+
name: str
|
371 |
+
insecure: Optional[bool] = None
|
372 |
+
stream: Optional[bool] = None
|
373 |
+
|
374 |
+
|
375 |
+
@app.delete("/api/push")
|
376 |
+
@app.delete("/api/push/{url_idx}")
|
377 |
+
async def push_model(
|
378 |
+
form_data: PushModelForm,
|
379 |
+
url_idx: Optional[int] = None,
|
380 |
+
user=Depends(get_admin_user),
|
381 |
+
):
|
382 |
+
if url_idx == None:
|
383 |
+
if form_data.name in app.state.MODELS:
|
384 |
+
url_idx = app.state.MODELS[form_data.name]["urls"][0]
|
385 |
+
else:
|
386 |
+
raise HTTPException(
|
387 |
+
status_code=400,
|
388 |
+
detail=ERROR_MESSAGES.MODEL_NOT_FOUND(form_data.name),
|
389 |
+
)
|
390 |
+
|
391 |
+
url = app.state.config.OLLAMA_BASE_URLS[url_idx]
|
392 |
+
log.debug(f"url: {url}")
|
393 |
+
|
394 |
+
r = None
|
395 |
+
|
396 |
+
def get_request():
|
397 |
+
nonlocal url
|
398 |
+
nonlocal r
|
399 |
+
try:
|
400 |
+
|
401 |
+
def stream_content():
|
402 |
+
for chunk in r.iter_content(chunk_size=8192):
|
403 |
+
yield chunk
|
404 |
+
|
405 |
+
r = requests.request(
|
406 |
+
method="POST",
|
407 |
+
url=f"{url}/api/push",
|
408 |
+
data=form_data.model_dump_json(exclude_none=True).encode(),
|
409 |
+
)
|
410 |
+
|
411 |
+
r.raise_for_status()
|
412 |
+
|
413 |
+
return StreamingResponse(
|
414 |
+
stream_content(),
|
415 |
+
status_code=r.status_code,
|
416 |
+
headers=dict(r.headers),
|
417 |
+
)
|
418 |
+
except Exception as e:
|
419 |
+
raise e
|
420 |
+
|
421 |
+
try:
|
422 |
+
return await run_in_threadpool(get_request)
|
423 |
+
except Exception as e:
|
424 |
+
log.exception(e)
|
425 |
+
error_detail = "Open WebUI: Server Connection Error"
|
426 |
+
if r is not None:
|
427 |
+
try:
|
428 |
+
res = r.json()
|
429 |
+
if "error" in res:
|
430 |
+
error_detail = f"Ollama: {res['error']}"
|
431 |
+
except:
|
432 |
+
error_detail = f"Ollama: {e}"
|
433 |
+
|
434 |
+
raise HTTPException(
|
435 |
+
status_code=r.status_code if r else 500,
|
436 |
+
detail=error_detail,
|
437 |
+
)
|
438 |
+
|
439 |
+
|
440 |
+
class CreateModelForm(BaseModel):
|
441 |
+
name: str
|
442 |
+
modelfile: Optional[str] = None
|
443 |
+
stream: Optional[bool] = None
|
444 |
+
path: Optional[str] = None
|
445 |
+
|
446 |
+
|
447 |
+
@app.post("/api/create")
|
448 |
+
@app.post("/api/create/{url_idx}")
|
449 |
+
async def create_model(
|
450 |
+
form_data: CreateModelForm, url_idx: int = 0, user=Depends(get_admin_user)
|
451 |
+
):
|
452 |
+
log.debug(f"form_data: {form_data}")
|
453 |
+
url = app.state.config.OLLAMA_BASE_URLS[url_idx]
|
454 |
+
log.info(f"url: {url}")
|
455 |
+
|
456 |
+
r = None
|
457 |
+
|
458 |
+
def get_request():
|
459 |
+
nonlocal url
|
460 |
+
nonlocal r
|
461 |
+
try:
|
462 |
+
|
463 |
+
def stream_content():
|
464 |
+
for chunk in r.iter_content(chunk_size=8192):
|
465 |
+
yield chunk
|
466 |
+
|
467 |
+
r = requests.request(
|
468 |
+
method="POST",
|
469 |
+
url=f"{url}/api/create",
|
470 |
+
data=form_data.model_dump_json(exclude_none=True).encode(),
|
471 |
+
stream=True,
|
472 |
+
)
|
473 |
+
|
474 |
+
r.raise_for_status()
|
475 |
+
|
476 |
+
log.debug(f"r: {r}")
|
477 |
+
|
478 |
+
return StreamingResponse(
|
479 |
+
stream_content(),
|
480 |
+
status_code=r.status_code,
|
481 |
+
headers=dict(r.headers),
|
482 |
+
)
|
483 |
+
except Exception as e:
|
484 |
+
raise e
|
485 |
+
|
486 |
+
try:
|
487 |
+
return await run_in_threadpool(get_request)
|
488 |
+
except Exception as e:
|
489 |
+
log.exception(e)
|
490 |
+
error_detail = "Open WebUI: Server Connection Error"
|
491 |
+
if r is not None:
|
492 |
+
try:
|
493 |
+
res = r.json()
|
494 |
+
if "error" in res:
|
495 |
+
error_detail = f"Ollama: {res['error']}"
|
496 |
+
except:
|
497 |
+
error_detail = f"Ollama: {e}"
|
498 |
+
|
499 |
+
raise HTTPException(
|
500 |
+
status_code=r.status_code if r else 500,
|
501 |
+
detail=error_detail,
|
502 |
+
)
|
503 |
+
|
504 |
+
|
505 |
+
class CopyModelForm(BaseModel):
|
506 |
+
source: str
|
507 |
+
destination: str
|
508 |
+
|
509 |
+
|
510 |
+
@app.post("/api/copy")
|
511 |
+
@app.post("/api/copy/{url_idx}")
|
512 |
+
async def copy_model(
|
513 |
+
form_data: CopyModelForm,
|
514 |
+
url_idx: Optional[int] = None,
|
515 |
+
user=Depends(get_admin_user),
|
516 |
+
):
|
517 |
+
if url_idx == None:
|
518 |
+
if form_data.source in app.state.MODELS:
|
519 |
+
url_idx = app.state.MODELS[form_data.source]["urls"][0]
|
520 |
+
else:
|
521 |
+
raise HTTPException(
|
522 |
+
status_code=400,
|
523 |
+
detail=ERROR_MESSAGES.MODEL_NOT_FOUND(form_data.source),
|
524 |
+
)
|
525 |
+
|
526 |
+
url = app.state.config.OLLAMA_BASE_URLS[url_idx]
|
527 |
+
log.info(f"url: {url}")
|
528 |
+
|
529 |
+
try:
|
530 |
+
r = requests.request(
|
531 |
+
method="POST",
|
532 |
+
url=f"{url}/api/copy",
|
533 |
+
data=form_data.model_dump_json(exclude_none=True).encode(),
|
534 |
+
)
|
535 |
+
r.raise_for_status()
|
536 |
+
|
537 |
+
log.debug(f"r.text: {r.text}")
|
538 |
+
|
539 |
+
return True
|
540 |
+
except Exception as e:
|
541 |
+
log.exception(e)
|
542 |
+
error_detail = "Open WebUI: Server Connection Error"
|
543 |
+
if r is not None:
|
544 |
+
try:
|
545 |
+
res = r.json()
|
546 |
+
if "error" in res:
|
547 |
+
error_detail = f"Ollama: {res['error']}"
|
548 |
+
except:
|
549 |
+
error_detail = f"Ollama: {e}"
|
550 |
+
|
551 |
+
raise HTTPException(
|
552 |
+
status_code=r.status_code if r else 500,
|
553 |
+
detail=error_detail,
|
554 |
+
)
|
555 |
+
|
556 |
+
|
557 |
+
@app.delete("/api/delete")
|
558 |
+
@app.delete("/api/delete/{url_idx}")
|
559 |
+
async def delete_model(
|
560 |
+
form_data: ModelNameForm,
|
561 |
+
url_idx: Optional[int] = None,
|
562 |
+
user=Depends(get_admin_user),
|
563 |
+
):
|
564 |
+
if url_idx == None:
|
565 |
+
if form_data.name in app.state.MODELS:
|
566 |
+
url_idx = app.state.MODELS[form_data.name]["urls"][0]
|
567 |
+
else:
|
568 |
+
raise HTTPException(
|
569 |
+
status_code=400,
|
570 |
+
detail=ERROR_MESSAGES.MODEL_NOT_FOUND(form_data.name),
|
571 |
+
)
|
572 |
+
|
573 |
+
url = app.state.config.OLLAMA_BASE_URLS[url_idx]
|
574 |
+
log.info(f"url: {url}")
|
575 |
+
|
576 |
+
try:
|
577 |
+
r = requests.request(
|
578 |
+
method="DELETE",
|
579 |
+
url=f"{url}/api/delete",
|
580 |
+
data=form_data.model_dump_json(exclude_none=True).encode(),
|
581 |
+
)
|
582 |
+
r.raise_for_status()
|
583 |
+
|
584 |
+
log.debug(f"r.text: {r.text}")
|
585 |
+
|
586 |
+
return True
|
587 |
+
except Exception as e:
|
588 |
+
log.exception(e)
|
589 |
+
error_detail = "Open WebUI: Server Connection Error"
|
590 |
+
if r is not None:
|
591 |
+
try:
|
592 |
+
res = r.json()
|
593 |
+
if "error" in res:
|
594 |
+
error_detail = f"Ollama: {res['error']}"
|
595 |
+
except:
|
596 |
+
error_detail = f"Ollama: {e}"
|
597 |
+
|
598 |
+
raise HTTPException(
|
599 |
+
status_code=r.status_code if r else 500,
|
600 |
+
detail=error_detail,
|
601 |
+
)
|
602 |
+
|
603 |
+
|
604 |
+
@app.post("/api/show")
|
605 |
+
async def show_model_info(form_data: ModelNameForm, user=Depends(get_verified_user)):
|
606 |
+
if form_data.name not in app.state.MODELS:
|
607 |
+
raise HTTPException(
|
608 |
+
status_code=400,
|
609 |
+
detail=ERROR_MESSAGES.MODEL_NOT_FOUND(form_data.name),
|
610 |
+
)
|
611 |
+
|
612 |
+
url_idx = random.choice(app.state.MODELS[form_data.name]["urls"])
|
613 |
+
url = app.state.config.OLLAMA_BASE_URLS[url_idx]
|
614 |
+
log.info(f"url: {url}")
|
615 |
+
|
616 |
+
try:
|
617 |
+
r = requests.request(
|
618 |
+
method="POST",
|
619 |
+
url=f"{url}/api/show",
|
620 |
+
data=form_data.model_dump_json(exclude_none=True).encode(),
|
621 |
+
)
|
622 |
+
r.raise_for_status()
|
623 |
+
|
624 |
+
return r.json()
|
625 |
+
except Exception as e:
|
626 |
+
log.exception(e)
|
627 |
+
error_detail = "Open WebUI: Server Connection Error"
|
628 |
+
if r is not None:
|
629 |
+
try:
|
630 |
+
res = r.json()
|
631 |
+
if "error" in res:
|
632 |
+
error_detail = f"Ollama: {res['error']}"
|
633 |
+
except:
|
634 |
+
error_detail = f"Ollama: {e}"
|
635 |
+
|
636 |
+
raise HTTPException(
|
637 |
+
status_code=r.status_code if r else 500,
|
638 |
+
detail=error_detail,
|
639 |
+
)
|
640 |
+
|
641 |
+
|
642 |
+
class GenerateEmbeddingsForm(BaseModel):
|
643 |
+
model: str
|
644 |
+
prompt: str
|
645 |
+
options: Optional[dict] = None
|
646 |
+
keep_alive: Optional[Union[int, str]] = None
|
647 |
+
|
648 |
+
|
649 |
+
@app.post("/api/embeddings")
|
650 |
+
@app.post("/api/embeddings/{url_idx}")
|
651 |
+
async def generate_embeddings(
|
652 |
+
form_data: GenerateEmbeddingsForm,
|
653 |
+
url_idx: Optional[int] = None,
|
654 |
+
user=Depends(get_verified_user),
|
655 |
+
):
|
656 |
+
if url_idx == None:
|
657 |
+
model = form_data.model
|
658 |
+
|
659 |
+
if ":" not in model:
|
660 |
+
model = f"{model}:latest"
|
661 |
+
|
662 |
+
if model in app.state.MODELS:
|
663 |
+
url_idx = random.choice(app.state.MODELS[model]["urls"])
|
664 |
+
else:
|
665 |
+
raise HTTPException(
|
666 |
+
status_code=400,
|
667 |
+
detail=ERROR_MESSAGES.MODEL_NOT_FOUND(form_data.model),
|
668 |
+
)
|
669 |
+
|
670 |
+
url = app.state.config.OLLAMA_BASE_URLS[url_idx]
|
671 |
+
log.info(f"url: {url}")
|
672 |
+
|
673 |
+
try:
|
674 |
+
r = requests.request(
|
675 |
+
method="POST",
|
676 |
+
url=f"{url}/api/embeddings",
|
677 |
+
data=form_data.model_dump_json(exclude_none=True).encode(),
|
678 |
+
)
|
679 |
+
r.raise_for_status()
|
680 |
+
|
681 |
+
return r.json()
|
682 |
+
except Exception as e:
|
683 |
+
log.exception(e)
|
684 |
+
error_detail = "Open WebUI: Server Connection Error"
|
685 |
+
if r is not None:
|
686 |
+
try:
|
687 |
+
res = r.json()
|
688 |
+
if "error" in res:
|
689 |
+
error_detail = f"Ollama: {res['error']}"
|
690 |
+
except:
|
691 |
+
error_detail = f"Ollama: {e}"
|
692 |
+
|
693 |
+
raise HTTPException(
|
694 |
+
status_code=r.status_code if r else 500,
|
695 |
+
detail=error_detail,
|
696 |
+
)
|
697 |
+
|
698 |
+
|
699 |
+
def generate_ollama_embeddings(
|
700 |
+
form_data: GenerateEmbeddingsForm,
|
701 |
+
url_idx: Optional[int] = None,
|
702 |
+
):
|
703 |
+
|
704 |
+
log.info(f"generate_ollama_embeddings {form_data}")
|
705 |
+
|
706 |
+
if url_idx == None:
|
707 |
+
model = form_data.model
|
708 |
+
|
709 |
+
if ":" not in model:
|
710 |
+
model = f"{model}:latest"
|
711 |
+
|
712 |
+
if model in app.state.MODELS:
|
713 |
+
url_idx = random.choice(app.state.MODELS[model]["urls"])
|
714 |
+
else:
|
715 |
+
raise HTTPException(
|
716 |
+
status_code=400,
|
717 |
+
detail=ERROR_MESSAGES.MODEL_NOT_FOUND(form_data.model),
|
718 |
+
)
|
719 |
+
|
720 |
+
url = app.state.config.OLLAMA_BASE_URLS[url_idx]
|
721 |
+
log.info(f"url: {url}")
|
722 |
+
|
723 |
+
try:
|
724 |
+
r = requests.request(
|
725 |
+
method="POST",
|
726 |
+
url=f"{url}/api/embeddings",
|
727 |
+
data=form_data.model_dump_json(exclude_none=True).encode(),
|
728 |
+
)
|
729 |
+
r.raise_for_status()
|
730 |
+
|
731 |
+
data = r.json()
|
732 |
+
|
733 |
+
log.info(f"generate_ollama_embeddings {data}")
|
734 |
+
|
735 |
+
if "embedding" in data:
|
736 |
+
return data["embedding"]
|
737 |
+
else:
|
738 |
+
raise "Something went wrong :/"
|
739 |
+
except Exception as e:
|
740 |
+
log.exception(e)
|
741 |
+
error_detail = "Open WebUI: Server Connection Error"
|
742 |
+
if r is not None:
|
743 |
+
try:
|
744 |
+
res = r.json()
|
745 |
+
if "error" in res:
|
746 |
+
error_detail = f"Ollama: {res['error']}"
|
747 |
+
except:
|
748 |
+
error_detail = f"Ollama: {e}"
|
749 |
+
|
750 |
+
raise error_detail
|
751 |
+
|
752 |
+
|
753 |
+
class GenerateCompletionForm(BaseModel):
|
754 |
+
model: str
|
755 |
+
prompt: str
|
756 |
+
images: Optional[List[str]] = None
|
757 |
+
format: Optional[str] = None
|
758 |
+
options: Optional[dict] = None
|
759 |
+
system: Optional[str] = None
|
760 |
+
template: Optional[str] = None
|
761 |
+
context: Optional[str] = None
|
762 |
+
stream: Optional[bool] = True
|
763 |
+
raw: Optional[bool] = None
|
764 |
+
keep_alive: Optional[Union[int, str]] = None
|
765 |
+
|
766 |
+
|
767 |
+
@app.post("/api/generate")
|
768 |
+
@app.post("/api/generate/{url_idx}")
|
769 |
+
async def generate_completion(
|
770 |
+
form_data: GenerateCompletionForm,
|
771 |
+
url_idx: Optional[int] = None,
|
772 |
+
user=Depends(get_verified_user),
|
773 |
+
):
|
774 |
+
|
775 |
+
if url_idx == None:
|
776 |
+
model = form_data.model
|
777 |
+
|
778 |
+
if ":" not in model:
|
779 |
+
model = f"{model}:latest"
|
780 |
+
|
781 |
+
if model in app.state.MODELS:
|
782 |
+
url_idx = random.choice(app.state.MODELS[model]["urls"])
|
783 |
+
else:
|
784 |
+
raise HTTPException(
|
785 |
+
status_code=400,
|
786 |
+
detail=ERROR_MESSAGES.MODEL_NOT_FOUND(form_data.model),
|
787 |
+
)
|
788 |
+
|
789 |
+
url = app.state.config.OLLAMA_BASE_URLS[url_idx]
|
790 |
+
log.info(f"url: {url}")
|
791 |
+
|
792 |
+
r = None
|
793 |
+
|
794 |
+
def get_request():
|
795 |
+
nonlocal form_data
|
796 |
+
nonlocal r
|
797 |
+
|
798 |
+
request_id = str(uuid.uuid4())
|
799 |
+
try:
|
800 |
+
REQUEST_POOL.append(request_id)
|
801 |
+
|
802 |
+
def stream_content():
|
803 |
+
try:
|
804 |
+
if form_data.stream:
|
805 |
+
yield json.dumps({"id": request_id, "done": False}) + "\n"
|
806 |
+
|
807 |
+
for chunk in r.iter_content(chunk_size=8192):
|
808 |
+
if request_id in REQUEST_POOL:
|
809 |
+
yield chunk
|
810 |
+
else:
|
811 |
+
log.warning("User: canceled request")
|
812 |
+
break
|
813 |
+
finally:
|
814 |
+
if hasattr(r, "close"):
|
815 |
+
r.close()
|
816 |
+
if request_id in REQUEST_POOL:
|
817 |
+
REQUEST_POOL.remove(request_id)
|
818 |
+
|
819 |
+
r = requests.request(
|
820 |
+
method="POST",
|
821 |
+
url=f"{url}/api/generate",
|
822 |
+
data=form_data.model_dump_json(exclude_none=True).encode(),
|
823 |
+
stream=True,
|
824 |
+
)
|
825 |
+
|
826 |
+
r.raise_for_status()
|
827 |
+
|
828 |
+
return StreamingResponse(
|
829 |
+
stream_content(),
|
830 |
+
status_code=r.status_code,
|
831 |
+
headers=dict(r.headers),
|
832 |
+
)
|
833 |
+
except Exception as e:
|
834 |
+
raise e
|
835 |
+
|
836 |
+
try:
|
837 |
+
return await run_in_threadpool(get_request)
|
838 |
+
except Exception as e:
|
839 |
+
error_detail = "Open WebUI: Server Connection Error"
|
840 |
+
if r is not None:
|
841 |
+
try:
|
842 |
+
res = r.json()
|
843 |
+
if "error" in res:
|
844 |
+
error_detail = f"Ollama: {res['error']}"
|
845 |
+
except:
|
846 |
+
error_detail = f"Ollama: {e}"
|
847 |
+
|
848 |
+
raise HTTPException(
|
849 |
+
status_code=r.status_code if r else 500,
|
850 |
+
detail=error_detail,
|
851 |
+
)
|
852 |
+
|
853 |
+
|
854 |
+
class ChatMessage(BaseModel):
|
855 |
+
role: str
|
856 |
+
content: str
|
857 |
+
images: Optional[List[str]] = None
|
858 |
+
|
859 |
+
|
860 |
+
class GenerateChatCompletionForm(BaseModel):
|
861 |
+
model: str
|
862 |
+
messages: List[ChatMessage]
|
863 |
+
format: Optional[str] = None
|
864 |
+
options: Optional[dict] = None
|
865 |
+
template: Optional[str] = None
|
866 |
+
stream: Optional[bool] = None
|
867 |
+
keep_alive: Optional[Union[int, str]] = None
|
868 |
+
|
869 |
+
|
870 |
+
@app.post("/api/chat")
|
871 |
+
@app.post("/api/chat/{url_idx}")
|
872 |
+
async def generate_chat_completion(
|
873 |
+
form_data: GenerateChatCompletionForm,
|
874 |
+
url_idx: Optional[int] = None,
|
875 |
+
user=Depends(get_verified_user),
|
876 |
+
):
|
877 |
+
|
878 |
+
if url_idx == None:
|
879 |
+
model = form_data.model
|
880 |
+
|
881 |
+
if ":" not in model:
|
882 |
+
model = f"{model}:latest"
|
883 |
+
|
884 |
+
if model in app.state.MODELS:
|
885 |
+
url_idx = random.choice(app.state.MODELS[model]["urls"])
|
886 |
+
else:
|
887 |
+
raise HTTPException(
|
888 |
+
status_code=400,
|
889 |
+
detail=ERROR_MESSAGES.MODEL_NOT_FOUND(form_data.model),
|
890 |
+
)
|
891 |
+
|
892 |
+
url = app.state.config.OLLAMA_BASE_URLS[url_idx]
|
893 |
+
log.info(f"url: {url}")
|
894 |
+
|
895 |
+
r = None
|
896 |
+
|
897 |
+
log.debug(
|
898 |
+
"form_data.model_dump_json(exclude_none=True).encode(): {0} ".format(
|
899 |
+
form_data.model_dump_json(exclude_none=True).encode()
|
900 |
+
)
|
901 |
+
)
|
902 |
+
|
903 |
+
def get_request():
|
904 |
+
nonlocal form_data
|
905 |
+
nonlocal r
|
906 |
+
|
907 |
+
request_id = str(uuid.uuid4())
|
908 |
+
try:
|
909 |
+
REQUEST_POOL.append(request_id)
|
910 |
+
|
911 |
+
def stream_content():
|
912 |
+
try:
|
913 |
+
if form_data.stream:
|
914 |
+
yield json.dumps({"id": request_id, "done": False}) + "\n"
|
915 |
+
|
916 |
+
for chunk in r.iter_content(chunk_size=8192):
|
917 |
+
if request_id in REQUEST_POOL:
|
918 |
+
yield chunk
|
919 |
+
else:
|
920 |
+
log.warning("User: canceled request")
|
921 |
+
break
|
922 |
+
finally:
|
923 |
+
if hasattr(r, "close"):
|
924 |
+
r.close()
|
925 |
+
if request_id in REQUEST_POOL:
|
926 |
+
REQUEST_POOL.remove(request_id)
|
927 |
+
|
928 |
+
r = requests.request(
|
929 |
+
method="POST",
|
930 |
+
url=f"{url}/api/chat",
|
931 |
+
data=form_data.model_dump_json(exclude_none=True).encode(),
|
932 |
+
stream=True,
|
933 |
+
)
|
934 |
+
|
935 |
+
r.raise_for_status()
|
936 |
+
|
937 |
+
return StreamingResponse(
|
938 |
+
stream_content(),
|
939 |
+
status_code=r.status_code,
|
940 |
+
headers=dict(r.headers),
|
941 |
+
)
|
942 |
+
except Exception as e:
|
943 |
+
log.exception(e)
|
944 |
+
raise e
|
945 |
+
|
946 |
+
try:
|
947 |
+
return await run_in_threadpool(get_request)
|
948 |
+
except Exception as e:
|
949 |
+
error_detail = "Open WebUI: Server Connection Error"
|
950 |
+
if r is not None:
|
951 |
+
try:
|
952 |
+
res = r.json()
|
953 |
+
if "error" in res:
|
954 |
+
error_detail = f"Ollama: {res['error']}"
|
955 |
+
except:
|
956 |
+
error_detail = f"Ollama: {e}"
|
957 |
+
|
958 |
+
raise HTTPException(
|
959 |
+
status_code=r.status_code if r else 500,
|
960 |
+
detail=error_detail,
|
961 |
+
)
|
962 |
+
|
963 |
+
|
964 |
+
# TODO: we should update this part once Ollama supports other types
|
965 |
+
class OpenAIChatMessage(BaseModel):
|
966 |
+
role: str
|
967 |
+
content: str
|
968 |
+
|
969 |
+
model_config = ConfigDict(extra="allow")
|
970 |
+
|
971 |
+
|
972 |
+
class OpenAIChatCompletionForm(BaseModel):
|
973 |
+
model: str
|
974 |
+
messages: List[OpenAIChatMessage]
|
975 |
+
|
976 |
+
model_config = ConfigDict(extra="allow")
|
977 |
+
|
978 |
+
|
979 |
+
@app.post("/v1/chat/completions")
|
980 |
+
@app.post("/v1/chat/completions/{url_idx}")
|
981 |
+
async def generate_openai_chat_completion(
|
982 |
+
form_data: OpenAIChatCompletionForm,
|
983 |
+
url_idx: Optional[int] = None,
|
984 |
+
user=Depends(get_verified_user),
|
985 |
+
):
|
986 |
+
|
987 |
+
if url_idx == None:
|
988 |
+
model = form_data.model
|
989 |
+
|
990 |
+
if ":" not in model:
|
991 |
+
model = f"{model}:latest"
|
992 |
+
|
993 |
+
if model in app.state.MODELS:
|
994 |
+
url_idx = random.choice(app.state.MODELS[model]["urls"])
|
995 |
+
else:
|
996 |
+
raise HTTPException(
|
997 |
+
status_code=400,
|
998 |
+
detail=ERROR_MESSAGES.MODEL_NOT_FOUND(form_data.model),
|
999 |
+
)
|
1000 |
+
|
1001 |
+
url = app.state.config.OLLAMA_BASE_URLS[url_idx]
|
1002 |
+
log.info(f"url: {url}")
|
1003 |
+
|
1004 |
+
r = None
|
1005 |
+
|
1006 |
+
def get_request():
|
1007 |
+
nonlocal form_data
|
1008 |
+
nonlocal r
|
1009 |
+
|
1010 |
+
request_id = str(uuid.uuid4())
|
1011 |
+
try:
|
1012 |
+
REQUEST_POOL.append(request_id)
|
1013 |
+
|
1014 |
+
def stream_content():
|
1015 |
+
try:
|
1016 |
+
if form_data.stream:
|
1017 |
+
yield json.dumps(
|
1018 |
+
{"request_id": request_id, "done": False}
|
1019 |
+
) + "\n"
|
1020 |
+
|
1021 |
+
for chunk in r.iter_content(chunk_size=8192):
|
1022 |
+
if request_id in REQUEST_POOL:
|
1023 |
+
yield chunk
|
1024 |
+
else:
|
1025 |
+
log.warning("User: canceled request")
|
1026 |
+
break
|
1027 |
+
finally:
|
1028 |
+
if hasattr(r, "close"):
|
1029 |
+
r.close()
|
1030 |
+
if request_id in REQUEST_POOL:
|
1031 |
+
REQUEST_POOL.remove(request_id)
|
1032 |
+
|
1033 |
+
r = requests.request(
|
1034 |
+
method="POST",
|
1035 |
+
url=f"{url}/v1/chat/completions",
|
1036 |
+
data=form_data.model_dump_json(exclude_none=True).encode(),
|
1037 |
+
stream=True,
|
1038 |
+
)
|
1039 |
+
|
1040 |
+
r.raise_for_status()
|
1041 |
+
|
1042 |
+
return StreamingResponse(
|
1043 |
+
stream_content(),
|
1044 |
+
status_code=r.status_code,
|
1045 |
+
headers=dict(r.headers),
|
1046 |
+
)
|
1047 |
+
except Exception as e:
|
1048 |
+
raise e
|
1049 |
+
|
1050 |
+
try:
|
1051 |
+
return await run_in_threadpool(get_request)
|
1052 |
+
except Exception as e:
|
1053 |
+
error_detail = "Open WebUI: Server Connection Error"
|
1054 |
+
if r is not None:
|
1055 |
+
try:
|
1056 |
+
res = r.json()
|
1057 |
+
if "error" in res:
|
1058 |
+
error_detail = f"Ollama: {res['error']}"
|
1059 |
+
except:
|
1060 |
+
error_detail = f"Ollama: {e}"
|
1061 |
+
|
1062 |
+
raise HTTPException(
|
1063 |
+
status_code=r.status_code if r else 500,
|
1064 |
+
detail=error_detail,
|
1065 |
+
)
|
1066 |
+
|
1067 |
+
|
1068 |
+
@app.get("/v1/models")
|
1069 |
+
@app.get("/v1/models/{url_idx}")
|
1070 |
+
async def get_openai_models(
|
1071 |
+
url_idx: Optional[int] = None,
|
1072 |
+
user=Depends(get_verified_user),
|
1073 |
+
):
|
1074 |
+
if url_idx == None:
|
1075 |
+
models = await get_all_models()
|
1076 |
+
|
1077 |
+
if app.state.config.ENABLE_MODEL_FILTER:
|
1078 |
+
if user.role == "user":
|
1079 |
+
models["models"] = list(
|
1080 |
+
filter(
|
1081 |
+
lambda model: model["name"]
|
1082 |
+
in app.state.config.MODEL_FILTER_LIST,
|
1083 |
+
models["models"],
|
1084 |
+
)
|
1085 |
+
)
|
1086 |
+
|
1087 |
+
return {
|
1088 |
+
"data": [
|
1089 |
+
{
|
1090 |
+
"id": model["model"],
|
1091 |
+
"object": "model",
|
1092 |
+
"created": int(time.time()),
|
1093 |
+
"owned_by": "openai",
|
1094 |
+
}
|
1095 |
+
for model in models["models"]
|
1096 |
+
],
|
1097 |
+
"object": "list",
|
1098 |
+
}
|
1099 |
+
|
1100 |
+
else:
|
1101 |
+
url = app.state.config.OLLAMA_BASE_URLS[url_idx]
|
1102 |
+
try:
|
1103 |
+
r = requests.request(method="GET", url=f"{url}/api/tags")
|
1104 |
+
r.raise_for_status()
|
1105 |
+
|
1106 |
+
models = r.json()
|
1107 |
+
|
1108 |
+
return {
|
1109 |
+
"data": [
|
1110 |
+
{
|
1111 |
+
"id": model["model"],
|
1112 |
+
"object": "model",
|
1113 |
+
"created": int(time.time()),
|
1114 |
+
"owned_by": "openai",
|
1115 |
+
}
|
1116 |
+
for model in models["models"]
|
1117 |
+
],
|
1118 |
+
"object": "list",
|
1119 |
+
}
|
1120 |
+
|
1121 |
+
except Exception as e:
|
1122 |
+
log.exception(e)
|
1123 |
+
error_detail = "Open WebUI: Server Connection Error"
|
1124 |
+
if r is not None:
|
1125 |
+
try:
|
1126 |
+
res = r.json()
|
1127 |
+
if "error" in res:
|
1128 |
+
error_detail = f"Ollama: {res['error']}"
|
1129 |
+
except:
|
1130 |
+
error_detail = f"Ollama: {e}"
|
1131 |
+
|
1132 |
+
raise HTTPException(
|
1133 |
+
status_code=r.status_code if r else 500,
|
1134 |
+
detail=error_detail,
|
1135 |
+
)
|
1136 |
+
|
1137 |
+
|
1138 |
+
class UrlForm(BaseModel):
|
1139 |
+
url: str
|
1140 |
+
|
1141 |
+
|
1142 |
+
class UploadBlobForm(BaseModel):
|
1143 |
+
filename: str
|
1144 |
+
|
1145 |
+
|
1146 |
+
def parse_huggingface_url(hf_url):
|
1147 |
+
try:
|
1148 |
+
# Parse the URL
|
1149 |
+
parsed_url = urlparse(hf_url)
|
1150 |
+
|
1151 |
+
# Get the path and split it into components
|
1152 |
+
path_components = parsed_url.path.split("/")
|
1153 |
+
|
1154 |
+
# Extract the desired output
|
1155 |
+
user_repo = "/".join(path_components[1:3])
|
1156 |
+
model_file = path_components[-1]
|
1157 |
+
|
1158 |
+
return model_file
|
1159 |
+
except ValueError:
|
1160 |
+
return None
|
1161 |
+
|
1162 |
+
|
1163 |
+
async def download_file_stream(
|
1164 |
+
ollama_url, file_url, file_path, file_name, chunk_size=1024 * 1024
|
1165 |
+
):
|
1166 |
+
done = False
|
1167 |
+
|
1168 |
+
if os.path.exists(file_path):
|
1169 |
+
current_size = os.path.getsize(file_path)
|
1170 |
+
else:
|
1171 |
+
current_size = 0
|
1172 |
+
|
1173 |
+
headers = {"Range": f"bytes={current_size}-"} if current_size > 0 else {}
|
1174 |
+
|
1175 |
+
timeout = aiohttp.ClientTimeout(total=600) # Set the timeout
|
1176 |
+
|
1177 |
+
async with aiohttp.ClientSession(timeout=timeout) as session:
|
1178 |
+
async with session.get(file_url, headers=headers) as response:
|
1179 |
+
total_size = int(response.headers.get("content-length", 0)) + current_size
|
1180 |
+
|
1181 |
+
with open(file_path, "ab+") as file:
|
1182 |
+
async for data in response.content.iter_chunked(chunk_size):
|
1183 |
+
current_size += len(data)
|
1184 |
+
file.write(data)
|
1185 |
+
|
1186 |
+
done = current_size == total_size
|
1187 |
+
progress = round((current_size / total_size) * 100, 2)
|
1188 |
+
|
1189 |
+
yield f'data: {{"progress": {progress}, "completed": {current_size}, "total": {total_size}}}\n\n'
|
1190 |
+
|
1191 |
+
if done:
|
1192 |
+
file.seek(0)
|
1193 |
+
hashed = calculate_sha256(file)
|
1194 |
+
file.seek(0)
|
1195 |
+
|
1196 |
+
url = f"{ollama_url}/api/blobs/sha256:{hashed}"
|
1197 |
+
response = requests.post(url, data=file)
|
1198 |
+
|
1199 |
+
if response.ok:
|
1200 |
+
res = {
|
1201 |
+
"done": done,
|
1202 |
+
"blob": f"sha256:{hashed}",
|
1203 |
+
"name": file_name,
|
1204 |
+
}
|
1205 |
+
os.remove(file_path)
|
1206 |
+
|
1207 |
+
yield f"data: {json.dumps(res)}\n\n"
|
1208 |
+
else:
|
1209 |
+
raise "Ollama: Could not create blob, Please try again."
|
1210 |
+
|
1211 |
+
|
1212 |
+
# def number_generator():
|
1213 |
+
# for i in range(1, 101):
|
1214 |
+
# yield f"data: {i}\n"
|
1215 |
+
|
1216 |
+
|
1217 |
+
# url = "https://huggingface.co/TheBloke/stablelm-zephyr-3b-GGUF/resolve/main/stablelm-zephyr-3b.Q2_K.gguf"
|
1218 |
+
@app.post("/models/download")
|
1219 |
+
@app.post("/models/download/{url_idx}")
|
1220 |
+
async def download_model(
|
1221 |
+
form_data: UrlForm,
|
1222 |
+
url_idx: Optional[int] = None,
|
1223 |
+
):
|
1224 |
+
|
1225 |
+
allowed_hosts = ["https://huggingface.co/", "https://github.com/"]
|
1226 |
+
|
1227 |
+
if not any(form_data.url.startswith(host) for host in allowed_hosts):
|
1228 |
+
raise HTTPException(
|
1229 |
+
status_code=400,
|
1230 |
+
detail="Invalid file_url. Only URLs from allowed hosts are permitted.",
|
1231 |
+
)
|
1232 |
+
|
1233 |
+
if url_idx == None:
|
1234 |
+
url_idx = 0
|
1235 |
+
url = app.state.config.OLLAMA_BASE_URLS[url_idx]
|
1236 |
+
|
1237 |
+
file_name = parse_huggingface_url(form_data.url)
|
1238 |
+
|
1239 |
+
if file_name:
|
1240 |
+
file_path = f"{UPLOAD_DIR}/{file_name}"
|
1241 |
+
|
1242 |
+
return StreamingResponse(
|
1243 |
+
download_file_stream(url, form_data.url, file_path, file_name),
|
1244 |
+
)
|
1245 |
+
else:
|
1246 |
+
return None
|
1247 |
+
|
1248 |
+
|
1249 |
+
@app.post("/models/upload")
|
1250 |
+
@app.post("/models/upload/{url_idx}")
|
1251 |
+
def upload_model(file: UploadFile = File(...), url_idx: Optional[int] = None):
|
1252 |
+
if url_idx == None:
|
1253 |
+
url_idx = 0
|
1254 |
+
ollama_url = app.state.config.OLLAMA_BASE_URLS[url_idx]
|
1255 |
+
|
1256 |
+
file_path = f"{UPLOAD_DIR}/{file.filename}"
|
1257 |
+
|
1258 |
+
# Save file in chunks
|
1259 |
+
with open(file_path, "wb+") as f:
|
1260 |
+
for chunk in file.file:
|
1261 |
+
f.write(chunk)
|
1262 |
+
|
1263 |
+
def file_process_stream():
|
1264 |
+
nonlocal ollama_url
|
1265 |
+
total_size = os.path.getsize(file_path)
|
1266 |
+
chunk_size = 1024 * 1024
|
1267 |
+
try:
|
1268 |
+
with open(file_path, "rb") as f:
|
1269 |
+
total = 0
|
1270 |
+
done = False
|
1271 |
+
|
1272 |
+
while not done:
|
1273 |
+
chunk = f.read(chunk_size)
|
1274 |
+
if not chunk:
|
1275 |
+
done = True
|
1276 |
+
continue
|
1277 |
+
|
1278 |
+
total += len(chunk)
|
1279 |
+
progress = round((total / total_size) * 100, 2)
|
1280 |
+
|
1281 |
+
res = {
|
1282 |
+
"progress": progress,
|
1283 |
+
"total": total_size,
|
1284 |
+
"completed": total,
|
1285 |
+
}
|
1286 |
+
yield f"data: {json.dumps(res)}\n\n"
|
1287 |
+
|
1288 |
+
if done:
|
1289 |
+
f.seek(0)
|
1290 |
+
hashed = calculate_sha256(f)
|
1291 |
+
f.seek(0)
|
1292 |
+
|
1293 |
+
url = f"{ollama_url}/api/blobs/sha256:{hashed}"
|
1294 |
+
response = requests.post(url, data=f)
|
1295 |
+
|
1296 |
+
if response.ok:
|
1297 |
+
res = {
|
1298 |
+
"done": done,
|
1299 |
+
"blob": f"sha256:{hashed}",
|
1300 |
+
"name": file.filename,
|
1301 |
+
}
|
1302 |
+
os.remove(file_path)
|
1303 |
+
yield f"data: {json.dumps(res)}\n\n"
|
1304 |
+
else:
|
1305 |
+
raise Exception(
|
1306 |
+
"Ollama: Could not create blob, Please try again."
|
1307 |
+
)
|
1308 |
+
|
1309 |
+
except Exception as e:
|
1310 |
+
res = {"error": str(e)}
|
1311 |
+
yield f"data: {json.dumps(res)}\n\n"
|
1312 |
+
|
1313 |
+
return StreamingResponse(file_process_stream(), media_type="text/event-stream")
|
1314 |
+
|
1315 |
+
|
1316 |
+
# async def upload_model(file: UploadFile = File(), url_idx: Optional[int] = None):
|
1317 |
+
# if url_idx == None:
|
1318 |
+
# url_idx = 0
|
1319 |
+
# url = app.state.config.OLLAMA_BASE_URLS[url_idx]
|
1320 |
+
|
1321 |
+
# file_location = os.path.join(UPLOAD_DIR, file.filename)
|
1322 |
+
# total_size = file.size
|
1323 |
+
|
1324 |
+
# async def file_upload_generator(file):
|
1325 |
+
# print(file)
|
1326 |
+
# try:
|
1327 |
+
# async with aiofiles.open(file_location, "wb") as f:
|
1328 |
+
# completed_size = 0
|
1329 |
+
# while True:
|
1330 |
+
# chunk = await file.read(1024*1024)
|
1331 |
+
# if not chunk:
|
1332 |
+
# break
|
1333 |
+
# await f.write(chunk)
|
1334 |
+
# completed_size += len(chunk)
|
1335 |
+
# progress = (completed_size / total_size) * 100
|
1336 |
+
|
1337 |
+
# print(progress)
|
1338 |
+
# yield f'data: {json.dumps({"status": "uploading", "percentage": progress, "total": total_size, "completed": completed_size, "done": False})}\n'
|
1339 |
+
# except Exception as e:
|
1340 |
+
# print(e)
|
1341 |
+
# yield f"data: {json.dumps({'status': 'error', 'message': str(e)})}\n"
|
1342 |
+
# finally:
|
1343 |
+
# await file.close()
|
1344 |
+
# print("done")
|
1345 |
+
# yield f'data: {json.dumps({"status": "completed", "percentage": 100, "total": total_size, "completed": completed_size, "done": True})}\n'
|
1346 |
+
|
1347 |
+
# return StreamingResponse(
|
1348 |
+
# file_upload_generator(copy.deepcopy(file)), media_type="text/event-stream"
|
1349 |
+
# )
|
1350 |
+
|
1351 |
+
|
1352 |
+
@app.api_route("/{path:path}", methods=["GET", "POST", "PUT", "DELETE"])
|
1353 |
+
async def deprecated_proxy(
|
1354 |
+
path: str, request: Request, user=Depends(get_verified_user)
|
1355 |
+
):
|
1356 |
+
url = app.state.config.OLLAMA_BASE_URLS[0]
|
1357 |
+
target_url = f"{url}/{path}"
|
1358 |
+
|
1359 |
+
body = await request.body()
|
1360 |
+
headers = dict(request.headers)
|
1361 |
+
|
1362 |
+
if user.role in ["user", "admin"]:
|
1363 |
+
if path in ["pull", "delete", "push", "copy", "create"]:
|
1364 |
+
if user.role != "admin":
|
1365 |
+
raise HTTPException(
|
1366 |
+
status_code=status.HTTP_401_UNAUTHORIZED,
|
1367 |
+
detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
|
1368 |
+
)
|
1369 |
+
else:
|
1370 |
+
raise HTTPException(
|
1371 |
+
status_code=status.HTTP_401_UNAUTHORIZED,
|
1372 |
+
detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
|
1373 |
+
)
|
1374 |
+
|
1375 |
+
headers.pop("host", None)
|
1376 |
+
headers.pop("authorization", None)
|
1377 |
+
headers.pop("origin", None)
|
1378 |
+
headers.pop("referer", None)
|
1379 |
+
|
1380 |
+
r = None
|
1381 |
+
|
1382 |
+
def get_request():
|
1383 |
+
nonlocal r
|
1384 |
+
|
1385 |
+
request_id = str(uuid.uuid4())
|
1386 |
+
try:
|
1387 |
+
REQUEST_POOL.append(request_id)
|
1388 |
+
|
1389 |
+
def stream_content():
|
1390 |
+
try:
|
1391 |
+
if path == "generate":
|
1392 |
+
data = json.loads(body.decode("utf-8"))
|
1393 |
+
|
1394 |
+
if not ("stream" in data and data["stream"] == False):
|
1395 |
+
yield json.dumps({"id": request_id, "done": False}) + "\n"
|
1396 |
+
|
1397 |
+
elif path == "chat":
|
1398 |
+
yield json.dumps({"id": request_id, "done": False}) + "\n"
|
1399 |
+
|
1400 |
+
for chunk in r.iter_content(chunk_size=8192):
|
1401 |
+
if request_id in REQUEST_POOL:
|
1402 |
+
yield chunk
|
1403 |
+
else:
|
1404 |
+
log.warning("User: canceled request")
|
1405 |
+
break
|
1406 |
+
finally:
|
1407 |
+
if hasattr(r, "close"):
|
1408 |
+
r.close()
|
1409 |
+
if request_id in REQUEST_POOL:
|
1410 |
+
REQUEST_POOL.remove(request_id)
|
1411 |
+
|
1412 |
+
r = requests.request(
|
1413 |
+
method=request.method,
|
1414 |
+
url=target_url,
|
1415 |
+
data=body,
|
1416 |
+
headers=headers,
|
1417 |
+
stream=True,
|
1418 |
+
)
|
1419 |
+
|
1420 |
+
r.raise_for_status()
|
1421 |
+
|
1422 |
+
# r.close()
|
1423 |
+
|
1424 |
+
return StreamingResponse(
|
1425 |
+
stream_content(),
|
1426 |
+
status_code=r.status_code,
|
1427 |
+
headers=dict(r.headers),
|
1428 |
+
)
|
1429 |
+
except Exception as e:
|
1430 |
+
raise e
|
1431 |
+
|
1432 |
+
try:
|
1433 |
+
return await run_in_threadpool(get_request)
|
1434 |
+
except Exception as e:
|
1435 |
+
error_detail = "Open WebUI: Server Connection Error"
|
1436 |
+
if r is not None:
|
1437 |
+
try:
|
1438 |
+
res = r.json()
|
1439 |
+
if "error" in res:
|
1440 |
+
error_detail = f"Ollama: {res['error']}"
|
1441 |
+
except:
|
1442 |
+
error_detail = f"Ollama: {e}"
|
1443 |
+
|
1444 |
+
raise HTTPException(
|
1445 |
+
status_code=r.status_code if r else 500,
|
1446 |
+
detail=error_detail,
|
1447 |
+
)
|
backend/apps/openai/main.py
ADDED
@@ -0,0 +1,394 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from fastapi import FastAPI, Request, Response, HTTPException, Depends
|
2 |
+
from fastapi.middleware.cors import CORSMiddleware
|
3 |
+
from fastapi.responses import StreamingResponse, JSONResponse, FileResponse
|
4 |
+
|
5 |
+
import requests
|
6 |
+
import aiohttp
|
7 |
+
import asyncio
|
8 |
+
import json
|
9 |
+
import logging
|
10 |
+
|
11 |
+
from pydantic import BaseModel
|
12 |
+
|
13 |
+
|
14 |
+
from apps.web.models.users import Users
|
15 |
+
from constants import ERROR_MESSAGES
|
16 |
+
from utils.utils import (
|
17 |
+
decode_token,
|
18 |
+
get_current_user,
|
19 |
+
get_verified_user,
|
20 |
+
get_admin_user,
|
21 |
+
)
|
22 |
+
from config import (
|
23 |
+
SRC_LOG_LEVELS,
|
24 |
+
ENABLE_OPENAI_API,
|
25 |
+
OPENAI_API_BASE_URLS,
|
26 |
+
OPENAI_API_KEYS,
|
27 |
+
CACHE_DIR,
|
28 |
+
ENABLE_MODEL_FILTER,
|
29 |
+
MODEL_FILTER_LIST,
|
30 |
+
AppConfig,
|
31 |
+
)
|
32 |
+
from typing import List, Optional
|
33 |
+
|
34 |
+
|
35 |
+
import hashlib
|
36 |
+
from pathlib import Path
|
37 |
+
|
38 |
+
log = logging.getLogger(__name__)
|
39 |
+
log.setLevel(SRC_LOG_LEVELS["OPENAI"])
|
40 |
+
|
41 |
+
app = FastAPI()
|
42 |
+
app.add_middleware(
|
43 |
+
CORSMiddleware,
|
44 |
+
allow_origins=["*"],
|
45 |
+
allow_credentials=True,
|
46 |
+
allow_methods=["*"],
|
47 |
+
allow_headers=["*"],
|
48 |
+
)
|
49 |
+
|
50 |
+
|
51 |
+
app.state.config = AppConfig()
|
52 |
+
|
53 |
+
app.state.config.ENABLE_MODEL_FILTER = ENABLE_MODEL_FILTER
|
54 |
+
app.state.config.MODEL_FILTER_LIST = MODEL_FILTER_LIST
|
55 |
+
|
56 |
+
|
57 |
+
app.state.config.ENABLE_OPENAI_API = ENABLE_OPENAI_API
|
58 |
+
app.state.config.OPENAI_API_BASE_URLS = OPENAI_API_BASE_URLS
|
59 |
+
app.state.config.OPENAI_API_KEYS = OPENAI_API_KEYS
|
60 |
+
|
61 |
+
app.state.MODELS = {}
|
62 |
+
|
63 |
+
|
64 |
+
@app.middleware("http")
|
65 |
+
async def check_url(request: Request, call_next):
|
66 |
+
if len(app.state.MODELS) == 0:
|
67 |
+
await get_all_models()
|
68 |
+
else:
|
69 |
+
pass
|
70 |
+
|
71 |
+
response = await call_next(request)
|
72 |
+
return response
|
73 |
+
|
74 |
+
|
75 |
+
@app.get("/config")
|
76 |
+
async def get_config(user=Depends(get_admin_user)):
|
77 |
+
return {"ENABLE_OPENAI_API": app.state.config.ENABLE_OPENAI_API}
|
78 |
+
|
79 |
+
|
80 |
+
class OpenAIConfigForm(BaseModel):
|
81 |
+
enable_openai_api: Optional[bool] = None
|
82 |
+
|
83 |
+
|
84 |
+
@app.post("/config/update")
|
85 |
+
async def update_config(form_data: OpenAIConfigForm, user=Depends(get_admin_user)):
|
86 |
+
app.state.config.ENABLE_OPENAI_API = form_data.enable_openai_api
|
87 |
+
return {"ENABLE_OPENAI_API": app.state.config.ENABLE_OPENAI_API}
|
88 |
+
|
89 |
+
|
90 |
+
class UrlsUpdateForm(BaseModel):
|
91 |
+
urls: List[str]
|
92 |
+
|
93 |
+
|
94 |
+
class KeysUpdateForm(BaseModel):
|
95 |
+
keys: List[str]
|
96 |
+
|
97 |
+
|
98 |
+
@app.get("/urls")
|
99 |
+
async def get_openai_urls(user=Depends(get_admin_user)):
|
100 |
+
return {"OPENAI_API_BASE_URLS": app.state.config.OPENAI_API_BASE_URLS}
|
101 |
+
|
102 |
+
|
103 |
+
@app.post("/urls/update")
|
104 |
+
async def update_openai_urls(form_data: UrlsUpdateForm, user=Depends(get_admin_user)):
|
105 |
+
await get_all_models()
|
106 |
+
app.state.config.OPENAI_API_BASE_URLS = form_data.urls
|
107 |
+
return {"OPENAI_API_BASE_URLS": app.state.config.OPENAI_API_BASE_URLS}
|
108 |
+
|
109 |
+
|
110 |
+
@app.get("/keys")
|
111 |
+
async def get_openai_keys(user=Depends(get_admin_user)):
|
112 |
+
return {"OPENAI_API_KEYS": app.state.config.OPENAI_API_KEYS}
|
113 |
+
|
114 |
+
|
115 |
+
@app.post("/keys/update")
|
116 |
+
async def update_openai_key(form_data: KeysUpdateForm, user=Depends(get_admin_user)):
|
117 |
+
app.state.config.OPENAI_API_KEYS = form_data.keys
|
118 |
+
return {"OPENAI_API_KEYS": app.state.config.OPENAI_API_KEYS}
|
119 |
+
|
120 |
+
|
121 |
+
@app.post("/audio/speech")
|
122 |
+
async def speech(request: Request, user=Depends(get_verified_user)):
|
123 |
+
idx = None
|
124 |
+
try:
|
125 |
+
idx = app.state.config.OPENAI_API_BASE_URLS.index("https://api.openai.com/v1")
|
126 |
+
body = await request.body()
|
127 |
+
name = hashlib.sha256(body).hexdigest()
|
128 |
+
|
129 |
+
SPEECH_CACHE_DIR = Path(CACHE_DIR).joinpath("./audio/speech/")
|
130 |
+
SPEECH_CACHE_DIR.mkdir(parents=True, exist_ok=True)
|
131 |
+
file_path = SPEECH_CACHE_DIR.joinpath(f"{name}.mp3")
|
132 |
+
file_body_path = SPEECH_CACHE_DIR.joinpath(f"{name}.json")
|
133 |
+
|
134 |
+
# Check if the file already exists in the cache
|
135 |
+
if file_path.is_file():
|
136 |
+
return FileResponse(file_path)
|
137 |
+
|
138 |
+
headers = {}
|
139 |
+
headers["Authorization"] = f"Bearer {app.state.config.OPENAI_API_KEYS[idx]}"
|
140 |
+
headers["Content-Type"] = "application/json"
|
141 |
+
if "openrouter.ai" in app.state.config.OPENAI_API_BASE_URLS[idx]:
|
142 |
+
headers["HTTP-Referer"] = "https://openwebui.com/"
|
143 |
+
headers["X-Title"] = "Open WebUI"
|
144 |
+
r = None
|
145 |
+
try:
|
146 |
+
r = requests.post(
|
147 |
+
url=f"{app.state.config.OPENAI_API_BASE_URLS[idx]}/audio/speech",
|
148 |
+
data=body,
|
149 |
+
headers=headers,
|
150 |
+
stream=True,
|
151 |
+
)
|
152 |
+
|
153 |
+
r.raise_for_status()
|
154 |
+
|
155 |
+
# Save the streaming content to a file
|
156 |
+
with open(file_path, "wb") as f:
|
157 |
+
for chunk in r.iter_content(chunk_size=8192):
|
158 |
+
f.write(chunk)
|
159 |
+
|
160 |
+
with open(file_body_path, "w") as f:
|
161 |
+
json.dump(json.loads(body.decode("utf-8")), f)
|
162 |
+
|
163 |
+
# Return the saved file
|
164 |
+
return FileResponse(file_path)
|
165 |
+
|
166 |
+
except Exception as e:
|
167 |
+
log.exception(e)
|
168 |
+
error_detail = "Open WebUI: Server Connection Error"
|
169 |
+
if r is not None:
|
170 |
+
try:
|
171 |
+
res = r.json()
|
172 |
+
if "error" in res:
|
173 |
+
error_detail = f"External: {res['error']}"
|
174 |
+
except:
|
175 |
+
error_detail = f"External: {e}"
|
176 |
+
|
177 |
+
raise HTTPException(
|
178 |
+
status_code=r.status_code if r else 500, detail=error_detail
|
179 |
+
)
|
180 |
+
|
181 |
+
except ValueError:
|
182 |
+
raise HTTPException(status_code=401, detail=ERROR_MESSAGES.OPENAI_NOT_FOUND)
|
183 |
+
|
184 |
+
|
185 |
+
async def fetch_url(url, key):
|
186 |
+
timeout = aiohttp.ClientTimeout(total=5)
|
187 |
+
try:
|
188 |
+
if key != "":
|
189 |
+
headers = {"Authorization": f"Bearer {key}"}
|
190 |
+
async with aiohttp.ClientSession(timeout=timeout) as session:
|
191 |
+
async with session.get(url, headers=headers) as response:
|
192 |
+
return await response.json()
|
193 |
+
else:
|
194 |
+
return None
|
195 |
+
except Exception as e:
|
196 |
+
# Handle connection error here
|
197 |
+
log.error(f"Connection error: {e}")
|
198 |
+
return None
|
199 |
+
|
200 |
+
|
201 |
+
def merge_models_lists(model_lists):
|
202 |
+
log.info(f"merge_models_lists {model_lists}")
|
203 |
+
merged_list = []
|
204 |
+
|
205 |
+
for idx, models in enumerate(model_lists):
|
206 |
+
if models is not None and "error" not in models:
|
207 |
+
merged_list.extend(
|
208 |
+
[
|
209 |
+
{**model, "urlIdx": idx}
|
210 |
+
for model in models
|
211 |
+
if "api.openai.com"
|
212 |
+
not in app.state.config.OPENAI_API_BASE_URLS[idx]
|
213 |
+
or "gpt" in model["id"]
|
214 |
+
]
|
215 |
+
)
|
216 |
+
|
217 |
+
return merged_list
|
218 |
+
|
219 |
+
|
220 |
+
async def get_all_models():
|
221 |
+
log.info("get_all_models()")
|
222 |
+
|
223 |
+
if (
|
224 |
+
len(app.state.config.OPENAI_API_KEYS) == 1
|
225 |
+
and app.state.config.OPENAI_API_KEYS[0] == ""
|
226 |
+
) or not app.state.config.ENABLE_OPENAI_API:
|
227 |
+
models = {"data": []}
|
228 |
+
else:
|
229 |
+
tasks = [
|
230 |
+
fetch_url(f"{url}/models", app.state.config.OPENAI_API_KEYS[idx])
|
231 |
+
for idx, url in enumerate(app.state.config.OPENAI_API_BASE_URLS)
|
232 |
+
]
|
233 |
+
|
234 |
+
responses = await asyncio.gather(*tasks)
|
235 |
+
log.info(f"get_all_models:responses() {responses}")
|
236 |
+
|
237 |
+
models = {
|
238 |
+
"data": merge_models_lists(
|
239 |
+
list(
|
240 |
+
map(
|
241 |
+
lambda response: (
|
242 |
+
response["data"]
|
243 |
+
if (response and "data" in response)
|
244 |
+
else (response if isinstance(response, list) else None)
|
245 |
+
),
|
246 |
+
responses,
|
247 |
+
)
|
248 |
+
)
|
249 |
+
)
|
250 |
+
}
|
251 |
+
|
252 |
+
log.info(f"models: {models}")
|
253 |
+
app.state.MODELS = {model["id"]: model for model in models["data"]}
|
254 |
+
|
255 |
+
return models
|
256 |
+
|
257 |
+
|
258 |
+
@app.get("/models")
|
259 |
+
@app.get("/models/{url_idx}")
|
260 |
+
async def get_models(url_idx: Optional[int] = None, user=Depends(get_current_user)):
|
261 |
+
if url_idx == None:
|
262 |
+
models = await get_all_models()
|
263 |
+
if app.state.config.ENABLE_MODEL_FILTER:
|
264 |
+
if user.role == "user":
|
265 |
+
models["data"] = list(
|
266 |
+
filter(
|
267 |
+
lambda model: model["id"] in app.state.config.MODEL_FILTER_LIST,
|
268 |
+
models["data"],
|
269 |
+
)
|
270 |
+
)
|
271 |
+
return models
|
272 |
+
return models
|
273 |
+
else:
|
274 |
+
url = app.state.config.OPENAI_API_BASE_URLS[url_idx]
|
275 |
+
|
276 |
+
r = None
|
277 |
+
|
278 |
+
try:
|
279 |
+
r = requests.request(method="GET", url=f"{url}/models")
|
280 |
+
r.raise_for_status()
|
281 |
+
|
282 |
+
response_data = r.json()
|
283 |
+
if "api.openai.com" in url:
|
284 |
+
response_data["data"] = list(
|
285 |
+
filter(lambda model: "gpt" in model["id"], response_data["data"])
|
286 |
+
)
|
287 |
+
|
288 |
+
return response_data
|
289 |
+
except Exception as e:
|
290 |
+
log.exception(e)
|
291 |
+
error_detail = "Open WebUI: Server Connection Error"
|
292 |
+
if r is not None:
|
293 |
+
try:
|
294 |
+
res = r.json()
|
295 |
+
if "error" in res:
|
296 |
+
error_detail = f"External: {res['error']}"
|
297 |
+
except:
|
298 |
+
error_detail = f"External: {e}"
|
299 |
+
|
300 |
+
raise HTTPException(
|
301 |
+
status_code=r.status_code if r else 500,
|
302 |
+
detail=error_detail,
|
303 |
+
)
|
304 |
+
|
305 |
+
|
306 |
+
@app.api_route("/{path:path}", methods=["GET", "POST", "PUT", "DELETE"])
|
307 |
+
async def proxy(path: str, request: Request, user=Depends(get_verified_user)):
|
308 |
+
idx = 0
|
309 |
+
|
310 |
+
body = await request.body()
|
311 |
+
# TODO: Remove below after gpt-4-vision fix from Open AI
|
312 |
+
# Try to decode the body of the request from bytes to a UTF-8 string (Require add max_token to fix gpt-4-vision)
|
313 |
+
try:
|
314 |
+
body = body.decode("utf-8")
|
315 |
+
body = json.loads(body)
|
316 |
+
|
317 |
+
model = app.state.MODELS[body.get("model")]
|
318 |
+
|
319 |
+
idx = model["urlIdx"]
|
320 |
+
|
321 |
+
if "pipeline" in model and model.get("pipeline"):
|
322 |
+
body["user"] = {"name": user.name, "id": user.id}
|
323 |
+
body["title"] = (
|
324 |
+
True if body["stream"] == False and body["max_tokens"] == 50 else False
|
325 |
+
)
|
326 |
+
|
327 |
+
# Check if the model is "gpt-4-vision-preview" and set "max_tokens" to 4000
|
328 |
+
# This is a workaround until OpenAI fixes the issue with this model
|
329 |
+
if body.get("model") == "gpt-4-vision-preview":
|
330 |
+
if "max_tokens" not in body:
|
331 |
+
body["max_tokens"] = 4000
|
332 |
+
log.debug("Modified body_dict:", body)
|
333 |
+
|
334 |
+
# Fix for ChatGPT calls failing because the num_ctx key is in body
|
335 |
+
if "num_ctx" in body:
|
336 |
+
# If 'num_ctx' is in the dictionary, delete it
|
337 |
+
# Leaving it there generates an error with the
|
338 |
+
# OpenAI API (Feb 2024)
|
339 |
+
del body["num_ctx"]
|
340 |
+
|
341 |
+
# Convert the modified body back to JSON
|
342 |
+
body = json.dumps(body)
|
343 |
+
except json.JSONDecodeError as e:
|
344 |
+
log.error("Error loading request body into a dictionary:", e)
|
345 |
+
|
346 |
+
url = app.state.config.OPENAI_API_BASE_URLS[idx]
|
347 |
+
key = app.state.config.OPENAI_API_KEYS[idx]
|
348 |
+
|
349 |
+
target_url = f"{url}/{path}"
|
350 |
+
|
351 |
+
if key == "":
|
352 |
+
raise HTTPException(status_code=401, detail=ERROR_MESSAGES.API_KEY_NOT_FOUND)
|
353 |
+
|
354 |
+
headers = {}
|
355 |
+
headers["Authorization"] = f"Bearer {key}"
|
356 |
+
headers["Content-Type"] = "application/json"
|
357 |
+
|
358 |
+
r = None
|
359 |
+
|
360 |
+
try:
|
361 |
+
r = requests.request(
|
362 |
+
method=request.method,
|
363 |
+
url=target_url,
|
364 |
+
data=body,
|
365 |
+
headers=headers,
|
366 |
+
stream=True,
|
367 |
+
)
|
368 |
+
|
369 |
+
r.raise_for_status()
|
370 |
+
|
371 |
+
# Check if response is SSE
|
372 |
+
if "text/event-stream" in r.headers.get("Content-Type", ""):
|
373 |
+
return StreamingResponse(
|
374 |
+
r.iter_content(chunk_size=8192),
|
375 |
+
status_code=r.status_code,
|
376 |
+
headers=dict(r.headers),
|
377 |
+
)
|
378 |
+
else:
|
379 |
+
response_data = r.json()
|
380 |
+
return response_data
|
381 |
+
except Exception as e:
|
382 |
+
log.exception(e)
|
383 |
+
error_detail = "Open WebUI: Server Connection Error"
|
384 |
+
if r is not None:
|
385 |
+
try:
|
386 |
+
res = r.json()
|
387 |
+
if "error" in res:
|
388 |
+
error_detail = f"External: {res['error']['message'] if 'message' in res['error'] else res['error']}"
|
389 |
+
except:
|
390 |
+
error_detail = f"External: {e}"
|
391 |
+
|
392 |
+
raise HTTPException(
|
393 |
+
status_code=r.status_code if r else 500, detail=error_detail
|
394 |
+
)
|
backend/apps/rag/main.py
ADDED
@@ -0,0 +1,971 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from fastapi import (
|
2 |
+
FastAPI,
|
3 |
+
Depends,
|
4 |
+
HTTPException,
|
5 |
+
status,
|
6 |
+
UploadFile,
|
7 |
+
File,
|
8 |
+
Form,
|
9 |
+
)
|
10 |
+
from fastapi.middleware.cors import CORSMiddleware
|
11 |
+
import os, shutil, logging, re
|
12 |
+
|
13 |
+
from pathlib import Path
|
14 |
+
from typing import List
|
15 |
+
|
16 |
+
from chromadb.utils.batch_utils import create_batches
|
17 |
+
|
18 |
+
from langchain_community.document_loaders import (
|
19 |
+
WebBaseLoader,
|
20 |
+
TextLoader,
|
21 |
+
PyPDFLoader,
|
22 |
+
CSVLoader,
|
23 |
+
BSHTMLLoader,
|
24 |
+
Docx2txtLoader,
|
25 |
+
UnstructuredEPubLoader,
|
26 |
+
UnstructuredWordDocumentLoader,
|
27 |
+
UnstructuredMarkdownLoader,
|
28 |
+
UnstructuredXMLLoader,
|
29 |
+
UnstructuredRSTLoader,
|
30 |
+
UnstructuredExcelLoader,
|
31 |
+
UnstructuredPowerPointLoader,
|
32 |
+
YoutubeLoader,
|
33 |
+
)
|
34 |
+
from langchain.text_splitter import RecursiveCharacterTextSplitter
|
35 |
+
|
36 |
+
import validators
|
37 |
+
import urllib.parse
|
38 |
+
import socket
|
39 |
+
|
40 |
+
|
41 |
+
from pydantic import BaseModel
|
42 |
+
from typing import Optional
|
43 |
+
import mimetypes
|
44 |
+
import uuid
|
45 |
+
import json
|
46 |
+
|
47 |
+
import sentence_transformers
|
48 |
+
|
49 |
+
from apps.web.models.documents import (
|
50 |
+
Documents,
|
51 |
+
DocumentForm,
|
52 |
+
DocumentResponse,
|
53 |
+
)
|
54 |
+
|
55 |
+
from apps.rag.utils import (
|
56 |
+
get_model_path,
|
57 |
+
get_embedding_function,
|
58 |
+
query_doc,
|
59 |
+
query_doc_with_hybrid_search,
|
60 |
+
query_collection,
|
61 |
+
query_collection_with_hybrid_search,
|
62 |
+
)
|
63 |
+
|
64 |
+
from utils.misc import (
|
65 |
+
calculate_sha256,
|
66 |
+
calculate_sha256_string,
|
67 |
+
sanitize_filename,
|
68 |
+
extract_folders_after_data_docs,
|
69 |
+
)
|
70 |
+
from utils.utils import get_current_user, get_admin_user
|
71 |
+
|
72 |
+
from config import (
|
73 |
+
ENV,
|
74 |
+
SRC_LOG_LEVELS,
|
75 |
+
UPLOAD_DIR,
|
76 |
+
DOCS_DIR,
|
77 |
+
RAG_TOP_K,
|
78 |
+
RAG_RELEVANCE_THRESHOLD,
|
79 |
+
RAG_EMBEDDING_ENGINE,
|
80 |
+
RAG_EMBEDDING_MODEL,
|
81 |
+
RAG_EMBEDDING_MODEL_AUTO_UPDATE,
|
82 |
+
RAG_EMBEDDING_MODEL_TRUST_REMOTE_CODE,
|
83 |
+
ENABLE_RAG_HYBRID_SEARCH,
|
84 |
+
ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION,
|
85 |
+
RAG_RERANKING_MODEL,
|
86 |
+
PDF_EXTRACT_IMAGES,
|
87 |
+
RAG_RERANKING_MODEL_AUTO_UPDATE,
|
88 |
+
RAG_RERANKING_MODEL_TRUST_REMOTE_CODE,
|
89 |
+
RAG_OPENAI_API_BASE_URL,
|
90 |
+
RAG_OPENAI_API_KEY,
|
91 |
+
DEVICE_TYPE,
|
92 |
+
CHROMA_CLIENT,
|
93 |
+
CHUNK_SIZE,
|
94 |
+
CHUNK_OVERLAP,
|
95 |
+
RAG_TEMPLATE,
|
96 |
+
ENABLE_RAG_LOCAL_WEB_FETCH,
|
97 |
+
YOUTUBE_LOADER_LANGUAGE,
|
98 |
+
AppConfig,
|
99 |
+
)
|
100 |
+
|
101 |
+
from constants import ERROR_MESSAGES
|
102 |
+
|
103 |
+
log = logging.getLogger(__name__)
|
104 |
+
log.setLevel(SRC_LOG_LEVELS["RAG"])
|
105 |
+
|
106 |
+
app = FastAPI()
|
107 |
+
|
108 |
+
app.state.config = AppConfig()
|
109 |
+
|
110 |
+
app.state.config.TOP_K = RAG_TOP_K
|
111 |
+
app.state.config.RELEVANCE_THRESHOLD = RAG_RELEVANCE_THRESHOLD
|
112 |
+
|
113 |
+
app.state.config.ENABLE_RAG_HYBRID_SEARCH = ENABLE_RAG_HYBRID_SEARCH
|
114 |
+
app.state.config.ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION = (
|
115 |
+
ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION
|
116 |
+
)
|
117 |
+
|
118 |
+
app.state.config.CHUNK_SIZE = CHUNK_SIZE
|
119 |
+
app.state.config.CHUNK_OVERLAP = CHUNK_OVERLAP
|
120 |
+
|
121 |
+
app.state.config.RAG_EMBEDDING_ENGINE = RAG_EMBEDDING_ENGINE
|
122 |
+
app.state.config.RAG_EMBEDDING_MODEL = RAG_EMBEDDING_MODEL
|
123 |
+
app.state.config.RAG_RERANKING_MODEL = RAG_RERANKING_MODEL
|
124 |
+
app.state.config.RAG_TEMPLATE = RAG_TEMPLATE
|
125 |
+
|
126 |
+
|
127 |
+
app.state.config.OPENAI_API_BASE_URL = RAG_OPENAI_API_BASE_URL
|
128 |
+
app.state.config.OPENAI_API_KEY = RAG_OPENAI_API_KEY
|
129 |
+
|
130 |
+
app.state.config.PDF_EXTRACT_IMAGES = PDF_EXTRACT_IMAGES
|
131 |
+
|
132 |
+
|
133 |
+
app.state.config.YOUTUBE_LOADER_LANGUAGE = YOUTUBE_LOADER_LANGUAGE
|
134 |
+
app.state.YOUTUBE_LOADER_TRANSLATION = None
|
135 |
+
|
136 |
+
|
137 |
+
def update_embedding_model(
|
138 |
+
embedding_model: str,
|
139 |
+
update_model: bool = False,
|
140 |
+
):
|
141 |
+
if embedding_model and app.state.config.RAG_EMBEDDING_ENGINE == "":
|
142 |
+
app.state.sentence_transformer_ef = sentence_transformers.SentenceTransformer(
|
143 |
+
get_model_path(embedding_model, update_model),
|
144 |
+
device=DEVICE_TYPE,
|
145 |
+
trust_remote_code=RAG_EMBEDDING_MODEL_TRUST_REMOTE_CODE,
|
146 |
+
)
|
147 |
+
else:
|
148 |
+
app.state.sentence_transformer_ef = None
|
149 |
+
|
150 |
+
|
151 |
+
def update_reranking_model(
|
152 |
+
reranking_model: str,
|
153 |
+
update_model: bool = False,
|
154 |
+
):
|
155 |
+
if reranking_model:
|
156 |
+
app.state.sentence_transformer_rf = sentence_transformers.CrossEncoder(
|
157 |
+
get_model_path(reranking_model, update_model),
|
158 |
+
device=DEVICE_TYPE,
|
159 |
+
trust_remote_code=RAG_RERANKING_MODEL_TRUST_REMOTE_CODE,
|
160 |
+
)
|
161 |
+
else:
|
162 |
+
app.state.sentence_transformer_rf = None
|
163 |
+
|
164 |
+
|
165 |
+
update_embedding_model(
|
166 |
+
app.state.config.RAG_EMBEDDING_MODEL,
|
167 |
+
RAG_EMBEDDING_MODEL_AUTO_UPDATE,
|
168 |
+
)
|
169 |
+
|
170 |
+
update_reranking_model(
|
171 |
+
app.state.config.RAG_RERANKING_MODEL,
|
172 |
+
RAG_RERANKING_MODEL_AUTO_UPDATE,
|
173 |
+
)
|
174 |
+
|
175 |
+
|
176 |
+
app.state.EMBEDDING_FUNCTION = get_embedding_function(
|
177 |
+
app.state.config.RAG_EMBEDDING_ENGINE,
|
178 |
+
app.state.config.RAG_EMBEDDING_MODEL,
|
179 |
+
app.state.sentence_transformer_ef,
|
180 |
+
app.state.config.OPENAI_API_KEY,
|
181 |
+
app.state.config.OPENAI_API_BASE_URL,
|
182 |
+
)
|
183 |
+
|
184 |
+
origins = ["*"]
|
185 |
+
|
186 |
+
|
187 |
+
app.add_middleware(
|
188 |
+
CORSMiddleware,
|
189 |
+
allow_origins=origins,
|
190 |
+
allow_credentials=True,
|
191 |
+
allow_methods=["*"],
|
192 |
+
allow_headers=["*"],
|
193 |
+
)
|
194 |
+
|
195 |
+
|
196 |
+
class CollectionNameForm(BaseModel):
|
197 |
+
collection_name: Optional[str] = "test"
|
198 |
+
|
199 |
+
|
200 |
+
class UrlForm(CollectionNameForm):
|
201 |
+
url: str
|
202 |
+
|
203 |
+
|
204 |
+
@app.get("/")
|
205 |
+
async def get_status():
|
206 |
+
return {
|
207 |
+
"status": True,
|
208 |
+
"chunk_size": app.state.config.CHUNK_SIZE,
|
209 |
+
"chunk_overlap": app.state.config.CHUNK_OVERLAP,
|
210 |
+
"template": app.state.config.RAG_TEMPLATE,
|
211 |
+
"embedding_engine": app.state.config.RAG_EMBEDDING_ENGINE,
|
212 |
+
"embedding_model": app.state.config.RAG_EMBEDDING_MODEL,
|
213 |
+
"reranking_model": app.state.config.RAG_RERANKING_MODEL,
|
214 |
+
}
|
215 |
+
|
216 |
+
|
217 |
+
@app.get("/embedding")
|
218 |
+
async def get_embedding_config(user=Depends(get_admin_user)):
|
219 |
+
return {
|
220 |
+
"status": True,
|
221 |
+
"embedding_engine": app.state.config.RAG_EMBEDDING_ENGINE,
|
222 |
+
"embedding_model": app.state.config.RAG_EMBEDDING_MODEL,
|
223 |
+
"openai_config": {
|
224 |
+
"url": app.state.config.OPENAI_API_BASE_URL,
|
225 |
+
"key": app.state.config.OPENAI_API_KEY,
|
226 |
+
},
|
227 |
+
}
|
228 |
+
|
229 |
+
|
230 |
+
@app.get("/reranking")
|
231 |
+
async def get_reraanking_config(user=Depends(get_admin_user)):
|
232 |
+
return {
|
233 |
+
"status": True,
|
234 |
+
"reranking_model": app.state.config.RAG_RERANKING_MODEL,
|
235 |
+
}
|
236 |
+
|
237 |
+
|
238 |
+
class OpenAIConfigForm(BaseModel):
|
239 |
+
url: str
|
240 |
+
key: str
|
241 |
+
|
242 |
+
|
243 |
+
class EmbeddingModelUpdateForm(BaseModel):
|
244 |
+
openai_config: Optional[OpenAIConfigForm] = None
|
245 |
+
embedding_engine: str
|
246 |
+
embedding_model: str
|
247 |
+
|
248 |
+
|
249 |
+
@app.post("/embedding/update")
|
250 |
+
async def update_embedding_config(
|
251 |
+
form_data: EmbeddingModelUpdateForm, user=Depends(get_admin_user)
|
252 |
+
):
|
253 |
+
log.info(
|
254 |
+
f"Updating embedding model: {app.state.config.RAG_EMBEDDING_MODEL} to {form_data.embedding_model}"
|
255 |
+
)
|
256 |
+
try:
|
257 |
+
app.state.config.RAG_EMBEDDING_ENGINE = form_data.embedding_engine
|
258 |
+
app.state.config.RAG_EMBEDDING_MODEL = form_data.embedding_model
|
259 |
+
|
260 |
+
if app.state.config.RAG_EMBEDDING_ENGINE in ["ollama", "openai"]:
|
261 |
+
if form_data.openai_config != None:
|
262 |
+
app.state.config.OPENAI_API_BASE_URL = form_data.openai_config.url
|
263 |
+
app.state.config.OPENAI_API_KEY = form_data.openai_config.key
|
264 |
+
|
265 |
+
update_embedding_model(app.state.config.RAG_EMBEDDING_MODEL)
|
266 |
+
|
267 |
+
app.state.EMBEDDING_FUNCTION = get_embedding_function(
|
268 |
+
app.state.config.RAG_EMBEDDING_ENGINE,
|
269 |
+
app.state.config.RAG_EMBEDDING_MODEL,
|
270 |
+
app.state.sentence_transformer_ef,
|
271 |
+
app.state.config.OPENAI_API_KEY,
|
272 |
+
app.state.config.OPENAI_API_BASE_URL,
|
273 |
+
)
|
274 |
+
|
275 |
+
return {
|
276 |
+
"status": True,
|
277 |
+
"embedding_engine": app.state.config.RAG_EMBEDDING_ENGINE,
|
278 |
+
"embedding_model": app.state.config.RAG_EMBEDDING_MODEL,
|
279 |
+
"openai_config": {
|
280 |
+
"url": app.state.config.OPENAI_API_BASE_URL,
|
281 |
+
"key": app.state.config.OPENAI_API_KEY,
|
282 |
+
},
|
283 |
+
}
|
284 |
+
except Exception as e:
|
285 |
+
log.exception(f"Problem updating embedding model: {e}")
|
286 |
+
raise HTTPException(
|
287 |
+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
288 |
+
detail=ERROR_MESSAGES.DEFAULT(e),
|
289 |
+
)
|
290 |
+
|
291 |
+
|
292 |
+
class RerankingModelUpdateForm(BaseModel):
|
293 |
+
reranking_model: str
|
294 |
+
|
295 |
+
|
296 |
+
@app.post("/reranking/update")
|
297 |
+
async def update_reranking_config(
|
298 |
+
form_data: RerankingModelUpdateForm, user=Depends(get_admin_user)
|
299 |
+
):
|
300 |
+
log.info(
|
301 |
+
f"Updating reranking model: {app.state.config.RAG_RERANKING_MODEL} to {form_data.reranking_model}"
|
302 |
+
)
|
303 |
+
try:
|
304 |
+
app.state.config.RAG_RERANKING_MODEL = form_data.reranking_model
|
305 |
+
|
306 |
+
update_reranking_model(app.state.config.RAG_RERANKING_MODEL), True
|
307 |
+
|
308 |
+
return {
|
309 |
+
"status": True,
|
310 |
+
"reranking_model": app.state.config.RAG_RERANKING_MODEL,
|
311 |
+
}
|
312 |
+
except Exception as e:
|
313 |
+
log.exception(f"Problem updating reranking model: {e}")
|
314 |
+
raise HTTPException(
|
315 |
+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
316 |
+
detail=ERROR_MESSAGES.DEFAULT(e),
|
317 |
+
)
|
318 |
+
|
319 |
+
|
320 |
+
@app.get("/config")
|
321 |
+
async def get_rag_config(user=Depends(get_admin_user)):
|
322 |
+
return {
|
323 |
+
"status": True,
|
324 |
+
"pdf_extract_images": app.state.config.PDF_EXTRACT_IMAGES,
|
325 |
+
"chunk": {
|
326 |
+
"chunk_size": app.state.config.CHUNK_SIZE,
|
327 |
+
"chunk_overlap": app.state.config.CHUNK_OVERLAP,
|
328 |
+
},
|
329 |
+
"web_loader_ssl_verification": app.state.config.ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION,
|
330 |
+
"youtube": {
|
331 |
+
"language": app.state.config.YOUTUBE_LOADER_LANGUAGE,
|
332 |
+
"translation": app.state.YOUTUBE_LOADER_TRANSLATION,
|
333 |
+
},
|
334 |
+
}
|
335 |
+
|
336 |
+
|
337 |
+
class ChunkParamUpdateForm(BaseModel):
|
338 |
+
chunk_size: int
|
339 |
+
chunk_overlap: int
|
340 |
+
|
341 |
+
|
342 |
+
class YoutubeLoaderConfig(BaseModel):
|
343 |
+
language: List[str]
|
344 |
+
translation: Optional[str] = None
|
345 |
+
|
346 |
+
|
347 |
+
class ConfigUpdateForm(BaseModel):
|
348 |
+
pdf_extract_images: Optional[bool] = None
|
349 |
+
chunk: Optional[ChunkParamUpdateForm] = None
|
350 |
+
web_loader_ssl_verification: Optional[bool] = None
|
351 |
+
youtube: Optional[YoutubeLoaderConfig] = None
|
352 |
+
|
353 |
+
|
354 |
+
@app.post("/config/update")
|
355 |
+
async def update_rag_config(form_data: ConfigUpdateForm, user=Depends(get_admin_user)):
|
356 |
+
app.state.config.PDF_EXTRACT_IMAGES = (
|
357 |
+
form_data.pdf_extract_images
|
358 |
+
if form_data.pdf_extract_images is not None
|
359 |
+
else app.state.config.PDF_EXTRACT_IMAGES
|
360 |
+
)
|
361 |
+
|
362 |
+
app.state.config.CHUNK_SIZE = (
|
363 |
+
form_data.chunk.chunk_size
|
364 |
+
if form_data.chunk is not None
|
365 |
+
else app.state.config.CHUNK_SIZE
|
366 |
+
)
|
367 |
+
|
368 |
+
app.state.config.CHUNK_OVERLAP = (
|
369 |
+
form_data.chunk.chunk_overlap
|
370 |
+
if form_data.chunk is not None
|
371 |
+
else app.state.config.CHUNK_OVERLAP
|
372 |
+
)
|
373 |
+
|
374 |
+
app.state.config.ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION = (
|
375 |
+
form_data.web_loader_ssl_verification
|
376 |
+
if form_data.web_loader_ssl_verification != None
|
377 |
+
else app.state.config.ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION
|
378 |
+
)
|
379 |
+
|
380 |
+
app.state.config.YOUTUBE_LOADER_LANGUAGE = (
|
381 |
+
form_data.youtube.language
|
382 |
+
if form_data.youtube is not None
|
383 |
+
else app.state.config.YOUTUBE_LOADER_LANGUAGE
|
384 |
+
)
|
385 |
+
|
386 |
+
app.state.YOUTUBE_LOADER_TRANSLATION = (
|
387 |
+
form_data.youtube.translation
|
388 |
+
if form_data.youtube is not None
|
389 |
+
else app.state.YOUTUBE_LOADER_TRANSLATION
|
390 |
+
)
|
391 |
+
|
392 |
+
return {
|
393 |
+
"status": True,
|
394 |
+
"pdf_extract_images": app.state.config.PDF_EXTRACT_IMAGES,
|
395 |
+
"chunk": {
|
396 |
+
"chunk_size": app.state.config.CHUNK_SIZE,
|
397 |
+
"chunk_overlap": app.state.config.CHUNK_OVERLAP,
|
398 |
+
},
|
399 |
+
"web_loader_ssl_verification": app.state.config.ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION,
|
400 |
+
"youtube": {
|
401 |
+
"language": app.state.config.YOUTUBE_LOADER_LANGUAGE,
|
402 |
+
"translation": app.state.YOUTUBE_LOADER_TRANSLATION,
|
403 |
+
},
|
404 |
+
}
|
405 |
+
|
406 |
+
|
407 |
+
@app.get("/template")
|
408 |
+
async def get_rag_template(user=Depends(get_current_user)):
|
409 |
+
return {
|
410 |
+
"status": True,
|
411 |
+
"template": app.state.config.RAG_TEMPLATE,
|
412 |
+
}
|
413 |
+
|
414 |
+
|
415 |
+
@app.get("/query/settings")
|
416 |
+
async def get_query_settings(user=Depends(get_admin_user)):
|
417 |
+
return {
|
418 |
+
"status": True,
|
419 |
+
"template": app.state.config.RAG_TEMPLATE,
|
420 |
+
"k": app.state.config.TOP_K,
|
421 |
+
"r": app.state.config.RELEVANCE_THRESHOLD,
|
422 |
+
"hybrid": app.state.config.ENABLE_RAG_HYBRID_SEARCH,
|
423 |
+
}
|
424 |
+
|
425 |
+
|
426 |
+
class QuerySettingsForm(BaseModel):
|
427 |
+
k: Optional[int] = None
|
428 |
+
r: Optional[float] = None
|
429 |
+
template: Optional[str] = None
|
430 |
+
hybrid: Optional[bool] = None
|
431 |
+
|
432 |
+
|
433 |
+
@app.post("/query/settings/update")
|
434 |
+
async def update_query_settings(
|
435 |
+
form_data: QuerySettingsForm, user=Depends(get_admin_user)
|
436 |
+
):
|
437 |
+
app.state.config.RAG_TEMPLATE = (
|
438 |
+
form_data.template if form_data.template else RAG_TEMPLATE
|
439 |
+
)
|
440 |
+
app.state.config.TOP_K = form_data.k if form_data.k else 4
|
441 |
+
app.state.config.RELEVANCE_THRESHOLD = form_data.r if form_data.r else 0.0
|
442 |
+
app.state.config.ENABLE_RAG_HYBRID_SEARCH = (
|
443 |
+
form_data.hybrid if form_data.hybrid else False
|
444 |
+
)
|
445 |
+
return {
|
446 |
+
"status": True,
|
447 |
+
"template": app.state.config.RAG_TEMPLATE,
|
448 |
+
"k": app.state.config.TOP_K,
|
449 |
+
"r": app.state.config.RELEVANCE_THRESHOLD,
|
450 |
+
"hybrid": app.state.config.ENABLE_RAG_HYBRID_SEARCH,
|
451 |
+
}
|
452 |
+
|
453 |
+
|
454 |
+
class QueryDocForm(BaseModel):
|
455 |
+
collection_name: str
|
456 |
+
query: str
|
457 |
+
k: Optional[int] = None
|
458 |
+
r: Optional[float] = None
|
459 |
+
hybrid: Optional[bool] = None
|
460 |
+
|
461 |
+
|
462 |
+
@app.post("/query/doc")
|
463 |
+
def query_doc_handler(
|
464 |
+
form_data: QueryDocForm,
|
465 |
+
user=Depends(get_current_user),
|
466 |
+
):
|
467 |
+
try:
|
468 |
+
if app.state.config.ENABLE_RAG_HYBRID_SEARCH:
|
469 |
+
return query_doc_with_hybrid_search(
|
470 |
+
collection_name=form_data.collection_name,
|
471 |
+
query=form_data.query,
|
472 |
+
embedding_function=app.state.EMBEDDING_FUNCTION,
|
473 |
+
k=form_data.k if form_data.k else app.state.config.TOP_K,
|
474 |
+
reranking_function=app.state.sentence_transformer_rf,
|
475 |
+
r=(
|
476 |
+
form_data.r if form_data.r else app.state.config.RELEVANCE_THRESHOLD
|
477 |
+
),
|
478 |
+
)
|
479 |
+
else:
|
480 |
+
return query_doc(
|
481 |
+
collection_name=form_data.collection_name,
|
482 |
+
query=form_data.query,
|
483 |
+
embedding_function=app.state.EMBEDDING_FUNCTION,
|
484 |
+
k=form_data.k if form_data.k else app.state.config.TOP_K,
|
485 |
+
)
|
486 |
+
except Exception as e:
|
487 |
+
log.exception(e)
|
488 |
+
raise HTTPException(
|
489 |
+
status_code=status.HTTP_400_BAD_REQUEST,
|
490 |
+
detail=ERROR_MESSAGES.DEFAULT(e),
|
491 |
+
)
|
492 |
+
|
493 |
+
|
494 |
+
class QueryCollectionsForm(BaseModel):
|
495 |
+
collection_names: List[str]
|
496 |
+
query: str
|
497 |
+
k: Optional[int] = None
|
498 |
+
r: Optional[float] = None
|
499 |
+
hybrid: Optional[bool] = None
|
500 |
+
|
501 |
+
|
502 |
+
@app.post("/query/collection")
|
503 |
+
def query_collection_handler(
|
504 |
+
form_data: QueryCollectionsForm,
|
505 |
+
user=Depends(get_current_user),
|
506 |
+
):
|
507 |
+
try:
|
508 |
+
if app.state.config.ENABLE_RAG_HYBRID_SEARCH:
|
509 |
+
return query_collection_with_hybrid_search(
|
510 |
+
collection_names=form_data.collection_names,
|
511 |
+
query=form_data.query,
|
512 |
+
embedding_function=app.state.EMBEDDING_FUNCTION,
|
513 |
+
k=form_data.k if form_data.k else app.state.config.TOP_K,
|
514 |
+
reranking_function=app.state.sentence_transformer_rf,
|
515 |
+
r=(
|
516 |
+
form_data.r if form_data.r else app.state.config.RELEVANCE_THRESHOLD
|
517 |
+
),
|
518 |
+
)
|
519 |
+
else:
|
520 |
+
return query_collection(
|
521 |
+
collection_names=form_data.collection_names,
|
522 |
+
query=form_data.query,
|
523 |
+
embedding_function=app.state.EMBEDDING_FUNCTION,
|
524 |
+
k=form_data.k if form_data.k else app.state.config.TOP_K,
|
525 |
+
)
|
526 |
+
|
527 |
+
except Exception as e:
|
528 |
+
log.exception(e)
|
529 |
+
raise HTTPException(
|
530 |
+
status_code=status.HTTP_400_BAD_REQUEST,
|
531 |
+
detail=ERROR_MESSAGES.DEFAULT(e),
|
532 |
+
)
|
533 |
+
|
534 |
+
|
535 |
+
@app.post("/youtube")
|
536 |
+
def store_youtube_video(form_data: UrlForm, user=Depends(get_current_user)):
|
537 |
+
try:
|
538 |
+
loader = YoutubeLoader.from_youtube_url(
|
539 |
+
form_data.url,
|
540 |
+
add_video_info=True,
|
541 |
+
language=app.state.config.YOUTUBE_LOADER_LANGUAGE,
|
542 |
+
translation=app.state.YOUTUBE_LOADER_TRANSLATION,
|
543 |
+
)
|
544 |
+
data = loader.load()
|
545 |
+
|
546 |
+
collection_name = form_data.collection_name
|
547 |
+
if collection_name == "":
|
548 |
+
collection_name = calculate_sha256_string(form_data.url)[:63]
|
549 |
+
|
550 |
+
store_data_in_vector_db(data, collection_name, overwrite=True)
|
551 |
+
return {
|
552 |
+
"status": True,
|
553 |
+
"collection_name": collection_name,
|
554 |
+
"filename": form_data.url,
|
555 |
+
}
|
556 |
+
except Exception as e:
|
557 |
+
log.exception(e)
|
558 |
+
raise HTTPException(
|
559 |
+
status_code=status.HTTP_400_BAD_REQUEST,
|
560 |
+
detail=ERROR_MESSAGES.DEFAULT(e),
|
561 |
+
)
|
562 |
+
|
563 |
+
|
564 |
+
@app.post("/web")
|
565 |
+
def store_web(form_data: UrlForm, user=Depends(get_current_user)):
|
566 |
+
# "https://www.gutenberg.org/files/1727/1727-h/1727-h.htm"
|
567 |
+
try:
|
568 |
+
loader = get_web_loader(
|
569 |
+
form_data.url,
|
570 |
+
verify_ssl=app.state.config.ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION,
|
571 |
+
)
|
572 |
+
data = loader.load()
|
573 |
+
|
574 |
+
collection_name = form_data.collection_name
|
575 |
+
if collection_name == "":
|
576 |
+
collection_name = calculate_sha256_string(form_data.url)[:63]
|
577 |
+
|
578 |
+
store_data_in_vector_db(data, collection_name, overwrite=True)
|
579 |
+
return {
|
580 |
+
"status": True,
|
581 |
+
"collection_name": collection_name,
|
582 |
+
"filename": form_data.url,
|
583 |
+
}
|
584 |
+
except Exception as e:
|
585 |
+
log.exception(e)
|
586 |
+
raise HTTPException(
|
587 |
+
status_code=status.HTTP_400_BAD_REQUEST,
|
588 |
+
detail=ERROR_MESSAGES.DEFAULT(e),
|
589 |
+
)
|
590 |
+
|
591 |
+
|
592 |
+
def get_web_loader(url: str, verify_ssl: bool = True):
|
593 |
+
# Check if the URL is valid
|
594 |
+
if isinstance(validators.url(url), validators.ValidationError):
|
595 |
+
raise ValueError(ERROR_MESSAGES.INVALID_URL)
|
596 |
+
if not ENABLE_RAG_LOCAL_WEB_FETCH:
|
597 |
+
# Local web fetch is disabled, filter out any URLs that resolve to private IP addresses
|
598 |
+
parsed_url = urllib.parse.urlparse(url)
|
599 |
+
# Get IPv4 and IPv6 addresses
|
600 |
+
ipv4_addresses, ipv6_addresses = resolve_hostname(parsed_url.hostname)
|
601 |
+
# Check if any of the resolved addresses are private
|
602 |
+
# This is technically still vulnerable to DNS rebinding attacks, as we don't control WebBaseLoader
|
603 |
+
for ip in ipv4_addresses:
|
604 |
+
if validators.ipv4(ip, private=True):
|
605 |
+
raise ValueError(ERROR_MESSAGES.INVALID_URL)
|
606 |
+
for ip in ipv6_addresses:
|
607 |
+
if validators.ipv6(ip, private=True):
|
608 |
+
raise ValueError(ERROR_MESSAGES.INVALID_URL)
|
609 |
+
return WebBaseLoader(url, verify_ssl=verify_ssl)
|
610 |
+
|
611 |
+
|
612 |
+
def resolve_hostname(hostname):
|
613 |
+
# Get address information
|
614 |
+
addr_info = socket.getaddrinfo(hostname, None)
|
615 |
+
|
616 |
+
# Extract IP addresses from address information
|
617 |
+
ipv4_addresses = [info[4][0] for info in addr_info if info[0] == socket.AF_INET]
|
618 |
+
ipv6_addresses = [info[4][0] for info in addr_info if info[0] == socket.AF_INET6]
|
619 |
+
|
620 |
+
return ipv4_addresses, ipv6_addresses
|
621 |
+
|
622 |
+
|
623 |
+
def store_data_in_vector_db(data, collection_name, overwrite: bool = False) -> bool:
|
624 |
+
|
625 |
+
text_splitter = RecursiveCharacterTextSplitter(
|
626 |
+
chunk_size=app.state.config.CHUNK_SIZE,
|
627 |
+
chunk_overlap=app.state.config.CHUNK_OVERLAP,
|
628 |
+
add_start_index=True,
|
629 |
+
)
|
630 |
+
|
631 |
+
docs = text_splitter.split_documents(data)
|
632 |
+
|
633 |
+
if len(docs) > 0:
|
634 |
+
log.info(f"store_data_in_vector_db {docs}")
|
635 |
+
return store_docs_in_vector_db(docs, collection_name, overwrite), None
|
636 |
+
else:
|
637 |
+
raise ValueError(ERROR_MESSAGES.EMPTY_CONTENT)
|
638 |
+
|
639 |
+
|
640 |
+
def store_text_in_vector_db(
|
641 |
+
text, metadata, collection_name, overwrite: bool = False
|
642 |
+
) -> bool:
|
643 |
+
text_splitter = RecursiveCharacterTextSplitter(
|
644 |
+
chunk_size=app.state.config.CHUNK_SIZE,
|
645 |
+
chunk_overlap=app.state.config.CHUNK_OVERLAP,
|
646 |
+
add_start_index=True,
|
647 |
+
)
|
648 |
+
docs = text_splitter.create_documents([text], metadatas=[metadata])
|
649 |
+
return store_docs_in_vector_db(docs, collection_name, overwrite)
|
650 |
+
|
651 |
+
|
652 |
+
def store_docs_in_vector_db(docs, collection_name, overwrite: bool = False) -> bool:
|
653 |
+
log.info(f"store_docs_in_vector_db {docs} {collection_name}")
|
654 |
+
|
655 |
+
texts = [doc.page_content for doc in docs]
|
656 |
+
metadatas = [doc.metadata for doc in docs]
|
657 |
+
|
658 |
+
try:
|
659 |
+
if overwrite:
|
660 |
+
for collection in CHROMA_CLIENT.list_collections():
|
661 |
+
if collection_name == collection.name:
|
662 |
+
log.info(f"deleting existing collection {collection_name}")
|
663 |
+
CHROMA_CLIENT.delete_collection(name=collection_name)
|
664 |
+
|
665 |
+
collection = CHROMA_CLIENT.create_collection(name=collection_name)
|
666 |
+
|
667 |
+
embedding_func = get_embedding_function(
|
668 |
+
app.state.config.RAG_EMBEDDING_ENGINE,
|
669 |
+
app.state.config.RAG_EMBEDDING_MODEL,
|
670 |
+
app.state.sentence_transformer_ef,
|
671 |
+
app.state.config.OPENAI_API_KEY,
|
672 |
+
app.state.config.OPENAI_API_BASE_URL,
|
673 |
+
)
|
674 |
+
|
675 |
+
embedding_texts = list(map(lambda x: x.replace("\n", " "), texts))
|
676 |
+
embeddings = embedding_func(embedding_texts)
|
677 |
+
|
678 |
+
for batch in create_batches(
|
679 |
+
api=CHROMA_CLIENT,
|
680 |
+
ids=[str(uuid.uuid4()) for _ in texts],
|
681 |
+
metadatas=metadatas,
|
682 |
+
embeddings=embeddings,
|
683 |
+
documents=texts,
|
684 |
+
):
|
685 |
+
collection.add(*batch)
|
686 |
+
|
687 |
+
return True
|
688 |
+
except Exception as e:
|
689 |
+
log.exception(e)
|
690 |
+
if e.__class__.__name__ == "UniqueConstraintError":
|
691 |
+
return True
|
692 |
+
|
693 |
+
return False
|
694 |
+
|
695 |
+
|
696 |
+
def get_loader(filename: str, file_content_type: str, file_path: str):
|
697 |
+
file_ext = filename.split(".")[-1].lower()
|
698 |
+
known_type = True
|
699 |
+
|
700 |
+
known_source_ext = [
|
701 |
+
"go",
|
702 |
+
"py",
|
703 |
+
"java",
|
704 |
+
"sh",
|
705 |
+
"bat",
|
706 |
+
"ps1",
|
707 |
+
"cmd",
|
708 |
+
"js",
|
709 |
+
"ts",
|
710 |
+
"css",
|
711 |
+
"cpp",
|
712 |
+
"hpp",
|
713 |
+
"h",
|
714 |
+
"c",
|
715 |
+
"cs",
|
716 |
+
"sql",
|
717 |
+
"log",
|
718 |
+
"ini",
|
719 |
+
"pl",
|
720 |
+
"pm",
|
721 |
+
"r",
|
722 |
+
"dart",
|
723 |
+
"dockerfile",
|
724 |
+
"env",
|
725 |
+
"php",
|
726 |
+
"hs",
|
727 |
+
"hsc",
|
728 |
+
"lua",
|
729 |
+
"nginxconf",
|
730 |
+
"conf",
|
731 |
+
"m",
|
732 |
+
"mm",
|
733 |
+
"plsql",
|
734 |
+
"perl",
|
735 |
+
"rb",
|
736 |
+
"rs",
|
737 |
+
"db2",
|
738 |
+
"scala",
|
739 |
+
"bash",
|
740 |
+
"swift",
|
741 |
+
"vue",
|
742 |
+
"svelte",
|
743 |
+
]
|
744 |
+
|
745 |
+
if file_ext == "pdf":
|
746 |
+
loader = PyPDFLoader(
|
747 |
+
file_path, extract_images=app.state.config.PDF_EXTRACT_IMAGES
|
748 |
+
)
|
749 |
+
elif file_ext == "csv":
|
750 |
+
loader = CSVLoader(file_path)
|
751 |
+
elif file_ext == "rst":
|
752 |
+
loader = UnstructuredRSTLoader(file_path, mode="elements")
|
753 |
+
elif file_ext == "xml":
|
754 |
+
loader = UnstructuredXMLLoader(file_path)
|
755 |
+
elif file_ext in ["htm", "html"]:
|
756 |
+
loader = BSHTMLLoader(file_path, open_encoding="unicode_escape")
|
757 |
+
elif file_ext == "md":
|
758 |
+
loader = UnstructuredMarkdownLoader(file_path)
|
759 |
+
elif file_content_type == "application/epub+zip":
|
760 |
+
loader = UnstructuredEPubLoader(file_path)
|
761 |
+
elif (
|
762 |
+
file_content_type
|
763 |
+
== "application/vnd.openxmlformats-officedocument.wordprocessingml.document"
|
764 |
+
or file_ext in ["doc", "docx"]
|
765 |
+
):
|
766 |
+
loader = Docx2txtLoader(file_path)
|
767 |
+
elif file_content_type in [
|
768 |
+
"application/vnd.ms-excel",
|
769 |
+
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
770 |
+
] or file_ext in ["xls", "xlsx"]:
|
771 |
+
loader = UnstructuredExcelLoader(file_path)
|
772 |
+
elif file_content_type in [
|
773 |
+
"application/vnd.ms-powerpoint",
|
774 |
+
"application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
775 |
+
] or file_ext in ["ppt", "pptx"]:
|
776 |
+
loader = UnstructuredPowerPointLoader(file_path)
|
777 |
+
elif file_ext in known_source_ext or (
|
778 |
+
file_content_type and file_content_type.find("text/") >= 0
|
779 |
+
):
|
780 |
+
loader = TextLoader(file_path, autodetect_encoding=True)
|
781 |
+
else:
|
782 |
+
loader = TextLoader(file_path, autodetect_encoding=True)
|
783 |
+
known_type = False
|
784 |
+
|
785 |
+
return loader, known_type
|
786 |
+
|
787 |
+
|
788 |
+
@app.post("/doc")
|
789 |
+
def store_doc(
|
790 |
+
collection_name: Optional[str] = Form(None),
|
791 |
+
file: UploadFile = File(...),
|
792 |
+
user=Depends(get_current_user),
|
793 |
+
):
|
794 |
+
# "https://www.gutenberg.org/files/1727/1727-h/1727-h.htm"
|
795 |
+
|
796 |
+
log.info(f"file.content_type: {file.content_type}")
|
797 |
+
try:
|
798 |
+
unsanitized_filename = file.filename
|
799 |
+
filename = os.path.basename(unsanitized_filename)
|
800 |
+
|
801 |
+
file_path = f"{UPLOAD_DIR}/{filename}"
|
802 |
+
|
803 |
+
contents = file.file.read()
|
804 |
+
with open(file_path, "wb") as f:
|
805 |
+
f.write(contents)
|
806 |
+
f.close()
|
807 |
+
|
808 |
+
f = open(file_path, "rb")
|
809 |
+
if collection_name == None:
|
810 |
+
collection_name = calculate_sha256(f)[:63]
|
811 |
+
f.close()
|
812 |
+
|
813 |
+
loader, known_type = get_loader(filename, file.content_type, file_path)
|
814 |
+
data = loader.load()
|
815 |
+
|
816 |
+
try:
|
817 |
+
result = store_data_in_vector_db(data, collection_name)
|
818 |
+
|
819 |
+
if result:
|
820 |
+
return {
|
821 |
+
"status": True,
|
822 |
+
"collection_name": collection_name,
|
823 |
+
"filename": filename,
|
824 |
+
"known_type": known_type,
|
825 |
+
}
|
826 |
+
except Exception as e:
|
827 |
+
raise HTTPException(
|
828 |
+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
829 |
+
detail=e,
|
830 |
+
)
|
831 |
+
except Exception as e:
|
832 |
+
log.exception(e)
|
833 |
+
if "No pandoc was found" in str(e):
|
834 |
+
raise HTTPException(
|
835 |
+
status_code=status.HTTP_400_BAD_REQUEST,
|
836 |
+
detail=ERROR_MESSAGES.PANDOC_NOT_INSTALLED,
|
837 |
+
)
|
838 |
+
else:
|
839 |
+
raise HTTPException(
|
840 |
+
status_code=status.HTTP_400_BAD_REQUEST,
|
841 |
+
detail=ERROR_MESSAGES.DEFAULT(e),
|
842 |
+
)
|
843 |
+
|
844 |
+
|
845 |
+
class TextRAGForm(BaseModel):
|
846 |
+
name: str
|
847 |
+
content: str
|
848 |
+
collection_name: Optional[str] = None
|
849 |
+
|
850 |
+
|
851 |
+
@app.post("/text")
|
852 |
+
def store_text(
|
853 |
+
form_data: TextRAGForm,
|
854 |
+
user=Depends(get_current_user),
|
855 |
+
):
|
856 |
+
|
857 |
+
collection_name = form_data.collection_name
|
858 |
+
if collection_name == None:
|
859 |
+
collection_name = calculate_sha256_string(form_data.content)
|
860 |
+
|
861 |
+
result = store_text_in_vector_db(
|
862 |
+
form_data.content,
|
863 |
+
metadata={"name": form_data.name, "created_by": user.id},
|
864 |
+
collection_name=collection_name,
|
865 |
+
)
|
866 |
+
|
867 |
+
if result:
|
868 |
+
return {"status": True, "collection_name": collection_name}
|
869 |
+
else:
|
870 |
+
raise HTTPException(
|
871 |
+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
872 |
+
detail=ERROR_MESSAGES.DEFAULT(),
|
873 |
+
)
|
874 |
+
|
875 |
+
|
876 |
+
@app.get("/scan")
|
877 |
+
def scan_docs_dir(user=Depends(get_admin_user)):
|
878 |
+
for path in Path(DOCS_DIR).rglob("./**/*"):
|
879 |
+
try:
|
880 |
+
if path.is_file() and not path.name.startswith("."):
|
881 |
+
tags = extract_folders_after_data_docs(path)
|
882 |
+
filename = path.name
|
883 |
+
file_content_type = mimetypes.guess_type(path)
|
884 |
+
|
885 |
+
f = open(path, "rb")
|
886 |
+
collection_name = calculate_sha256(f)[:63]
|
887 |
+
f.close()
|
888 |
+
|
889 |
+
loader, known_type = get_loader(
|
890 |
+
filename, file_content_type[0], str(path)
|
891 |
+
)
|
892 |
+
data = loader.load()
|
893 |
+
|
894 |
+
try:
|
895 |
+
result = store_data_in_vector_db(data, collection_name)
|
896 |
+
|
897 |
+
if result:
|
898 |
+
sanitized_filename = sanitize_filename(filename)
|
899 |
+
doc = Documents.get_doc_by_name(sanitized_filename)
|
900 |
+
|
901 |
+
if doc == None:
|
902 |
+
doc = Documents.insert_new_doc(
|
903 |
+
user.id,
|
904 |
+
DocumentForm(
|
905 |
+
**{
|
906 |
+
"name": sanitized_filename,
|
907 |
+
"title": filename,
|
908 |
+
"collection_name": collection_name,
|
909 |
+
"filename": filename,
|
910 |
+
"content": (
|
911 |
+
json.dumps(
|
912 |
+
{
|
913 |
+
"tags": list(
|
914 |
+
map(
|
915 |
+
lambda name: {"name": name},
|
916 |
+
tags,
|
917 |
+
)
|
918 |
+
)
|
919 |
+
}
|
920 |
+
)
|
921 |
+
if len(tags)
|
922 |
+
else "{}"
|
923 |
+
),
|
924 |
+
}
|
925 |
+
),
|
926 |
+
)
|
927 |
+
except Exception as e:
|
928 |
+
log.exception(e)
|
929 |
+
pass
|
930 |
+
|
931 |
+
except Exception as e:
|
932 |
+
log.exception(e)
|
933 |
+
|
934 |
+
return True
|
935 |
+
|
936 |
+
|
937 |
+
@app.get("/reset/db")
|
938 |
+
def reset_vector_db(user=Depends(get_admin_user)):
|
939 |
+
CHROMA_CLIENT.reset()
|
940 |
+
|
941 |
+
|
942 |
+
@app.get("/reset")
|
943 |
+
def reset(user=Depends(get_admin_user)) -> bool:
|
944 |
+
folder = f"{UPLOAD_DIR}"
|
945 |
+
for filename in os.listdir(folder):
|
946 |
+
file_path = os.path.join(folder, filename)
|
947 |
+
try:
|
948 |
+
if os.path.isfile(file_path) or os.path.islink(file_path):
|
949 |
+
os.unlink(file_path)
|
950 |
+
elif os.path.isdir(file_path):
|
951 |
+
shutil.rmtree(file_path)
|
952 |
+
except Exception as e:
|
953 |
+
log.error("Failed to delete %s. Reason: %s" % (file_path, e))
|
954 |
+
|
955 |
+
try:
|
956 |
+
CHROMA_CLIENT.reset()
|
957 |
+
except Exception as e:
|
958 |
+
log.exception(e)
|
959 |
+
|
960 |
+
return True
|
961 |
+
|
962 |
+
|
963 |
+
if ENV == "dev":
|
964 |
+
|
965 |
+
@app.get("/ef")
|
966 |
+
async def get_embeddings():
|
967 |
+
return {"result": app.state.EMBEDDING_FUNCTION("hello world")}
|
968 |
+
|
969 |
+
@app.get("/ef/{text}")
|
970 |
+
async def get_embeddings_text(text: str):
|
971 |
+
return {"result": app.state.EMBEDDING_FUNCTION(text)}
|
backend/apps/rag/utils.py
ADDED
@@ -0,0 +1,522 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import os
|
2 |
+
import logging
|
3 |
+
import requests
|
4 |
+
|
5 |
+
from typing import List
|
6 |
+
|
7 |
+
from apps.ollama.main import (
|
8 |
+
generate_ollama_embeddings,
|
9 |
+
GenerateEmbeddingsForm,
|
10 |
+
)
|
11 |
+
|
12 |
+
from huggingface_hub import snapshot_download
|
13 |
+
|
14 |
+
from langchain_core.documents import Document
|
15 |
+
from langchain_community.retrievers import BM25Retriever
|
16 |
+
from langchain.retrievers import (
|
17 |
+
ContextualCompressionRetriever,
|
18 |
+
EnsembleRetriever,
|
19 |
+
)
|
20 |
+
|
21 |
+
from typing import Optional
|
22 |
+
from config import SRC_LOG_LEVELS, CHROMA_CLIENT
|
23 |
+
|
24 |
+
|
25 |
+
log = logging.getLogger(__name__)
|
26 |
+
log.setLevel(SRC_LOG_LEVELS["RAG"])
|
27 |
+
|
28 |
+
|
29 |
+
def query_doc(
|
30 |
+
collection_name: str,
|
31 |
+
query: str,
|
32 |
+
embedding_function,
|
33 |
+
k: int,
|
34 |
+
):
|
35 |
+
try:
|
36 |
+
collection = CHROMA_CLIENT.get_collection(name=collection_name)
|
37 |
+
query_embeddings = embedding_function(query)
|
38 |
+
|
39 |
+
result = collection.query(
|
40 |
+
query_embeddings=[query_embeddings],
|
41 |
+
n_results=k,
|
42 |
+
)
|
43 |
+
|
44 |
+
log.info(f"query_doc:result {result}")
|
45 |
+
return result
|
46 |
+
except Exception as e:
|
47 |
+
raise e
|
48 |
+
|
49 |
+
|
50 |
+
def query_doc_with_hybrid_search(
|
51 |
+
collection_name: str,
|
52 |
+
query: str,
|
53 |
+
embedding_function,
|
54 |
+
k: int,
|
55 |
+
reranking_function,
|
56 |
+
r: float,
|
57 |
+
):
|
58 |
+
try:
|
59 |
+
collection = CHROMA_CLIENT.get_collection(name=collection_name)
|
60 |
+
documents = collection.get() # get all documents
|
61 |
+
|
62 |
+
bm25_retriever = BM25Retriever.from_texts(
|
63 |
+
texts=documents.get("documents"),
|
64 |
+
metadatas=documents.get("metadatas"),
|
65 |
+
)
|
66 |
+
bm25_retriever.k = k
|
67 |
+
|
68 |
+
chroma_retriever = ChromaRetriever(
|
69 |
+
collection=collection,
|
70 |
+
embedding_function=embedding_function,
|
71 |
+
top_n=k,
|
72 |
+
)
|
73 |
+
|
74 |
+
ensemble_retriever = EnsembleRetriever(
|
75 |
+
retrievers=[bm25_retriever, chroma_retriever], weights=[0.5, 0.5]
|
76 |
+
)
|
77 |
+
|
78 |
+
compressor = RerankCompressor(
|
79 |
+
embedding_function=embedding_function,
|
80 |
+
top_n=k,
|
81 |
+
reranking_function=reranking_function,
|
82 |
+
r_score=r,
|
83 |
+
)
|
84 |
+
|
85 |
+
compression_retriever = ContextualCompressionRetriever(
|
86 |
+
base_compressor=compressor, base_retriever=ensemble_retriever
|
87 |
+
)
|
88 |
+
|
89 |
+
result = compression_retriever.invoke(query)
|
90 |
+
result = {
|
91 |
+
"distances": [[d.metadata.get("score") for d in result]],
|
92 |
+
"documents": [[d.page_content for d in result]],
|
93 |
+
"metadatas": [[d.metadata for d in result]],
|
94 |
+
}
|
95 |
+
|
96 |
+
log.info(f"query_doc_with_hybrid_search:result {result}")
|
97 |
+
return result
|
98 |
+
except Exception as e:
|
99 |
+
raise e
|
100 |
+
|
101 |
+
|
102 |
+
def merge_and_sort_query_results(query_results, k, reverse=False):
|
103 |
+
# Initialize lists to store combined data
|
104 |
+
combined_distances = []
|
105 |
+
combined_documents = []
|
106 |
+
combined_metadatas = []
|
107 |
+
|
108 |
+
for data in query_results:
|
109 |
+
combined_distances.extend(data["distances"][0])
|
110 |
+
combined_documents.extend(data["documents"][0])
|
111 |
+
combined_metadatas.extend(data["metadatas"][0])
|
112 |
+
|
113 |
+
# Create a list of tuples (distance, document, metadata)
|
114 |
+
combined = list(zip(combined_distances, combined_documents, combined_metadatas))
|
115 |
+
|
116 |
+
# Sort the list based on distances
|
117 |
+
combined.sort(key=lambda x: x[0], reverse=reverse)
|
118 |
+
|
119 |
+
# We don't have anything :-(
|
120 |
+
if not combined:
|
121 |
+
sorted_distances = []
|
122 |
+
sorted_documents = []
|
123 |
+
sorted_metadatas = []
|
124 |
+
else:
|
125 |
+
# Unzip the sorted list
|
126 |
+
sorted_distances, sorted_documents, sorted_metadatas = zip(*combined)
|
127 |
+
|
128 |
+
# Slicing the lists to include only k elements
|
129 |
+
sorted_distances = list(sorted_distances)[:k]
|
130 |
+
sorted_documents = list(sorted_documents)[:k]
|
131 |
+
sorted_metadatas = list(sorted_metadatas)[:k]
|
132 |
+
|
133 |
+
# Create the output dictionary
|
134 |
+
result = {
|
135 |
+
"distances": [sorted_distances],
|
136 |
+
"documents": [sorted_documents],
|
137 |
+
"metadatas": [sorted_metadatas],
|
138 |
+
}
|
139 |
+
|
140 |
+
return result
|
141 |
+
|
142 |
+
|
143 |
+
def query_collection(
|
144 |
+
collection_names: List[str],
|
145 |
+
query: str,
|
146 |
+
embedding_function,
|
147 |
+
k: int,
|
148 |
+
):
|
149 |
+
results = []
|
150 |
+
for collection_name in collection_names:
|
151 |
+
try:
|
152 |
+
result = query_doc(
|
153 |
+
collection_name=collection_name,
|
154 |
+
query=query,
|
155 |
+
k=k,
|
156 |
+
embedding_function=embedding_function,
|
157 |
+
)
|
158 |
+
results.append(result)
|
159 |
+
except:
|
160 |
+
pass
|
161 |
+
return merge_and_sort_query_results(results, k=k)
|
162 |
+
|
163 |
+
|
164 |
+
def query_collection_with_hybrid_search(
|
165 |
+
collection_names: List[str],
|
166 |
+
query: str,
|
167 |
+
embedding_function,
|
168 |
+
k: int,
|
169 |
+
reranking_function,
|
170 |
+
r: float,
|
171 |
+
):
|
172 |
+
results = []
|
173 |
+
for collection_name in collection_names:
|
174 |
+
try:
|
175 |
+
result = query_doc_with_hybrid_search(
|
176 |
+
collection_name=collection_name,
|
177 |
+
query=query,
|
178 |
+
embedding_function=embedding_function,
|
179 |
+
k=k,
|
180 |
+
reranking_function=reranking_function,
|
181 |
+
r=r,
|
182 |
+
)
|
183 |
+
results.append(result)
|
184 |
+
except:
|
185 |
+
pass
|
186 |
+
return merge_and_sort_query_results(results, k=k, reverse=True)
|
187 |
+
|
188 |
+
|
189 |
+
def rag_template(template: str, context: str, query: str):
|
190 |
+
template = template.replace("[context]", context)
|
191 |
+
template = template.replace("[query]", query)
|
192 |
+
return template
|
193 |
+
|
194 |
+
|
195 |
+
def get_embedding_function(
|
196 |
+
embedding_engine,
|
197 |
+
embedding_model,
|
198 |
+
embedding_function,
|
199 |
+
openai_key,
|
200 |
+
openai_url,
|
201 |
+
):
|
202 |
+
if embedding_engine == "":
|
203 |
+
return lambda query: embedding_function.encode(query).tolist()
|
204 |
+
elif embedding_engine in ["ollama", "openai"]:
|
205 |
+
if embedding_engine == "ollama":
|
206 |
+
func = lambda query: generate_ollama_embeddings(
|
207 |
+
GenerateEmbeddingsForm(
|
208 |
+
**{
|
209 |
+
"model": embedding_model,
|
210 |
+
"prompt": query,
|
211 |
+
}
|
212 |
+
)
|
213 |
+
)
|
214 |
+
elif embedding_engine == "openai":
|
215 |
+
func = lambda query: generate_openai_embeddings(
|
216 |
+
model=embedding_model,
|
217 |
+
text=query,
|
218 |
+
key=openai_key,
|
219 |
+
url=openai_url,
|
220 |
+
)
|
221 |
+
|
222 |
+
def generate_multiple(query, f):
|
223 |
+
if isinstance(query, list):
|
224 |
+
return [f(q) for q in query]
|
225 |
+
else:
|
226 |
+
return f(query)
|
227 |
+
|
228 |
+
return lambda query: generate_multiple(query, func)
|
229 |
+
|
230 |
+
|
231 |
+
def rag_messages(
|
232 |
+
docs,
|
233 |
+
messages,
|
234 |
+
template,
|
235 |
+
embedding_function,
|
236 |
+
k,
|
237 |
+
reranking_function,
|
238 |
+
r,
|
239 |
+
hybrid_search,
|
240 |
+
):
|
241 |
+
log.debug(f"docs: {docs} {messages} {embedding_function} {reranking_function}")
|
242 |
+
|
243 |
+
last_user_message_idx = None
|
244 |
+
for i in range(len(messages) - 1, -1, -1):
|
245 |
+
if messages[i]["role"] == "user":
|
246 |
+
last_user_message_idx = i
|
247 |
+
break
|
248 |
+
|
249 |
+
user_message = messages[last_user_message_idx]
|
250 |
+
|
251 |
+
if isinstance(user_message["content"], list):
|
252 |
+
# Handle list content input
|
253 |
+
content_type = "list"
|
254 |
+
query = ""
|
255 |
+
for content_item in user_message["content"]:
|
256 |
+
if content_item["type"] == "text":
|
257 |
+
query = content_item["text"]
|
258 |
+
break
|
259 |
+
elif isinstance(user_message["content"], str):
|
260 |
+
# Handle text content input
|
261 |
+
content_type = "text"
|
262 |
+
query = user_message["content"]
|
263 |
+
else:
|
264 |
+
# Fallback in case the input does not match expected types
|
265 |
+
content_type = None
|
266 |
+
query = ""
|
267 |
+
|
268 |
+
extracted_collections = []
|
269 |
+
relevant_contexts = []
|
270 |
+
|
271 |
+
for doc in docs:
|
272 |
+
context = None
|
273 |
+
|
274 |
+
collection_names = (
|
275 |
+
doc["collection_names"]
|
276 |
+
if doc["type"] == "collection"
|
277 |
+
else [doc["collection_name"]]
|
278 |
+
)
|
279 |
+
|
280 |
+
collection_names = set(collection_names).difference(extracted_collections)
|
281 |
+
if not collection_names:
|
282 |
+
log.debug(f"skipping {doc} as it has already been extracted")
|
283 |
+
continue
|
284 |
+
|
285 |
+
try:
|
286 |
+
if doc["type"] == "text":
|
287 |
+
context = doc["content"]
|
288 |
+
else:
|
289 |
+
if hybrid_search:
|
290 |
+
context = query_collection_with_hybrid_search(
|
291 |
+
collection_names=collection_names,
|
292 |
+
query=query,
|
293 |
+
embedding_function=embedding_function,
|
294 |
+
k=k,
|
295 |
+
reranking_function=reranking_function,
|
296 |
+
r=r,
|
297 |
+
)
|
298 |
+
else:
|
299 |
+
context = query_collection(
|
300 |
+
collection_names=collection_names,
|
301 |
+
query=query,
|
302 |
+
embedding_function=embedding_function,
|
303 |
+
k=k,
|
304 |
+
)
|
305 |
+
except Exception as e:
|
306 |
+
log.exception(e)
|
307 |
+
context = None
|
308 |
+
|
309 |
+
if context:
|
310 |
+
relevant_contexts.append({**context, "source": doc})
|
311 |
+
|
312 |
+
extracted_collections.extend(collection_names)
|
313 |
+
|
314 |
+
context_string = ""
|
315 |
+
|
316 |
+
citations = []
|
317 |
+
for context in relevant_contexts:
|
318 |
+
try:
|
319 |
+
if "documents" in context:
|
320 |
+
context_string += "\n\n".join(
|
321 |
+
[text for text in context["documents"][0] if text is not None]
|
322 |
+
)
|
323 |
+
|
324 |
+
if "metadatas" in context:
|
325 |
+
citations.append(
|
326 |
+
{
|
327 |
+
"source": context["source"],
|
328 |
+
"document": context["documents"][0],
|
329 |
+
"metadata": context["metadatas"][0],
|
330 |
+
}
|
331 |
+
)
|
332 |
+
except Exception as e:
|
333 |
+
log.exception(e)
|
334 |
+
|
335 |
+
context_string = context_string.strip()
|
336 |
+
|
337 |
+
ra_content = rag_template(
|
338 |
+
template=template,
|
339 |
+
context=context_string,
|
340 |
+
query=query,
|
341 |
+
)
|
342 |
+
|
343 |
+
log.debug(f"ra_content: {ra_content}")
|
344 |
+
|
345 |
+
if content_type == "list":
|
346 |
+
new_content = []
|
347 |
+
for content_item in user_message["content"]:
|
348 |
+
if content_item["type"] == "text":
|
349 |
+
# Update the text item's content with ra_content
|
350 |
+
new_content.append({"type": "text", "text": ra_content})
|
351 |
+
else:
|
352 |
+
# Keep other types of content as they are
|
353 |
+
new_content.append(content_item)
|
354 |
+
new_user_message = {**user_message, "content": new_content}
|
355 |
+
else:
|
356 |
+
new_user_message = {
|
357 |
+
**user_message,
|
358 |
+
"content": ra_content,
|
359 |
+
}
|
360 |
+
|
361 |
+
messages[last_user_message_idx] = new_user_message
|
362 |
+
|
363 |
+
return messages, citations
|
364 |
+
|
365 |
+
|
366 |
+
def get_model_path(model: str, update_model: bool = False):
|
367 |
+
# Construct huggingface_hub kwargs with local_files_only to return the snapshot path
|
368 |
+
cache_dir = os.getenv("SENTENCE_TRANSFORMERS_HOME")
|
369 |
+
|
370 |
+
local_files_only = not update_model
|
371 |
+
|
372 |
+
snapshot_kwargs = {
|
373 |
+
"cache_dir": cache_dir,
|
374 |
+
"local_files_only": local_files_only,
|
375 |
+
}
|
376 |
+
|
377 |
+
log.debug(f"model: {model}")
|
378 |
+
log.debug(f"snapshot_kwargs: {snapshot_kwargs}")
|
379 |
+
|
380 |
+
# Inspiration from upstream sentence_transformers
|
381 |
+
if (
|
382 |
+
os.path.exists(model)
|
383 |
+
or ("\\" in model or model.count("/") > 1)
|
384 |
+
and local_files_only
|
385 |
+
):
|
386 |
+
# If fully qualified path exists, return input, else set repo_id
|
387 |
+
return model
|
388 |
+
elif "/" not in model:
|
389 |
+
# Set valid repo_id for model short-name
|
390 |
+
model = "sentence-transformers" + "/" + model
|
391 |
+
|
392 |
+
snapshot_kwargs["repo_id"] = model
|
393 |
+
|
394 |
+
# Attempt to query the huggingface_hub library to determine the local path and/or to update
|
395 |
+
try:
|
396 |
+
model_repo_path = snapshot_download(**snapshot_kwargs)
|
397 |
+
log.debug(f"model_repo_path: {model_repo_path}")
|
398 |
+
return model_repo_path
|
399 |
+
except Exception as e:
|
400 |
+
log.exception(f"Cannot determine model snapshot path: {e}")
|
401 |
+
return model
|
402 |
+
|
403 |
+
|
404 |
+
def generate_openai_embeddings(
|
405 |
+
model: str, text: str, key: str, url: str = "https://api.openai.com/v1"
|
406 |
+
):
|
407 |
+
try:
|
408 |
+
r = requests.post(
|
409 |
+
f"{url}/embeddings",
|
410 |
+
headers={
|
411 |
+
"Content-Type": "application/json",
|
412 |
+
"Authorization": f"Bearer {key}",
|
413 |
+
},
|
414 |
+
json={"input": text, "model": model},
|
415 |
+
)
|
416 |
+
r.raise_for_status()
|
417 |
+
data = r.json()
|
418 |
+
if "data" in data:
|
419 |
+
return data["data"][0]["embedding"]
|
420 |
+
else:
|
421 |
+
raise "Something went wrong :/"
|
422 |
+
except Exception as e:
|
423 |
+
print(e)
|
424 |
+
return None
|
425 |
+
|
426 |
+
|
427 |
+
from typing import Any
|
428 |
+
|
429 |
+
from langchain_core.retrievers import BaseRetriever
|
430 |
+
from langchain_core.callbacks import CallbackManagerForRetrieverRun
|
431 |
+
|
432 |
+
|
433 |
+
class ChromaRetriever(BaseRetriever):
|
434 |
+
collection: Any
|
435 |
+
embedding_function: Any
|
436 |
+
top_n: int
|
437 |
+
|
438 |
+
def _get_relevant_documents(
|
439 |
+
self,
|
440 |
+
query: str,
|
441 |
+
*,
|
442 |
+
run_manager: CallbackManagerForRetrieverRun,
|
443 |
+
) -> List[Document]:
|
444 |
+
query_embeddings = self.embedding_function(query)
|
445 |
+
|
446 |
+
results = self.collection.query(
|
447 |
+
query_embeddings=[query_embeddings],
|
448 |
+
n_results=self.top_n,
|
449 |
+
)
|
450 |
+
|
451 |
+
ids = results["ids"][0]
|
452 |
+
metadatas = results["metadatas"][0]
|
453 |
+
documents = results["documents"][0]
|
454 |
+
|
455 |
+
results = []
|
456 |
+
for idx in range(len(ids)):
|
457 |
+
results.append(
|
458 |
+
Document(
|
459 |
+
metadata=metadatas[idx],
|
460 |
+
page_content=documents[idx],
|
461 |
+
)
|
462 |
+
)
|
463 |
+
return results
|
464 |
+
|
465 |
+
|
466 |
+
import operator
|
467 |
+
|
468 |
+
from typing import Optional, Sequence
|
469 |
+
|
470 |
+
from langchain_core.documents import BaseDocumentCompressor, Document
|
471 |
+
from langchain_core.callbacks import Callbacks
|
472 |
+
from langchain_core.pydantic_v1 import Extra
|
473 |
+
|
474 |
+
from sentence_transformers import util
|
475 |
+
|
476 |
+
|
477 |
+
class RerankCompressor(BaseDocumentCompressor):
|
478 |
+
embedding_function: Any
|
479 |
+
top_n: int
|
480 |
+
reranking_function: Any
|
481 |
+
r_score: float
|
482 |
+
|
483 |
+
class Config:
|
484 |
+
extra = Extra.forbid
|
485 |
+
arbitrary_types_allowed = True
|
486 |
+
|
487 |
+
def compress_documents(
|
488 |
+
self,
|
489 |
+
documents: Sequence[Document],
|
490 |
+
query: str,
|
491 |
+
callbacks: Optional[Callbacks] = None,
|
492 |
+
) -> Sequence[Document]:
|
493 |
+
reranking = self.reranking_function is not None
|
494 |
+
|
495 |
+
if reranking:
|
496 |
+
scores = self.reranking_function.predict(
|
497 |
+
[(query, doc.page_content) for doc in documents]
|
498 |
+
)
|
499 |
+
else:
|
500 |
+
query_embedding = self.embedding_function(query)
|
501 |
+
document_embedding = self.embedding_function(
|
502 |
+
[doc.page_content for doc in documents]
|
503 |
+
)
|
504 |
+
scores = util.cos_sim(query_embedding, document_embedding)[0]
|
505 |
+
|
506 |
+
docs_with_scores = list(zip(documents, scores.tolist()))
|
507 |
+
if self.r_score:
|
508 |
+
docs_with_scores = [
|
509 |
+
(d, s) for d, s in docs_with_scores if s >= self.r_score
|
510 |
+
]
|
511 |
+
|
512 |
+
result = sorted(docs_with_scores, key=operator.itemgetter(1), reverse=True)
|
513 |
+
final_results = []
|
514 |
+
for doc, doc_score in result[: self.top_n]:
|
515 |
+
metadata = doc.metadata
|
516 |
+
metadata["score"] = doc_score
|
517 |
+
doc = Document(
|
518 |
+
page_content=doc.page_content,
|
519 |
+
metadata=metadata,
|
520 |
+
)
|
521 |
+
final_results.append(doc)
|
522 |
+
return final_results
|
backend/apps/web/internal/db.py
ADDED
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from peewee import *
|
2 |
+
from peewee_migrate import Router
|
3 |
+
from playhouse.db_url import connect
|
4 |
+
from config import SRC_LOG_LEVELS, DATA_DIR, DATABASE_URL, BACKEND_DIR
|
5 |
+
import os
|
6 |
+
import logging
|
7 |
+
|
8 |
+
log = logging.getLogger(__name__)
|
9 |
+
log.setLevel(SRC_LOG_LEVELS["DB"])
|
10 |
+
|
11 |
+
# Check if the file exists
|
12 |
+
if os.path.exists(f"{DATA_DIR}/ollama.db"):
|
13 |
+
# Rename the file
|
14 |
+
os.rename(f"{DATA_DIR}/ollama.db", f"{DATA_DIR}/webui.db")
|
15 |
+
log.info("Database migrated from Ollama-WebUI successfully.")
|
16 |
+
else:
|
17 |
+
pass
|
18 |
+
|
19 |
+
DB = connect(DATABASE_URL)
|
20 |
+
log.info(f"Connected to a {DB.__class__.__name__} database.")
|
21 |
+
router = Router(
|
22 |
+
DB, migrate_dir=BACKEND_DIR / "apps" / "web" / "internal" / "migrations", logger=log
|
23 |
+
)
|
24 |
+
router.run()
|
25 |
+
DB.connect(reuse_if_open=True)
|
backend/apps/web/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/apps/web/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/apps/web/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/apps/web/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/apps/web/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/apps/web/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/apps/web/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))
|
backend/apps/web/internal/migrations/008_add_memory.py
ADDED
@@ -0,0 +1,53 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 |
+
@migrator.create_model
|
39 |
+
class Memory(pw.Model):
|
40 |
+
id = pw.CharField(max_length=255, unique=True)
|
41 |
+
user_id = pw.CharField(max_length=255)
|
42 |
+
content = pw.TextField(null=False)
|
43 |
+
updated_at = pw.BigIntegerField(null=False)
|
44 |
+
created_at = pw.BigIntegerField(null=False)
|
45 |
+
|
46 |
+
class Meta:
|
47 |
+
table_name = "memory"
|
48 |
+
|
49 |
+
|
50 |
+
def rollback(migrator: Migrator, database: pw.Database, *, fake=False):
|
51 |
+
"""Write your rollback migrations here."""
|
52 |
+
|
53 |
+
migrator.remove_model("memory")
|