Spaces:
Sleeping
Sleeping
github-actions[bot]
commited on
Commit
•
0a4928b
0
Parent(s):
GitHub deploy: 63eda0fe4252d66b046de833ef37862a2a898bf2
Browse filesThis view is limited to 50 files because it contains too many changes.
See raw diff
- .dockerignore +19 -0
- .env.example +13 -0
- .eslintignore +13 -0
- .eslintrc.cjs +31 -0
- .gitattributes +2 -0
- .github/FUNDING.yml +1 -0
- .github/ISSUE_TEMPLATE/bug_report.md +63 -0
- .github/ISSUE_TEMPLATE/feature_request.md +19 -0
- .github/dependabot.yml +12 -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 +478 -0
- .github/workflows/format-backend.yaml +39 -0
- .github/workflows/format-build-frontend.yaml +57 -0
- .github/workflows/integration-test.yml +243 -0
- .github/workflows/lint-backend.disabled +27 -0
- .github/workflows/lint-frontend.disabled +21 -0
- .github/workflows/release-pypi.yml +31 -0
- .gitignore +309 -0
- .npmrc +1 -0
- .prettierignore +316 -0
- .prettierrc +9 -0
- CHANGELOG.md +744 -0
- CODE_OF_CONDUCT.md +77 -0
- Caddyfile.localhost +64 -0
- Dockerfile +161 -0
- INSTALLATION.md +35 -0
- LICENSE +21 -0
- Makefile +33 -0
- README.md +211 -0
- TROUBLESHOOTING.md +36 -0
- backend/.dockerignore +14 -0
- backend/.gitignore +16 -0
- backend/alembic.ini +114 -0
- backend/apps/audio/main.py +437 -0
- backend/apps/images/main.py +568 -0
- backend/apps/images/utils/comfyui.py +250 -0
- backend/apps/ollama/main.py +1280 -0
- backend/apps/openai/main.py +576 -0
- backend/apps/rag/main.py +1463 -0
- backend/apps/rag/search/brave.py +42 -0
- backend/apps/rag/search/duckduckgo.py +49 -0
- backend/apps/rag/search/google_pse.py +51 -0
- backend/apps/rag/search/jina_search.py +41 -0
- backend/apps/rag/search/main.py +20 -0
- backend/apps/rag/search/searxng.py +92 -0
- backend/apps/rag/search/serper.py +43 -0
- backend/apps/rag/search/serply.py +70 -0
- backend/apps/rag/search/serpstack.py +49 -0
.dockerignore
ADDED
@@ -0,0 +1,19 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
.github
|
2 |
+
.DS_Store
|
3 |
+
docs
|
4 |
+
kubernetes
|
5 |
+
node_modules
|
6 |
+
/.svelte-kit
|
7 |
+
/package
|
8 |
+
.env
|
9 |
+
.env.*
|
10 |
+
vite.config.js.timestamp-*
|
11 |
+
vite.config.ts.timestamp-*
|
12 |
+
__pycache__
|
13 |
+
.idea
|
14 |
+
venv
|
15 |
+
_old
|
16 |
+
uploads
|
17 |
+
.ipynb_checkpoints
|
18 |
+
**/*.db
|
19 |
+
_test
|
.env.example
ADDED
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Ollama URL for the backend to connect
|
2 |
+
# The path '/ollama' will be redirected to the specified backend URL
|
3 |
+
OLLAMA_BASE_URL='http://localhost:11434'
|
4 |
+
|
5 |
+
OPENAI_API_BASE_URL=''
|
6 |
+
OPENAI_API_KEY=''
|
7 |
+
|
8 |
+
# AUTOMATIC1111_BASE_URL="http://localhost:7860"
|
9 |
+
|
10 |
+
# DO NOT TRACK
|
11 |
+
SCARF_NO_ANALYTICS=true
|
12 |
+
DO_NOT_TRACK=true
|
13 |
+
ANONYMIZED_TELEMETRY=false
|
.eslintignore
ADDED
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
.DS_Store
|
2 |
+
node_modules
|
3 |
+
/build
|
4 |
+
/.svelte-kit
|
5 |
+
/package
|
6 |
+
.env
|
7 |
+
.env.*
|
8 |
+
!.env.example
|
9 |
+
|
10 |
+
# Ignore files for PNPM, NPM and YARN
|
11 |
+
pnpm-lock.yaml
|
12 |
+
package-lock.json
|
13 |
+
yarn.lock
|
.eslintrc.cjs
ADDED
@@ -0,0 +1,31 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
module.exports = {
|
2 |
+
root: true,
|
3 |
+
extends: [
|
4 |
+
'eslint:recommended',
|
5 |
+
'plugin:@typescript-eslint/recommended',
|
6 |
+
'plugin:svelte/recommended',
|
7 |
+
'plugin:cypress/recommended',
|
8 |
+
'prettier'
|
9 |
+
],
|
10 |
+
parser: '@typescript-eslint/parser',
|
11 |
+
plugins: ['@typescript-eslint'],
|
12 |
+
parserOptions: {
|
13 |
+
sourceType: 'module',
|
14 |
+
ecmaVersion: 2020,
|
15 |
+
extraFileExtensions: ['.svelte']
|
16 |
+
},
|
17 |
+
env: {
|
18 |
+
browser: true,
|
19 |
+
es2017: true,
|
20 |
+
node: true
|
21 |
+
},
|
22 |
+
overrides: [
|
23 |
+
{
|
24 |
+
files: ['*.svelte'],
|
25 |
+
parser: 'svelte-eslint-parser',
|
26 |
+
parserOptions: {
|
27 |
+
parser: '@typescript-eslint/parser'
|
28 |
+
}
|
29 |
+
}
|
30 |
+
]
|
31 |
+
};
|
.gitattributes
ADDED
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
1 |
+
*.sh text eol=lf
|
2 |
+
*.ttf filter=lfs diff=lfs merge=lfs -text
|
.github/FUNDING.yml
ADDED
@@ -0,0 +1 @@
|
|
|
|
|
1 |
+
github: tjbck
|
.github/ISSUE_TEMPLATE/bug_report.md
ADDED
@@ -0,0 +1,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.yml
ADDED
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
version: 2
|
2 |
+
updates:
|
3 |
+
- package-ecosystem: pip
|
4 |
+
directory: '/backend'
|
5 |
+
schedule:
|
6 |
+
interval: weekly
|
7 |
+
target-branch: 'dev'
|
8 |
+
- package-ecosystem: 'github-actions'
|
9 |
+
directory: '/'
|
10 |
+
schedule:
|
11 |
+
# Check for updates to GitHub Actions every week
|
12 |
+
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 |
+
- [ ] **Prefix:** To cleary categorize this pull request, prefix the pull request title, using one of the following:
|
15 |
+
- **BREAKING CHANGE**: Significant changes that may affect compatibility
|
16 |
+
- **build**: Changes that affect the build system or external dependencies
|
17 |
+
- **ci**: Changes to our continuous integration processes or workflows
|
18 |
+
- **chore**: Refactor, cleanup, or other non-functional code changes
|
19 |
+
- **docs**: Documentation update or addition
|
20 |
+
- **feat**: Introduces a new feature or enhancement to the codebase
|
21 |
+
- **fix**: Bug fix or error correction
|
22 |
+
- **i18n**: Internationalization or localization changes
|
23 |
+
- **perf**: Performance improvement
|
24 |
+
- **refactor**: Code restructuring for better maintainability, readability, or scalability
|
25 |
+
- **style**: Changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc.)
|
26 |
+
- **test**: Adding missing tests or correcting existing tests
|
27 |
+
- **WIP**: Work in progress, a temporary label for incomplete or ongoing work
|
28 |
+
|
29 |
+
# Changelog Entry
|
30 |
+
|
31 |
+
### Description
|
32 |
+
|
33 |
+
- [Concisely describe the changes made in this pull request, including any relevant motivation and impact (e.g., fixing a bug, adding a feature, or improving performance)]
|
34 |
+
|
35 |
+
### Added
|
36 |
+
|
37 |
+
- [List any new features, functionalities, or additions]
|
38 |
+
|
39 |
+
### Changed
|
40 |
+
|
41 |
+
- [List any changes, updates, refactorings, or optimizations]
|
42 |
+
|
43 |
+
### Deprecated
|
44 |
+
|
45 |
+
- [List any deprecated functionality or features that have been removed]
|
46 |
+
|
47 |
+
### Removed
|
48 |
+
|
49 |
+
- [List any removed features, files, or functionalities]
|
50 |
+
|
51 |
+
### Fixed
|
52 |
+
|
53 |
+
- [List any fixes, corrections, or bug fixes]
|
54 |
+
|
55 |
+
### Security
|
56 |
+
|
57 |
+
- [List any new or updated security-related changes, including vulnerability fixes]
|
58 |
+
|
59 |
+
### Breaking Changes
|
60 |
+
|
61 |
+
- **BREAKING CHANGE**: [List any breaking changes affecting compatibility or functionality]
|
62 |
+
|
63 |
+
---
|
64 |
+
|
65 |
+
### Additional Information
|
66 |
+
|
67 |
+
- [Insert any additional context, notes, or explanations for the changes]
|
68 |
+
- [Reference any related issues, commits, or other relevant information]
|
69 |
+
|
70 |
+
### Screenshots or Videos
|
71 |
+
|
72 |
+
- [Attach any relevant screenshots or videos demonstrating the changes]
|
.github/workflows/build-release.yml
ADDED
@@ -0,0 +1,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,478 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
name: Create and publish Docker images with specific build args
|
2 |
+
|
3 |
+
on:
|
4 |
+
workflow_dispatch:
|
5 |
+
push:
|
6 |
+
branches:
|
7 |
+
- main
|
8 |
+
- dev
|
9 |
+
tags:
|
10 |
+
- v*
|
11 |
+
|
12 |
+
env:
|
13 |
+
REGISTRY: ghcr.io
|
14 |
+
|
15 |
+
jobs:
|
16 |
+
build-main-image:
|
17 |
+
runs-on: ubuntu-latest
|
18 |
+
permissions:
|
19 |
+
contents: read
|
20 |
+
packages: write
|
21 |
+
strategy:
|
22 |
+
fail-fast: false
|
23 |
+
matrix:
|
24 |
+
platform:
|
25 |
+
- linux/amd64
|
26 |
+
- linux/arm64
|
27 |
+
|
28 |
+
steps:
|
29 |
+
# GitHub Packages requires the entire repository name to be in lowercase
|
30 |
+
# although the repository owner has a lowercase username, this prevents some people from running actions after forking
|
31 |
+
- name: Set repository and image name to lowercase
|
32 |
+
run: |
|
33 |
+
echo "IMAGE_NAME=${IMAGE_NAME,,}" >>${GITHUB_ENV}
|
34 |
+
echo "FULL_IMAGE_NAME=ghcr.io/${IMAGE_NAME,,}" >>${GITHUB_ENV}
|
35 |
+
env:
|
36 |
+
IMAGE_NAME: '${{ github.repository }}'
|
37 |
+
|
38 |
+
- name: Prepare
|
39 |
+
run: |
|
40 |
+
platform=${{ matrix.platform }}
|
41 |
+
echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV
|
42 |
+
|
43 |
+
- name: Checkout repository
|
44 |
+
uses: actions/checkout@v4
|
45 |
+
|
46 |
+
- name: Set up QEMU
|
47 |
+
uses: docker/setup-qemu-action@v3
|
48 |
+
|
49 |
+
- name: Set up Docker Buildx
|
50 |
+
uses: docker/setup-buildx-action@v3
|
51 |
+
|
52 |
+
- name: Log in to the Container registry
|
53 |
+
uses: docker/login-action@v3
|
54 |
+
with:
|
55 |
+
registry: ${{ env.REGISTRY }}
|
56 |
+
username: ${{ github.actor }}
|
57 |
+
password: ${{ secrets.GITHUB_TOKEN }}
|
58 |
+
|
59 |
+
- name: Extract metadata for Docker images (default latest tag)
|
60 |
+
id: meta
|
61 |
+
uses: docker/metadata-action@v5
|
62 |
+
with:
|
63 |
+
images: ${{ env.FULL_IMAGE_NAME }}
|
64 |
+
tags: |
|
65 |
+
type=ref,event=branch
|
66 |
+
type=ref,event=tag
|
67 |
+
type=sha,prefix=git-
|
68 |
+
type=semver,pattern={{version}}
|
69 |
+
type=semver,pattern={{major}}.{{minor}}
|
70 |
+
flavor: |
|
71 |
+
latest=${{ github.ref == 'refs/heads/main' }}
|
72 |
+
|
73 |
+
- name: Extract metadata for Docker cache
|
74 |
+
id: cache-meta
|
75 |
+
uses: docker/metadata-action@v5
|
76 |
+
with:
|
77 |
+
images: ${{ env.FULL_IMAGE_NAME }}
|
78 |
+
tags: |
|
79 |
+
type=ref,event=branch
|
80 |
+
${{ github.ref_type == 'tag' && 'type=raw,value=main' || '' }}
|
81 |
+
flavor: |
|
82 |
+
prefix=cache-${{ matrix.platform }}-
|
83 |
+
latest=false
|
84 |
+
|
85 |
+
- name: Build Docker image (latest)
|
86 |
+
uses: docker/build-push-action@v5
|
87 |
+
id: build
|
88 |
+
with:
|
89 |
+
context: .
|
90 |
+
push: true
|
91 |
+
platforms: ${{ matrix.platform }}
|
92 |
+
labels: ${{ steps.meta.outputs.labels }}
|
93 |
+
outputs: type=image,name=${{ env.FULL_IMAGE_NAME }},push-by-digest=true,name-canonical=true,push=true
|
94 |
+
cache-from: type=registry,ref=${{ steps.cache-meta.outputs.tags }}
|
95 |
+
cache-to: type=registry,ref=${{ steps.cache-meta.outputs.tags }},mode=max
|
96 |
+
build-args: |
|
97 |
+
BUILD_HASH=${{ github.sha }}
|
98 |
+
|
99 |
+
- name: Export digest
|
100 |
+
run: |
|
101 |
+
mkdir -p /tmp/digests
|
102 |
+
digest="${{ steps.build.outputs.digest }}"
|
103 |
+
touch "/tmp/digests/${digest#sha256:}"
|
104 |
+
|
105 |
+
- name: Upload digest
|
106 |
+
uses: actions/upload-artifact@v4
|
107 |
+
with:
|
108 |
+
name: digests-main-${{ env.PLATFORM_PAIR }}
|
109 |
+
path: /tmp/digests/*
|
110 |
+
if-no-files-found: error
|
111 |
+
retention-days: 1
|
112 |
+
|
113 |
+
build-cuda-image:
|
114 |
+
runs-on: ubuntu-latest
|
115 |
+
permissions:
|
116 |
+
contents: read
|
117 |
+
packages: write
|
118 |
+
strategy:
|
119 |
+
fail-fast: false
|
120 |
+
matrix:
|
121 |
+
platform:
|
122 |
+
- linux/amd64
|
123 |
+
- linux/arm64
|
124 |
+
|
125 |
+
steps:
|
126 |
+
# GitHub Packages requires the entire repository name to be in lowercase
|
127 |
+
# although the repository owner has a lowercase username, this prevents some people from running actions after forking
|
128 |
+
- name: Set repository and image name to lowercase
|
129 |
+
run: |
|
130 |
+
echo "IMAGE_NAME=${IMAGE_NAME,,}" >>${GITHUB_ENV}
|
131 |
+
echo "FULL_IMAGE_NAME=ghcr.io/${IMAGE_NAME,,}" >>${GITHUB_ENV}
|
132 |
+
env:
|
133 |
+
IMAGE_NAME: '${{ github.repository }}'
|
134 |
+
|
135 |
+
- name: Prepare
|
136 |
+
run: |
|
137 |
+
platform=${{ matrix.platform }}
|
138 |
+
echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV
|
139 |
+
|
140 |
+
- name: Checkout repository
|
141 |
+
uses: actions/checkout@v4
|
142 |
+
|
143 |
+
- name: Set up QEMU
|
144 |
+
uses: docker/setup-qemu-action@v3
|
145 |
+
|
146 |
+
- name: Set up Docker Buildx
|
147 |
+
uses: docker/setup-buildx-action@v3
|
148 |
+
|
149 |
+
- name: Log in to the Container registry
|
150 |
+
uses: docker/login-action@v3
|
151 |
+
with:
|
152 |
+
registry: ${{ env.REGISTRY }}
|
153 |
+
username: ${{ github.actor }}
|
154 |
+
password: ${{ secrets.GITHUB_TOKEN }}
|
155 |
+
|
156 |
+
- name: Extract metadata for Docker images (cuda tag)
|
157 |
+
id: meta
|
158 |
+
uses: docker/metadata-action@v5
|
159 |
+
with:
|
160 |
+
images: ${{ env.FULL_IMAGE_NAME }}
|
161 |
+
tags: |
|
162 |
+
type=ref,event=branch
|
163 |
+
type=ref,event=tag
|
164 |
+
type=sha,prefix=git-
|
165 |
+
type=semver,pattern={{version}}
|
166 |
+
type=semver,pattern={{major}}.{{minor}}
|
167 |
+
type=raw,enable=${{ github.ref == 'refs/heads/main' }},prefix=,suffix=,value=cuda
|
168 |
+
flavor: |
|
169 |
+
latest=${{ github.ref == 'refs/heads/main' }}
|
170 |
+
suffix=-cuda,onlatest=true
|
171 |
+
|
172 |
+
- name: Extract metadata for Docker cache
|
173 |
+
id: cache-meta
|
174 |
+
uses: docker/metadata-action@v5
|
175 |
+
with:
|
176 |
+
images: ${{ env.FULL_IMAGE_NAME }}
|
177 |
+
tags: |
|
178 |
+
type=ref,event=branch
|
179 |
+
${{ github.ref_type == 'tag' && 'type=raw,value=main' || '' }}
|
180 |
+
flavor: |
|
181 |
+
prefix=cache-cuda-${{ matrix.platform }}-
|
182 |
+
latest=false
|
183 |
+
|
184 |
+
- name: Build Docker image (cuda)
|
185 |
+
uses: docker/build-push-action@v5
|
186 |
+
id: build
|
187 |
+
with:
|
188 |
+
context: .
|
189 |
+
push: true
|
190 |
+
platforms: ${{ matrix.platform }}
|
191 |
+
labels: ${{ steps.meta.outputs.labels }}
|
192 |
+
outputs: type=image,name=${{ env.FULL_IMAGE_NAME }},push-by-digest=true,name-canonical=true,push=true
|
193 |
+
cache-from: type=registry,ref=${{ steps.cache-meta.outputs.tags }}
|
194 |
+
cache-to: type=registry,ref=${{ steps.cache-meta.outputs.tags }},mode=max
|
195 |
+
build-args: |
|
196 |
+
BUILD_HASH=${{ github.sha }}
|
197 |
+
USE_CUDA=true
|
198 |
+
|
199 |
+
- name: Export digest
|
200 |
+
run: |
|
201 |
+
mkdir -p /tmp/digests
|
202 |
+
digest="${{ steps.build.outputs.digest }}"
|
203 |
+
touch "/tmp/digests/${digest#sha256:}"
|
204 |
+
|
205 |
+
- name: Upload digest
|
206 |
+
uses: actions/upload-artifact@v4
|
207 |
+
with:
|
208 |
+
name: digests-cuda-${{ env.PLATFORM_PAIR }}
|
209 |
+
path: /tmp/digests/*
|
210 |
+
if-no-files-found: error
|
211 |
+
retention-days: 1
|
212 |
+
|
213 |
+
build-ollama-image:
|
214 |
+
runs-on: ubuntu-latest
|
215 |
+
permissions:
|
216 |
+
contents: read
|
217 |
+
packages: write
|
218 |
+
strategy:
|
219 |
+
fail-fast: false
|
220 |
+
matrix:
|
221 |
+
platform:
|
222 |
+
- linux/amd64
|
223 |
+
- linux/arm64
|
224 |
+
|
225 |
+
steps:
|
226 |
+
# GitHub Packages requires the entire repository name to be in lowercase
|
227 |
+
# although the repository owner has a lowercase username, this prevents some people from running actions after forking
|
228 |
+
- name: Set repository and image name to lowercase
|
229 |
+
run: |
|
230 |
+
echo "IMAGE_NAME=${IMAGE_NAME,,}" >>${GITHUB_ENV}
|
231 |
+
echo "FULL_IMAGE_NAME=ghcr.io/${IMAGE_NAME,,}" >>${GITHUB_ENV}
|
232 |
+
env:
|
233 |
+
IMAGE_NAME: '${{ github.repository }}'
|
234 |
+
|
235 |
+
- name: Prepare
|
236 |
+
run: |
|
237 |
+
platform=${{ matrix.platform }}
|
238 |
+
echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV
|
239 |
+
|
240 |
+
- name: Checkout repository
|
241 |
+
uses: actions/checkout@v4
|
242 |
+
|
243 |
+
- name: Set up QEMU
|
244 |
+
uses: docker/setup-qemu-action@v3
|
245 |
+
|
246 |
+
- name: Set up Docker Buildx
|
247 |
+
uses: docker/setup-buildx-action@v3
|
248 |
+
|
249 |
+
- name: Log in to the Container registry
|
250 |
+
uses: docker/login-action@v3
|
251 |
+
with:
|
252 |
+
registry: ${{ env.REGISTRY }}
|
253 |
+
username: ${{ github.actor }}
|
254 |
+
password: ${{ secrets.GITHUB_TOKEN }}
|
255 |
+
|
256 |
+
- name: Extract metadata for Docker images (ollama tag)
|
257 |
+
id: meta
|
258 |
+
uses: docker/metadata-action@v5
|
259 |
+
with:
|
260 |
+
images: ${{ env.FULL_IMAGE_NAME }}
|
261 |
+
tags: |
|
262 |
+
type=ref,event=branch
|
263 |
+
type=ref,event=tag
|
264 |
+
type=sha,prefix=git-
|
265 |
+
type=semver,pattern={{version}}
|
266 |
+
type=semver,pattern={{major}}.{{minor}}
|
267 |
+
type=raw,enable=${{ github.ref == 'refs/heads/main' }},prefix=,suffix=,value=ollama
|
268 |
+
flavor: |
|
269 |
+
latest=${{ github.ref == 'refs/heads/main' }}
|
270 |
+
suffix=-ollama,onlatest=true
|
271 |
+
|
272 |
+
- name: Extract metadata for Docker cache
|
273 |
+
id: cache-meta
|
274 |
+
uses: docker/metadata-action@v5
|
275 |
+
with:
|
276 |
+
images: ${{ env.FULL_IMAGE_NAME }}
|
277 |
+
tags: |
|
278 |
+
type=ref,event=branch
|
279 |
+
${{ github.ref_type == 'tag' && 'type=raw,value=main' || '' }}
|
280 |
+
flavor: |
|
281 |
+
prefix=cache-ollama-${{ matrix.platform }}-
|
282 |
+
latest=false
|
283 |
+
|
284 |
+
- name: Build Docker image (ollama)
|
285 |
+
uses: docker/build-push-action@v5
|
286 |
+
id: build
|
287 |
+
with:
|
288 |
+
context: .
|
289 |
+
push: true
|
290 |
+
platforms: ${{ matrix.platform }}
|
291 |
+
labels: ${{ steps.meta.outputs.labels }}
|
292 |
+
outputs: type=image,name=${{ env.FULL_IMAGE_NAME }},push-by-digest=true,name-canonical=true,push=true
|
293 |
+
cache-from: type=registry,ref=${{ steps.cache-meta.outputs.tags }}
|
294 |
+
cache-to: type=registry,ref=${{ steps.cache-meta.outputs.tags }},mode=max
|
295 |
+
build-args: |
|
296 |
+
BUILD_HASH=${{ github.sha }}
|
297 |
+
USE_OLLAMA=true
|
298 |
+
|
299 |
+
- name: Export digest
|
300 |
+
run: |
|
301 |
+
mkdir -p /tmp/digests
|
302 |
+
digest="${{ steps.build.outputs.digest }}"
|
303 |
+
touch "/tmp/digests/${digest#sha256:}"
|
304 |
+
|
305 |
+
- name: Upload digest
|
306 |
+
uses: actions/upload-artifact@v4
|
307 |
+
with:
|
308 |
+
name: digests-ollama-${{ env.PLATFORM_PAIR }}
|
309 |
+
path: /tmp/digests/*
|
310 |
+
if-no-files-found: error
|
311 |
+
retention-days: 1
|
312 |
+
|
313 |
+
merge-main-images:
|
314 |
+
runs-on: ubuntu-latest
|
315 |
+
needs: [ build-main-image ]
|
316 |
+
steps:
|
317 |
+
# GitHub Packages requires the entire repository name to be in lowercase
|
318 |
+
# although the repository owner has a lowercase username, this prevents some people from running actions after forking
|
319 |
+
- name: Set repository and image name to lowercase
|
320 |
+
run: |
|
321 |
+
echo "IMAGE_NAME=${IMAGE_NAME,,}" >>${GITHUB_ENV}
|
322 |
+
echo "FULL_IMAGE_NAME=ghcr.io/${IMAGE_NAME,,}" >>${GITHUB_ENV}
|
323 |
+
env:
|
324 |
+
IMAGE_NAME: '${{ github.repository }}'
|
325 |
+
|
326 |
+
- name: Download digests
|
327 |
+
uses: actions/download-artifact@v4
|
328 |
+
with:
|
329 |
+
pattern: digests-main-*
|
330 |
+
path: /tmp/digests
|
331 |
+
merge-multiple: true
|
332 |
+
|
333 |
+
- name: Set up Docker Buildx
|
334 |
+
uses: docker/setup-buildx-action@v3
|
335 |
+
|
336 |
+
- name: Log in to the Container registry
|
337 |
+
uses: docker/login-action@v3
|
338 |
+
with:
|
339 |
+
registry: ${{ env.REGISTRY }}
|
340 |
+
username: ${{ github.actor }}
|
341 |
+
password: ${{ secrets.GITHUB_TOKEN }}
|
342 |
+
|
343 |
+
- name: Extract metadata for Docker images (default latest tag)
|
344 |
+
id: meta
|
345 |
+
uses: docker/metadata-action@v5
|
346 |
+
with:
|
347 |
+
images: ${{ env.FULL_IMAGE_NAME }}
|
348 |
+
tags: |
|
349 |
+
type=ref,event=branch
|
350 |
+
type=ref,event=tag
|
351 |
+
type=sha,prefix=git-
|
352 |
+
type=semver,pattern={{version}}
|
353 |
+
type=semver,pattern={{major}}.{{minor}}
|
354 |
+
flavor: |
|
355 |
+
latest=${{ github.ref == 'refs/heads/main' }}
|
356 |
+
|
357 |
+
- name: Create manifest list and push
|
358 |
+
working-directory: /tmp/digests
|
359 |
+
run: |
|
360 |
+
docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
|
361 |
+
$(printf '${{ env.FULL_IMAGE_NAME }}@sha256:%s ' *)
|
362 |
+
|
363 |
+
- name: Inspect image
|
364 |
+
run: |
|
365 |
+
docker buildx imagetools inspect ${{ env.FULL_IMAGE_NAME }}:${{ steps.meta.outputs.version }}
|
366 |
+
|
367 |
+
|
368 |
+
merge-cuda-images:
|
369 |
+
runs-on: ubuntu-latest
|
370 |
+
needs: [ build-cuda-image ]
|
371 |
+
steps:
|
372 |
+
# GitHub Packages requires the entire repository name to be in lowercase
|
373 |
+
# although the repository owner has a lowercase username, this prevents some people from running actions after forking
|
374 |
+
- name: Set repository and image name to lowercase
|
375 |
+
run: |
|
376 |
+
echo "IMAGE_NAME=${IMAGE_NAME,,}" >>${GITHUB_ENV}
|
377 |
+
echo "FULL_IMAGE_NAME=ghcr.io/${IMAGE_NAME,,}" >>${GITHUB_ENV}
|
378 |
+
env:
|
379 |
+
IMAGE_NAME: '${{ github.repository }}'
|
380 |
+
|
381 |
+
- name: Download digests
|
382 |
+
uses: actions/download-artifact@v4
|
383 |
+
with:
|
384 |
+
pattern: digests-cuda-*
|
385 |
+
path: /tmp/digests
|
386 |
+
merge-multiple: true
|
387 |
+
|
388 |
+
- name: Set up Docker Buildx
|
389 |
+
uses: docker/setup-buildx-action@v3
|
390 |
+
|
391 |
+
- name: Log in to the Container registry
|
392 |
+
uses: docker/login-action@v3
|
393 |
+
with:
|
394 |
+
registry: ${{ env.REGISTRY }}
|
395 |
+
username: ${{ github.actor }}
|
396 |
+
password: ${{ secrets.GITHUB_TOKEN }}
|
397 |
+
|
398 |
+
- name: Extract metadata for Docker images (default latest tag)
|
399 |
+
id: meta
|
400 |
+
uses: docker/metadata-action@v5
|
401 |
+
with:
|
402 |
+
images: ${{ env.FULL_IMAGE_NAME }}
|
403 |
+
tags: |
|
404 |
+
type=ref,event=branch
|
405 |
+
type=ref,event=tag
|
406 |
+
type=sha,prefix=git-
|
407 |
+
type=semver,pattern={{version}}
|
408 |
+
type=semver,pattern={{major}}.{{minor}}
|
409 |
+
type=raw,enable=${{ github.ref == 'refs/heads/main' }},prefix=,suffix=,value=cuda
|
410 |
+
flavor: |
|
411 |
+
latest=${{ github.ref == 'refs/heads/main' }}
|
412 |
+
suffix=-cuda,onlatest=true
|
413 |
+
|
414 |
+
- name: Create manifest list and push
|
415 |
+
working-directory: /tmp/digests
|
416 |
+
run: |
|
417 |
+
docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
|
418 |
+
$(printf '${{ env.FULL_IMAGE_NAME }}@sha256:%s ' *)
|
419 |
+
|
420 |
+
- name: Inspect image
|
421 |
+
run: |
|
422 |
+
docker buildx imagetools inspect ${{ env.FULL_IMAGE_NAME }}:${{ steps.meta.outputs.version }}
|
423 |
+
|
424 |
+
merge-ollama-images:
|
425 |
+
runs-on: ubuntu-latest
|
426 |
+
needs: [ build-ollama-image ]
|
427 |
+
steps:
|
428 |
+
# GitHub Packages requires the entire repository name to be in lowercase
|
429 |
+
# although the repository owner has a lowercase username, this prevents some people from running actions after forking
|
430 |
+
- name: Set repository and image name to lowercase
|
431 |
+
run: |
|
432 |
+
echo "IMAGE_NAME=${IMAGE_NAME,,}" >>${GITHUB_ENV}
|
433 |
+
echo "FULL_IMAGE_NAME=ghcr.io/${IMAGE_NAME,,}" >>${GITHUB_ENV}
|
434 |
+
env:
|
435 |
+
IMAGE_NAME: '${{ github.repository }}'
|
436 |
+
|
437 |
+
- name: Download digests
|
438 |
+
uses: actions/download-artifact@v4
|
439 |
+
with:
|
440 |
+
pattern: digests-ollama-*
|
441 |
+
path: /tmp/digests
|
442 |
+
merge-multiple: true
|
443 |
+
|
444 |
+
- name: Set up Docker Buildx
|
445 |
+
uses: docker/setup-buildx-action@v3
|
446 |
+
|
447 |
+
- name: Log in to the Container registry
|
448 |
+
uses: docker/login-action@v3
|
449 |
+
with:
|
450 |
+
registry: ${{ env.REGISTRY }}
|
451 |
+
username: ${{ github.actor }}
|
452 |
+
password: ${{ secrets.GITHUB_TOKEN }}
|
453 |
+
|
454 |
+
- name: Extract metadata for Docker images (default ollama tag)
|
455 |
+
id: meta
|
456 |
+
uses: docker/metadata-action@v5
|
457 |
+
with:
|
458 |
+
images: ${{ env.FULL_IMAGE_NAME }}
|
459 |
+
tags: |
|
460 |
+
type=ref,event=branch
|
461 |
+
type=ref,event=tag
|
462 |
+
type=sha,prefix=git-
|
463 |
+
type=semver,pattern={{version}}
|
464 |
+
type=semver,pattern={{major}}.{{minor}}
|
465 |
+
type=raw,enable=${{ github.ref == 'refs/heads/main' }},prefix=,suffix=,value=ollama
|
466 |
+
flavor: |
|
467 |
+
latest=${{ github.ref == 'refs/heads/main' }}
|
468 |
+
suffix=-ollama,onlatest=true
|
469 |
+
|
470 |
+
- name: Create manifest list and push
|
471 |
+
working-directory: /tmp/digests
|
472 |
+
run: |
|
473 |
+
docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
|
474 |
+
$(printf '${{ env.FULL_IMAGE_NAME }}@sha256:%s ' *)
|
475 |
+
|
476 |
+
- name: Inspect image
|
477 |
+
run: |
|
478 |
+
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,243 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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: Delete Docker build cache
|
39 |
+
run: |
|
40 |
+
docker builder prune --all --force
|
41 |
+
|
42 |
+
- name: Preload Ollama model
|
43 |
+
run: |
|
44 |
+
docker exec ollama ollama pull qwen:0.5b-chat-v1.5-q2_K
|
45 |
+
|
46 |
+
- name: Cypress run
|
47 |
+
uses: cypress-io/github-action@v6
|
48 |
+
with:
|
49 |
+
browser: chrome
|
50 |
+
wait-on: 'http://localhost:3000'
|
51 |
+
config: baseUrl=http://localhost:3000
|
52 |
+
|
53 |
+
- uses: actions/upload-artifact@v4
|
54 |
+
if: always()
|
55 |
+
name: Upload Cypress videos
|
56 |
+
with:
|
57 |
+
name: cypress-videos
|
58 |
+
path: cypress/videos
|
59 |
+
if-no-files-found: ignore
|
60 |
+
|
61 |
+
- name: Extract Compose logs
|
62 |
+
if: always()
|
63 |
+
run: |
|
64 |
+
docker compose logs > compose-logs.txt
|
65 |
+
|
66 |
+
- uses: actions/upload-artifact@v4
|
67 |
+
if: always()
|
68 |
+
name: Upload Compose logs
|
69 |
+
with:
|
70 |
+
name: compose-logs
|
71 |
+
path: compose-logs.txt
|
72 |
+
if-no-files-found: ignore
|
73 |
+
|
74 |
+
# pytest:
|
75 |
+
# name: Run Backend Tests
|
76 |
+
# runs-on: ubuntu-latest
|
77 |
+
# steps:
|
78 |
+
# - uses: actions/checkout@v4
|
79 |
+
|
80 |
+
# - name: Set up Python
|
81 |
+
# uses: actions/setup-python@v4
|
82 |
+
# with:
|
83 |
+
# python-version: ${{ matrix.python-version }}
|
84 |
+
|
85 |
+
# - name: Install dependencies
|
86 |
+
# run: |
|
87 |
+
# python -m pip install --upgrade pip
|
88 |
+
# pip install -r backend/requirements.txt
|
89 |
+
|
90 |
+
# - name: pytest run
|
91 |
+
# run: |
|
92 |
+
# ls -al
|
93 |
+
# cd backend
|
94 |
+
# PYTHONPATH=. pytest . -o log_cli=true -o log_cli_level=INFO
|
95 |
+
|
96 |
+
migration_test:
|
97 |
+
name: Run Migration Tests
|
98 |
+
runs-on: ubuntu-latest
|
99 |
+
services:
|
100 |
+
postgres:
|
101 |
+
image: postgres
|
102 |
+
env:
|
103 |
+
POSTGRES_PASSWORD: postgres
|
104 |
+
options: >-
|
105 |
+
--health-cmd pg_isready
|
106 |
+
--health-interval 10s
|
107 |
+
--health-timeout 5s
|
108 |
+
--health-retries 5
|
109 |
+
ports:
|
110 |
+
- 5432:5432
|
111 |
+
# mysql:
|
112 |
+
# image: mysql
|
113 |
+
# env:
|
114 |
+
# MYSQL_ROOT_PASSWORD: mysql
|
115 |
+
# MYSQL_DATABASE: mysql
|
116 |
+
# options: >-
|
117 |
+
# --health-cmd "mysqladmin ping -h localhost"
|
118 |
+
# --health-interval 10s
|
119 |
+
# --health-timeout 5s
|
120 |
+
# --health-retries 5
|
121 |
+
# ports:
|
122 |
+
# - 3306:3306
|
123 |
+
steps:
|
124 |
+
- name: Checkout Repository
|
125 |
+
uses: actions/checkout@v4
|
126 |
+
|
127 |
+
- name: Set up Python
|
128 |
+
uses: actions/setup-python@v5
|
129 |
+
with:
|
130 |
+
python-version: ${{ matrix.python-version }}
|
131 |
+
|
132 |
+
- name: Set up uv
|
133 |
+
uses: yezz123/setup-uv@v4
|
134 |
+
with:
|
135 |
+
uv-venv: venv
|
136 |
+
|
137 |
+
- name: Activate virtualenv
|
138 |
+
run: |
|
139 |
+
. venv/bin/activate
|
140 |
+
echo PATH=$PATH >> $GITHUB_ENV
|
141 |
+
|
142 |
+
- name: Install dependencies
|
143 |
+
run: |
|
144 |
+
uv pip install -r backend/requirements.txt
|
145 |
+
|
146 |
+
- name: Test backend with SQLite
|
147 |
+
id: sqlite
|
148 |
+
env:
|
149 |
+
WEBUI_SECRET_KEY: secret-key
|
150 |
+
GLOBAL_LOG_LEVEL: debug
|
151 |
+
run: |
|
152 |
+
cd backend
|
153 |
+
uvicorn main:app --port "8080" --forwarded-allow-ips '*' &
|
154 |
+
UVICORN_PID=$!
|
155 |
+
# Wait up to 40 seconds for the server to start
|
156 |
+
for i in {1..40}; do
|
157 |
+
curl -s http://localhost:8080/api/config > /dev/null && break
|
158 |
+
sleep 1
|
159 |
+
if [ $i -eq 40 ]; then
|
160 |
+
echo "Server failed to start"
|
161 |
+
kill -9 $UVICORN_PID
|
162 |
+
exit 1
|
163 |
+
fi
|
164 |
+
done
|
165 |
+
# Check that the server is still running after 5 seconds
|
166 |
+
sleep 5
|
167 |
+
if ! kill -0 $UVICORN_PID; then
|
168 |
+
echo "Server has stopped"
|
169 |
+
exit 1
|
170 |
+
fi
|
171 |
+
|
172 |
+
- name: Test backend with Postgres
|
173 |
+
if: success() || steps.sqlite.conclusion == 'failure'
|
174 |
+
env:
|
175 |
+
WEBUI_SECRET_KEY: secret-key
|
176 |
+
GLOBAL_LOG_LEVEL: debug
|
177 |
+
DATABASE_URL: postgresql://postgres:postgres@localhost:5432/postgres
|
178 |
+
run: |
|
179 |
+
cd backend
|
180 |
+
uvicorn main:app --port "8081" --forwarded-allow-ips '*' &
|
181 |
+
UVICORN_PID=$!
|
182 |
+
# Wait up to 20 seconds for the server to start
|
183 |
+
for i in {1..20}; do
|
184 |
+
curl -s http://localhost:8081/api/config > /dev/null && break
|
185 |
+
sleep 1
|
186 |
+
if [ $i -eq 20 ]; then
|
187 |
+
echo "Server failed to start"
|
188 |
+
kill -9 $UVICORN_PID
|
189 |
+
exit 1
|
190 |
+
fi
|
191 |
+
done
|
192 |
+
# Check that the server is still running after 5 seconds
|
193 |
+
sleep 5
|
194 |
+
if ! kill -0 $UVICORN_PID; then
|
195 |
+
echo "Server has stopped"
|
196 |
+
exit 1
|
197 |
+
fi
|
198 |
+
|
199 |
+
# Check that service will reconnect to postgres when connection will be closed
|
200 |
+
status_code=$(curl --write-out %{http_code} -s --output /dev/null http://localhost:8081/health/db)
|
201 |
+
if [[ "$status_code" -ne 200 ]] ; then
|
202 |
+
echo "Server has failed before postgres reconnect check"
|
203 |
+
exit 1
|
204 |
+
fi
|
205 |
+
|
206 |
+
echo "Terminating all connections to postgres..."
|
207 |
+
python -c "import os, psycopg2 as pg2; \
|
208 |
+
conn = pg2.connect(dsn=os.environ['DATABASE_URL'].replace('+pool', '')); \
|
209 |
+
cur = conn.cursor(); \
|
210 |
+
cur.execute('SELECT pg_terminate_backend(psa.pid) FROM pg_stat_activity psa WHERE datname = current_database() AND pid <> pg_backend_pid();')"
|
211 |
+
|
212 |
+
status_code=$(curl --write-out %{http_code} -s --output /dev/null http://localhost:8081/health/db)
|
213 |
+
if [[ "$status_code" -ne 200 ]] ; then
|
214 |
+
echo "Server has not reconnected to postgres after connection was closed: returned status $status_code"
|
215 |
+
exit 1
|
216 |
+
fi
|
217 |
+
|
218 |
+
# - name: Test backend with MySQL
|
219 |
+
# if: success() || steps.sqlite.conclusion == 'failure' || steps.postgres.conclusion == 'failure'
|
220 |
+
# env:
|
221 |
+
# WEBUI_SECRET_KEY: secret-key
|
222 |
+
# GLOBAL_LOG_LEVEL: debug
|
223 |
+
# DATABASE_URL: mysql://root:mysql@localhost:3306/mysql
|
224 |
+
# run: |
|
225 |
+
# cd backend
|
226 |
+
# uvicorn main:app --port "8083" --forwarded-allow-ips '*' &
|
227 |
+
# UVICORN_PID=$!
|
228 |
+
# # Wait up to 20 seconds for the server to start
|
229 |
+
# for i in {1..20}; do
|
230 |
+
# curl -s http://localhost:8083/api/config > /dev/null && break
|
231 |
+
# sleep 1
|
232 |
+
# if [ $i -eq 20 ]; then
|
233 |
+
# echo "Server failed to start"
|
234 |
+
# kill -9 $UVICORN_PID
|
235 |
+
# exit 1
|
236 |
+
# fi
|
237 |
+
# done
|
238 |
+
# # Check that the server is still running after 5 seconds
|
239 |
+
# sleep 5
|
240 |
+
# if ! kill -0 $UVICORN_PID; then
|
241 |
+
# echo "Server has stopped"
|
242 |
+
# exit 1
|
243 |
+
# 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,31 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
name: Release to PyPI
|
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 |
+
environment:
|
12 |
+
name: pypi
|
13 |
+
url: https://pypi.org/p/open-webui
|
14 |
+
permissions:
|
15 |
+
id-token: write
|
16 |
+
steps:
|
17 |
+
- name: Checkout repository
|
18 |
+
uses: actions/checkout@v4
|
19 |
+
- uses: actions/setup-node@v4
|
20 |
+
with:
|
21 |
+
node-version: 18
|
22 |
+
- uses: actions/setup-python@v5
|
23 |
+
with:
|
24 |
+
python-version: 3.11
|
25 |
+
- name: Build
|
26 |
+
run: |
|
27 |
+
python -m pip install --upgrade pip
|
28 |
+
pip install build
|
29 |
+
python -m build .
|
30 |
+
- name: Publish package distributions to PyPI
|
31 |
+
uses: pypa/gh-action-pypi-publish@release/v1
|
.gitignore
ADDED
@@ -0,0 +1,309 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
.DS_Store
|
2 |
+
node_modules
|
3 |
+
/build
|
4 |
+
/.svelte-kit
|
5 |
+
/package
|
6 |
+
.env
|
7 |
+
.env.*
|
8 |
+
!.env.example
|
9 |
+
vite.config.js.timestamp-*
|
10 |
+
vite.config.ts.timestamp-*
|
11 |
+
# Byte-compiled / optimized / DLL files
|
12 |
+
__pycache__/
|
13 |
+
*.py[cod]
|
14 |
+
*$py.class
|
15 |
+
|
16 |
+
# C extensions
|
17 |
+
*.so
|
18 |
+
|
19 |
+
# Pyodide distribution
|
20 |
+
static/pyodide/*
|
21 |
+
!static/pyodide/pyodide-lock.json
|
22 |
+
|
23 |
+
# Distribution / packaging
|
24 |
+
.Python
|
25 |
+
build/
|
26 |
+
develop-eggs/
|
27 |
+
dist/
|
28 |
+
downloads/
|
29 |
+
eggs/
|
30 |
+
.eggs/
|
31 |
+
lib64/
|
32 |
+
parts/
|
33 |
+
sdist/
|
34 |
+
var/
|
35 |
+
wheels/
|
36 |
+
share/python-wheels/
|
37 |
+
*.egg-info/
|
38 |
+
.installed.cfg
|
39 |
+
*.egg
|
40 |
+
MANIFEST
|
41 |
+
|
42 |
+
# PyInstaller
|
43 |
+
# Usually these files are written by a python script from a template
|
44 |
+
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
45 |
+
*.manifest
|
46 |
+
*.spec
|
47 |
+
|
48 |
+
# Installer logs
|
49 |
+
pip-log.txt
|
50 |
+
pip-delete-this-directory.txt
|
51 |
+
|
52 |
+
# Unit test / coverage reports
|
53 |
+
htmlcov/
|
54 |
+
.tox/
|
55 |
+
.nox/
|
56 |
+
.coverage
|
57 |
+
.coverage.*
|
58 |
+
.cache
|
59 |
+
nosetests.xml
|
60 |
+
coverage.xml
|
61 |
+
*.cover
|
62 |
+
*.py,cover
|
63 |
+
.hypothesis/
|
64 |
+
.pytest_cache/
|
65 |
+
cover/
|
66 |
+
|
67 |
+
# Translations
|
68 |
+
*.mo
|
69 |
+
*.pot
|
70 |
+
|
71 |
+
# Django stuff:
|
72 |
+
*.log
|
73 |
+
local_settings.py
|
74 |
+
db.sqlite3
|
75 |
+
db.sqlite3-journal
|
76 |
+
|
77 |
+
# Flask stuff:
|
78 |
+
instance/
|
79 |
+
.webassets-cache
|
80 |
+
|
81 |
+
# Scrapy stuff:
|
82 |
+
.scrapy
|
83 |
+
|
84 |
+
# Sphinx documentation
|
85 |
+
docs/_build/
|
86 |
+
|
87 |
+
# PyBuilder
|
88 |
+
.pybuilder/
|
89 |
+
target/
|
90 |
+
|
91 |
+
# Jupyter Notebook
|
92 |
+
.ipynb_checkpoints
|
93 |
+
|
94 |
+
# IPython
|
95 |
+
profile_default/
|
96 |
+
ipython_config.py
|
97 |
+
|
98 |
+
# pyenv
|
99 |
+
# For a library or package, you might want to ignore these files since the code is
|
100 |
+
# intended to run in multiple environments; otherwise, check them in:
|
101 |
+
# .python-version
|
102 |
+
|
103 |
+
# pipenv
|
104 |
+
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
105 |
+
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
106 |
+
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
107 |
+
# install all needed dependencies.
|
108 |
+
#Pipfile.lock
|
109 |
+
|
110 |
+
# poetry
|
111 |
+
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
112 |
+
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
113 |
+
# commonly ignored for libraries.
|
114 |
+
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
115 |
+
#poetry.lock
|
116 |
+
|
117 |
+
# pdm
|
118 |
+
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
119 |
+
#pdm.lock
|
120 |
+
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
|
121 |
+
# in version control.
|
122 |
+
# https://pdm.fming.dev/#use-with-ide
|
123 |
+
.pdm.toml
|
124 |
+
|
125 |
+
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
126 |
+
__pypackages__/
|
127 |
+
|
128 |
+
# Celery stuff
|
129 |
+
celerybeat-schedule
|
130 |
+
celerybeat.pid
|
131 |
+
|
132 |
+
# SageMath parsed files
|
133 |
+
*.sage.py
|
134 |
+
|
135 |
+
# Environments
|
136 |
+
.env
|
137 |
+
.venv
|
138 |
+
env/
|
139 |
+
venv/
|
140 |
+
ENV/
|
141 |
+
env.bak/
|
142 |
+
venv.bak/
|
143 |
+
|
144 |
+
# Spyder project settings
|
145 |
+
.spyderproject
|
146 |
+
.spyproject
|
147 |
+
|
148 |
+
# Rope project settings
|
149 |
+
.ropeproject
|
150 |
+
|
151 |
+
# mkdocs documentation
|
152 |
+
/site
|
153 |
+
|
154 |
+
# mypy
|
155 |
+
.mypy_cache/
|
156 |
+
.dmypy.json
|
157 |
+
dmypy.json
|
158 |
+
|
159 |
+
# Pyre type checker
|
160 |
+
.pyre/
|
161 |
+
|
162 |
+
# pytype static type analyzer
|
163 |
+
.pytype/
|
164 |
+
|
165 |
+
# Cython debug symbols
|
166 |
+
cython_debug/
|
167 |
+
|
168 |
+
# PyCharm
|
169 |
+
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
170 |
+
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
171 |
+
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
172 |
+
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
173 |
+
.idea/
|
174 |
+
|
175 |
+
# Logs
|
176 |
+
logs
|
177 |
+
*.log
|
178 |
+
npm-debug.log*
|
179 |
+
yarn-debug.log*
|
180 |
+
yarn-error.log*
|
181 |
+
lerna-debug.log*
|
182 |
+
.pnpm-debug.log*
|
183 |
+
|
184 |
+
# Diagnostic reports (https://nodejs.org/api/report.html)
|
185 |
+
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
186 |
+
|
187 |
+
# Runtime data
|
188 |
+
pids
|
189 |
+
*.pid
|
190 |
+
*.seed
|
191 |
+
*.pid.lock
|
192 |
+
|
193 |
+
# Directory for instrumented libs generated by jscoverage/JSCover
|
194 |
+
lib-cov
|
195 |
+
|
196 |
+
# Coverage directory used by tools like istanbul
|
197 |
+
coverage
|
198 |
+
*.lcov
|
199 |
+
|
200 |
+
# nyc test coverage
|
201 |
+
.nyc_output
|
202 |
+
|
203 |
+
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
|
204 |
+
.grunt
|
205 |
+
|
206 |
+
# Bower dependency directory (https://bower.io/)
|
207 |
+
bower_components
|
208 |
+
|
209 |
+
# node-waf configuration
|
210 |
+
.lock-wscript
|
211 |
+
|
212 |
+
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
213 |
+
build/Release
|
214 |
+
|
215 |
+
# Dependency directories
|
216 |
+
node_modules/
|
217 |
+
jspm_packages/
|
218 |
+
|
219 |
+
# Snowpack dependency directory (https://snowpack.dev/)
|
220 |
+
web_modules/
|
221 |
+
|
222 |
+
# TypeScript cache
|
223 |
+
*.tsbuildinfo
|
224 |
+
|
225 |
+
# Optional npm cache directory
|
226 |
+
.npm
|
227 |
+
|
228 |
+
# Optional eslint cache
|
229 |
+
.eslintcache
|
230 |
+
|
231 |
+
# Optional stylelint cache
|
232 |
+
.stylelintcache
|
233 |
+
|
234 |
+
# Microbundle cache
|
235 |
+
.rpt2_cache/
|
236 |
+
.rts2_cache_cjs/
|
237 |
+
.rts2_cache_es/
|
238 |
+
.rts2_cache_umd/
|
239 |
+
|
240 |
+
# Optional REPL history
|
241 |
+
.node_repl_history
|
242 |
+
|
243 |
+
# Output of 'npm pack'
|
244 |
+
*.tgz
|
245 |
+
|
246 |
+
# Yarn Integrity file
|
247 |
+
.yarn-integrity
|
248 |
+
|
249 |
+
# dotenv environment variable files
|
250 |
+
.env
|
251 |
+
.env.development.local
|
252 |
+
.env.test.local
|
253 |
+
.env.production.local
|
254 |
+
.env.local
|
255 |
+
|
256 |
+
# parcel-bundler cache (https://parceljs.org/)
|
257 |
+
.cache
|
258 |
+
.parcel-cache
|
259 |
+
|
260 |
+
# Next.js build output
|
261 |
+
.next
|
262 |
+
out
|
263 |
+
|
264 |
+
# Nuxt.js build / generate output
|
265 |
+
.nuxt
|
266 |
+
dist
|
267 |
+
|
268 |
+
# Gatsby files
|
269 |
+
.cache/
|
270 |
+
# Comment in the public line in if your project uses Gatsby and not Next.js
|
271 |
+
# https://nextjs.org/blog/next-9-1#public-directory-support
|
272 |
+
# public
|
273 |
+
|
274 |
+
# vuepress build output
|
275 |
+
.vuepress/dist
|
276 |
+
|
277 |
+
# vuepress v2.x temp and cache directory
|
278 |
+
.temp
|
279 |
+
.cache
|
280 |
+
|
281 |
+
# Docusaurus cache and generated files
|
282 |
+
.docusaurus
|
283 |
+
|
284 |
+
# Serverless directories
|
285 |
+
.serverless/
|
286 |
+
|
287 |
+
# FuseBox cache
|
288 |
+
.fusebox/
|
289 |
+
|
290 |
+
# DynamoDB Local files
|
291 |
+
.dynamodb/
|
292 |
+
|
293 |
+
# TernJS port file
|
294 |
+
.tern-port
|
295 |
+
|
296 |
+
# Stores VSCode versions used for testing VSCode extensions
|
297 |
+
.vscode-test
|
298 |
+
|
299 |
+
# yarn v2
|
300 |
+
.yarn/cache
|
301 |
+
.yarn/unplugged
|
302 |
+
.yarn/build-state.yml
|
303 |
+
.yarn/install-state.gz
|
304 |
+
.pnp.*
|
305 |
+
|
306 |
+
# cypress artifacts
|
307 |
+
cypress/videos
|
308 |
+
cypress/screenshots
|
309 |
+
.vscode/settings.json
|
.npmrc
ADDED
@@ -0,0 +1 @@
|
|
|
|
|
1 |
+
engine-strict=true
|
.prettierignore
ADDED
@@ -0,0 +1,316 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Ignore files for PNPM, NPM and YARN
|
2 |
+
pnpm-lock.yaml
|
3 |
+
package-lock.json
|
4 |
+
yarn.lock
|
5 |
+
|
6 |
+
kubernetes/
|
7 |
+
|
8 |
+
# Copy of .gitignore
|
9 |
+
.DS_Store
|
10 |
+
node_modules
|
11 |
+
/build
|
12 |
+
/.svelte-kit
|
13 |
+
/package
|
14 |
+
.env
|
15 |
+
.env.*
|
16 |
+
!.env.example
|
17 |
+
vite.config.js.timestamp-*
|
18 |
+
vite.config.ts.timestamp-*
|
19 |
+
# Byte-compiled / optimized / DLL files
|
20 |
+
__pycache__/
|
21 |
+
*.py[cod]
|
22 |
+
*$py.class
|
23 |
+
|
24 |
+
# C extensions
|
25 |
+
*.so
|
26 |
+
|
27 |
+
# Distribution / packaging
|
28 |
+
.Python
|
29 |
+
build/
|
30 |
+
develop-eggs/
|
31 |
+
dist/
|
32 |
+
downloads/
|
33 |
+
eggs/
|
34 |
+
.eggs/
|
35 |
+
lib64/
|
36 |
+
parts/
|
37 |
+
sdist/
|
38 |
+
var/
|
39 |
+
wheels/
|
40 |
+
share/python-wheels/
|
41 |
+
*.egg-info/
|
42 |
+
.installed.cfg
|
43 |
+
*.egg
|
44 |
+
MANIFEST
|
45 |
+
|
46 |
+
# PyInstaller
|
47 |
+
# Usually these files are written by a python script from a template
|
48 |
+
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
49 |
+
*.manifest
|
50 |
+
*.spec
|
51 |
+
|
52 |
+
# Installer logs
|
53 |
+
pip-log.txt
|
54 |
+
pip-delete-this-directory.txt
|
55 |
+
|
56 |
+
# Unit test / coverage reports
|
57 |
+
htmlcov/
|
58 |
+
.tox/
|
59 |
+
.nox/
|
60 |
+
.coverage
|
61 |
+
.coverage.*
|
62 |
+
.cache
|
63 |
+
nosetests.xml
|
64 |
+
coverage.xml
|
65 |
+
*.cover
|
66 |
+
*.py,cover
|
67 |
+
.hypothesis/
|
68 |
+
.pytest_cache/
|
69 |
+
cover/
|
70 |
+
|
71 |
+
# Translations
|
72 |
+
*.mo
|
73 |
+
*.pot
|
74 |
+
|
75 |
+
# Django stuff:
|
76 |
+
*.log
|
77 |
+
local_settings.py
|
78 |
+
db.sqlite3
|
79 |
+
db.sqlite3-journal
|
80 |
+
|
81 |
+
# Flask stuff:
|
82 |
+
instance/
|
83 |
+
.webassets-cache
|
84 |
+
|
85 |
+
# Scrapy stuff:
|
86 |
+
.scrapy
|
87 |
+
|
88 |
+
# Sphinx documentation
|
89 |
+
docs/_build/
|
90 |
+
|
91 |
+
# PyBuilder
|
92 |
+
.pybuilder/
|
93 |
+
target/
|
94 |
+
|
95 |
+
# Jupyter Notebook
|
96 |
+
.ipynb_checkpoints
|
97 |
+
|
98 |
+
# IPython
|
99 |
+
profile_default/
|
100 |
+
ipython_config.py
|
101 |
+
|
102 |
+
# pyenv
|
103 |
+
# For a library or package, you might want to ignore these files since the code is
|
104 |
+
# intended to run in multiple environments; otherwise, check them in:
|
105 |
+
# .python-version
|
106 |
+
|
107 |
+
# pipenv
|
108 |
+
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
109 |
+
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
110 |
+
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
111 |
+
# install all needed dependencies.
|
112 |
+
#Pipfile.lock
|
113 |
+
|
114 |
+
# poetry
|
115 |
+
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
116 |
+
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
117 |
+
# commonly ignored for libraries.
|
118 |
+
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
119 |
+
#poetry.lock
|
120 |
+
|
121 |
+
# pdm
|
122 |
+
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
123 |
+
#pdm.lock
|
124 |
+
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
|
125 |
+
# in version control.
|
126 |
+
# https://pdm.fming.dev/#use-with-ide
|
127 |
+
.pdm.toml
|
128 |
+
|
129 |
+
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
130 |
+
__pypackages__/
|
131 |
+
|
132 |
+
# Celery stuff
|
133 |
+
celerybeat-schedule
|
134 |
+
celerybeat.pid
|
135 |
+
|
136 |
+
# SageMath parsed files
|
137 |
+
*.sage.py
|
138 |
+
|
139 |
+
# Environments
|
140 |
+
.env
|
141 |
+
.venv
|
142 |
+
env/
|
143 |
+
venv/
|
144 |
+
ENV/
|
145 |
+
env.bak/
|
146 |
+
venv.bak/
|
147 |
+
|
148 |
+
# Spyder project settings
|
149 |
+
.spyderproject
|
150 |
+
.spyproject
|
151 |
+
|
152 |
+
# Rope project settings
|
153 |
+
.ropeproject
|
154 |
+
|
155 |
+
# mkdocs documentation
|
156 |
+
/site
|
157 |
+
|
158 |
+
# mypy
|
159 |
+
.mypy_cache/
|
160 |
+
.dmypy.json
|
161 |
+
dmypy.json
|
162 |
+
|
163 |
+
# Pyre type checker
|
164 |
+
.pyre/
|
165 |
+
|
166 |
+
# pytype static type analyzer
|
167 |
+
.pytype/
|
168 |
+
|
169 |
+
# Cython debug symbols
|
170 |
+
cython_debug/
|
171 |
+
|
172 |
+
# PyCharm
|
173 |
+
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
174 |
+
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
175 |
+
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
176 |
+
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
177 |
+
.idea/
|
178 |
+
|
179 |
+
# Logs
|
180 |
+
logs
|
181 |
+
*.log
|
182 |
+
npm-debug.log*
|
183 |
+
yarn-debug.log*
|
184 |
+
yarn-error.log*
|
185 |
+
lerna-debug.log*
|
186 |
+
.pnpm-debug.log*
|
187 |
+
|
188 |
+
# Diagnostic reports (https://nodejs.org/api/report.html)
|
189 |
+
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
190 |
+
|
191 |
+
# Runtime data
|
192 |
+
pids
|
193 |
+
*.pid
|
194 |
+
*.seed
|
195 |
+
*.pid.lock
|
196 |
+
|
197 |
+
# Directory for instrumented libs generated by jscoverage/JSCover
|
198 |
+
lib-cov
|
199 |
+
|
200 |
+
# Coverage directory used by tools like istanbul
|
201 |
+
coverage
|
202 |
+
*.lcov
|
203 |
+
|
204 |
+
# nyc test coverage
|
205 |
+
.nyc_output
|
206 |
+
|
207 |
+
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
|
208 |
+
.grunt
|
209 |
+
|
210 |
+
# Bower dependency directory (https://bower.io/)
|
211 |
+
bower_components
|
212 |
+
|
213 |
+
# node-waf configuration
|
214 |
+
.lock-wscript
|
215 |
+
|
216 |
+
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
217 |
+
build/Release
|
218 |
+
|
219 |
+
# Dependency directories
|
220 |
+
node_modules/
|
221 |
+
jspm_packages/
|
222 |
+
|
223 |
+
# Snowpack dependency directory (https://snowpack.dev/)
|
224 |
+
web_modules/
|
225 |
+
|
226 |
+
# TypeScript cache
|
227 |
+
*.tsbuildinfo
|
228 |
+
|
229 |
+
# Optional npm cache directory
|
230 |
+
.npm
|
231 |
+
|
232 |
+
# Optional eslint cache
|
233 |
+
.eslintcache
|
234 |
+
|
235 |
+
# Optional stylelint cache
|
236 |
+
.stylelintcache
|
237 |
+
|
238 |
+
# Microbundle cache
|
239 |
+
.rpt2_cache/
|
240 |
+
.rts2_cache_cjs/
|
241 |
+
.rts2_cache_es/
|
242 |
+
.rts2_cache_umd/
|
243 |
+
|
244 |
+
# Optional REPL history
|
245 |
+
.node_repl_history
|
246 |
+
|
247 |
+
# Output of 'npm pack'
|
248 |
+
*.tgz
|
249 |
+
|
250 |
+
# Yarn Integrity file
|
251 |
+
.yarn-integrity
|
252 |
+
|
253 |
+
# dotenv environment variable files
|
254 |
+
.env
|
255 |
+
.env.development.local
|
256 |
+
.env.test.local
|
257 |
+
.env.production.local
|
258 |
+
.env.local
|
259 |
+
|
260 |
+
# parcel-bundler cache (https://parceljs.org/)
|
261 |
+
.cache
|
262 |
+
.parcel-cache
|
263 |
+
|
264 |
+
# Next.js build output
|
265 |
+
.next
|
266 |
+
out
|
267 |
+
|
268 |
+
# Nuxt.js build / generate output
|
269 |
+
.nuxt
|
270 |
+
dist
|
271 |
+
|
272 |
+
# Gatsby files
|
273 |
+
.cache/
|
274 |
+
# Comment in the public line in if your project uses Gatsby and not Next.js
|
275 |
+
# https://nextjs.org/blog/next-9-1#public-directory-support
|
276 |
+
# public
|
277 |
+
|
278 |
+
# vuepress build output
|
279 |
+
.vuepress/dist
|
280 |
+
|
281 |
+
# vuepress v2.x temp and cache directory
|
282 |
+
.temp
|
283 |
+
.cache
|
284 |
+
|
285 |
+
# Docusaurus cache and generated files
|
286 |
+
.docusaurus
|
287 |
+
|
288 |
+
# Serverless directories
|
289 |
+
.serverless/
|
290 |
+
|
291 |
+
# FuseBox cache
|
292 |
+
.fusebox/
|
293 |
+
|
294 |
+
# DynamoDB Local files
|
295 |
+
.dynamodb/
|
296 |
+
|
297 |
+
# TernJS port file
|
298 |
+
.tern-port
|
299 |
+
|
300 |
+
# Stores VSCode versions used for testing VSCode extensions
|
301 |
+
.vscode-test
|
302 |
+
|
303 |
+
# yarn v2
|
304 |
+
.yarn/cache
|
305 |
+
.yarn/unplugged
|
306 |
+
.yarn/build-state.yml
|
307 |
+
.yarn/install-state.gz
|
308 |
+
.pnp.*
|
309 |
+
|
310 |
+
# cypress artifacts
|
311 |
+
cypress/videos
|
312 |
+
cypress/screenshots
|
313 |
+
|
314 |
+
|
315 |
+
|
316 |
+
/static/*
|
.prettierrc
ADDED
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
{
|
2 |
+
"useTabs": true,
|
3 |
+
"singleQuote": true,
|
4 |
+
"trailingComma": "none",
|
5 |
+
"printWidth": 100,
|
6 |
+
"plugins": ["prettier-plugin-svelte"],
|
7 |
+
"pluginSearchDirs": ["."],
|
8 |
+
"overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }]
|
9 |
+
}
|
CHANGELOG.md
ADDED
@@ -0,0 +1,744 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Changelog
|
2 |
+
|
3 |
+
All notable changes to this project will be documented in this file.
|
4 |
+
|
5 |
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
6 |
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
7 |
+
|
8 |
+
## [0.3.10] - 2024-07-17
|
9 |
+
|
10 |
+
### Fixed
|
11 |
+
|
12 |
+
- **🔄 Improved File Upload**: Addressed the issue where file uploads lacked animation.
|
13 |
+
- **💬 Chat Continuity**: Fixed a problem where existing chats were not functioning properly in some instances.
|
14 |
+
- **🗂️ Chat File Reset**: Resolved the issue of chat files not resetting for new conversations, now ensuring a clean slate for each chat session.
|
15 |
+
- **📁 Document Workspace Uploads**: Corrected the handling of document uploads in the workspace using the Files API.
|
16 |
+
|
17 |
+
## [0.3.9] - 2024-07-17
|
18 |
+
|
19 |
+
### Added
|
20 |
+
|
21 |
+
- **📁 Files Chat Controls**: We've reverted to the old file handling behavior where uploaded files are always included. You can now manage files directly within the chat controls section, giving you the ability to remove files as needed.
|
22 |
+
- **🔧 "Action" Function Support**: Introducing a new "Action" function to write custom buttons to the message toolbar. This feature enables more interactive messaging, with documentation coming soon.
|
23 |
+
- **📜 Citations Handling**: For newly uploaded files in documents workspace, citations will now display the actual filename. Additionally, you can click on these filenames to open the file in a new tab for easier access.
|
24 |
+
- **🛠️ Event Emitter and Call Updates**: Enhanced 'event_emitter' to allow message replacement and 'event_call' to support text input for Tools and Functions. Detailed documentation will be provided shortly.
|
25 |
+
- **🎨 Styling Refactor**: Various styling updates for a cleaner and more cohesive user interface.
|
26 |
+
- **🌐 Enhanced Translations**: Improved translations for Catalan, Ukrainian, and Brazilian Portuguese.
|
27 |
+
|
28 |
+
### Fixed
|
29 |
+
|
30 |
+
- **🔧 Chat Controls Priority**: Resolved an issue where Chat Controls values were being overridden by model information parameters. The priority is now Chat Controls, followed by Global Settings, then Model Settings.
|
31 |
+
- **🪲 Debug Logs**: Fixed an issue where debug logs were not being logged properly.
|
32 |
+
- **🔑 Automatic1111 Auth Key**: The auth key for Automatic1111 is no longer required.
|
33 |
+
- **📝 Title Generation**: Ensured that the title generation runs only once, even when multiple models are in a chat.
|
34 |
+
- **✅ Boolean Values in Params**: Added support for boolean values in parameters.
|
35 |
+
- **🖼️ Files Overlay Styling**: Fixed the styling issue with the files overlay.
|
36 |
+
|
37 |
+
### Changed
|
38 |
+
|
39 |
+
- **⬆️ Dependency Updates**
|
40 |
+
- Upgraded 'pydantic' from version 2.7.1 to 2.8.2.
|
41 |
+
- Upgraded 'sqlalchemy' from version 2.0.30 to 2.0.31.
|
42 |
+
- Upgraded 'unstructured' from version 0.14.9 to 0.14.10.
|
43 |
+
- Upgraded 'chromadb' from version 0.5.3 to 0.5.4.
|
44 |
+
|
45 |
+
## [0.3.8] - 2024-07-09
|
46 |
+
|
47 |
+
### Added
|
48 |
+
|
49 |
+
- **💬 Chat Controls**: Easily adjust parameters for each chat session, offering more precise control over your interactions.
|
50 |
+
- **📌 Pinned Chats**: Support for pinned chats, allowing you to keep important conversations easily accessible.
|
51 |
+
- **📄 Apache Tika Integration**: Added support for using Apache Tika as a document loader, enhancing document processing capabilities.
|
52 |
+
- **🛠️ Custom Environment for OpenID Claims**: Allows setting custom claims for OpenID, providing more flexibility in user authentication.
|
53 |
+
- **🔧 Enhanced Tools & Functions API**: Introduced 'event_emitter' and 'event_call', now you can also add citations for better documentation and tracking. Detailed documentation will be provided on our documentation website.
|
54 |
+
- **↔️ Sideways Scrolling in Settings**: Settings tabs container now supports horizontal scrolling for easier navigation.
|
55 |
+
- **🌑 Darker OLED Theme**: Includes a new, darker OLED theme and improved styling for the light theme, enhancing visual appeal.
|
56 |
+
- **🌐 Language Updates**: Updated translations for Indonesian, German, French, and Catalan languages, expanding accessibility.
|
57 |
+
|
58 |
+
### Fixed
|
59 |
+
|
60 |
+
- **⏰ OpenAI Streaming Timeout**: Resolved issues with OpenAI streaming response using the 'AIOHTTP_CLIENT_TIMEOUT' setting, ensuring reliable performance.
|
61 |
+
- **💡 User Valves**: Fixed malfunctioning user valves, ensuring proper functionality.
|
62 |
+
- **🔄 Collapsible Components**: Addressed issues with collapsible components not working, restoring expected behavior.
|
63 |
+
|
64 |
+
### Changed
|
65 |
+
|
66 |
+
- **🗃️ Database Backend**: Switched from Peewee to SQLAlchemy for improved concurrency support, enhancing database performance.
|
67 |
+
- **⬆️ ChromaDB Update**: Upgraded to version 0.5.3. Ensure your remote ChromaDB instance matches this version.
|
68 |
+
- **🔤 Primary Font Styling**: Updated primary font to Archivo for better visual consistency.
|
69 |
+
- **🔄 Font Change for Windows**: Replaced Arimo with Inter font for Windows users, improving readability.
|
70 |
+
- **🚀 Lazy Loading**: Implemented lazy loading for 'faster_whisper' and 'sentence_transformers' to reduce startup memory usage.
|
71 |
+
- **📋 Task Generation Payload**: Task generations now include only the "task" field in the body instead of "title".
|
72 |
+
|
73 |
+
## [0.3.7] - 2024-06-29
|
74 |
+
|
75 |
+
### Added
|
76 |
+
|
77 |
+
- **🌐 Enhanced Internationalization (i18n)**: Newly introduced Indonesian translation, and updated translations for Turkish, Chinese, and Catalan languages to improve user accessibility.
|
78 |
+
|
79 |
+
### Fixed
|
80 |
+
|
81 |
+
- **🕵️♂️ Browser Language Detection**: Corrected the issue where the application was not properly detecting and adapting to the browser's language settings.
|
82 |
+
- **🔐 OIDC Admin Role Assignment**: Fixed a bug where the admin role was not being assigned to the first user who signed up via OpenID Connect (OIDC).
|
83 |
+
- **💬 Chat/Completions Endpoint**: Resolved an issue where the chat/completions endpoint was non-functional when the stream option was set to False.
|
84 |
+
- **🚫 'WEBUI_AUTH' Configuration**: Addressed the problem where setting 'WEBUI_AUTH' to False was not being applied correctly.
|
85 |
+
|
86 |
+
### Changed
|
87 |
+
|
88 |
+
- **📦 Dependency Update**: Upgraded 'authlib' from version 1.3.0 to 1.3.1 to ensure better security and performance enhancements.
|
89 |
+
|
90 |
+
## [0.3.6] - 2024-06-27
|
91 |
+
|
92 |
+
### Added
|
93 |
+
|
94 |
+
- **✨ "Functions" Feature**: You can now utilize "Functions" like filters (middleware) and pipe (model) functions directly within the WebUI. While largely compatible with Pipelines, these native functions can be executed easily within Open WebUI. Example use cases for filter functions include usage monitoring, real-time translation, moderation, and automemory. For pipe functions, the scope ranges from Cohere and Anthropic integration directly within Open WebUI, enabling "Valves" for per-user OpenAI API key usage, and much more. If you encounter issues, SAFE_MODE has been introduced.
|
95 |
+
- **📁 Files API**: Compatible with OpenAI, this feature allows for custom Retrieval-Augmented Generation (RAG) in conjunction with the Filter Function. More examples will be shared on our community platform and official documentation website.
|
96 |
+
- **🛠️ Tool Enhancements**: Tools now support citations and "Valves". Documentation will be available shortly.
|
97 |
+
- **🔗 Iframe Support via Files API**: Enables rendering HTML directly into your chat interface using functions and tools. Use cases include playing games like DOOM and Snake, displaying a weather applet, and implementing Anthropic "artifacts"-like features. Stay tuned for updates on our community platform and documentation.
|
98 |
+
- **🔒 Experimental OAuth Support**: New experimental OAuth support. Check our documentation for more details.
|
99 |
+
- **🖼️ Custom Background Support**: Set a custom background from Settings > Interface to personalize your experience.
|
100 |
+
- **🔑 AUTOMATIC1111_API_AUTH Support**: Enhanced security for the AUTOMATIC1111 API.
|
101 |
+
- **🎨 Code Highlight Optimization**: Improved code highlighting features.
|
102 |
+
- **🎙️ Voice Interruption Feature**: Reintroduced and now toggleable from Settings > Interface.
|
103 |
+
- **💤 Wakelock API**: Now in use to prevent screen dimming during important tasks.
|
104 |
+
- **🔐 API Key Privacy**: All API keys are now hidden by default for better security.
|
105 |
+
- **🔍 New Web Search Provider**: Added jina_search as a new option.
|
106 |
+
- **🌐 Enhanced Internationalization (i18n)**: Improved Korean translation and updated Chinese and Ukrainian translations.
|
107 |
+
|
108 |
+
### Fixed
|
109 |
+
|
110 |
+
- **🔧 Conversation Mode Issue**: Fixed the issue where Conversation Mode remained active after being removed from settings.
|
111 |
+
- **📏 Scroll Button Obstruction**: Resolved the issue where the scrollToBottom button container obstructed clicks on buttons beneath it.
|
112 |
+
|
113 |
+
### Changed
|
114 |
+
|
115 |
+
- **⏲️ AIOHTTP_CLIENT_TIMEOUT**: Now set to 'None' by default for improved configuration flexibility.
|
116 |
+
- **📞 Voice Call Enhancements**: Improved by skipping code blocks and expressions during calls.
|
117 |
+
- **🚫 Error Message Handling**: Disabled the continuation of operations with error messages.
|
118 |
+
- **🗂️ Playground Relocation**: Moved the Playground from the workspace to the user menu for better user experience.
|
119 |
+
|
120 |
+
## [0.3.5] - 2024-06-16
|
121 |
+
|
122 |
+
### Added
|
123 |
+
|
124 |
+
- **📞 Enhanced Voice Call**: Text-to-speech (TTS) callback now operates in real-time for each sentence, reducing latency by not waiting for full completion.
|
125 |
+
- **👆 Tap to Interrupt**: During a call, you can now stop the assistant from speaking by simply tapping, instead of using voice. This resolves the issue of the speaker's voice being mistakenly registered as input.
|
126 |
+
- **😊 Emoji Call**: Toggle this feature on from the Settings > Interface, allowing LLMs to express emotions using emojis during voice calls for a more dynamic interaction.
|
127 |
+
- **🖱️ Quick Archive/Delete**: Use the Shift key + mouseover on the chat list to swiftly archive or delete items.
|
128 |
+
- **📝 Markdown Support in Model Descriptions**: You can now format model descriptions with markdown, enabling bold text, links, etc.
|
129 |
+
- **🧠 Editable Memories**: Adds the capability to modify memories.
|
130 |
+
- **📋 Admin Panel Sorting**: Introduces the ability to sort users/chats within the admin panel.
|
131 |
+
- **🌑 Dark Mode for Quick Selectors**: Dark mode now available for chat quick selectors (prompts, models, documents).
|
132 |
+
- **🔧 Advanced Parameters**: Adds 'num_keep' and 'num_batch' to advanced parameters for customization.
|
133 |
+
- **📅 Dynamic System Prompts**: New variables '{{CURRENT_DATETIME}}', '{{CURRENT_TIME}}', '{{USER_LOCATION}}' added for system prompts. Ensure '{{USER_LOCATION}}' is toggled on from Settings > Interface.
|
134 |
+
- **🌐 Tavily Web Search**: Includes Tavily as a web search provider option.
|
135 |
+
- **🖊️ Federated Auth Usernames**: Ability to set user names for federated authentication.
|
136 |
+
- **🔗 Auto Clean URLs**: When adding connection URLs, trailing slashes are now automatically removed.
|
137 |
+
- **🌐 Enhanced Translations**: Improved Chinese and Swedish translations.
|
138 |
+
|
139 |
+
### Fixed
|
140 |
+
|
141 |
+
- **⏳ AIOHTTP_CLIENT_TIMEOUT**: Introduced a new environment variable 'AIOHTTP_CLIENT_TIMEOUT' for requests to Ollama lasting longer than 5 minutes. Default is 300 seconds; set to blank ('') for no timeout.
|
142 |
+
- **❌ Message Delete Freeze**: Resolved an issue where message deletion would sometimes cause the web UI to freeze.
|
143 |
+
|
144 |
+
## [0.3.4] - 2024-06-12
|
145 |
+
|
146 |
+
### Fixed
|
147 |
+
|
148 |
+
- **🔒 Mixed Content with HTTPS Issue**: Resolved a problem where mixed content (HTTP and HTTPS) was causing security warnings and blocking resources on HTTPS sites.
|
149 |
+
- **🔍 Web Search Issue**: Addressed the problem where web search functionality was not working correctly. The 'ENABLE_RAG_LOCAL_WEB_FETCH' option has been reintroduced to restore proper web searching capabilities.
|
150 |
+
- **💾 RAG Template Not Being Saved**: Fixed an issue where the RAG template was not being saved correctly, ensuring your custom templates are now preserved as expected.
|
151 |
+
|
152 |
+
## [0.3.3] - 2024-06-12
|
153 |
+
|
154 |
+
### Added
|
155 |
+
|
156 |
+
- **🛠️ Native Python Function Calling**: Introducing native Python function calling within Open WebUI. We’ve also included a built-in code editor to seamlessly develop and integrate function code within the 'Tools' workspace. With this, you can significantly enhance your LLM’s capabilities by creating custom RAG pipelines, web search tools, and even agent-like features such as sending Discord messages.
|
157 |
+
- **🌐 DuckDuckGo Integration**: Added DuckDuckGo as a web search provider, giving you more search options.
|
158 |
+
- **🌏 Enhanced Translations**: Improved translations for Vietnamese and Chinese languages, making the interface more accessible.
|
159 |
+
|
160 |
+
### Fixed
|
161 |
+
|
162 |
+
- **🔗 Web Search URL Error Handling**: Fixed the issue where a single URL error would disrupt the data loading process in Web Search mode. Now, such errors will be handled gracefully to ensure uninterrupted data loading.
|
163 |
+
- **🖥️ Frontend Responsiveness**: Resolved the problem where the frontend would stop responding if the backend encounters an error while downloading a model. Improved error handling to maintain frontend stability.
|
164 |
+
- **🔧 Dependency Issues in pip**: Fixed issues related to pip installations, ensuring all dependencies are correctly managed to prevent installation errors.
|
165 |
+
|
166 |
+
## [0.3.2] - 2024-06-10
|
167 |
+
|
168 |
+
### Added
|
169 |
+
|
170 |
+
- **🔍 Web Search Query Status**: The web search query will now persist in the results section to aid in easier debugging and tracking of search queries.
|
171 |
+
- **🌐 New Web Search Provider**: We have added Serply as a new option for web search providers, giving you more choices for your search needs.
|
172 |
+
- **🌏 Improved Translations**: We've enhanced translations for Chinese and Portuguese.
|
173 |
+
|
174 |
+
### Fixed
|
175 |
+
|
176 |
+
- **🎤 Audio File Upload Issue**: The bug that prevented audio files from being uploaded in chat input has been fixed, ensuring smooth communication.
|
177 |
+
- **💬 Message Input Handling**: Improved the handling of message inputs by instantly clearing images and text after sending, along with immediate visual indications when a response message is loading, enhancing user feedback.
|
178 |
+
- **⚙️ Parameter Registration and Validation**: Fixed the issue where parameters were not registering in certain cases and addressed the problem where users were unable to save due to invalid input errors.
|
179 |
+
|
180 |
+
## [0.3.1] - 2024-06-09
|
181 |
+
|
182 |
+
### Fixed
|
183 |
+
|
184 |
+
- **💬 Chat Functionality**: Resolved the issue where chat functionality was not working for specific models.
|
185 |
+
|
186 |
+
## [0.3.0] - 2024-06-09
|
187 |
+
|
188 |
+
### Added
|
189 |
+
|
190 |
+
- **📚 Knowledge Support for Models**: Attach documents directly to models from the models workspace, enhancing the information available to each model.
|
191 |
+
- **🎙️ Hands-Free Voice Call Feature**: Initiate voice calls without needing to use your hands, making interactions more seamless.
|
192 |
+
- **📹 Video Call Feature**: Enable video calls with supported vision models like Llava and GPT-4o, adding a visual dimension to your communications.
|
193 |
+
- **🎛️ Enhanced UI for Voice Recording**: Improved user interface for the voice recording feature, making it more intuitive and user-friendly.
|
194 |
+
- **🌐 External STT Support**: Now support for external Speech-To-Text services, providing more flexibility in choosing your STT provider.
|
195 |
+
- **⚙️ Unified Settings**: Consolidated settings including document settings under a new admin settings section for easier management.
|
196 |
+
- **🌑 Dark Mode Splash Screen**: A new splash screen for dark mode, ensuring a consistent and visually appealing experience for dark mode users.
|
197 |
+
- **📥 Upload Pipeline**: Directly upload pipelines from the admin settings > pipelines section, streamlining the pipeline management process.
|
198 |
+
- **🌍 Improved Language Support**: Enhanced support for Chinese and Ukrainian languages, better catering to a global user base.
|
199 |
+
|
200 |
+
### Fixed
|
201 |
+
|
202 |
+
- **🛠️ Playground Issue**: Fixed the playground not functioning properly, ensuring a smoother user experience.
|
203 |
+
- **🔥 Temperature Parameter Issue**: Corrected the issue where the temperature value '0' was not being passed correctly.
|
204 |
+
- **📝 Prompt Input Clearing**: Resolved prompt input textarea not being cleared right away, ensuring a clean slate for new inputs.
|
205 |
+
- **✨ Various UI Styling Issues**: Fixed numerous user interface styling problems for a more cohesive look.
|
206 |
+
- **👥 Active Users Display**: Fixed active users showing active sessions instead of actual users, now reflecting accurate user activity.
|
207 |
+
- **🌐 Community Platform Compatibility**: The Community Platform is back online and fully compatible with Open WebUI.
|
208 |
+
|
209 |
+
### Changed
|
210 |
+
|
211 |
+
- **📝 RAG Implementation**: Updated the RAG (Retrieval-Augmented Generation) implementation to use a system prompt for context, instead of overriding the user's prompt.
|
212 |
+
- **🔄 Settings Relocation**: Moved Models, Connections, Audio, and Images settings to the admin settings for better organization.
|
213 |
+
- **✍️ Improved Title Generation**: Enhanced the default prompt for title generation, yielding better results.
|
214 |
+
- **🔧 Backend Task Management**: Tasks like title generation and search query generation are now managed on the backend side and controlled only by the admin.
|
215 |
+
- **🔍 Editable Search Query Prompt**: You can now edit the search query generation prompt, offering more control over how queries are generated.
|
216 |
+
- **📏 Prompt Length Threshold**: Set the prompt length threshold for search query generation from the admin settings, giving more customization options.
|
217 |
+
- **📣 Settings Consolidation**: Merged the Banners admin setting with the Interface admin setting for a more streamlined settings area.
|
218 |
+
|
219 |
+
## [0.2.5] - 2024-06-05
|
220 |
+
|
221 |
+
### Added
|
222 |
+
|
223 |
+
- **👥 Active Users Indicator**: Now you can see how many people are currently active and what they are running. This helps you gauge when performance might slow down due to a high number of users.
|
224 |
+
- **🗂️ Create Ollama Modelfile**: The option to create a modelfile for Ollama has been reintroduced in the Settings > Models section, making it easier to manage your models.
|
225 |
+
- **⚙️ Default Model Setting**: Added an option to set the default model from Settings > Interface. This feature is now easily accessible, especially convenient for mobile users as it was previously hidden.
|
226 |
+
- **🌐 Enhanced Translations**: We've improved the Chinese translations and added support for Turkmen and Norwegian languages to make the interface more accessible globally.
|
227 |
+
|
228 |
+
### Fixed
|
229 |
+
|
230 |
+
- **📱 Mobile View Improvements**: The UI now uses dvh (dynamic viewport height) instead of vh (viewport height), providing a better and more responsive experience for mobile users.
|
231 |
+
|
232 |
+
## [0.2.4] - 2024-06-03
|
233 |
+
|
234 |
+
### Added
|
235 |
+
|
236 |
+
- **👤 Improved Account Pending Page**: The account pending page now displays admin details by default to avoid confusion. You can disable this feature in the admin settings if needed.
|
237 |
+
- **🌐 HTTP Proxy Support**: We have enabled the use of the 'http_proxy' environment variable in OpenAI and Ollama API calls, making it easier to configure network settings.
|
238 |
+
- **❓ Quick Access to Documentation**: You can now easily access Open WebUI documents via a question mark button located at the bottom right corner of the screen (available on larger screens like PCs).
|
239 |
+
- **🌍 Enhanced Translation**: Improvements have been made to translations.
|
240 |
+
|
241 |
+
### Fixed
|
242 |
+
|
243 |
+
- **🔍 SearxNG Web Search**: Fixed the issue where the SearxNG web search functionality was not working properly.
|
244 |
+
|
245 |
+
## [0.2.3] - 2024-06-03
|
246 |
+
|
247 |
+
### Added
|
248 |
+
|
249 |
+
- **📁 Export Chat as JSON**: You can now export individual chats as JSON files from the navbar menu by navigating to 'Download > Export Chat'. This makes sharing specific conversations easier.
|
250 |
+
- **✏️ Edit Titles with Double Click**: Double-click on titles to rename them quickly and efficiently.
|
251 |
+
- **🧩 Batch Multiple Embeddings**: Introduced 'RAG_EMBEDDING_OPENAI_BATCH_SIZE' to process multiple embeddings in a batch, enhancing performance for large datasets.
|
252 |
+
- **🌍 Improved Translations**: Enhanced the translation quality across various languages for a better user experience.
|
253 |
+
|
254 |
+
### Fixed
|
255 |
+
|
256 |
+
- **🛠️ Modelfile Migration Script**: Fixed an issue where the modelfile migration script would fail if an invalid modelfile was encountered.
|
257 |
+
- **💬 Zhuyin Input Method on Mac**: Resolved an issue where using the Zhuyin input method in the Web UI on a Mac caused text to send immediately upon pressing the enter key, leading to incorrect input.
|
258 |
+
- **🔊 Local TTS Voice Selection**: Fixed the issue where the selected local Text-to-Speech (TTS) voice was not being displayed in settings.
|
259 |
+
|
260 |
+
## [0.2.2] - 2024-06-02
|
261 |
+
|
262 |
+
### Added
|
263 |
+
|
264 |
+
- **🌊 Mermaid Rendering Support**: We've included support for Mermaid rendering. This allows you to create beautiful diagrams and flowcharts directly within Open WebUI.
|
265 |
+
- **🔄 New Environment Variable 'RESET_CONFIG_ON_START'**: Introducing a new environment variable: 'RESET_CONFIG_ON_START'. Set this variable to reset your configuration settings upon starting the application, making it easier to revert to default settings.
|
266 |
+
|
267 |
+
### Fixed
|
268 |
+
|
269 |
+
- **🔧 Pipelines Filter Issue**: We've addressed an issue with the pipelines where filters were not functioning as expected.
|
270 |
+
|
271 |
+
## [0.2.1] - 2024-06-02
|
272 |
+
|
273 |
+
### Added
|
274 |
+
|
275 |
+
- **🖱️ Single Model Export Button**: Easily export models with just one click using the new single model export button.
|
276 |
+
- **🖥️ Advanced Parameters Support**: Added support for 'num_thread', 'use_mmap', and 'use_mlock' parameters for Ollama.
|
277 |
+
- **🌐 Improved Vietnamese Translation**: Enhanced Vietnamese language support for a better user experience for our Vietnamese-speaking community.
|
278 |
+
|
279 |
+
### Fixed
|
280 |
+
|
281 |
+
- **🔧 OpenAI URL API Save Issue**: Corrected a problem preventing the saving of OpenAI URL API settings.
|
282 |
+
- **🚫 Display Issue with Disabled Ollama API**: Fixed the display bug causing models to appear in settings when the Ollama API was disabled.
|
283 |
+
|
284 |
+
### Changed
|
285 |
+
|
286 |
+
- **💡 Versioning Update**: As a reminder from our previous update, version 0.2.y will focus primarily on bug fixes, while major updates will be designated as 0.x from now on for better version tracking.
|
287 |
+
|
288 |
+
## [0.2.0] - 2024-06-01
|
289 |
+
|
290 |
+
### Added
|
291 |
+
|
292 |
+
- **🔧 Pipelines Support**: Open WebUI now includes a plugin framework for enhanced customization and functionality (https://github.com/open-webui/pipelines). Easily add custom logic and integrate Python libraries, from AI agents to home automation APIs.
|
293 |
+
- **🔗 Function Calling via Pipelines**: Integrate function calling seamlessly through Pipelines.
|
294 |
+
- **⚖️ User Rate Limiting via Pipelines**: Implement user-specific rate limits to manage API usage efficiently.
|
295 |
+
- **📊 Usage Monitoring with Langfuse**: Track and analyze usage statistics with Langfuse integration through Pipelines.
|
296 |
+
- **🕒 Conversation Turn Limits**: Set limits on conversation turns to manage interactions better through Pipelines.
|
297 |
+
- **🛡️ Toxic Message Filtering**: Automatically filter out toxic messages to maintain a safe environment using Pipelines.
|
298 |
+
- **🔍 Web Search Support**: Introducing built-in web search capabilities via RAG API, allowing users to search using SearXNG, Google Programmatic Search Engine, Brave Search, serpstack, and serper. Activate it effortlessly by adding necessary variables from Document settings > Web Params.
|
299 |
+
- **🗂️ Models Workspace**: Create and manage model presets for both Ollama/OpenAI API. Note: The old Modelfiles workspace is deprecated.
|
300 |
+
- **🛠️ Model Builder Feature**: Build and edit all models with persistent builder mode.
|
301 |
+
- **🏷️ Model Tagging Support**: Organize models with tagging features in the models workspace.
|
302 |
+
- **📋 Model Ordering Support**: Effortlessly organize models by dragging and dropping them into the desired positions within the models workspace.
|
303 |
+
- **📈 OpenAI Generation Stats**: Access detailed generation statistics for OpenAI models.
|
304 |
+
- **📅 System Prompt Variables**: New variables added: '{{CURRENT_DATE}}' and '{{USER_NAME}}' for dynamic prompts.
|
305 |
+
- **📢 Global Banner Support**: Manage global banners from admin settings > banners.
|
306 |
+
- **🗃️ Enhanced Archived Chats Modal**: Search and export archived chats easily.
|
307 |
+
- **📂 Archive All Button**: Quickly archive all chats from settings > chats.
|
308 |
+
- **🌐 Improved Translations**: Added and improved translations for French, Croatian, Cebuano, and Vietnamese.
|
309 |
+
|
310 |
+
### Fixed
|
311 |
+
|
312 |
+
- **🔍 Archived Chats Visibility**: Resolved issue with archived chats not showing in the admin panel.
|
313 |
+
- **💬 Message Styling**: Fixed styling issues affecting message appearance.
|
314 |
+
- **🔗 Shared Chat Responses**: Corrected the issue where shared chat response messages were not readonly.
|
315 |
+
- **🖥️ UI Enhancement**: Fixed the scrollbar overlapping issue with the message box in the user interface.
|
316 |
+
|
317 |
+
### Changed
|
318 |
+
|
319 |
+
- **💾 User Settings Storage**: User settings are now saved on the backend, ensuring consistency across all devices.
|
320 |
+
- **📡 Unified API Requests**: The API request for getting models is now unified to '/api/models' for easier usage.
|
321 |
+
- **🔄 Versioning Update**: Our versioning will now follow the format 0.x for major updates and 0.x.y for patches.
|
322 |
+
- **📦 Export All Chats (All Users)**: Moved this functionality to the Admin Panel settings for better organization and accessibility.
|
323 |
+
|
324 |
+
### Removed
|
325 |
+
|
326 |
+
- **🚫 Bundled LiteLLM Support Deprecated**: Migrate your LiteLLM config.yaml to a self-hosted LiteLLM instance. LiteLLM can still be added via OpenAI Connections. Download the LiteLLM config.yaml from admin settings > database > export LiteLLM config.yaml.
|
327 |
+
|
328 |
+
## [0.1.125] - 2024-05-19
|
329 |
+
|
330 |
+
### Added
|
331 |
+
|
332 |
+
- **🔄 Updated UI**: Chat interface revamped with chat bubbles. Easily switch back to the old style via settings > interface > chat bubble UI.
|
333 |
+
- **📂 Enhanced Sidebar UI**: Model files, documents, prompts, and playground merged into Workspace for streamlined access.
|
334 |
+
- **🚀 Improved Many Model Interaction**: All responses now displayed simultaneously for a smoother experience.
|
335 |
+
- **🐍 Python Code Execution**: Execute Python code locally in the browser with libraries like 'requests', 'beautifulsoup4', 'numpy', 'pandas', 'seaborn', 'matplotlib', 'scikit-learn', 'scipy', 'regex'.
|
336 |
+
- **🧠 Experimental Memory Feature**: Manually input personal information you want LLMs to remember via settings > personalization > memory.
|
337 |
+
- **💾 Persistent Settings**: Settings now saved as config.json for convenience.
|
338 |
+
- **🩺 Health Check Endpoint**: Added for Docker deployment.
|
339 |
+
- **↕️ RTL Support**: Toggle chat direction via settings > interface > chat direction.
|
340 |
+
- **🖥️ PowerPoint Support**: RAG pipeline now supports PowerPoint documents.
|
341 |
+
- **🌐 Language Updates**: Ukrainian, Turkish, Arabic, Chinese, Serbian, Vietnamese updated; Punjabi added.
|
342 |
+
|
343 |
+
### Changed
|
344 |
+
|
345 |
+
- **👤 Shared Chat Update**: Shared chat now includes creator user information.
|
346 |
+
|
347 |
+
## [0.1.124] - 2024-05-08
|
348 |
+
|
349 |
+
### Added
|
350 |
+
|
351 |
+
- **🖼️ Improved Chat Sidebar**: Now conveniently displays time ranges and organizes chats by today, yesterday, and more.
|
352 |
+
- **📜 Citations in RAG Feature**: Easily track the context fed to the LLM with added citations in the RAG feature.
|
353 |
+
- **🔒 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.
|
354 |
+
- **📹 Enhanced YouTube RAG Pipeline**: Now supports non-English videos for an enriched experience.
|
355 |
+
- **🔊 Specify OpenAI TTS Models**: Customize your TTS experience by specifying OpenAI TTS models.
|
356 |
+
- **🔧 Additional Environment Variables**: Discover more environment variables in our comprehensive documentation at Open WebUI Documentation (https://docs.openwebui.com).
|
357 |
+
- **🌐 Language Support**: Arabic, Finnish, and Hindi added; Improved support for German, Vietnamese, and Chinese.
|
358 |
+
|
359 |
+
### Fixed
|
360 |
+
|
361 |
+
- **🛠️ Model Selector Styling**: Addressed styling issues for improved user experience.
|
362 |
+
- **⚠️ Warning Messages**: Resolved backend warning messages.
|
363 |
+
|
364 |
+
### Changed
|
365 |
+
|
366 |
+
- **📝 Title Generation**: Limited output to 50 tokens.
|
367 |
+
- **📦 Helm Charts**: Removed Helm charts, now available in a separate repository (https://github.com/open-webui/helm-charts).
|
368 |
+
|
369 |
+
## [0.1.123] - 2024-05-02
|
370 |
+
|
371 |
+
### Added
|
372 |
+
|
373 |
+
- **🎨 New Landing Page Design**: Refreshed design for a more modern look and optimized use of screen space.
|
374 |
+
- **📹 Youtube RAG Pipeline**: Introduces dedicated RAG pipeline for Youtube videos, enabling interaction with video transcriptions directly.
|
375 |
+
- **🔧 Enhanced Admin Panel**: Streamlined user management with options to add users directly or in bulk via CSV import.
|
376 |
+
- **👥 '@' Model Integration**: Easily switch to specific models during conversations; old collaborative chat feature phased out.
|
377 |
+
- **🌐 Language Enhancements**: Swedish translation added, plus improvements to German, Spanish, and the addition of Doge translation.
|
378 |
+
|
379 |
+
### Fixed
|
380 |
+
|
381 |
+
- **🗑️ Delete Chat Shortcut**: Addressed issue where shortcut wasn't functioning.
|
382 |
+
- **🖼️ Modal Closing Bug**: Resolved unexpected closure of modal when dragging from within.
|
383 |
+
- **✏️ Edit Button Styling**: Fixed styling inconsistency with edit buttons.
|
384 |
+
- **🌐 Image Generation Compatibility Issue**: Rectified image generation compatibility issue with third-party APIs.
|
385 |
+
- **📱 iOS PWA Icon Fix**: Corrected iOS PWA home screen icon shape.
|
386 |
+
- **🔍 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.
|
387 |
+
|
388 |
+
### Changed
|
389 |
+
|
390 |
+
- **🔄 Unlimited Context Length**: Advanced settings now allow unlimited max context length (previously limited to 16000).
|
391 |
+
- **👑 Super Admin Assignment**: The first signup is automatically assigned a super admin role, unchangeable by other admins.
|
392 |
+
- **🛡️ Admin User Restrictions**: User action buttons from the admin panel are now disabled for users with admin roles.
|
393 |
+
- **🔝 Default Model Selector**: Set as default model option now exclusively available on the landing page.
|
394 |
+
|
395 |
+
## [0.1.122] - 2024-04-27
|
396 |
+
|
397 |
+
### Added
|
398 |
+
|
399 |
+
- **🌟 Enhanced RAG Pipeline**: Now with hybrid searching via 'BM25', reranking powered by 'CrossEncoder', and configurable relevance score thresholds.
|
400 |
+
- **🛢️ External Database Support**: Seamlessly connect to custom SQLite or Postgres databases using the 'DATABASE_URL' environment variable.
|
401 |
+
- **🌐 Remote ChromaDB Support**: Introducing the capability to connect to remote ChromaDB servers.
|
402 |
+
- **👨💼 Improved Admin Panel**: Admins can now conveniently check users' chat lists and last active status directly from the admin panel.
|
403 |
+
- **🎨 Splash Screen**: Introducing a loading splash screen for a smoother user experience.
|
404 |
+
- **🌍 Language Support Expansion**: Added support for Bangla (bn-BD), along with enhancements to Chinese, Spanish, and Ukrainian translations.
|
405 |
+
- **💻 Improved LaTeX Rendering Performance**: Enjoy faster rendering times for LaTeX equations.
|
406 |
+
- **🔧 More Environment Variables**: Explore additional environment variables in our documentation (https://docs.openwebui.com), including the 'ENABLE_LITELLM' option to manage memory usage.
|
407 |
+
|
408 |
+
### Fixed
|
409 |
+
|
410 |
+
- **🔧 Ollama Compatibility**: Resolved errors occurring when Ollama server version isn't an integer, such as SHA builds or RCs.
|
411 |
+
- **🐛 Various OpenAI API Issues**: Addressed several issues related to the OpenAI API.
|
412 |
+
- **🛑 Stop Sequence Issue**: Fixed the problem where the stop sequence with a backslash '\' was not functioning.
|
413 |
+
- **🔤 Font Fallback**: Corrected font fallback issue.
|
414 |
+
|
415 |
+
### Changed
|
416 |
+
|
417 |
+
- **⌨️ Prompt Input Behavior on Mobile**: Enter key prompt submission disabled on mobile devices for improved user experience.
|
418 |
+
|
419 |
+
## [0.1.121] - 2024-04-24
|
420 |
+
|
421 |
+
### Fixed
|
422 |
+
|
423 |
+
- **🔧 Translation Issues**: Addressed various translation discrepancies.
|
424 |
+
- **🔒 LiteLLM Security Fix**: Updated LiteLLM version to resolve a security vulnerability.
|
425 |
+
- **🖥️ HTML Tag Display**: Rectified the issue where the '< br >' tag wasn't displaying correctly.
|
426 |
+
- **🔗 WebSocket Connection**: Resolved the failure of WebSocket connection under HTTPS security for ComfyUI server.
|
427 |
+
- **📜 FileReader Optimization**: Implemented FileReader initialization per image in multi-file drag & drop to ensure reusability.
|
428 |
+
- **🏷️ Tag Display**: Corrected tag display inconsistencies.
|
429 |
+
- **📦 Archived Chat Styling**: Fixed styling issues in archived chat.
|
430 |
+
- **🔖 Safari Copy Button Bug**: Addressed the bug where the copy button failed to copy links in Safari.
|
431 |
+
|
432 |
+
## [0.1.120] - 2024-04-20
|
433 |
+
|
434 |
+
### Added
|
435 |
+
|
436 |
+
- **📦 Archive Chat Feature**: Easily archive chats with a new sidebar button, and access archived chats via the profile button > archived chats.
|
437 |
+
- **🔊 Configurable Text-to-Speech Endpoint**: Customize your Text-to-Speech experience with configurable OpenAI endpoints.
|
438 |
+
- **🛠️ Improved Error Handling**: Enhanced error message handling for connection failures.
|
439 |
+
- **⌨️ Enhanced Shortcut**: When editing messages, use ctrl/cmd+enter to save and submit, and esc to close.
|
440 |
+
- **🌐 Language Support**: Added support for Georgian and enhanced translations for Portuguese and Vietnamese.
|
441 |
+
|
442 |
+
### Fixed
|
443 |
+
|
444 |
+
- **🔧 Model Selector**: Resolved issue where default model selection was not saving.
|
445 |
+
- **🔗 Share Link Copy Button**: Fixed bug where the copy button wasn't copying links in Safari.
|
446 |
+
- **🎨 Light Theme Styling**: Addressed styling issue with the light theme.
|
447 |
+
|
448 |
+
## [0.1.119] - 2024-04-16
|
449 |
+
|
450 |
+
### Added
|
451 |
+
|
452 |
+
- **🌟 Enhanced RAG Embedding Support**: Ollama, and OpenAI models can now be used for RAG embedding model.
|
453 |
+
- **🔄 Seamless Integration**: Copy 'ollama run <model name>' directly from Ollama page to easily select and pull models.
|
454 |
+
- **🏷️ Tagging Feature**: Add tags to chats directly via the sidebar chat menu.
|
455 |
+
- **📱 Mobile Accessibility**: Swipe left and right on mobile to effortlessly open and close the sidebar.
|
456 |
+
- **🔍 Improved Navigation**: Admin panel now supports pagination for user list.
|
457 |
+
- **🌍 Additional Language Support**: Added Polish language support.
|
458 |
+
|
459 |
+
### Fixed
|
460 |
+
|
461 |
+
- **🌍 Language Enhancements**: Vietnamese and Spanish translations have been improved.
|
462 |
+
- **🔧 Helm Fixes**: Resolved issues with Helm trailing slash and manifest.json.
|
463 |
+
|
464 |
+
### Changed
|
465 |
+
|
466 |
+
- **🐳 Docker Optimization**: Updated docker image build process to utilize 'uv' for significantly faster builds compared to 'pip3'.
|
467 |
+
|
468 |
+
## [0.1.118] - 2024-04-10
|
469 |
+
|
470 |
+
### Added
|
471 |
+
|
472 |
+
- **🦙 Ollama and CUDA Images**: Added support for ':ollama' and ':cuda' tagged images.
|
473 |
+
- **👍 Enhanced Response Rating**: Now you can annotate your ratings for better feedback.
|
474 |
+
- **👤 User Initials Profile Photo**: User initials are now the default profile photo.
|
475 |
+
- **🔍 Update RAG Embedding Model**: Customize RAG embedding model directly in document settings.
|
476 |
+
- **🌍 Additional Language Support**: Added Turkish language support.
|
477 |
+
|
478 |
+
### Fixed
|
479 |
+
|
480 |
+
- **🔒 Share Chat Permission**: Resolved issue with chat sharing permissions.
|
481 |
+
- **🛠 Modal Close**: Modals can now be closed using the Esc key.
|
482 |
+
|
483 |
+
### Changed
|
484 |
+
|
485 |
+
- **🎨 Admin Panel Styling**: Refreshed styling for the admin panel.
|
486 |
+
- **🐳 Docker Image Build**: Updated docker image build process for improved efficiency.
|
487 |
+
|
488 |
+
## [0.1.117] - 2024-04-03
|
489 |
+
|
490 |
+
### Added
|
491 |
+
|
492 |
+
- 🗨️ **Local Chat Sharing**: Share chat links seamlessly between users.
|
493 |
+
- 🔑 **API Key Generation Support**: Generate secret keys to leverage Open WebUI with OpenAI libraries.
|
494 |
+
- 📄 **Chat Download as PDF**: Easily download chats in PDF format.
|
495 |
+
- 📝 **Improved Logging**: Enhancements to logging functionality.
|
496 |
+
- 📧 **Trusted Email Authentication**: Authenticate using a trusted email header.
|
497 |
+
|
498 |
+
### Fixed
|
499 |
+
|
500 |
+
- 🌷 **Enhanced Dutch Translation**: Improved translation for Dutch users.
|
501 |
+
- ⚪ **White Theme Styling**: Resolved styling issue with the white theme.
|
502 |
+
- 📜 **LaTeX Chat Screen Overflow**: Fixed screen overflow issue with LaTeX rendering.
|
503 |
+
- 🔒 **Security Patches**: Applied necessary security patches.
|
504 |
+
|
505 |
+
## [0.1.116] - 2024-03-31
|
506 |
+
|
507 |
+
### Added
|
508 |
+
|
509 |
+
- **🔄 Enhanced UI**: Model selector now conveniently located in the navbar, enabling seamless switching between multiple models during conversations.
|
510 |
+
- **🔍 Improved Model Selector**: Directly pull a model from the selector/Models now display detailed information for better understanding.
|
511 |
+
- **💬 Webhook Support**: Now compatible with Google Chat and Microsoft Teams.
|
512 |
+
- **🌐 Localization**: Korean translation (I18n) now available.
|
513 |
+
- **🌑 Dark Theme**: OLED dark theme introduced for reduced strain during prolonged usage.
|
514 |
+
- **🏷️ Tag Autocomplete**: Dropdown feature added for effortless chat tagging.
|
515 |
+
|
516 |
+
### Fixed
|
517 |
+
|
518 |
+
- **🔽 Auto-Scrolling**: Addressed OpenAI auto-scrolling issue.
|
519 |
+
- **🏷️ Tag Validation**: Implemented tag validation to prevent empty string tags.
|
520 |
+
- **🚫 Model Whitelisting**: Resolved LiteLLM model whitelisting issue.
|
521 |
+
- **✅ Spelling**: Corrected various spelling issues for improved readability.
|
522 |
+
|
523 |
+
## [0.1.115] - 2024-03-24
|
524 |
+
|
525 |
+
### Added
|
526 |
+
|
527 |
+
- **🔍 Custom Model Selector**: Easily find and select custom models with the new search filter feature.
|
528 |
+
- **🛑 Cancel Model Download**: Added the ability to cancel model downloads.
|
529 |
+
- **🎨 Image Generation ComfyUI**: Image generation now supports ComfyUI.
|
530 |
+
- **🌟 Updated Light Theme**: Updated the light theme for a fresh look.
|
531 |
+
- **🌍 Additional Language Support**: Now supporting Bulgarian, Italian, Portuguese, Japanese, and Dutch.
|
532 |
+
|
533 |
+
### Fixed
|
534 |
+
|
535 |
+
- **🔧 Fixed Broken Experimental GGUF Upload**: Resolved issues with experimental GGUF upload functionality.
|
536 |
+
|
537 |
+
### Changed
|
538 |
+
|
539 |
+
- **🔄 Vector Storage Reset Button**: Moved the reset vector storage button to document settings.
|
540 |
+
|
541 |
+
## [0.1.114] - 2024-03-20
|
542 |
+
|
543 |
+
### Added
|
544 |
+
|
545 |
+
- **🔗 Webhook Integration**: Now you can subscribe to new user sign-up events via webhook. Simply navigate to the admin panel > admin settings > webhook URL.
|
546 |
+
- **🛡️ Enhanced Model Filtering**: Alongside Ollama, OpenAI proxy model whitelisting, we've added model filtering functionality for LiteLLM proxy.
|
547 |
+
- **🌍 Expanded Language Support**: Spanish, Catalan, and Vietnamese languages are now available, with improvements made to others.
|
548 |
+
|
549 |
+
### Fixed
|
550 |
+
|
551 |
+
- **🔧 Input Field Spelling**: Resolved issue with spelling mistakes in input fields.
|
552 |
+
- **🖊️ Light Mode Styling**: Fixed styling issue with light mode in document adding.
|
553 |
+
|
554 |
+
### Changed
|
555 |
+
|
556 |
+
- **🔄 Language Sorting**: Languages are now sorted alphabetically by their code for improved organization.
|
557 |
+
|
558 |
+
## [0.1.113] - 2024-03-18
|
559 |
+
|
560 |
+
### Added
|
561 |
+
|
562 |
+
- 🌍 **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).
|
563 |
+
- 🎨 **System-wide Theme**: Introducing a new system-wide theme for enhanced visual experience.
|
564 |
+
|
565 |
+
### Fixed
|
566 |
+
|
567 |
+
- 🌑 **Dark Background on Select Fields**: Improved readability by adding a dark background to select fields, addressing issues on certain browsers/devices.
|
568 |
+
- **Multiple OPENAI_API_BASE_URLS Issue**: Resolved issue where multiple base URLs caused conflicts when one wasn't functioning.
|
569 |
+
- **RAG Encoding Issue**: Fixed encoding problem in RAG.
|
570 |
+
- **npm Audit Fix**: Addressed npm audit findings.
|
571 |
+
- **Reduced Scroll Threshold**: Improved auto-scroll experience by reducing the scroll threshold from 50px to 5px.
|
572 |
+
|
573 |
+
### Changed
|
574 |
+
|
575 |
+
- 🔄 **Sidebar UI Update**: Updated sidebar UI to feature a chat menu dropdown, replacing two icons for improved navigation.
|
576 |
+
|
577 |
+
## [0.1.112] - 2024-03-15
|
578 |
+
|
579 |
+
### Fixed
|
580 |
+
|
581 |
+
- 🗨️ Resolved chat malfunction after image generation.
|
582 |
+
- 🎨 Fixed various RAG issues.
|
583 |
+
- 🧪 Rectified experimental broken GGUF upload logic.
|
584 |
+
|
585 |
+
## [0.1.111] - 2024-03-10
|
586 |
+
|
587 |
+
### Added
|
588 |
+
|
589 |
+
- 🛡️ **Model Whitelisting**: Admins now have the ability to whitelist models for users with the 'user' role.
|
590 |
+
- 🔄 **Update All Models**: Added a convenient button to update all models at once.
|
591 |
+
- 📄 **Toggle PDF OCR**: Users can now toggle PDF OCR option for improved parsing performance.
|
592 |
+
- 🎨 **DALL-E Integration**: Introduced DALL-E integration for image generation alongside automatic1111.
|
593 |
+
- 🛠️ **RAG API Refactoring**: Refactored RAG logic and exposed its API, with additional documentation to follow.
|
594 |
+
|
595 |
+
### Fixed
|
596 |
+
|
597 |
+
- 🔒 **Max Token Settings**: Added max token settings for anthropic/claude-3-sonnet-20240229 (Issue #1094).
|
598 |
+
- 🔧 **Misalignment Issue**: Corrected misalignment of Edit and Delete Icons when Chat Title is Empty (Issue #1104).
|
599 |
+
- 🔄 **Context Loss Fix**: Resolved RAG losing context on model response regeneration with Groq models via API key (Issue #1105).
|
600 |
+
- 📁 **File Handling Bug**: Addressed File Not Found Notification when Dropping a Conversation Element (Issue #1098).
|
601 |
+
- 🖱️ **Dragged File Styling**: Fixed dragged file layover styling issue.
|
602 |
+
|
603 |
+
## [0.1.110] - 2024-03-06
|
604 |
+
|
605 |
+
### Added
|
606 |
+
|
607 |
+
- **🌐 Multiple OpenAI Servers Support**: Enjoy seamless integration with multiple OpenAI-compatible APIs, now supported natively.
|
608 |
+
|
609 |
+
### Fixed
|
610 |
+
|
611 |
+
- **🔍 OCR Issue**: Resolved PDF parsing issue caused by OCR malfunction.
|
612 |
+
- **🚫 RAG Issue**: Fixed the RAG functionality, ensuring it operates smoothly.
|
613 |
+
- **📄 "Add Docs" Model Button**: Addressed the non-functional behavior of the "Add Docs" model button.
|
614 |
+
|
615 |
+
## [0.1.109] - 2024-03-06
|
616 |
+
|
617 |
+
### Added
|
618 |
+
|
619 |
+
- **🔄 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).
|
620 |
+
- **🔧 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).
|
621 |
+
- **🔍 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).
|
622 |
+
|
623 |
+
### Fixed
|
624 |
+
|
625 |
+
- **🛠️ RAG Collection**: Implemented a dynamic mechanism to recreate RAG collections, ensuring users have up-to-date and accurate data (#1031).
|
626 |
+
- **📝 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).
|
627 |
+
- **⏹️ Playground Cancel Functionality**: Introducing a new "Cancel" option for stopping Ollama generation in the Playground, enhancing user control and usability (#1006).
|
628 |
+
- **🔤 Typographical Error in 'ASSISTANT' Field**: Corrected a typographical error in the 'ASSISTANT' field within the GGUF model upload template for accuracy and consistency (#1061).
|
629 |
+
|
630 |
+
### Changed
|
631 |
+
|
632 |
+
- **🔄 Refactored Message Deletion Logic**: Streamlined message deletion process for improved efficiency and user experience, simplifying interactions within the platform (#1004).
|
633 |
+
- **⚠️ 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.
|
634 |
+
|
635 |
+
## [0.1.108] - 2024-03-02
|
636 |
+
|
637 |
+
### Added
|
638 |
+
|
639 |
+
- **🎮 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.
|
640 |
+
- **🛠️ Direct Database Download for Admins**: Admins can now download the database directly from the WebUI via the admin settings.
|
641 |
+
- **🎨 Additional RAG Settings**: Customize your RAG process with the ability to edit the TOP K value. Navigate to Documents > Settings > General to make changes.
|
642 |
+
- **🖥️ UI Improvements**: Tooltips now available in the input area and sidebar handle. More tooltips will be added across other parts of the UI.
|
643 |
+
|
644 |
+
### Fixed
|
645 |
+
|
646 |
+
- Resolved input autofocus issue on mobile when the sidebar is open, making it easier to use.
|
647 |
+
- Corrected numbered list display issue in Safari (#963).
|
648 |
+
- Restricted user ability to delete chats without proper permissions (#993).
|
649 |
+
|
650 |
+
### Changed
|
651 |
+
|
652 |
+
- **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.
|
653 |
+
- **Database Renaming**: Starting from this release, `ollama.db` will be automatically renamed to `webui.db`.
|
654 |
+
|
655 |
+
## [0.1.107] - 2024-03-01
|
656 |
+
|
657 |
+
### Added
|
658 |
+
|
659 |
+
- **🚀 Makefile and LLM Update Script**: Included Makefile and a script for LLM updates in the repository.
|
660 |
+
|
661 |
+
### Fixed
|
662 |
+
|
663 |
+
- Corrected issue where links in the settings modal didn't appear clickable (#960).
|
664 |
+
- Fixed problem with web UI port not taking effect due to incorrect environment variable name in run-compose.sh (#996).
|
665 |
+
- Enhanced user experience by displaying chat in browser title and enabling automatic scrolling to the bottom (#992).
|
666 |
+
|
667 |
+
### Changed
|
668 |
+
|
669 |
+
- Upgraded toast library from `svelte-french-toast` to `svelte-sonner` for a more polished UI.
|
670 |
+
- Enhanced accessibility with the addition of dark mode on the authentication page.
|
671 |
+
|
672 |
+
## [0.1.106] - 2024-02-27
|
673 |
+
|
674 |
+
### Added
|
675 |
+
|
676 |
+
- **🎯 Auto-focus Feature**: The input area now automatically focuses when initiating or opening a chat conversation.
|
677 |
+
|
678 |
+
### Fixed
|
679 |
+
|
680 |
+
- Corrected typo from "HuggingFace" to "Hugging Face" (Issue #924).
|
681 |
+
- Resolved bug causing errors in chat completion API calls to OpenAI due to missing "num_ctx" parameter (Issue #927).
|
682 |
+
- Fixed issues preventing text editing, selection, and cursor retention in the input field (Issue #940).
|
683 |
+
- 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)
|
684 |
+
|
685 |
+
## [0.1.105] - 2024-02-25
|
686 |
+
|
687 |
+
### Added
|
688 |
+
|
689 |
+
- **📄 Document Selection**: Now you can select and delete multiple documents at once for easier management.
|
690 |
+
|
691 |
+
### Changed
|
692 |
+
|
693 |
+
- **🏷️ 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.
|
694 |
+
|
695 |
+
## [0.1.104] - 2024-02-25
|
696 |
+
|
697 |
+
### Added
|
698 |
+
|
699 |
+
- **🔄 Check for Updates**: Keep your system current by checking for updates conveniently located in Settings > About.
|
700 |
+
- **🗑️ Automatic Tag Deletion**: Unused tags on the sidebar will now be deleted automatically with just a click.
|
701 |
+
|
702 |
+
### Changed
|
703 |
+
|
704 |
+
- **🎨 Modernized Styling**: Enjoy a refreshed look with updated styling for a more contemporary experience.
|
705 |
+
|
706 |
+
## [0.1.103] - 2024-02-25
|
707 |
+
|
708 |
+
### Added
|
709 |
+
|
710 |
+
- **🔗 Built-in LiteLLM Proxy**: Now includes LiteLLM proxy within Open WebUI for enhanced functionality.
|
711 |
+
|
712 |
+
- Easily integrate existing LiteLLM configurations using `-v /path/to/config.yaml:/app/backend/data/litellm/config.yaml` flag.
|
713 |
+
- When utilizing Docker container to run Open WebUI, ensure connections to localhost use `host.docker.internal`.
|
714 |
+
|
715 |
+
- **🖼️ Image Generation Enhancements**: Introducing Advanced Settings with Image Preview Feature.
|
716 |
+
- Customize image generation by setting the number of steps; defaults to A1111 value.
|
717 |
+
|
718 |
+
### Fixed
|
719 |
+
|
720 |
+
- Resolved issue with RAG scan halting document loading upon encountering unsupported MIME types or exceptions (Issue #866).
|
721 |
+
|
722 |
+
### Changed
|
723 |
+
|
724 |
+
- Ollama is no longer required to run Open WebUI.
|
725 |
+
- Access our comprehensive documentation at [Open WebUI Documentation](https://docs.openwebui.com/).
|
726 |
+
|
727 |
+
## [0.1.102] - 2024-02-22
|
728 |
+
|
729 |
+
### Added
|
730 |
+
|
731 |
+
- **🖼️ Image Generation**: Generate Images using the AUTOMATIC1111/stable-diffusion-webui API. You can set this up in Settings > Images.
|
732 |
+
- **📝 Change title generation prompt**: Change the prompt used to generate titles for your chats. You can set this up in the Settings > Interface.
|
733 |
+
- **🤖 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.
|
734 |
+
- **📢 CHANGELOG.md/Popup**: This popup will show you the latest changes.
|
735 |
+
|
736 |
+
## [0.1.101] - 2024-02-22
|
737 |
+
|
738 |
+
### Fixed
|
739 |
+
|
740 |
+
- LaTex output formatting issue (#828)
|
741 |
+
|
742 |
+
### Changed
|
743 |
+
|
744 |
+
- Instead of having the previous 1.0.0-alpha.101, we switched to semantic versioning as a way to respect global conventions.
|
CODE_OF_CONDUCT.md
ADDED
@@ -0,0 +1,77 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Contributor Covenant Code of Conduct
|
2 |
+
|
3 |
+
## Our Pledge
|
4 |
+
|
5 |
+
We as members, contributors, and leaders pledge to make participation in our
|
6 |
+
community a harassment-free experience for everyone, regardless of age, body
|
7 |
+
size, visible or invisible disability, ethnicity, sex characteristics, gender
|
8 |
+
identity and expression, level of experience, education, socio-economic status,
|
9 |
+
nationality, personal appearance, race, religion, or sexual identity
|
10 |
+
and orientation.
|
11 |
+
|
12 |
+
We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community.
|
13 |
+
|
14 |
+
## Our Standards
|
15 |
+
|
16 |
+
Examples of behavior that contribute to a positive environment for our community include:
|
17 |
+
|
18 |
+
- Demonstrating empathy and kindness toward other people
|
19 |
+
- Being respectful of differing opinions, viewpoints, and experiences
|
20 |
+
- Giving and gracefully accepting constructive feedback
|
21 |
+
- Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience
|
22 |
+
- Focusing on what is best not just for us as individuals, but for the overall community
|
23 |
+
|
24 |
+
Examples of unacceptable behavior include:
|
25 |
+
|
26 |
+
- The use of sexualized language or imagery, and sexual attention or advances of any kind
|
27 |
+
- Trolling, insulting or derogatory comments, and personal or political attacks
|
28 |
+
- Public or private harassment
|
29 |
+
- Publishing others' private information, such as a physical or email address, without their explicit permission
|
30 |
+
- **Spamming of any kind**
|
31 |
+
- Aggressive sales tactics targeting our community members are strictly prohibited. You can mention your product if it's relevant to the discussion, but under no circumstances should you push it forcefully
|
32 |
+
- Other conduct which could reasonably be considered inappropriate in a professional setting
|
33 |
+
|
34 |
+
## Enforcement Responsibilities
|
35 |
+
|
36 |
+
Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful.
|
37 |
+
|
38 |
+
## Scope
|
39 |
+
|
40 |
+
This Code of Conduct applies within all community spaces and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event.
|
41 |
+
|
42 |
+
## Enforcement
|
43 |
+
|
44 |
+
Instances of abusive, harassing, spamming, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at hello@openwebui.com. All complaints will be reviewed and investigated promptly and fairly.
|
45 |
+
|
46 |
+
All community leaders are obligated to respect the privacy and security of the reporter of any incident.
|
47 |
+
|
48 |
+
## Enforcement Guidelines
|
49 |
+
|
50 |
+
Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct:
|
51 |
+
|
52 |
+
### 1. Temporary Ban
|
53 |
+
|
54 |
+
**Community Impact**: Any violation of community standards, including but not limited to inappropriate language, unprofessional behavior, harassment, or spamming.
|
55 |
+
|
56 |
+
**Consequence**: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban.
|
57 |
+
|
58 |
+
### 2. Permanent Ban
|
59 |
+
|
60 |
+
**Community Impact**: Repeated or severe violations of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals.
|
61 |
+
|
62 |
+
**Consequence**: A permanent ban from any sort of public interaction within the community.
|
63 |
+
|
64 |
+
## Attribution
|
65 |
+
|
66 |
+
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
|
67 |
+
version 2.0, available at
|
68 |
+
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
|
69 |
+
|
70 |
+
Community Impact Guidelines were inspired by [Mozilla's code of conduct
|
71 |
+
enforcement ladder](https://github.com/mozilla/diversity).
|
72 |
+
|
73 |
+
[homepage]: https://www.contributor-covenant.org
|
74 |
+
|
75 |
+
For answers to common questions about this code of conduct, see the FAQ at
|
76 |
+
https://www.contributor-covenant.org/faq. Translations are available at
|
77 |
+
https://www.contributor-covenant.org/translations.
|
Caddyfile.localhost
ADDED
@@ -0,0 +1,64 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Run with
|
2 |
+
# caddy run --envfile ./example.env --config ./Caddyfile.localhost
|
3 |
+
#
|
4 |
+
# This is configured for
|
5 |
+
# - Automatic HTTPS (even for localhost)
|
6 |
+
# - Reverse Proxying to Ollama API Base URL (http://localhost:11434/api)
|
7 |
+
# - CORS
|
8 |
+
# - HTTP Basic Auth API Tokens (uncomment basicauth section)
|
9 |
+
|
10 |
+
|
11 |
+
# CORS Preflight (OPTIONS) + Request (GET, POST, PATCH, PUT, DELETE)
|
12 |
+
(cors-api) {
|
13 |
+
@match-cors-api-preflight method OPTIONS
|
14 |
+
handle @match-cors-api-preflight {
|
15 |
+
header {
|
16 |
+
Access-Control-Allow-Origin "{http.request.header.origin}"
|
17 |
+
Access-Control-Allow-Methods "GET, POST, PUT, PATCH, DELETE, OPTIONS"
|
18 |
+
Access-Control-Allow-Headers "Origin, Accept, Authorization, Content-Type, X-Requested-With"
|
19 |
+
Access-Control-Allow-Credentials "true"
|
20 |
+
Access-Control-Max-Age "3600"
|
21 |
+
defer
|
22 |
+
}
|
23 |
+
respond "" 204
|
24 |
+
}
|
25 |
+
|
26 |
+
@match-cors-api-request {
|
27 |
+
not {
|
28 |
+
header Origin "{http.request.scheme}://{http.request.host}"
|
29 |
+
}
|
30 |
+
header Origin "{http.request.header.origin}"
|
31 |
+
}
|
32 |
+
handle @match-cors-api-request {
|
33 |
+
header {
|
34 |
+
Access-Control-Allow-Origin "{http.request.header.origin}"
|
35 |
+
Access-Control-Allow-Methods "GET, POST, PUT, PATCH, DELETE, OPTIONS"
|
36 |
+
Access-Control-Allow-Headers "Origin, Accept, Authorization, Content-Type, X-Requested-With"
|
37 |
+
Access-Control-Allow-Credentials "true"
|
38 |
+
Access-Control-Max-Age "3600"
|
39 |
+
defer
|
40 |
+
}
|
41 |
+
}
|
42 |
+
}
|
43 |
+
|
44 |
+
# replace localhost with example.com or whatever
|
45 |
+
localhost {
|
46 |
+
## HTTP Basic Auth
|
47 |
+
## (uncomment to enable)
|
48 |
+
# basicauth {
|
49 |
+
# # see .example.env for how to generate tokens
|
50 |
+
# {env.OLLAMA_API_ID} {env.OLLAMA_API_TOKEN_DIGEST}
|
51 |
+
# }
|
52 |
+
|
53 |
+
handle /api/* {
|
54 |
+
# Comment to disable CORS
|
55 |
+
import cors-api
|
56 |
+
|
57 |
+
reverse_proxy localhost:11434
|
58 |
+
}
|
59 |
+
|
60 |
+
# Same-Origin Static Web Server
|
61 |
+
file_server {
|
62 |
+
root ./build/
|
63 |
+
}
|
64 |
+
}
|
Dockerfile
ADDED
@@ -0,0 +1,161 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# syntax=docker/dockerfile:1
|
2 |
+
# Initialize device type args
|
3 |
+
# use build args in the docker build commmand with --build-arg="BUILDARG=true"
|
4 |
+
ARG USE_CUDA=false
|
5 |
+
ARG USE_OLLAMA=false
|
6 |
+
# Tested with cu117 for CUDA 11 and cu121 for CUDA 12 (default)
|
7 |
+
ARG USE_CUDA_VER=cu121
|
8 |
+
# any sentence transformer model; models to use can be found at https://huggingface.co/models?library=sentence-transformers
|
9 |
+
# Leaderboard: https://huggingface.co/spaces/mteb/leaderboard
|
10 |
+
# for better performance and multilangauge support use "intfloat/multilingual-e5-large" (~2.5GB) or "intfloat/multilingual-e5-base" (~1.5GB)
|
11 |
+
# IMPORTANT: If you change the embedding model (sentence-transformers/all-MiniLM-L6-v2) and vice versa, you aren't able to use RAG Chat with your previous documents loaded in the WebUI! You need to re-embed them.
|
12 |
+
ARG USE_EMBEDDING_MODEL=sentence-transformers/all-MiniLM-L6-v2
|
13 |
+
ARG USE_RERANKING_MODEL=""
|
14 |
+
ARG BUILD_HASH=dev-build
|
15 |
+
# Override at your own risk - non-root configurations are untested
|
16 |
+
ARG UID=0
|
17 |
+
ARG GID=0
|
18 |
+
|
19 |
+
######## WebUI frontend ########
|
20 |
+
FROM --platform=$BUILDPLATFORM node:21-alpine3.19 as build
|
21 |
+
ARG BUILD_HASH
|
22 |
+
|
23 |
+
WORKDIR /app
|
24 |
+
|
25 |
+
COPY package.json package-lock.json ./
|
26 |
+
RUN npm ci
|
27 |
+
|
28 |
+
COPY . .
|
29 |
+
ENV APP_BUILD_HASH=${BUILD_HASH}
|
30 |
+
RUN npm run build
|
31 |
+
|
32 |
+
######## WebUI backend ########
|
33 |
+
FROM python:3.11-slim-bookworm as base
|
34 |
+
|
35 |
+
# Use args
|
36 |
+
ARG USE_CUDA
|
37 |
+
ARG USE_OLLAMA
|
38 |
+
ARG USE_CUDA_VER
|
39 |
+
ARG USE_EMBEDDING_MODEL
|
40 |
+
ARG USE_RERANKING_MODEL
|
41 |
+
ARG UID
|
42 |
+
ARG GID
|
43 |
+
|
44 |
+
## Basis ##
|
45 |
+
ENV ENV=prod \
|
46 |
+
PORT=8080 \
|
47 |
+
# pass build args to the build
|
48 |
+
USE_OLLAMA_DOCKER=${USE_OLLAMA} \
|
49 |
+
USE_CUDA_DOCKER=${USE_CUDA} \
|
50 |
+
USE_CUDA_DOCKER_VER=${USE_CUDA_VER} \
|
51 |
+
USE_EMBEDDING_MODEL_DOCKER=${USE_EMBEDDING_MODEL} \
|
52 |
+
USE_RERANKING_MODEL_DOCKER=${USE_RERANKING_MODEL}
|
53 |
+
|
54 |
+
## Basis URL Config ##
|
55 |
+
ENV OLLAMA_BASE_URL="/ollama" \
|
56 |
+
OPENAI_API_BASE_URL=""
|
57 |
+
|
58 |
+
## API Key and Security Config ##
|
59 |
+
ENV OPENAI_API_KEY="" \
|
60 |
+
WEBUI_SECRET_KEY="" \
|
61 |
+
SCARF_NO_ANALYTICS=true \
|
62 |
+
DO_NOT_TRACK=true \
|
63 |
+
ANONYMIZED_TELEMETRY=false
|
64 |
+
|
65 |
+
#### Other models #########################################################
|
66 |
+
## whisper TTS model settings ##
|
67 |
+
ENV WHISPER_MODEL="base" \
|
68 |
+
WHISPER_MODEL_DIR="/app/backend/data/cache/whisper/models"
|
69 |
+
|
70 |
+
## RAG Embedding model settings ##
|
71 |
+
ENV RAG_EMBEDDING_MODEL="$USE_EMBEDDING_MODEL_DOCKER" \
|
72 |
+
RAG_RERANKING_MODEL="$USE_RERANKING_MODEL_DOCKER" \
|
73 |
+
SENTENCE_TRANSFORMERS_HOME="/app/backend/data/cache/embedding/models"
|
74 |
+
|
75 |
+
## Hugging Face download cache ##
|
76 |
+
ENV HF_HOME="/app/backend/data/cache/embedding/models"
|
77 |
+
#### Other models ##########################################################
|
78 |
+
|
79 |
+
WORKDIR /app/backend
|
80 |
+
|
81 |
+
ENV HOME /root
|
82 |
+
# Create user and group if not root
|
83 |
+
RUN if [ $UID -ne 0 ]; then \
|
84 |
+
if [ $GID -ne 0 ]; then \
|
85 |
+
addgroup --gid $GID app; \
|
86 |
+
fi; \
|
87 |
+
adduser --uid $UID --gid $GID --home $HOME --disabled-password --no-create-home app; \
|
88 |
+
fi
|
89 |
+
|
90 |
+
RUN mkdir -p $HOME/.cache/chroma
|
91 |
+
RUN echo -n 00000000-0000-0000-0000-000000000000 > $HOME/.cache/chroma/telemetry_user_id
|
92 |
+
|
93 |
+
# Make sure the user has access to the app and root directory
|
94 |
+
RUN chown -R $UID:$GID /app $HOME
|
95 |
+
|
96 |
+
RUN if [ "$USE_OLLAMA" = "true" ]; then \
|
97 |
+
apt-get update && \
|
98 |
+
# Install pandoc and netcat
|
99 |
+
apt-get install -y --no-install-recommends pandoc netcat-openbsd curl && \
|
100 |
+
apt-get install -y --no-install-recommends gcc python3-dev && \
|
101 |
+
# for RAG OCR
|
102 |
+
apt-get install -y --no-install-recommends ffmpeg libsm6 libxext6 && \
|
103 |
+
# install helper tools
|
104 |
+
apt-get install -y --no-install-recommends curl jq && \
|
105 |
+
# install ollama
|
106 |
+
curl -fsSL https://ollama.com/install.sh | sh && \
|
107 |
+
# cleanup
|
108 |
+
rm -rf /var/lib/apt/lists/*; \
|
109 |
+
else \
|
110 |
+
apt-get update && \
|
111 |
+
# Install pandoc, netcat and gcc
|
112 |
+
apt-get install -y --no-install-recommends pandoc gcc netcat-openbsd curl jq && \
|
113 |
+
apt-get install -y --no-install-recommends gcc python3-dev && \
|
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 |
+
ARG BUILD_HASH
|
159 |
+
ENV WEBUI_BUILD_VERSION=${BUILD_HASH}
|
160 |
+
|
161 |
+
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,211 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
---
|
2 |
+
title: Open WebUI
|
3 |
+
emoji: 🐳
|
4 |
+
colorFrom: purple
|
5 |
+
colorTo: gray
|
6 |
+
sdk: docker
|
7 |
+
app_port: 8080
|
8 |
+
---
|
9 |
+
# Open WebUI (Formerly Ollama WebUI) 👋
|
10 |
+
|
11 |
+
![GitHub stars](https://img.shields.io/github/stars/open-webui/open-webui?style=social)
|
12 |
+
![GitHub forks](https://img.shields.io/github/forks/open-webui/open-webui?style=social)
|
13 |
+
![GitHub watchers](https://img.shields.io/github/watchers/open-webui/open-webui?style=social)
|
14 |
+
![GitHub repo size](https://img.shields.io/github/repo-size/open-webui/open-webui)
|
15 |
+
![GitHub language count](https://img.shields.io/github/languages/count/open-webui/open-webui)
|
16 |
+
![GitHub top language](https://img.shields.io/github/languages/top/open-webui/open-webui)
|
17 |
+
![GitHub last commit](https://img.shields.io/github/last-commit/open-webui/open-webui?color=red)
|
18 |
+
![Hits](https://hits.seeyoufarm.com/api/count/incr/badge.svg?url=https%3A%2F%2Fgithub.com%2Follama-webui%2Follama-wbui&count_bg=%2379C83D&title_bg=%23555555&icon=&icon_color=%23E7E7E7&title=hits&edge_flat=false)
|
19 |
+
[![Discord](https://img.shields.io/badge/Discord-Open_WebUI-blue?logo=discord&logoColor=white)](https://discord.gg/5rJgQTnV4s)
|
20 |
+
[![](https://img.shields.io/static/v1?label=Sponsor&message=%E2%9D%A4&logo=GitHub&color=%23fe8e86)](https://github.com/sponsors/tjbck)
|
21 |
+
|
22 |
+
Open WebUI is an [extensible](https://github.com/open-webui/pipelines), feature-rich, and user-friendly self-hosted WebUI designed to operate entirely offline. It supports various LLM runners, including Ollama and OpenAI-compatible APIs. For more information, be sure to check out our [Open WebUI Documentation](https://docs.openwebui.com/).
|
23 |
+
|
24 |
+
![Open WebUI Demo](./demo.gif)
|
25 |
+
|
26 |
+
## Key Features of Open WebUI ⭐
|
27 |
+
|
28 |
+
- 🚀 **Effortless Setup**: Install seamlessly using Docker or Kubernetes (kubectl, kustomize or helm) for a hassle-free experience with support for both `:ollama` and `:cuda` tagged images.
|
29 |
+
|
30 |
+
- 🤝 **Ollama/OpenAI API Integration**: Effortlessly integrate OpenAI-compatible APIs for versatile conversations alongside Ollama models. Customize the OpenAI API URL to link with **LMStudio, GroqCloud, Mistral, OpenRouter, and more**.
|
31 |
+
|
32 |
+
- 🧩 **Pipelines, Open WebUI Plugin Support**: Seamlessly integrate custom logic and Python libraries into Open WebUI using [Pipelines Plugin Framework](https://github.com/open-webui/pipelines). Launch your Pipelines instance, set the OpenAI URL to the Pipelines URL, and explore endless possibilities. [Examples](https://github.com/open-webui/pipelines/tree/main/examples) include **Function Calling**, User **Rate Limiting** to control access, **Usage Monitoring** with tools like Langfuse, **Live Translation with LibreTranslate** for multilingual support, **Toxic Message Filtering** and much more.
|
33 |
+
|
34 |
+
- 📱 **Responsive Design**: Enjoy a seamless experience across Desktop PC, Laptop, and Mobile devices.
|
35 |
+
|
36 |
+
- 📱 **Progressive Web App (PWA) for Mobile**: Enjoy a native app-like experience on your mobile device with our PWA, providing offline access on localhost and a seamless user interface.
|
37 |
+
|
38 |
+
- ✒️🔢 **Full Markdown and LaTeX Support**: Elevate your LLM experience with comprehensive Markdown and LaTeX capabilities for enriched interaction.
|
39 |
+
|
40 |
+
- 🎤📹 **Hands-Free Voice/Video Call**: Experience seamless communication with integrated hands-free voice and video call features, allowing for a more dynamic and interactive chat environment.
|
41 |
+
|
42 |
+
- 🛠️ **Model Builder**: Easily create Ollama models via the Web UI. Create and add custom characters/agents, customize chat elements, and import models effortlessly through [Open WebUI Community](https://openwebui.com/) integration.
|
43 |
+
|
44 |
+
- 🐍 **Native Python Function Calling Tool**: Enhance your LLMs with built-in code editor support in the tools workspace. Bring Your Own Function (BYOF) by simply adding your pure Python functions, enabling seamless integration with LLMs.
|
45 |
+
|
46 |
+
- 📚 **Local RAG Integration**: Dive into the future of chat interactions with groundbreaking Retrieval Augmented Generation (RAG) support. This feature seamlessly integrates document interactions into your chat experience. You can load documents directly into the chat or add files to your document library, effortlessly accessing them using the `#` command before a query.
|
47 |
+
|
48 |
+
- 🔍 **Web Search for RAG**: Perform web searches using providers like `SearXNG`, `Google PSE`, `Brave Search`, `serpstack`, `serper`, `Serply`, `DuckDuckGo` and `TavilySearch` and inject the results directly into your chat experience.
|
49 |
+
|
50 |
+
- 🌐 **Web Browsing Capability**: Seamlessly integrate websites into your chat experience using the `#` command followed by a URL. This feature allows you to incorporate web content directly into your conversations, enhancing the richness and depth of your interactions.
|
51 |
+
|
52 |
+
- 🎨 **Image Generation Integration**: Seamlessly incorporate image generation capabilities using options such as AUTOMATIC1111 API or ComfyUI (local), and OpenAI's DALL-E (external), enriching your chat experience with dynamic visual content.
|
53 |
+
|
54 |
+
- ⚙️ **Many Models Conversations**: Effortlessly engage with various models simultaneously, harnessing their unique strengths for optimal responses. Enhance your experience by leveraging a diverse set of models in parallel.
|
55 |
+
|
56 |
+
- 🔐 **Role-Based Access Control (RBAC)**: Ensure secure access with restricted permissions; only authorized individuals can access your Ollama, and exclusive model creation/pulling rights are reserved for administrators.
|
57 |
+
|
58 |
+
- 🌐🌍 **Multilingual Support**: Experience Open WebUI in your preferred language with our internationalization (i18n) support. Join us in expanding our supported languages! We're actively seeking contributors!
|
59 |
+
|
60 |
+
- 🌟 **Continuous Updates**: We are committed to improving Open WebUI with regular updates, fixes, and new features.
|
61 |
+
|
62 |
+
Want to learn more about Open WebUI's features? Check out our [Open WebUI documentation](https://docs.openwebui.com/features) for a comprehensive overview!
|
63 |
+
|
64 |
+
## 🔗 Also Check Out Open WebUI Community!
|
65 |
+
|
66 |
+
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! 🚀
|
67 |
+
|
68 |
+
## How to Install 🚀
|
69 |
+
|
70 |
+
> [!NOTE]
|
71 |
+
> 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.
|
72 |
+
|
73 |
+
### Quick Start with Docker 🐳
|
74 |
+
|
75 |
+
> [!WARNING]
|
76 |
+
> 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.
|
77 |
+
|
78 |
+
> [!TIP]
|
79 |
+
> 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.
|
80 |
+
|
81 |
+
### Installation with Default Configuration
|
82 |
+
|
83 |
+
- **If Ollama is on your computer**, use this command:
|
84 |
+
|
85 |
+
```bash
|
86 |
+
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
|
87 |
+
```
|
88 |
+
|
89 |
+
- **If Ollama is on a Different Server**, use this command:
|
90 |
+
|
91 |
+
To connect to Ollama on another server, change the `OLLAMA_BASE_URL` to the server's URL:
|
92 |
+
|
93 |
+
```bash
|
94 |
+
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
|
95 |
+
```
|
96 |
+
|
97 |
+
- **To run Open WebUI with Nvidia GPU support**, use this command:
|
98 |
+
|
99 |
+
```bash
|
100 |
+
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
|
101 |
+
```
|
102 |
+
|
103 |
+
### Installation for OpenAI API Usage Only
|
104 |
+
|
105 |
+
- **If you're only using OpenAI API**, use this command:
|
106 |
+
|
107 |
+
```bash
|
108 |
+
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
|
109 |
+
```
|
110 |
+
|
111 |
+
### Installing Open WebUI with Bundled Ollama Support
|
112 |
+
|
113 |
+
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:
|
114 |
+
|
115 |
+
- **With GPU Support**:
|
116 |
+
Utilize GPU resources by running the following command:
|
117 |
+
|
118 |
+
```bash
|
119 |
+
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
|
120 |
+
```
|
121 |
+
|
122 |
+
- **For CPU Only**:
|
123 |
+
If you're not using a GPU, use this command instead:
|
124 |
+
|
125 |
+
```bash
|
126 |
+
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
|
127 |
+
```
|
128 |
+
|
129 |
+
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.
|
130 |
+
|
131 |
+
After installation, you can access Open WebUI at [http://localhost:3000](http://localhost:3000). Enjoy! 😄
|
132 |
+
|
133 |
+
### Other Installation Methods
|
134 |
+
|
135 |
+
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.
|
136 |
+
|
137 |
+
### Troubleshooting
|
138 |
+
|
139 |
+
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).
|
140 |
+
|
141 |
+
#### Open WebUI: Server Connection Error
|
142 |
+
|
143 |
+
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`.
|
144 |
+
|
145 |
+
**Example Docker Command**:
|
146 |
+
|
147 |
+
```bash
|
148 |
+
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
|
149 |
+
```
|
150 |
+
|
151 |
+
### Keeping Your Docker Installation Up-to-Date
|
152 |
+
|
153 |
+
In case you want to update your local Docker installation to the latest version, you can do it with [Watchtower](https://containrrr.dev/watchtower/):
|
154 |
+
|
155 |
+
```bash
|
156 |
+
docker run --rm --volume /var/run/docker.sock:/var/run/docker.sock containrrr/watchtower --run-once open-webui
|
157 |
+
```
|
158 |
+
|
159 |
+
In the last part of the command, replace `open-webui` with your container name if it is different.
|
160 |
+
|
161 |
+
Check our Migration Guide available in our [Open WebUI Documentation](https://docs.openwebui.com/migration/).
|
162 |
+
|
163 |
+
### Using the Dev Branch 🌙
|
164 |
+
|
165 |
+
> [!WARNING]
|
166 |
+
> The `:dev` branch contains the latest unstable features and changes. Use it at your own risk as it may have bugs or incomplete features.
|
167 |
+
|
168 |
+
If you want to try out the latest bleeding-edge features and are okay with occasional instability, you can use the `:dev` tag like this:
|
169 |
+
|
170 |
+
```bash
|
171 |
+
docker run -d -p 3000:8080 -v open-webui:/app/backend/data --name open-webui --add-host=host.docker.internal:host-gateway --restart always ghcr.io/open-webui/open-webui:dev
|
172 |
+
```
|
173 |
+
|
174 |
+
## What's Next? 🌟
|
175 |
+
|
176 |
+
Discover upcoming features on our roadmap in the [Open WebUI Documentation](https://docs.openwebui.com/roadmap/).
|
177 |
+
|
178 |
+
## Supporters ✨
|
179 |
+
|
180 |
+
A big shoutout to our amazing supporters who's helping to make this project possible! 🙏
|
181 |
+
|
182 |
+
### Platinum Sponsors 🤍
|
183 |
+
|
184 |
+
- We're looking for Sponsors!
|
185 |
+
|
186 |
+
### Acknowledgments
|
187 |
+
|
188 |
+
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! 🙌
|
189 |
+
|
190 |
+
## License 📜
|
191 |
+
|
192 |
+
This project is licensed under the [MIT License](LICENSE) - see the [LICENSE](LICENSE) file for details. 📄
|
193 |
+
|
194 |
+
## Support 💬
|
195 |
+
|
196 |
+
If you have any questions, suggestions, or need assistance, please open an issue or join our
|
197 |
+
[Open WebUI Discord community](https://discord.gg/5rJgQTnV4s) to connect with us! 🤝
|
198 |
+
|
199 |
+
## Star History
|
200 |
+
|
201 |
+
<a href="https://star-history.com/#open-webui/open-webui&Date">
|
202 |
+
<picture>
|
203 |
+
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=open-webui/open-webui&type=Date&theme=dark" />
|
204 |
+
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=open-webui/open-webui&type=Date" />
|
205 |
+
<img alt="Star History Chart" src="https://api.star-history.com/svg?repos=open-webui/open-webui&type=Date" />
|
206 |
+
</picture>
|
207 |
+
</a>
|
208 |
+
|
209 |
+
---
|
210 |
+
|
211 |
+
Created by [Timothy J. Baek](https://github.com/tjbck) - Let's make Open WebUI even more amazing together! 💪
|
TROUBLESHOOTING.md
ADDED
@@ -0,0 +1,36 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Open WebUI Troubleshooting Guide
|
2 |
+
|
3 |
+
## Understanding the Open WebUI Architecture
|
4 |
+
|
5 |
+
The Open WebUI system is designed to streamline interactions between the client (your browser) and the Ollama API. At the heart of this design is a backend reverse proxy, enhancing security and resolving CORS issues.
|
6 |
+
|
7 |
+
- **How it Works**: The Open WebUI is designed to interact with the Ollama API through a specific route. When a request is made from the WebUI to Ollama, it is not directly sent to the Ollama API. Initially, the request is sent to the Open WebUI backend via `/ollama` route. From there, the backend is responsible for forwarding the request to the Ollama API. This forwarding is accomplished by using the route specified in the `OLLAMA_BASE_URL` environment variable. Therefore, a request made to `/ollama` in the WebUI is effectively the same as making a request to `OLLAMA_BASE_URL` in the backend. For instance, a request to `/ollama/api/tags` in the WebUI is equivalent to `OLLAMA_BASE_URL/api/tags` in the backend.
|
8 |
+
|
9 |
+
- **Security Benefits**: This design prevents direct exposure of the Ollama API to the frontend, safeguarding against potential CORS (Cross-Origin Resource Sharing) issues and unauthorized access. Requiring authentication to access the Ollama API further enhances this security layer.
|
10 |
+
|
11 |
+
## Open WebUI: Server Connection Error
|
12 |
+
|
13 |
+
If you're experiencing connection issues, it’s often due to the WebUI docker container not being able to reach the Ollama server at 127.0.0.1:11434 (host.docker.internal:11434) inside the container . Use the `--network=host` flag in your docker command to resolve this. Note that the port changes from 3000 to 8080, resulting in the link: `http://localhost:8080`.
|
14 |
+
|
15 |
+
**Example Docker Command**:
|
16 |
+
|
17 |
+
```bash
|
18 |
+
docker run -d --network=host -v open-webui:/app/backend/data -e OLLAMA_BASE_URL=http://127.0.0.1:11434 --name open-webui --restart always ghcr.io/open-webui/open-webui:main
|
19 |
+
```
|
20 |
+
|
21 |
+
### Error on Slow Reponses for Ollama
|
22 |
+
|
23 |
+
Open WebUI has a default timeout of 5 minutes for Ollama to finish generating the response. If needed, this can be adjusted via the environment variable AIOHTTP_CLIENT_TIMEOUT, which sets the timeout in seconds.
|
24 |
+
|
25 |
+
### General Connection Errors
|
26 |
+
|
27 |
+
**Ensure Ollama Version is Up-to-Date**: Always start by checking that you have the latest version of Ollama. Visit [Ollama's official site](https://ollama.com/) for the latest updates.
|
28 |
+
|
29 |
+
**Troubleshooting Steps**:
|
30 |
+
|
31 |
+
1. **Verify Ollama URL Format**:
|
32 |
+
- When running the Web UI container, ensure the `OLLAMA_BASE_URL` is correctly set. (e.g., `http://192.168.1.1:11434` for different host setups).
|
33 |
+
- In the Open WebUI, navigate to "Settings" > "General".
|
34 |
+
- Confirm that the Ollama Server URL is correctly set to `[OLLAMA URL]` (e.g., `http://localhost:11434`).
|
35 |
+
|
36 |
+
By following these enhanced troubleshooting steps, connection issues should be effectively resolved. For further assistance or queries, feel free to reach out to us on our community Discord.
|
backend/.dockerignore
ADDED
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
__pycache__
|
2 |
+
.env
|
3 |
+
_old
|
4 |
+
uploads
|
5 |
+
.ipynb_checkpoints
|
6 |
+
*.db
|
7 |
+
_test
|
8 |
+
!/data
|
9 |
+
/data/*
|
10 |
+
!/data/litellm
|
11 |
+
/data/litellm/*
|
12 |
+
!data/litellm/config.yaml
|
13 |
+
|
14 |
+
!data/config.json
|
backend/.gitignore
ADDED
@@ -0,0 +1,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/alembic.ini
ADDED
@@ -0,0 +1,114 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# A generic, single database configuration.
|
2 |
+
|
3 |
+
[alembic]
|
4 |
+
# path to migration scripts
|
5 |
+
script_location = migrations
|
6 |
+
|
7 |
+
# template used to generate migration file names; The default value is %%(rev)s_%%(slug)s
|
8 |
+
# Uncomment the line below if you want the files to be prepended with date and time
|
9 |
+
# file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s
|
10 |
+
|
11 |
+
# sys.path path, will be prepended to sys.path if present.
|
12 |
+
# defaults to the current working directory.
|
13 |
+
prepend_sys_path = .
|
14 |
+
|
15 |
+
# timezone to use when rendering the date within the migration file
|
16 |
+
# as well as the filename.
|
17 |
+
# If specified, requires the python>=3.9 or backports.zoneinfo library.
|
18 |
+
# Any required deps can installed by adding `alembic[tz]` to the pip requirements
|
19 |
+
# string value is passed to ZoneInfo()
|
20 |
+
# leave blank for localtime
|
21 |
+
# timezone =
|
22 |
+
|
23 |
+
# max length of characters to apply to the
|
24 |
+
# "slug" field
|
25 |
+
# truncate_slug_length = 40
|
26 |
+
|
27 |
+
# set to 'true' to run the environment during
|
28 |
+
# the 'revision' command, regardless of autogenerate
|
29 |
+
# revision_environment = false
|
30 |
+
|
31 |
+
# set to 'true' to allow .pyc and .pyo files without
|
32 |
+
# a source .py file to be detected as revisions in the
|
33 |
+
# versions/ directory
|
34 |
+
# sourceless = false
|
35 |
+
|
36 |
+
# version location specification; This defaults
|
37 |
+
# to migrations/versions. When using multiple version
|
38 |
+
# directories, initial revisions must be specified with --version-path.
|
39 |
+
# The path separator used here should be the separator specified by "version_path_separator" below.
|
40 |
+
# version_locations = %(here)s/bar:%(here)s/bat:migrations/versions
|
41 |
+
|
42 |
+
# version path separator; As mentioned above, this is the character used to split
|
43 |
+
# version_locations. The default within new alembic.ini files is "os", which uses os.pathsep.
|
44 |
+
# If this key is omitted entirely, it falls back to the legacy behavior of splitting on spaces and/or commas.
|
45 |
+
# Valid values for version_path_separator are:
|
46 |
+
#
|
47 |
+
# version_path_separator = :
|
48 |
+
# version_path_separator = ;
|
49 |
+
# version_path_separator = space
|
50 |
+
version_path_separator = os # Use os.pathsep. Default configuration used for new projects.
|
51 |
+
|
52 |
+
# set to 'true' to search source files recursively
|
53 |
+
# in each "version_locations" directory
|
54 |
+
# new in Alembic version 1.10
|
55 |
+
# recursive_version_locations = false
|
56 |
+
|
57 |
+
# the output encoding used when revision files
|
58 |
+
# are written from script.py.mako
|
59 |
+
# output_encoding = utf-8
|
60 |
+
|
61 |
+
# sqlalchemy.url = REPLACE_WITH_DATABASE_URL
|
62 |
+
|
63 |
+
|
64 |
+
[post_write_hooks]
|
65 |
+
# post_write_hooks defines scripts or Python functions that are run
|
66 |
+
# on newly generated revision scripts. See the documentation for further
|
67 |
+
# detail and examples
|
68 |
+
|
69 |
+
# format using "black" - use the console_scripts runner, against the "black" entrypoint
|
70 |
+
# hooks = black
|
71 |
+
# black.type = console_scripts
|
72 |
+
# black.entrypoint = black
|
73 |
+
# black.options = -l 79 REVISION_SCRIPT_FILENAME
|
74 |
+
|
75 |
+
# lint with attempts to fix using "ruff" - use the exec runner, execute a binary
|
76 |
+
# hooks = ruff
|
77 |
+
# ruff.type = exec
|
78 |
+
# ruff.executable = %(here)s/.venv/bin/ruff
|
79 |
+
# ruff.options = --fix REVISION_SCRIPT_FILENAME
|
80 |
+
|
81 |
+
# Logging configuration
|
82 |
+
[loggers]
|
83 |
+
keys = root,sqlalchemy,alembic
|
84 |
+
|
85 |
+
[handlers]
|
86 |
+
keys = console
|
87 |
+
|
88 |
+
[formatters]
|
89 |
+
keys = generic
|
90 |
+
|
91 |
+
[logger_root]
|
92 |
+
level = WARN
|
93 |
+
handlers = console
|
94 |
+
qualname =
|
95 |
+
|
96 |
+
[logger_sqlalchemy]
|
97 |
+
level = WARN
|
98 |
+
handlers =
|
99 |
+
qualname = sqlalchemy.engine
|
100 |
+
|
101 |
+
[logger_alembic]
|
102 |
+
level = INFO
|
103 |
+
handlers =
|
104 |
+
qualname = alembic
|
105 |
+
|
106 |
+
[handler_console]
|
107 |
+
class = StreamHandler
|
108 |
+
args = (sys.stderr,)
|
109 |
+
level = NOTSET
|
110 |
+
formatter = generic
|
111 |
+
|
112 |
+
[formatter_generic]
|
113 |
+
format = %(levelname)-5.5s [%(name)s] %(message)s
|
114 |
+
datefmt = %H:%M:%S
|
backend/apps/audio/main.py
ADDED
@@ -0,0 +1,437 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 pydantic import BaseModel
|
18 |
+
|
19 |
+
import uuid
|
20 |
+
import requests
|
21 |
+
import hashlib
|
22 |
+
from pathlib import Path
|
23 |
+
import json
|
24 |
+
|
25 |
+
from constants import ERROR_MESSAGES
|
26 |
+
from utils.utils import (
|
27 |
+
decode_token,
|
28 |
+
get_current_user,
|
29 |
+
get_verified_user,
|
30 |
+
get_admin_user,
|
31 |
+
)
|
32 |
+
from utils.misc import calculate_sha256
|
33 |
+
|
34 |
+
from config import (
|
35 |
+
SRC_LOG_LEVELS,
|
36 |
+
CACHE_DIR,
|
37 |
+
UPLOAD_DIR,
|
38 |
+
WHISPER_MODEL,
|
39 |
+
WHISPER_MODEL_DIR,
|
40 |
+
WHISPER_MODEL_AUTO_UPDATE,
|
41 |
+
DEVICE_TYPE,
|
42 |
+
AUDIO_STT_OPENAI_API_BASE_URL,
|
43 |
+
AUDIO_STT_OPENAI_API_KEY,
|
44 |
+
AUDIO_TTS_OPENAI_API_BASE_URL,
|
45 |
+
AUDIO_TTS_OPENAI_API_KEY,
|
46 |
+
AUDIO_TTS_API_KEY,
|
47 |
+
AUDIO_STT_ENGINE,
|
48 |
+
AUDIO_STT_MODEL,
|
49 |
+
AUDIO_TTS_ENGINE,
|
50 |
+
AUDIO_TTS_MODEL,
|
51 |
+
AUDIO_TTS_VOICE,
|
52 |
+
AppConfig,
|
53 |
+
)
|
54 |
+
|
55 |
+
log = logging.getLogger(__name__)
|
56 |
+
log.setLevel(SRC_LOG_LEVELS["AUDIO"])
|
57 |
+
|
58 |
+
app = FastAPI()
|
59 |
+
app.add_middleware(
|
60 |
+
CORSMiddleware,
|
61 |
+
allow_origins=["*"],
|
62 |
+
allow_credentials=True,
|
63 |
+
allow_methods=["*"],
|
64 |
+
allow_headers=["*"],
|
65 |
+
)
|
66 |
+
|
67 |
+
app.state.config = AppConfig()
|
68 |
+
|
69 |
+
app.state.config.STT_OPENAI_API_BASE_URL = AUDIO_STT_OPENAI_API_BASE_URL
|
70 |
+
app.state.config.STT_OPENAI_API_KEY = AUDIO_STT_OPENAI_API_KEY
|
71 |
+
app.state.config.STT_ENGINE = AUDIO_STT_ENGINE
|
72 |
+
app.state.config.STT_MODEL = AUDIO_STT_MODEL
|
73 |
+
|
74 |
+
app.state.config.TTS_OPENAI_API_BASE_URL = AUDIO_TTS_OPENAI_API_BASE_URL
|
75 |
+
app.state.config.TTS_OPENAI_API_KEY = AUDIO_TTS_OPENAI_API_KEY
|
76 |
+
app.state.config.TTS_ENGINE = AUDIO_TTS_ENGINE
|
77 |
+
app.state.config.TTS_MODEL = AUDIO_TTS_MODEL
|
78 |
+
app.state.config.TTS_VOICE = AUDIO_TTS_VOICE
|
79 |
+
app.state.config.TTS_API_KEY = AUDIO_TTS_API_KEY
|
80 |
+
|
81 |
+
# setting device type for whisper model
|
82 |
+
whisper_device_type = DEVICE_TYPE if DEVICE_TYPE and DEVICE_TYPE == "cuda" else "cpu"
|
83 |
+
log.info(f"whisper_device_type: {whisper_device_type}")
|
84 |
+
|
85 |
+
SPEECH_CACHE_DIR = Path(CACHE_DIR).joinpath("./audio/speech/")
|
86 |
+
SPEECH_CACHE_DIR.mkdir(parents=True, exist_ok=True)
|
87 |
+
|
88 |
+
|
89 |
+
class TTSConfigForm(BaseModel):
|
90 |
+
OPENAI_API_BASE_URL: str
|
91 |
+
OPENAI_API_KEY: str
|
92 |
+
API_KEY: str
|
93 |
+
ENGINE: str
|
94 |
+
MODEL: str
|
95 |
+
VOICE: str
|
96 |
+
|
97 |
+
|
98 |
+
class STTConfigForm(BaseModel):
|
99 |
+
OPENAI_API_BASE_URL: str
|
100 |
+
OPENAI_API_KEY: str
|
101 |
+
ENGINE: str
|
102 |
+
MODEL: str
|
103 |
+
|
104 |
+
|
105 |
+
class AudioConfigUpdateForm(BaseModel):
|
106 |
+
tts: TTSConfigForm
|
107 |
+
stt: STTConfigForm
|
108 |
+
|
109 |
+
|
110 |
+
from pydub import AudioSegment
|
111 |
+
from pydub.utils import mediainfo
|
112 |
+
|
113 |
+
|
114 |
+
def is_mp4_audio(file_path):
|
115 |
+
"""Check if the given file is an MP4 audio file."""
|
116 |
+
if not os.path.isfile(file_path):
|
117 |
+
print(f"File not found: {file_path}")
|
118 |
+
return False
|
119 |
+
|
120 |
+
info = mediainfo(file_path)
|
121 |
+
if (
|
122 |
+
info.get("codec_name") == "aac"
|
123 |
+
and info.get("codec_type") == "audio"
|
124 |
+
and info.get("codec_tag_string") == "mp4a"
|
125 |
+
):
|
126 |
+
return True
|
127 |
+
return False
|
128 |
+
|
129 |
+
|
130 |
+
def convert_mp4_to_wav(file_path, output_path):
|
131 |
+
"""Convert MP4 audio file to WAV format."""
|
132 |
+
audio = AudioSegment.from_file(file_path, format="mp4")
|
133 |
+
audio.export(output_path, format="wav")
|
134 |
+
print(f"Converted {file_path} to {output_path}")
|
135 |
+
|
136 |
+
|
137 |
+
@app.get("/config")
|
138 |
+
async def get_audio_config(user=Depends(get_admin_user)):
|
139 |
+
return {
|
140 |
+
"tts": {
|
141 |
+
"OPENAI_API_BASE_URL": app.state.config.TTS_OPENAI_API_BASE_URL,
|
142 |
+
"OPENAI_API_KEY": app.state.config.TTS_OPENAI_API_KEY,
|
143 |
+
"API_KEY": app.state.config.TTS_API_KEY,
|
144 |
+
"ENGINE": app.state.config.TTS_ENGINE,
|
145 |
+
"MODEL": app.state.config.TTS_MODEL,
|
146 |
+
"VOICE": app.state.config.TTS_VOICE,
|
147 |
+
},
|
148 |
+
"stt": {
|
149 |
+
"OPENAI_API_BASE_URL": app.state.config.STT_OPENAI_API_BASE_URL,
|
150 |
+
"OPENAI_API_KEY": app.state.config.STT_OPENAI_API_KEY,
|
151 |
+
"ENGINE": app.state.config.STT_ENGINE,
|
152 |
+
"MODEL": app.state.config.STT_MODEL,
|
153 |
+
},
|
154 |
+
}
|
155 |
+
|
156 |
+
|
157 |
+
@app.post("/config/update")
|
158 |
+
async def update_audio_config(
|
159 |
+
form_data: AudioConfigUpdateForm, user=Depends(get_admin_user)
|
160 |
+
):
|
161 |
+
app.state.config.TTS_OPENAI_API_BASE_URL = form_data.tts.OPENAI_API_BASE_URL
|
162 |
+
app.state.config.TTS_OPENAI_API_KEY = form_data.tts.OPENAI_API_KEY
|
163 |
+
app.state.config.TTS_API_KEY = form_data.tts.API_KEY
|
164 |
+
app.state.config.TTS_ENGINE = form_data.tts.ENGINE
|
165 |
+
app.state.config.TTS_MODEL = form_data.tts.MODEL
|
166 |
+
app.state.config.TTS_VOICE = form_data.tts.VOICE
|
167 |
+
|
168 |
+
app.state.config.STT_OPENAI_API_BASE_URL = form_data.stt.OPENAI_API_BASE_URL
|
169 |
+
app.state.config.STT_OPENAI_API_KEY = form_data.stt.OPENAI_API_KEY
|
170 |
+
app.state.config.STT_ENGINE = form_data.stt.ENGINE
|
171 |
+
app.state.config.STT_MODEL = form_data.stt.MODEL
|
172 |
+
|
173 |
+
return {
|
174 |
+
"tts": {
|
175 |
+
"OPENAI_API_BASE_URL": app.state.config.TTS_OPENAI_API_BASE_URL,
|
176 |
+
"OPENAI_API_KEY": app.state.config.TTS_OPENAI_API_KEY,
|
177 |
+
"API_KEY": app.state.config.TTS_API_KEY,
|
178 |
+
"ENGINE": app.state.config.TTS_ENGINE,
|
179 |
+
"MODEL": app.state.config.TTS_MODEL,
|
180 |
+
"VOICE": app.state.config.TTS_VOICE,
|
181 |
+
},
|
182 |
+
"stt": {
|
183 |
+
"OPENAI_API_BASE_URL": app.state.config.STT_OPENAI_API_BASE_URL,
|
184 |
+
"OPENAI_API_KEY": app.state.config.STT_OPENAI_API_KEY,
|
185 |
+
"ENGINE": app.state.config.STT_ENGINE,
|
186 |
+
"MODEL": app.state.config.STT_MODEL,
|
187 |
+
},
|
188 |
+
}
|
189 |
+
|
190 |
+
|
191 |
+
@app.post("/speech")
|
192 |
+
async def speech(request: Request, user=Depends(get_verified_user)):
|
193 |
+
body = await request.body()
|
194 |
+
name = hashlib.sha256(body).hexdigest()
|
195 |
+
|
196 |
+
file_path = SPEECH_CACHE_DIR.joinpath(f"{name}.mp3")
|
197 |
+
file_body_path = SPEECH_CACHE_DIR.joinpath(f"{name}.json")
|
198 |
+
|
199 |
+
# Check if the file already exists in the cache
|
200 |
+
if file_path.is_file():
|
201 |
+
return FileResponse(file_path)
|
202 |
+
|
203 |
+
if app.state.config.TTS_ENGINE == "openai":
|
204 |
+
headers = {}
|
205 |
+
headers["Authorization"] = f"Bearer {app.state.config.TTS_OPENAI_API_KEY}"
|
206 |
+
headers["Content-Type"] = "application/json"
|
207 |
+
|
208 |
+
try:
|
209 |
+
body = body.decode("utf-8")
|
210 |
+
body = json.loads(body)
|
211 |
+
body["model"] = app.state.config.TTS_MODEL
|
212 |
+
body = json.dumps(body).encode("utf-8")
|
213 |
+
except Exception as e:
|
214 |
+
pass
|
215 |
+
|
216 |
+
r = None
|
217 |
+
try:
|
218 |
+
r = requests.post(
|
219 |
+
url=f"{app.state.config.TTS_OPENAI_API_BASE_URL}/audio/speech",
|
220 |
+
data=body,
|
221 |
+
headers=headers,
|
222 |
+
stream=True,
|
223 |
+
)
|
224 |
+
|
225 |
+
r.raise_for_status()
|
226 |
+
|
227 |
+
# Save the streaming content to a file
|
228 |
+
with open(file_path, "wb") as f:
|
229 |
+
for chunk in r.iter_content(chunk_size=8192):
|
230 |
+
f.write(chunk)
|
231 |
+
|
232 |
+
with open(file_body_path, "w") as f:
|
233 |
+
json.dump(json.loads(body.decode("utf-8")), f)
|
234 |
+
|
235 |
+
# Return the saved file
|
236 |
+
return FileResponse(file_path)
|
237 |
+
|
238 |
+
except Exception as e:
|
239 |
+
log.exception(e)
|
240 |
+
error_detail = "Open WebUI: Server Connection Error"
|
241 |
+
if r is not None:
|
242 |
+
try:
|
243 |
+
res = r.json()
|
244 |
+
if "error" in res:
|
245 |
+
error_detail = f"External: {res['error']['message']}"
|
246 |
+
except:
|
247 |
+
error_detail = f"External: {e}"
|
248 |
+
|
249 |
+
raise HTTPException(
|
250 |
+
status_code=r.status_code if r != None else 500,
|
251 |
+
detail=error_detail,
|
252 |
+
)
|
253 |
+
|
254 |
+
elif app.state.config.TTS_ENGINE == "elevenlabs":
|
255 |
+
|
256 |
+
payload = None
|
257 |
+
try:
|
258 |
+
payload = json.loads(body.decode("utf-8"))
|
259 |
+
except Exception as e:
|
260 |
+
log.exception(e)
|
261 |
+
pass
|
262 |
+
|
263 |
+
url = f"https://api.elevenlabs.io/v1/text-to-speech/{payload['voice']}"
|
264 |
+
|
265 |
+
headers = {
|
266 |
+
"Accept": "audio/mpeg",
|
267 |
+
"Content-Type": "application/json",
|
268 |
+
"xi-api-key": app.state.config.TTS_API_KEY,
|
269 |
+
}
|
270 |
+
|
271 |
+
data = {
|
272 |
+
"text": payload["input"],
|
273 |
+
"model_id": app.state.config.TTS_MODEL,
|
274 |
+
"voice_settings": {"stability": 0.5, "similarity_boost": 0.5},
|
275 |
+
}
|
276 |
+
|
277 |
+
try:
|
278 |
+
r = requests.post(url, json=data, headers=headers)
|
279 |
+
|
280 |
+
r.raise_for_status()
|
281 |
+
|
282 |
+
# Save the streaming content to a file
|
283 |
+
with open(file_path, "wb") as f:
|
284 |
+
for chunk in r.iter_content(chunk_size=8192):
|
285 |
+
f.write(chunk)
|
286 |
+
|
287 |
+
with open(file_body_path, "w") as f:
|
288 |
+
json.dump(json.loads(body.decode("utf-8")), f)
|
289 |
+
|
290 |
+
# Return the saved file
|
291 |
+
return FileResponse(file_path)
|
292 |
+
|
293 |
+
except Exception as e:
|
294 |
+
log.exception(e)
|
295 |
+
error_detail = "Open WebUI: Server Connection Error"
|
296 |
+
if r is not None:
|
297 |
+
try:
|
298 |
+
res = r.json()
|
299 |
+
if "error" in res:
|
300 |
+
error_detail = f"External: {res['error']['message']}"
|
301 |
+
except:
|
302 |
+
error_detail = f"External: {e}"
|
303 |
+
|
304 |
+
raise HTTPException(
|
305 |
+
status_code=r.status_code if r != None else 500,
|
306 |
+
detail=error_detail,
|
307 |
+
)
|
308 |
+
|
309 |
+
|
310 |
+
@app.post("/transcriptions")
|
311 |
+
def transcribe(
|
312 |
+
file: UploadFile = File(...),
|
313 |
+
user=Depends(get_current_user),
|
314 |
+
):
|
315 |
+
log.info(f"file.content_type: {file.content_type}")
|
316 |
+
|
317 |
+
if file.content_type not in ["audio/mpeg", "audio/wav"]:
|
318 |
+
raise HTTPException(
|
319 |
+
status_code=status.HTTP_400_BAD_REQUEST,
|
320 |
+
detail=ERROR_MESSAGES.FILE_NOT_SUPPORTED,
|
321 |
+
)
|
322 |
+
|
323 |
+
try:
|
324 |
+
ext = file.filename.split(".")[-1]
|
325 |
+
|
326 |
+
id = uuid.uuid4()
|
327 |
+
filename = f"{id}.{ext}"
|
328 |
+
|
329 |
+
file_dir = f"{CACHE_DIR}/audio/transcriptions"
|
330 |
+
os.makedirs(file_dir, exist_ok=True)
|
331 |
+
file_path = f"{file_dir}/{filename}"
|
332 |
+
|
333 |
+
print(filename)
|
334 |
+
|
335 |
+
contents = file.file.read()
|
336 |
+
with open(file_path, "wb") as f:
|
337 |
+
f.write(contents)
|
338 |
+
f.close()
|
339 |
+
|
340 |
+
if app.state.config.STT_ENGINE == "":
|
341 |
+
from faster_whisper import WhisperModel
|
342 |
+
|
343 |
+
whisper_kwargs = {
|
344 |
+
"model_size_or_path": WHISPER_MODEL,
|
345 |
+
"device": whisper_device_type,
|
346 |
+
"compute_type": "int8",
|
347 |
+
"download_root": WHISPER_MODEL_DIR,
|
348 |
+
"local_files_only": not WHISPER_MODEL_AUTO_UPDATE,
|
349 |
+
}
|
350 |
+
|
351 |
+
log.debug(f"whisper_kwargs: {whisper_kwargs}")
|
352 |
+
|
353 |
+
try:
|
354 |
+
model = WhisperModel(**whisper_kwargs)
|
355 |
+
except:
|
356 |
+
log.warning(
|
357 |
+
"WhisperModel initialization failed, attempting download with local_files_only=False"
|
358 |
+
)
|
359 |
+
whisper_kwargs["local_files_only"] = False
|
360 |
+
model = WhisperModel(**whisper_kwargs)
|
361 |
+
|
362 |
+
segments, info = model.transcribe(file_path, beam_size=5)
|
363 |
+
log.info(
|
364 |
+
"Detected language '%s' with probability %f"
|
365 |
+
% (info.language, info.language_probability)
|
366 |
+
)
|
367 |
+
|
368 |
+
transcript = "".join([segment.text for segment in list(segments)])
|
369 |
+
|
370 |
+
data = {"text": transcript.strip()}
|
371 |
+
|
372 |
+
# save the transcript to a json file
|
373 |
+
transcript_file = f"{file_dir}/{id}.json"
|
374 |
+
with open(transcript_file, "w") as f:
|
375 |
+
json.dump(data, f)
|
376 |
+
|
377 |
+
print(data)
|
378 |
+
|
379 |
+
return data
|
380 |
+
|
381 |
+
elif app.state.config.STT_ENGINE == "openai":
|
382 |
+
if is_mp4_audio(file_path):
|
383 |
+
print("is_mp4_audio")
|
384 |
+
os.rename(file_path, file_path.replace(".wav", ".mp4"))
|
385 |
+
# Convert MP4 audio file to WAV format
|
386 |
+
convert_mp4_to_wav(file_path.replace(".wav", ".mp4"), file_path)
|
387 |
+
|
388 |
+
headers = {"Authorization": f"Bearer {app.state.config.STT_OPENAI_API_KEY}"}
|
389 |
+
|
390 |
+
files = {"file": (filename, open(file_path, "rb"))}
|
391 |
+
data = {"model": app.state.config.STT_MODEL}
|
392 |
+
|
393 |
+
print(files, data)
|
394 |
+
|
395 |
+
r = None
|
396 |
+
try:
|
397 |
+
r = requests.post(
|
398 |
+
url=f"{app.state.config.STT_OPENAI_API_BASE_URL}/audio/transcriptions",
|
399 |
+
headers=headers,
|
400 |
+
files=files,
|
401 |
+
data=data,
|
402 |
+
)
|
403 |
+
|
404 |
+
r.raise_for_status()
|
405 |
+
|
406 |
+
data = r.json()
|
407 |
+
|
408 |
+
# save the transcript to a json file
|
409 |
+
transcript_file = f"{file_dir}/{id}.json"
|
410 |
+
with open(transcript_file, "w") as f:
|
411 |
+
json.dump(data, f)
|
412 |
+
|
413 |
+
print(data)
|
414 |
+
return data
|
415 |
+
except Exception as e:
|
416 |
+
log.exception(e)
|
417 |
+
error_detail = "Open WebUI: Server Connection Error"
|
418 |
+
if r is not None:
|
419 |
+
try:
|
420 |
+
res = r.json()
|
421 |
+
if "error" in res:
|
422 |
+
error_detail = f"External: {res['error']['message']}"
|
423 |
+
except:
|
424 |
+
error_detail = f"External: {e}"
|
425 |
+
|
426 |
+
raise HTTPException(
|
427 |
+
status_code=r.status_code if r != None else 500,
|
428 |
+
detail=error_detail,
|
429 |
+
)
|
430 |
+
|
431 |
+
except Exception as e:
|
432 |
+
log.exception(e)
|
433 |
+
|
434 |
+
raise HTTPException(
|
435 |
+
status_code=status.HTTP_400_BAD_REQUEST,
|
436 |
+
detail=ERROR_MESSAGES.DEFAULT(e),
|
437 |
+
)
|
backend/apps/images/main.py
ADDED
@@ -0,0 +1,568 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import re
|
2 |
+
import requests
|
3 |
+
import base64
|
4 |
+
from fastapi import (
|
5 |
+
FastAPI,
|
6 |
+
Request,
|
7 |
+
Depends,
|
8 |
+
HTTPException,
|
9 |
+
status,
|
10 |
+
UploadFile,
|
11 |
+
File,
|
12 |
+
Form,
|
13 |
+
)
|
14 |
+
from fastapi.middleware.cors import CORSMiddleware
|
15 |
+
|
16 |
+
from constants import ERROR_MESSAGES
|
17 |
+
from utils.utils import (
|
18 |
+
get_verified_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 |
+
AUTOMATIC1111_API_AUTH,
|
40 |
+
COMFYUI_BASE_URL,
|
41 |
+
COMFYUI_CFG_SCALE,
|
42 |
+
COMFYUI_SAMPLER,
|
43 |
+
COMFYUI_SCHEDULER,
|
44 |
+
COMFYUI_SD3,
|
45 |
+
IMAGES_OPENAI_API_BASE_URL,
|
46 |
+
IMAGES_OPENAI_API_KEY,
|
47 |
+
IMAGE_GENERATION_MODEL,
|
48 |
+
IMAGE_SIZE,
|
49 |
+
IMAGE_STEPS,
|
50 |
+
AppConfig,
|
51 |
+
)
|
52 |
+
|
53 |
+
log = logging.getLogger(__name__)
|
54 |
+
log.setLevel(SRC_LOG_LEVELS["IMAGES"])
|
55 |
+
|
56 |
+
IMAGE_CACHE_DIR = Path(CACHE_DIR).joinpath("./image/generations/")
|
57 |
+
IMAGE_CACHE_DIR.mkdir(parents=True, exist_ok=True)
|
58 |
+
|
59 |
+
app = FastAPI()
|
60 |
+
app.add_middleware(
|
61 |
+
CORSMiddleware,
|
62 |
+
allow_origins=["*"],
|
63 |
+
allow_credentials=True,
|
64 |
+
allow_methods=["*"],
|
65 |
+
allow_headers=["*"],
|
66 |
+
)
|
67 |
+
|
68 |
+
app.state.config = AppConfig()
|
69 |
+
|
70 |
+
app.state.config.ENGINE = IMAGE_GENERATION_ENGINE
|
71 |
+
app.state.config.ENABLED = ENABLE_IMAGE_GENERATION
|
72 |
+
|
73 |
+
app.state.config.OPENAI_API_BASE_URL = IMAGES_OPENAI_API_BASE_URL
|
74 |
+
app.state.config.OPENAI_API_KEY = IMAGES_OPENAI_API_KEY
|
75 |
+
|
76 |
+
app.state.config.MODEL = IMAGE_GENERATION_MODEL
|
77 |
+
|
78 |
+
app.state.config.AUTOMATIC1111_BASE_URL = AUTOMATIC1111_BASE_URL
|
79 |
+
app.state.config.AUTOMATIC1111_API_AUTH = AUTOMATIC1111_API_AUTH
|
80 |
+
app.state.config.COMFYUI_BASE_URL = COMFYUI_BASE_URL
|
81 |
+
|
82 |
+
app.state.config.IMAGE_SIZE = IMAGE_SIZE
|
83 |
+
app.state.config.IMAGE_STEPS = IMAGE_STEPS
|
84 |
+
app.state.config.COMFYUI_CFG_SCALE = COMFYUI_CFG_SCALE
|
85 |
+
app.state.config.COMFYUI_SAMPLER = COMFYUI_SAMPLER
|
86 |
+
app.state.config.COMFYUI_SCHEDULER = COMFYUI_SCHEDULER
|
87 |
+
app.state.config.COMFYUI_SD3 = COMFYUI_SD3
|
88 |
+
|
89 |
+
|
90 |
+
def get_automatic1111_api_auth():
|
91 |
+
if app.state.config.AUTOMATIC1111_API_AUTH == None:
|
92 |
+
return ""
|
93 |
+
else:
|
94 |
+
auth1111_byte_string = app.state.config.AUTOMATIC1111_API_AUTH.encode("utf-8")
|
95 |
+
auth1111_base64_encoded_bytes = base64.b64encode(auth1111_byte_string)
|
96 |
+
auth1111_base64_encoded_string = auth1111_base64_encoded_bytes.decode("utf-8")
|
97 |
+
return f"Basic {auth1111_base64_encoded_string}"
|
98 |
+
|
99 |
+
|
100 |
+
@app.get("/config")
|
101 |
+
async def get_config(request: Request, user=Depends(get_admin_user)):
|
102 |
+
return {
|
103 |
+
"engine": app.state.config.ENGINE,
|
104 |
+
"enabled": app.state.config.ENABLED,
|
105 |
+
}
|
106 |
+
|
107 |
+
|
108 |
+
class ConfigUpdateForm(BaseModel):
|
109 |
+
engine: str
|
110 |
+
enabled: bool
|
111 |
+
|
112 |
+
|
113 |
+
@app.post("/config/update")
|
114 |
+
async def update_config(form_data: ConfigUpdateForm, user=Depends(get_admin_user)):
|
115 |
+
app.state.config.ENGINE = form_data.engine
|
116 |
+
app.state.config.ENABLED = form_data.enabled
|
117 |
+
return {
|
118 |
+
"engine": app.state.config.ENGINE,
|
119 |
+
"enabled": app.state.config.ENABLED,
|
120 |
+
}
|
121 |
+
|
122 |
+
|
123 |
+
class EngineUrlUpdateForm(BaseModel):
|
124 |
+
AUTOMATIC1111_BASE_URL: Optional[str] = None
|
125 |
+
AUTOMATIC1111_API_AUTH: Optional[str] = None
|
126 |
+
COMFYUI_BASE_URL: Optional[str] = None
|
127 |
+
|
128 |
+
|
129 |
+
@app.get("/url")
|
130 |
+
async def get_engine_url(user=Depends(get_admin_user)):
|
131 |
+
return {
|
132 |
+
"AUTOMATIC1111_BASE_URL": app.state.config.AUTOMATIC1111_BASE_URL,
|
133 |
+
"AUTOMATIC1111_API_AUTH": app.state.config.AUTOMATIC1111_API_AUTH,
|
134 |
+
"COMFYUI_BASE_URL": app.state.config.COMFYUI_BASE_URL,
|
135 |
+
}
|
136 |
+
|
137 |
+
|
138 |
+
@app.post("/url/update")
|
139 |
+
async def update_engine_url(
|
140 |
+
form_data: EngineUrlUpdateForm, user=Depends(get_admin_user)
|
141 |
+
):
|
142 |
+
if form_data.AUTOMATIC1111_BASE_URL == None:
|
143 |
+
app.state.config.AUTOMATIC1111_BASE_URL = AUTOMATIC1111_BASE_URL
|
144 |
+
else:
|
145 |
+
url = form_data.AUTOMATIC1111_BASE_URL.strip("/")
|
146 |
+
try:
|
147 |
+
r = requests.head(url)
|
148 |
+
app.state.config.AUTOMATIC1111_BASE_URL = url
|
149 |
+
except Exception as e:
|
150 |
+
raise HTTPException(status_code=400, detail=ERROR_MESSAGES.DEFAULT(e))
|
151 |
+
|
152 |
+
if form_data.COMFYUI_BASE_URL == None:
|
153 |
+
app.state.config.COMFYUI_BASE_URL = COMFYUI_BASE_URL
|
154 |
+
else:
|
155 |
+
url = form_data.COMFYUI_BASE_URL.strip("/")
|
156 |
+
|
157 |
+
try:
|
158 |
+
r = requests.head(url)
|
159 |
+
app.state.config.COMFYUI_BASE_URL = url
|
160 |
+
except Exception as e:
|
161 |
+
raise HTTPException(status_code=400, detail=ERROR_MESSAGES.DEFAULT(e))
|
162 |
+
|
163 |
+
if form_data.AUTOMATIC1111_API_AUTH == None:
|
164 |
+
app.state.config.AUTOMATIC1111_API_AUTH = AUTOMATIC1111_API_AUTH
|
165 |
+
else:
|
166 |
+
app.state.config.AUTOMATIC1111_API_AUTH = form_data.AUTOMATIC1111_API_AUTH
|
167 |
+
|
168 |
+
return {
|
169 |
+
"AUTOMATIC1111_BASE_URL": app.state.config.AUTOMATIC1111_BASE_URL,
|
170 |
+
"AUTOMATIC1111_API_AUTH": app.state.config.AUTOMATIC1111_API_AUTH,
|
171 |
+
"COMFYUI_BASE_URL": app.state.config.COMFYUI_BASE_URL,
|
172 |
+
"status": True,
|
173 |
+
}
|
174 |
+
|
175 |
+
|
176 |
+
class OpenAIConfigUpdateForm(BaseModel):
|
177 |
+
url: str
|
178 |
+
key: str
|
179 |
+
|
180 |
+
|
181 |
+
@app.get("/openai/config")
|
182 |
+
async def get_openai_config(user=Depends(get_admin_user)):
|
183 |
+
return {
|
184 |
+
"OPENAI_API_BASE_URL": app.state.config.OPENAI_API_BASE_URL,
|
185 |
+
"OPENAI_API_KEY": app.state.config.OPENAI_API_KEY,
|
186 |
+
}
|
187 |
+
|
188 |
+
|
189 |
+
@app.post("/openai/config/update")
|
190 |
+
async def update_openai_config(
|
191 |
+
form_data: OpenAIConfigUpdateForm, user=Depends(get_admin_user)
|
192 |
+
):
|
193 |
+
if form_data.key == "":
|
194 |
+
raise HTTPException(status_code=400, detail=ERROR_MESSAGES.API_KEY_NOT_FOUND)
|
195 |
+
|
196 |
+
app.state.config.OPENAI_API_BASE_URL = form_data.url
|
197 |
+
app.state.config.OPENAI_API_KEY = form_data.key
|
198 |
+
|
199 |
+
return {
|
200 |
+
"status": True,
|
201 |
+
"OPENAI_API_BASE_URL": app.state.config.OPENAI_API_BASE_URL,
|
202 |
+
"OPENAI_API_KEY": app.state.config.OPENAI_API_KEY,
|
203 |
+
}
|
204 |
+
|
205 |
+
|
206 |
+
class ImageSizeUpdateForm(BaseModel):
|
207 |
+
size: str
|
208 |
+
|
209 |
+
|
210 |
+
@app.get("/size")
|
211 |
+
async def get_image_size(user=Depends(get_admin_user)):
|
212 |
+
return {"IMAGE_SIZE": app.state.config.IMAGE_SIZE}
|
213 |
+
|
214 |
+
|
215 |
+
@app.post("/size/update")
|
216 |
+
async def update_image_size(
|
217 |
+
form_data: ImageSizeUpdateForm, user=Depends(get_admin_user)
|
218 |
+
):
|
219 |
+
pattern = r"^\d+x\d+$" # Regular expression pattern
|
220 |
+
if re.match(pattern, form_data.size):
|
221 |
+
app.state.config.IMAGE_SIZE = form_data.size
|
222 |
+
return {
|
223 |
+
"IMAGE_SIZE": app.state.config.IMAGE_SIZE,
|
224 |
+
"status": True,
|
225 |
+
}
|
226 |
+
else:
|
227 |
+
raise HTTPException(
|
228 |
+
status_code=400,
|
229 |
+
detail=ERROR_MESSAGES.INCORRECT_FORMAT(" (e.g., 512x512)."),
|
230 |
+
)
|
231 |
+
|
232 |
+
|
233 |
+
class ImageStepsUpdateForm(BaseModel):
|
234 |
+
steps: int
|
235 |
+
|
236 |
+
|
237 |
+
@app.get("/steps")
|
238 |
+
async def get_image_size(user=Depends(get_admin_user)):
|
239 |
+
return {"IMAGE_STEPS": app.state.config.IMAGE_STEPS}
|
240 |
+
|
241 |
+
|
242 |
+
@app.post("/steps/update")
|
243 |
+
async def update_image_size(
|
244 |
+
form_data: ImageStepsUpdateForm, user=Depends(get_admin_user)
|
245 |
+
):
|
246 |
+
if form_data.steps >= 0:
|
247 |
+
app.state.config.IMAGE_STEPS = form_data.steps
|
248 |
+
return {
|
249 |
+
"IMAGE_STEPS": app.state.config.IMAGE_STEPS,
|
250 |
+
"status": True,
|
251 |
+
}
|
252 |
+
else:
|
253 |
+
raise HTTPException(
|
254 |
+
status_code=400,
|
255 |
+
detail=ERROR_MESSAGES.INCORRECT_FORMAT(" (e.g., 50)."),
|
256 |
+
)
|
257 |
+
|
258 |
+
|
259 |
+
@app.get("/models")
|
260 |
+
def get_models(user=Depends(get_verified_user)):
|
261 |
+
try:
|
262 |
+
if app.state.config.ENGINE == "openai":
|
263 |
+
return [
|
264 |
+
{"id": "dall-e-2", "name": "DALL·E 2"},
|
265 |
+
{"id": "dall-e-3", "name": "DALL·E 3"},
|
266 |
+
]
|
267 |
+
elif app.state.config.ENGINE == "comfyui":
|
268 |
+
|
269 |
+
r = requests.get(url=f"{app.state.config.COMFYUI_BASE_URL}/object_info")
|
270 |
+
info = r.json()
|
271 |
+
|
272 |
+
return list(
|
273 |
+
map(
|
274 |
+
lambda model: {"id": model, "name": model},
|
275 |
+
info["CheckpointLoaderSimple"]["input"]["required"]["ckpt_name"][0],
|
276 |
+
)
|
277 |
+
)
|
278 |
+
|
279 |
+
else:
|
280 |
+
r = requests.get(
|
281 |
+
url=f"{app.state.config.AUTOMATIC1111_BASE_URL}/sdapi/v1/sd-models",
|
282 |
+
headers={"authorization": get_automatic1111_api_auth()},
|
283 |
+
)
|
284 |
+
models = r.json()
|
285 |
+
return list(
|
286 |
+
map(
|
287 |
+
lambda model: {"id": model["title"], "name": model["model_name"]},
|
288 |
+
models,
|
289 |
+
)
|
290 |
+
)
|
291 |
+
except Exception as e:
|
292 |
+
app.state.config.ENABLED = False
|
293 |
+
raise HTTPException(status_code=400, detail=ERROR_MESSAGES.DEFAULT(e))
|
294 |
+
|
295 |
+
|
296 |
+
@app.get("/models/default")
|
297 |
+
async def get_default_model(user=Depends(get_admin_user)):
|
298 |
+
try:
|
299 |
+
if app.state.config.ENGINE == "openai":
|
300 |
+
return {
|
301 |
+
"model": (
|
302 |
+
app.state.config.MODEL if app.state.config.MODEL else "dall-e-2"
|
303 |
+
)
|
304 |
+
}
|
305 |
+
elif app.state.config.ENGINE == "comfyui":
|
306 |
+
return {"model": (app.state.config.MODEL if app.state.config.MODEL else "")}
|
307 |
+
else:
|
308 |
+
r = requests.get(
|
309 |
+
url=f"{app.state.config.AUTOMATIC1111_BASE_URL}/sdapi/v1/options",
|
310 |
+
headers={"authorization": get_automatic1111_api_auth()},
|
311 |
+
)
|
312 |
+
options = r.json()
|
313 |
+
return {"model": options["sd_model_checkpoint"]}
|
314 |
+
except Exception as e:
|
315 |
+
app.state.config.ENABLED = False
|
316 |
+
raise HTTPException(status_code=400, detail=ERROR_MESSAGES.DEFAULT(e))
|
317 |
+
|
318 |
+
|
319 |
+
class UpdateModelForm(BaseModel):
|
320 |
+
model: str
|
321 |
+
|
322 |
+
|
323 |
+
def set_model_handler(model: str):
|
324 |
+
if app.state.config.ENGINE in ["openai", "comfyui"]:
|
325 |
+
app.state.config.MODEL = model
|
326 |
+
return app.state.config.MODEL
|
327 |
+
else:
|
328 |
+
api_auth = get_automatic1111_api_auth()
|
329 |
+
r = requests.get(
|
330 |
+
url=f"{app.state.config.AUTOMATIC1111_BASE_URL}/sdapi/v1/options",
|
331 |
+
headers={"authorization": api_auth},
|
332 |
+
)
|
333 |
+
options = r.json()
|
334 |
+
|
335 |
+
if model != options["sd_model_checkpoint"]:
|
336 |
+
options["sd_model_checkpoint"] = model
|
337 |
+
r = requests.post(
|
338 |
+
url=f"{app.state.config.AUTOMATIC1111_BASE_URL}/sdapi/v1/options",
|
339 |
+
json=options,
|
340 |
+
headers={"authorization": api_auth},
|
341 |
+
)
|
342 |
+
|
343 |
+
return options
|
344 |
+
|
345 |
+
|
346 |
+
@app.post("/models/default/update")
|
347 |
+
def update_default_model(
|
348 |
+
form_data: UpdateModelForm,
|
349 |
+
user=Depends(get_verified_user),
|
350 |
+
):
|
351 |
+
return set_model_handler(form_data.model)
|
352 |
+
|
353 |
+
|
354 |
+
class GenerateImageForm(BaseModel):
|
355 |
+
model: Optional[str] = None
|
356 |
+
prompt: str
|
357 |
+
n: int = 1
|
358 |
+
size: Optional[str] = None
|
359 |
+
negative_prompt: Optional[str] = None
|
360 |
+
|
361 |
+
|
362 |
+
def save_b64_image(b64_str):
|
363 |
+
try:
|
364 |
+
image_id = str(uuid.uuid4())
|
365 |
+
|
366 |
+
if "," in b64_str:
|
367 |
+
header, encoded = b64_str.split(",", 1)
|
368 |
+
mime_type = header.split(";")[0]
|
369 |
+
|
370 |
+
img_data = base64.b64decode(encoded)
|
371 |
+
image_format = mimetypes.guess_extension(mime_type)
|
372 |
+
|
373 |
+
image_filename = f"{image_id}{image_format}"
|
374 |
+
file_path = IMAGE_CACHE_DIR / f"{image_filename}"
|
375 |
+
with open(file_path, "wb") as f:
|
376 |
+
f.write(img_data)
|
377 |
+
return image_filename
|
378 |
+
else:
|
379 |
+
image_filename = f"{image_id}.png"
|
380 |
+
file_path = IMAGE_CACHE_DIR.joinpath(image_filename)
|
381 |
+
|
382 |
+
img_data = base64.b64decode(b64_str)
|
383 |
+
|
384 |
+
# Write the image data to a file
|
385 |
+
with open(file_path, "wb") as f:
|
386 |
+
f.write(img_data)
|
387 |
+
return image_filename
|
388 |
+
|
389 |
+
except Exception as e:
|
390 |
+
log.exception(f"Error saving image: {e}")
|
391 |
+
return None
|
392 |
+
|
393 |
+
|
394 |
+
def save_url_image(url):
|
395 |
+
image_id = str(uuid.uuid4())
|
396 |
+
try:
|
397 |
+
r = requests.get(url)
|
398 |
+
r.raise_for_status()
|
399 |
+
if r.headers["content-type"].split("/")[0] == "image":
|
400 |
+
|
401 |
+
mime_type = r.headers["content-type"]
|
402 |
+
image_format = mimetypes.guess_extension(mime_type)
|
403 |
+
|
404 |
+
if not image_format:
|
405 |
+
raise ValueError("Could not determine image type from MIME type")
|
406 |
+
|
407 |
+
image_filename = f"{image_id}{image_format}"
|
408 |
+
|
409 |
+
file_path = IMAGE_CACHE_DIR.joinpath(f"{image_filename}")
|
410 |
+
with open(file_path, "wb") as image_file:
|
411 |
+
for chunk in r.iter_content(chunk_size=8192):
|
412 |
+
image_file.write(chunk)
|
413 |
+
return image_filename
|
414 |
+
else:
|
415 |
+
log.error(f"Url does not point to an image.")
|
416 |
+
return None
|
417 |
+
|
418 |
+
except Exception as e:
|
419 |
+
log.exception(f"Error saving image: {e}")
|
420 |
+
return None
|
421 |
+
|
422 |
+
|
423 |
+
@app.post("/generations")
|
424 |
+
async def image_generations(
|
425 |
+
form_data: GenerateImageForm,
|
426 |
+
user=Depends(get_verified_user),
|
427 |
+
):
|
428 |
+
width, height = tuple(map(int, app.state.config.IMAGE_SIZE.split("x")))
|
429 |
+
|
430 |
+
r = None
|
431 |
+
try:
|
432 |
+
if app.state.config.ENGINE == "openai":
|
433 |
+
|
434 |
+
headers = {}
|
435 |
+
headers["Authorization"] = f"Bearer {app.state.config.OPENAI_API_KEY}"
|
436 |
+
headers["Content-Type"] = "application/json"
|
437 |
+
|
438 |
+
data = {
|
439 |
+
"model": (
|
440 |
+
app.state.config.MODEL
|
441 |
+
if app.state.config.MODEL != ""
|
442 |
+
else "dall-e-2"
|
443 |
+
),
|
444 |
+
"prompt": form_data.prompt,
|
445 |
+
"n": form_data.n,
|
446 |
+
"size": (
|
447 |
+
form_data.size if form_data.size else app.state.config.IMAGE_SIZE
|
448 |
+
),
|
449 |
+
"response_format": "b64_json",
|
450 |
+
}
|
451 |
+
|
452 |
+
r = requests.post(
|
453 |
+
url=f"{app.state.config.OPENAI_API_BASE_URL}/images/generations",
|
454 |
+
json=data,
|
455 |
+
headers=headers,
|
456 |
+
)
|
457 |
+
|
458 |
+
r.raise_for_status()
|
459 |
+
res = r.json()
|
460 |
+
|
461 |
+
images = []
|
462 |
+
|
463 |
+
for image in res["data"]:
|
464 |
+
image_filename = save_b64_image(image["b64_json"])
|
465 |
+
images.append({"url": f"/cache/image/generations/{image_filename}"})
|
466 |
+
file_body_path = IMAGE_CACHE_DIR.joinpath(f"{image_filename}.json")
|
467 |
+
|
468 |
+
with open(file_body_path, "w") as f:
|
469 |
+
json.dump(data, f)
|
470 |
+
|
471 |
+
return images
|
472 |
+
|
473 |
+
elif app.state.config.ENGINE == "comfyui":
|
474 |
+
|
475 |
+
data = {
|
476 |
+
"prompt": form_data.prompt,
|
477 |
+
"width": width,
|
478 |
+
"height": height,
|
479 |
+
"n": form_data.n,
|
480 |
+
}
|
481 |
+
|
482 |
+
if app.state.config.IMAGE_STEPS is not None:
|
483 |
+
data["steps"] = app.state.config.IMAGE_STEPS
|
484 |
+
|
485 |
+
if form_data.negative_prompt is not None:
|
486 |
+
data["negative_prompt"] = form_data.negative_prompt
|
487 |
+
|
488 |
+
if app.state.config.COMFYUI_CFG_SCALE:
|
489 |
+
data["cfg_scale"] = app.state.config.COMFYUI_CFG_SCALE
|
490 |
+
|
491 |
+
if app.state.config.COMFYUI_SAMPLER is not None:
|
492 |
+
data["sampler"] = app.state.config.COMFYUI_SAMPLER
|
493 |
+
|
494 |
+
if app.state.config.COMFYUI_SCHEDULER is not None:
|
495 |
+
data["scheduler"] = app.state.config.COMFYUI_SCHEDULER
|
496 |
+
|
497 |
+
if app.state.config.COMFYUI_SD3 is not None:
|
498 |
+
data["sd3"] = app.state.config.COMFYUI_SD3
|
499 |
+
|
500 |
+
data = ImageGenerationPayload(**data)
|
501 |
+
|
502 |
+
res = comfyui_generate_image(
|
503 |
+
app.state.config.MODEL,
|
504 |
+
data,
|
505 |
+
user.id,
|
506 |
+
app.state.config.COMFYUI_BASE_URL,
|
507 |
+
)
|
508 |
+
log.debug(f"res: {res}")
|
509 |
+
|
510 |
+
images = []
|
511 |
+
|
512 |
+
for image in res["data"]:
|
513 |
+
image_filename = save_url_image(image["url"])
|
514 |
+
images.append({"url": f"/cache/image/generations/{image_filename}"})
|
515 |
+
file_body_path = IMAGE_CACHE_DIR.joinpath(f"{image_filename}.json")
|
516 |
+
|
517 |
+
with open(file_body_path, "w") as f:
|
518 |
+
json.dump(data.model_dump(exclude_none=True), f)
|
519 |
+
|
520 |
+
log.debug(f"images: {images}")
|
521 |
+
return images
|
522 |
+
else:
|
523 |
+
if form_data.model:
|
524 |
+
set_model_handler(form_data.model)
|
525 |
+
|
526 |
+
data = {
|
527 |
+
"prompt": form_data.prompt,
|
528 |
+
"batch_size": form_data.n,
|
529 |
+
"width": width,
|
530 |
+
"height": height,
|
531 |
+
}
|
532 |
+
|
533 |
+
if app.state.config.IMAGE_STEPS is not None:
|
534 |
+
data["steps"] = app.state.config.IMAGE_STEPS
|
535 |
+
|
536 |
+
if form_data.negative_prompt is not None:
|
537 |
+
data["negative_prompt"] = form_data.negative_prompt
|
538 |
+
|
539 |
+
r = requests.post(
|
540 |
+
url=f"{app.state.config.AUTOMATIC1111_BASE_URL}/sdapi/v1/txt2img",
|
541 |
+
json=data,
|
542 |
+
headers={"authorization": get_automatic1111_api_auth()},
|
543 |
+
)
|
544 |
+
|
545 |
+
res = r.json()
|
546 |
+
|
547 |
+
log.debug(f"res: {res}")
|
548 |
+
|
549 |
+
images = []
|
550 |
+
|
551 |
+
for image in res["images"]:
|
552 |
+
image_filename = save_b64_image(image)
|
553 |
+
images.append({"url": f"/cache/image/generations/{image_filename}"})
|
554 |
+
file_body_path = IMAGE_CACHE_DIR.joinpath(f"{image_filename}.json")
|
555 |
+
|
556 |
+
with open(file_body_path, "w") as f:
|
557 |
+
json.dump({**data, "info": res["info"]}, f)
|
558 |
+
|
559 |
+
return images
|
560 |
+
|
561 |
+
except Exception as e:
|
562 |
+
error = e
|
563 |
+
|
564 |
+
if r != None:
|
565 |
+
data = r.json()
|
566 |
+
if "error" in data:
|
567 |
+
error = data["error"]["message"]
|
568 |
+
raise HTTPException(status_code=400, detail=ERROR_MESSAGES.DEFAULT(error))
|
backend/apps/images/utils/comfyui.py
ADDED
@@ -0,0 +1,250 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 |
+
cfg_scale: Optional[float] = None
|
194 |
+
sampler: Optional[str] = None
|
195 |
+
scheduler: Optional[str] = None
|
196 |
+
sd3: Optional[bool] = None
|
197 |
+
|
198 |
+
|
199 |
+
def comfyui_generate_image(
|
200 |
+
model: str, payload: ImageGenerationPayload, client_id, base_url
|
201 |
+
):
|
202 |
+
ws_url = base_url.replace("http://", "ws://").replace("https://", "wss://")
|
203 |
+
|
204 |
+
comfyui_prompt = json.loads(COMFYUI_DEFAULT_PROMPT)
|
205 |
+
|
206 |
+
if payload.cfg_scale:
|
207 |
+
comfyui_prompt["3"]["inputs"]["cfg"] = payload.cfg_scale
|
208 |
+
|
209 |
+
if payload.sampler:
|
210 |
+
comfyui_prompt["3"]["inputs"]["sampler"] = payload.sampler
|
211 |
+
|
212 |
+
if payload.scheduler:
|
213 |
+
comfyui_prompt["3"]["inputs"]["scheduler"] = payload.scheduler
|
214 |
+
|
215 |
+
if payload.sd3:
|
216 |
+
comfyui_prompt["5"]["class_type"] = "EmptySD3LatentImage"
|
217 |
+
|
218 |
+
comfyui_prompt["4"]["inputs"]["ckpt_name"] = model
|
219 |
+
comfyui_prompt["5"]["inputs"]["batch_size"] = payload.n
|
220 |
+
comfyui_prompt["5"]["inputs"]["width"] = payload.width
|
221 |
+
comfyui_prompt["5"]["inputs"]["height"] = payload.height
|
222 |
+
|
223 |
+
# set the text prompt for our positive CLIPTextEncode
|
224 |
+
comfyui_prompt["6"]["inputs"]["text"] = payload.prompt
|
225 |
+
comfyui_prompt["7"]["inputs"]["text"] = payload.negative_prompt
|
226 |
+
|
227 |
+
if payload.steps:
|
228 |
+
comfyui_prompt["3"]["inputs"]["steps"] = payload.steps
|
229 |
+
|
230 |
+
comfyui_prompt["3"]["inputs"]["seed"] = (
|
231 |
+
payload.seed if payload.seed else random.randint(0, 18446744073709551614)
|
232 |
+
)
|
233 |
+
|
234 |
+
try:
|
235 |
+
ws = websocket.WebSocket()
|
236 |
+
ws.connect(f"{ws_url}/ws?clientId={client_id}")
|
237 |
+
log.info("WebSocket connection established.")
|
238 |
+
except Exception as e:
|
239 |
+
log.exception(f"Failed to connect to WebSocket server: {e}")
|
240 |
+
return None
|
241 |
+
|
242 |
+
try:
|
243 |
+
images = get_images(ws, comfyui_prompt, client_id, base_url)
|
244 |
+
except Exception as e:
|
245 |
+
log.exception(f"Error while receiving images: {e}")
|
246 |
+
images = None
|
247 |
+
|
248 |
+
ws.close()
|
249 |
+
|
250 |
+
return images
|
backend/apps/ollama/main.py
ADDED
@@ -0,0 +1,1280 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 |
+
from starlette.background import BackgroundTask
|
33 |
+
|
34 |
+
from apps.webui.models.models import Models
|
35 |
+
from apps.webui.models.users import Users
|
36 |
+
from constants import ERROR_MESSAGES
|
37 |
+
from utils.utils import (
|
38 |
+
decode_token,
|
39 |
+
get_current_user,
|
40 |
+
get_verified_user,
|
41 |
+
get_admin_user,
|
42 |
+
)
|
43 |
+
from utils.task import prompt_template
|
44 |
+
|
45 |
+
|
46 |
+
from config import (
|
47 |
+
SRC_LOG_LEVELS,
|
48 |
+
OLLAMA_BASE_URLS,
|
49 |
+
ENABLE_OLLAMA_API,
|
50 |
+
AIOHTTP_CLIENT_TIMEOUT,
|
51 |
+
ENABLE_MODEL_FILTER,
|
52 |
+
MODEL_FILTER_LIST,
|
53 |
+
UPLOAD_DIR,
|
54 |
+
AppConfig,
|
55 |
+
)
|
56 |
+
from utils.misc import calculate_sha256, add_or_update_system_message
|
57 |
+
|
58 |
+
log = logging.getLogger(__name__)
|
59 |
+
log.setLevel(SRC_LOG_LEVELS["OLLAMA"])
|
60 |
+
|
61 |
+
app = FastAPI()
|
62 |
+
app.add_middleware(
|
63 |
+
CORSMiddleware,
|
64 |
+
allow_origins=["*"],
|
65 |
+
allow_credentials=True,
|
66 |
+
allow_methods=["*"],
|
67 |
+
allow_headers=["*"],
|
68 |
+
)
|
69 |
+
|
70 |
+
app.state.config = AppConfig()
|
71 |
+
|
72 |
+
app.state.config.ENABLE_MODEL_FILTER = ENABLE_MODEL_FILTER
|
73 |
+
app.state.config.MODEL_FILTER_LIST = MODEL_FILTER_LIST
|
74 |
+
|
75 |
+
app.state.config.ENABLE_OLLAMA_API = ENABLE_OLLAMA_API
|
76 |
+
app.state.config.OLLAMA_BASE_URLS = OLLAMA_BASE_URLS
|
77 |
+
app.state.MODELS = {}
|
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 |
+
async def fetch_url(url):
|
135 |
+
timeout = aiohttp.ClientTimeout(total=5)
|
136 |
+
try:
|
137 |
+
async with aiohttp.ClientSession(timeout=timeout, trust_env=True) as session:
|
138 |
+
async with session.get(url) as response:
|
139 |
+
return await response.json()
|
140 |
+
except Exception as e:
|
141 |
+
# Handle connection error here
|
142 |
+
log.error(f"Connection error: {e}")
|
143 |
+
return None
|
144 |
+
|
145 |
+
|
146 |
+
async def cleanup_response(
|
147 |
+
response: Optional[aiohttp.ClientResponse],
|
148 |
+
session: Optional[aiohttp.ClientSession],
|
149 |
+
):
|
150 |
+
if response:
|
151 |
+
response.close()
|
152 |
+
if session:
|
153 |
+
await session.close()
|
154 |
+
|
155 |
+
|
156 |
+
async def post_streaming_url(url: str, payload: str, stream: bool = True):
|
157 |
+
r = None
|
158 |
+
try:
|
159 |
+
session = aiohttp.ClientSession(
|
160 |
+
trust_env=True, timeout=aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT)
|
161 |
+
)
|
162 |
+
r = await session.post(url, data=payload)
|
163 |
+
r.raise_for_status()
|
164 |
+
|
165 |
+
if stream:
|
166 |
+
return StreamingResponse(
|
167 |
+
r.content,
|
168 |
+
status_code=r.status,
|
169 |
+
headers=dict(r.headers),
|
170 |
+
background=BackgroundTask(
|
171 |
+
cleanup_response, response=r, session=session
|
172 |
+
),
|
173 |
+
)
|
174 |
+
else:
|
175 |
+
res = await r.json()
|
176 |
+
await cleanup_response(r, session)
|
177 |
+
return res
|
178 |
+
|
179 |
+
except Exception as e:
|
180 |
+
error_detail = "Open WebUI: Server Connection Error"
|
181 |
+
if r is not None:
|
182 |
+
try:
|
183 |
+
res = await r.json()
|
184 |
+
if "error" in res:
|
185 |
+
error_detail = f"Ollama: {res['error']}"
|
186 |
+
except:
|
187 |
+
error_detail = f"Ollama: {e}"
|
188 |
+
|
189 |
+
raise HTTPException(
|
190 |
+
status_code=r.status if r else 500,
|
191 |
+
detail=error_detail,
|
192 |
+
)
|
193 |
+
|
194 |
+
|
195 |
+
def merge_models_lists(model_lists):
|
196 |
+
merged_models = {}
|
197 |
+
|
198 |
+
for idx, model_list in enumerate(model_lists):
|
199 |
+
if model_list is not None:
|
200 |
+
for model in model_list:
|
201 |
+
digest = model["digest"]
|
202 |
+
if digest not in merged_models:
|
203 |
+
model["urls"] = [idx]
|
204 |
+
merged_models[digest] = model
|
205 |
+
else:
|
206 |
+
merged_models[digest]["urls"].append(idx)
|
207 |
+
|
208 |
+
return list(merged_models.values())
|
209 |
+
|
210 |
+
|
211 |
+
async def get_all_models():
|
212 |
+
log.info("get_all_models()")
|
213 |
+
|
214 |
+
if app.state.config.ENABLE_OLLAMA_API:
|
215 |
+
tasks = [
|
216 |
+
fetch_url(f"{url}/api/tags") for url in app.state.config.OLLAMA_BASE_URLS
|
217 |
+
]
|
218 |
+
responses = await asyncio.gather(*tasks)
|
219 |
+
|
220 |
+
models = {
|
221 |
+
"models": merge_models_lists(
|
222 |
+
map(
|
223 |
+
lambda response: response["models"] if response else None, responses
|
224 |
+
)
|
225 |
+
)
|
226 |
+
}
|
227 |
+
|
228 |
+
else:
|
229 |
+
models = {"models": []}
|
230 |
+
|
231 |
+
app.state.MODELS = {model["model"]: model for model in models["models"]}
|
232 |
+
|
233 |
+
return models
|
234 |
+
|
235 |
+
|
236 |
+
@app.get("/api/tags")
|
237 |
+
@app.get("/api/tags/{url_idx}")
|
238 |
+
async def get_ollama_tags(
|
239 |
+
url_idx: Optional[int] = None, user=Depends(get_verified_user)
|
240 |
+
):
|
241 |
+
if url_idx == None:
|
242 |
+
models = await get_all_models()
|
243 |
+
|
244 |
+
if app.state.config.ENABLE_MODEL_FILTER:
|
245 |
+
if user.role == "user":
|
246 |
+
models["models"] = list(
|
247 |
+
filter(
|
248 |
+
lambda model: model["name"]
|
249 |
+
in app.state.config.MODEL_FILTER_LIST,
|
250 |
+
models["models"],
|
251 |
+
)
|
252 |
+
)
|
253 |
+
return models
|
254 |
+
return models
|
255 |
+
else:
|
256 |
+
url = app.state.config.OLLAMA_BASE_URLS[url_idx]
|
257 |
+
|
258 |
+
r = None
|
259 |
+
try:
|
260 |
+
r = requests.request(method="GET", url=f"{url}/api/tags")
|
261 |
+
r.raise_for_status()
|
262 |
+
|
263 |
+
return r.json()
|
264 |
+
except Exception as e:
|
265 |
+
log.exception(e)
|
266 |
+
error_detail = "Open WebUI: Server Connection Error"
|
267 |
+
if r is not None:
|
268 |
+
try:
|
269 |
+
res = r.json()
|
270 |
+
if "error" in res:
|
271 |
+
error_detail = f"Ollama: {res['error']}"
|
272 |
+
except:
|
273 |
+
error_detail = f"Ollama: {e}"
|
274 |
+
|
275 |
+
raise HTTPException(
|
276 |
+
status_code=r.status_code if r else 500,
|
277 |
+
detail=error_detail,
|
278 |
+
)
|
279 |
+
|
280 |
+
|
281 |
+
@app.get("/api/version")
|
282 |
+
@app.get("/api/version/{url_idx}")
|
283 |
+
async def get_ollama_versions(url_idx: Optional[int] = None):
|
284 |
+
if app.state.config.ENABLE_OLLAMA_API:
|
285 |
+
if url_idx == None:
|
286 |
+
|
287 |
+
# returns lowest version
|
288 |
+
tasks = [
|
289 |
+
fetch_url(f"{url}/api/version")
|
290 |
+
for url in app.state.config.OLLAMA_BASE_URLS
|
291 |
+
]
|
292 |
+
responses = await asyncio.gather(*tasks)
|
293 |
+
responses = list(filter(lambda x: x is not None, responses))
|
294 |
+
|
295 |
+
if len(responses) > 0:
|
296 |
+
lowest_version = min(
|
297 |
+
responses,
|
298 |
+
key=lambda x: tuple(
|
299 |
+
map(int, re.sub(r"^v|-.*", "", x["version"]).split("."))
|
300 |
+
),
|
301 |
+
)
|
302 |
+
|
303 |
+
return {"version": lowest_version["version"]}
|
304 |
+
else:
|
305 |
+
raise HTTPException(
|
306 |
+
status_code=500,
|
307 |
+
detail=ERROR_MESSAGES.OLLAMA_NOT_FOUND,
|
308 |
+
)
|
309 |
+
else:
|
310 |
+
url = app.state.config.OLLAMA_BASE_URLS[url_idx]
|
311 |
+
|
312 |
+
r = None
|
313 |
+
try:
|
314 |
+
r = requests.request(method="GET", url=f"{url}/api/version")
|
315 |
+
r.raise_for_status()
|
316 |
+
|
317 |
+
return r.json()
|
318 |
+
except Exception as e:
|
319 |
+
log.exception(e)
|
320 |
+
error_detail = "Open WebUI: Server Connection Error"
|
321 |
+
if r is not None:
|
322 |
+
try:
|
323 |
+
res = r.json()
|
324 |
+
if "error" in res:
|
325 |
+
error_detail = f"Ollama: {res['error']}"
|
326 |
+
except:
|
327 |
+
error_detail = f"Ollama: {e}"
|
328 |
+
|
329 |
+
raise HTTPException(
|
330 |
+
status_code=r.status_code if r else 500,
|
331 |
+
detail=error_detail,
|
332 |
+
)
|
333 |
+
else:
|
334 |
+
return {"version": False}
|
335 |
+
|
336 |
+
|
337 |
+
class ModelNameForm(BaseModel):
|
338 |
+
name: str
|
339 |
+
|
340 |
+
|
341 |
+
@app.post("/api/pull")
|
342 |
+
@app.post("/api/pull/{url_idx}")
|
343 |
+
async def pull_model(
|
344 |
+
form_data: ModelNameForm, url_idx: int = 0, user=Depends(get_admin_user)
|
345 |
+
):
|
346 |
+
url = app.state.config.OLLAMA_BASE_URLS[url_idx]
|
347 |
+
log.info(f"url: {url}")
|
348 |
+
|
349 |
+
r = None
|
350 |
+
|
351 |
+
# Admin should be able to pull models from any source
|
352 |
+
payload = {**form_data.model_dump(exclude_none=True), "insecure": True}
|
353 |
+
|
354 |
+
return await post_streaming_url(f"{url}/api/pull", json.dumps(payload))
|
355 |
+
|
356 |
+
|
357 |
+
class PushModelForm(BaseModel):
|
358 |
+
name: str
|
359 |
+
insecure: Optional[bool] = None
|
360 |
+
stream: Optional[bool] = None
|
361 |
+
|
362 |
+
|
363 |
+
@app.delete("/api/push")
|
364 |
+
@app.delete("/api/push/{url_idx}")
|
365 |
+
async def push_model(
|
366 |
+
form_data: PushModelForm,
|
367 |
+
url_idx: Optional[int] = None,
|
368 |
+
user=Depends(get_admin_user),
|
369 |
+
):
|
370 |
+
if url_idx == None:
|
371 |
+
if form_data.name in app.state.MODELS:
|
372 |
+
url_idx = app.state.MODELS[form_data.name]["urls"][0]
|
373 |
+
else:
|
374 |
+
raise HTTPException(
|
375 |
+
status_code=400,
|
376 |
+
detail=ERROR_MESSAGES.MODEL_NOT_FOUND(form_data.name),
|
377 |
+
)
|
378 |
+
|
379 |
+
url = app.state.config.OLLAMA_BASE_URLS[url_idx]
|
380 |
+
log.debug(f"url: {url}")
|
381 |
+
|
382 |
+
return await post_streaming_url(
|
383 |
+
f"{url}/api/push", form_data.model_dump_json(exclude_none=True).encode()
|
384 |
+
)
|
385 |
+
|
386 |
+
|
387 |
+
class CreateModelForm(BaseModel):
|
388 |
+
name: str
|
389 |
+
modelfile: Optional[str] = None
|
390 |
+
stream: Optional[bool] = None
|
391 |
+
path: Optional[str] = None
|
392 |
+
|
393 |
+
|
394 |
+
@app.post("/api/create")
|
395 |
+
@app.post("/api/create/{url_idx}")
|
396 |
+
async def create_model(
|
397 |
+
form_data: CreateModelForm, url_idx: int = 0, user=Depends(get_admin_user)
|
398 |
+
):
|
399 |
+
log.debug(f"form_data: {form_data}")
|
400 |
+
url = app.state.config.OLLAMA_BASE_URLS[url_idx]
|
401 |
+
log.info(f"url: {url}")
|
402 |
+
|
403 |
+
return await post_streaming_url(
|
404 |
+
f"{url}/api/create", form_data.model_dump_json(exclude_none=True).encode()
|
405 |
+
)
|
406 |
+
|
407 |
+
|
408 |
+
class CopyModelForm(BaseModel):
|
409 |
+
source: str
|
410 |
+
destination: str
|
411 |
+
|
412 |
+
|
413 |
+
@app.post("/api/copy")
|
414 |
+
@app.post("/api/copy/{url_idx}")
|
415 |
+
async def copy_model(
|
416 |
+
form_data: CopyModelForm,
|
417 |
+
url_idx: Optional[int] = None,
|
418 |
+
user=Depends(get_admin_user),
|
419 |
+
):
|
420 |
+
if url_idx == None:
|
421 |
+
if form_data.source in app.state.MODELS:
|
422 |
+
url_idx = app.state.MODELS[form_data.source]["urls"][0]
|
423 |
+
else:
|
424 |
+
raise HTTPException(
|
425 |
+
status_code=400,
|
426 |
+
detail=ERROR_MESSAGES.MODEL_NOT_FOUND(form_data.source),
|
427 |
+
)
|
428 |
+
|
429 |
+
url = app.state.config.OLLAMA_BASE_URLS[url_idx]
|
430 |
+
log.info(f"url: {url}")
|
431 |
+
|
432 |
+
try:
|
433 |
+
r = requests.request(
|
434 |
+
method="POST",
|
435 |
+
url=f"{url}/api/copy",
|
436 |
+
data=form_data.model_dump_json(exclude_none=True).encode(),
|
437 |
+
)
|
438 |
+
r.raise_for_status()
|
439 |
+
|
440 |
+
log.debug(f"r.text: {r.text}")
|
441 |
+
|
442 |
+
return True
|
443 |
+
except Exception as e:
|
444 |
+
log.exception(e)
|
445 |
+
error_detail = "Open WebUI: Server Connection Error"
|
446 |
+
if r is not None:
|
447 |
+
try:
|
448 |
+
res = r.json()
|
449 |
+
if "error" in res:
|
450 |
+
error_detail = f"Ollama: {res['error']}"
|
451 |
+
except:
|
452 |
+
error_detail = f"Ollama: {e}"
|
453 |
+
|
454 |
+
raise HTTPException(
|
455 |
+
status_code=r.status_code if r else 500,
|
456 |
+
detail=error_detail,
|
457 |
+
)
|
458 |
+
|
459 |
+
|
460 |
+
@app.delete("/api/delete")
|
461 |
+
@app.delete("/api/delete/{url_idx}")
|
462 |
+
async def delete_model(
|
463 |
+
form_data: ModelNameForm,
|
464 |
+
url_idx: Optional[int] = None,
|
465 |
+
user=Depends(get_admin_user),
|
466 |
+
):
|
467 |
+
if url_idx == None:
|
468 |
+
if form_data.name in app.state.MODELS:
|
469 |
+
url_idx = app.state.MODELS[form_data.name]["urls"][0]
|
470 |
+
else:
|
471 |
+
raise HTTPException(
|
472 |
+
status_code=400,
|
473 |
+
detail=ERROR_MESSAGES.MODEL_NOT_FOUND(form_data.name),
|
474 |
+
)
|
475 |
+
|
476 |
+
url = app.state.config.OLLAMA_BASE_URLS[url_idx]
|
477 |
+
log.info(f"url: {url}")
|
478 |
+
|
479 |
+
try:
|
480 |
+
r = requests.request(
|
481 |
+
method="DELETE",
|
482 |
+
url=f"{url}/api/delete",
|
483 |
+
data=form_data.model_dump_json(exclude_none=True).encode(),
|
484 |
+
)
|
485 |
+
r.raise_for_status()
|
486 |
+
|
487 |
+
log.debug(f"r.text: {r.text}")
|
488 |
+
|
489 |
+
return True
|
490 |
+
except Exception as e:
|
491 |
+
log.exception(e)
|
492 |
+
error_detail = "Open WebUI: Server Connection Error"
|
493 |
+
if r is not None:
|
494 |
+
try:
|
495 |
+
res = r.json()
|
496 |
+
if "error" in res:
|
497 |
+
error_detail = f"Ollama: {res['error']}"
|
498 |
+
except:
|
499 |
+
error_detail = f"Ollama: {e}"
|
500 |
+
|
501 |
+
raise HTTPException(
|
502 |
+
status_code=r.status_code if r else 500,
|
503 |
+
detail=error_detail,
|
504 |
+
)
|
505 |
+
|
506 |
+
|
507 |
+
@app.post("/api/show")
|
508 |
+
async def show_model_info(form_data: ModelNameForm, user=Depends(get_verified_user)):
|
509 |
+
if form_data.name not in app.state.MODELS:
|
510 |
+
raise HTTPException(
|
511 |
+
status_code=400,
|
512 |
+
detail=ERROR_MESSAGES.MODEL_NOT_FOUND(form_data.name),
|
513 |
+
)
|
514 |
+
|
515 |
+
url_idx = random.choice(app.state.MODELS[form_data.name]["urls"])
|
516 |
+
url = app.state.config.OLLAMA_BASE_URLS[url_idx]
|
517 |
+
log.info(f"url: {url}")
|
518 |
+
|
519 |
+
try:
|
520 |
+
r = requests.request(
|
521 |
+
method="POST",
|
522 |
+
url=f"{url}/api/show",
|
523 |
+
data=form_data.model_dump_json(exclude_none=True).encode(),
|
524 |
+
)
|
525 |
+
r.raise_for_status()
|
526 |
+
|
527 |
+
return r.json()
|
528 |
+
except Exception as e:
|
529 |
+
log.exception(e)
|
530 |
+
error_detail = "Open WebUI: Server Connection Error"
|
531 |
+
if r is not None:
|
532 |
+
try:
|
533 |
+
res = r.json()
|
534 |
+
if "error" in res:
|
535 |
+
error_detail = f"Ollama: {res['error']}"
|
536 |
+
except:
|
537 |
+
error_detail = f"Ollama: {e}"
|
538 |
+
|
539 |
+
raise HTTPException(
|
540 |
+
status_code=r.status_code if r else 500,
|
541 |
+
detail=error_detail,
|
542 |
+
)
|
543 |
+
|
544 |
+
|
545 |
+
class GenerateEmbeddingsForm(BaseModel):
|
546 |
+
model: str
|
547 |
+
prompt: str
|
548 |
+
options: Optional[dict] = None
|
549 |
+
keep_alive: Optional[Union[int, str]] = None
|
550 |
+
|
551 |
+
|
552 |
+
@app.post("/api/embeddings")
|
553 |
+
@app.post("/api/embeddings/{url_idx}")
|
554 |
+
async def generate_embeddings(
|
555 |
+
form_data: GenerateEmbeddingsForm,
|
556 |
+
url_idx: Optional[int] = None,
|
557 |
+
user=Depends(get_verified_user),
|
558 |
+
):
|
559 |
+
if url_idx == None:
|
560 |
+
model = form_data.model
|
561 |
+
|
562 |
+
if ":" not in model:
|
563 |
+
model = f"{model}:latest"
|
564 |
+
|
565 |
+
if model in app.state.MODELS:
|
566 |
+
url_idx = random.choice(app.state.MODELS[model]["urls"])
|
567 |
+
else:
|
568 |
+
raise HTTPException(
|
569 |
+
status_code=400,
|
570 |
+
detail=ERROR_MESSAGES.MODEL_NOT_FOUND(form_data.model),
|
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="POST",
|
579 |
+
url=f"{url}/api/embeddings",
|
580 |
+
data=form_data.model_dump_json(exclude_none=True).encode(),
|
581 |
+
)
|
582 |
+
r.raise_for_status()
|
583 |
+
|
584 |
+
return r.json()
|
585 |
+
except Exception as e:
|
586 |
+
log.exception(e)
|
587 |
+
error_detail = "Open WebUI: Server Connection Error"
|
588 |
+
if r is not None:
|
589 |
+
try:
|
590 |
+
res = r.json()
|
591 |
+
if "error" in res:
|
592 |
+
error_detail = f"Ollama: {res['error']}"
|
593 |
+
except:
|
594 |
+
error_detail = f"Ollama: {e}"
|
595 |
+
|
596 |
+
raise HTTPException(
|
597 |
+
status_code=r.status_code if r else 500,
|
598 |
+
detail=error_detail,
|
599 |
+
)
|
600 |
+
|
601 |
+
|
602 |
+
def generate_ollama_embeddings(
|
603 |
+
form_data: GenerateEmbeddingsForm,
|
604 |
+
url_idx: Optional[int] = None,
|
605 |
+
):
|
606 |
+
|
607 |
+
log.info(f"generate_ollama_embeddings {form_data}")
|
608 |
+
|
609 |
+
if url_idx == None:
|
610 |
+
model = form_data.model
|
611 |
+
|
612 |
+
if ":" not in model:
|
613 |
+
model = f"{model}:latest"
|
614 |
+
|
615 |
+
if model in app.state.MODELS:
|
616 |
+
url_idx = random.choice(app.state.MODELS[model]["urls"])
|
617 |
+
else:
|
618 |
+
raise HTTPException(
|
619 |
+
status_code=400,
|
620 |
+
detail=ERROR_MESSAGES.MODEL_NOT_FOUND(form_data.model),
|
621 |
+
)
|
622 |
+
|
623 |
+
url = app.state.config.OLLAMA_BASE_URLS[url_idx]
|
624 |
+
log.info(f"url: {url}")
|
625 |
+
|
626 |
+
try:
|
627 |
+
r = requests.request(
|
628 |
+
method="POST",
|
629 |
+
url=f"{url}/api/embeddings",
|
630 |
+
data=form_data.model_dump_json(exclude_none=True).encode(),
|
631 |
+
)
|
632 |
+
r.raise_for_status()
|
633 |
+
|
634 |
+
data = r.json()
|
635 |
+
|
636 |
+
log.info(f"generate_ollama_embeddings {data}")
|
637 |
+
|
638 |
+
if "embedding" in data:
|
639 |
+
return data["embedding"]
|
640 |
+
else:
|
641 |
+
raise "Something went wrong :/"
|
642 |
+
except Exception as e:
|
643 |
+
log.exception(e)
|
644 |
+
error_detail = "Open WebUI: Server Connection Error"
|
645 |
+
if r is not None:
|
646 |
+
try:
|
647 |
+
res = r.json()
|
648 |
+
if "error" in res:
|
649 |
+
error_detail = f"Ollama: {res['error']}"
|
650 |
+
except:
|
651 |
+
error_detail = f"Ollama: {e}"
|
652 |
+
|
653 |
+
raise error_detail
|
654 |
+
|
655 |
+
|
656 |
+
class GenerateCompletionForm(BaseModel):
|
657 |
+
model: str
|
658 |
+
prompt: str
|
659 |
+
images: Optional[List[str]] = None
|
660 |
+
format: Optional[str] = None
|
661 |
+
options: Optional[dict] = None
|
662 |
+
system: Optional[str] = None
|
663 |
+
template: Optional[str] = None
|
664 |
+
context: Optional[str] = None
|
665 |
+
stream: Optional[bool] = True
|
666 |
+
raw: Optional[bool] = None
|
667 |
+
keep_alive: Optional[Union[int, str]] = None
|
668 |
+
|
669 |
+
|
670 |
+
@app.post("/api/generate")
|
671 |
+
@app.post("/api/generate/{url_idx}")
|
672 |
+
async def generate_completion(
|
673 |
+
form_data: GenerateCompletionForm,
|
674 |
+
url_idx: Optional[int] = None,
|
675 |
+
user=Depends(get_verified_user),
|
676 |
+
):
|
677 |
+
|
678 |
+
if url_idx == None:
|
679 |
+
model = form_data.model
|
680 |
+
|
681 |
+
if ":" not in model:
|
682 |
+
model = f"{model}:latest"
|
683 |
+
|
684 |
+
if model in app.state.MODELS:
|
685 |
+
url_idx = random.choice(app.state.MODELS[model]["urls"])
|
686 |
+
else:
|
687 |
+
raise HTTPException(
|
688 |
+
status_code=400,
|
689 |
+
detail=ERROR_MESSAGES.MODEL_NOT_FOUND(form_data.model),
|
690 |
+
)
|
691 |
+
|
692 |
+
url = app.state.config.OLLAMA_BASE_URLS[url_idx]
|
693 |
+
log.info(f"url: {url}")
|
694 |
+
|
695 |
+
return await post_streaming_url(
|
696 |
+
f"{url}/api/generate", form_data.model_dump_json(exclude_none=True).encode()
|
697 |
+
)
|
698 |
+
|
699 |
+
|
700 |
+
class ChatMessage(BaseModel):
|
701 |
+
role: str
|
702 |
+
content: str
|
703 |
+
images: Optional[List[str]] = None
|
704 |
+
|
705 |
+
|
706 |
+
class GenerateChatCompletionForm(BaseModel):
|
707 |
+
model: str
|
708 |
+
messages: List[ChatMessage]
|
709 |
+
format: Optional[str] = None
|
710 |
+
options: Optional[dict] = None
|
711 |
+
template: Optional[str] = None
|
712 |
+
stream: Optional[bool] = None
|
713 |
+
keep_alive: Optional[Union[int, str]] = None
|
714 |
+
|
715 |
+
|
716 |
+
@app.post("/api/chat")
|
717 |
+
@app.post("/api/chat/{url_idx}")
|
718 |
+
async def generate_chat_completion(
|
719 |
+
form_data: GenerateChatCompletionForm,
|
720 |
+
url_idx: Optional[int] = None,
|
721 |
+
user=Depends(get_verified_user),
|
722 |
+
):
|
723 |
+
|
724 |
+
log.debug(
|
725 |
+
"form_data.model_dump_json(exclude_none=True).encode(): {0} ".format(
|
726 |
+
form_data.model_dump_json(exclude_none=True).encode()
|
727 |
+
)
|
728 |
+
)
|
729 |
+
|
730 |
+
payload = {
|
731 |
+
**form_data.model_dump(exclude_none=True, exclude=["metadata"]),
|
732 |
+
}
|
733 |
+
if "metadata" in payload:
|
734 |
+
del payload["metadata"]
|
735 |
+
|
736 |
+
model_id = form_data.model
|
737 |
+
model_info = Models.get_model_by_id(model_id)
|
738 |
+
|
739 |
+
if model_info:
|
740 |
+
if model_info.base_model_id:
|
741 |
+
payload["model"] = model_info.base_model_id
|
742 |
+
|
743 |
+
model_info.params = model_info.params.model_dump()
|
744 |
+
|
745 |
+
if model_info.params:
|
746 |
+
if payload.get("options") is None:
|
747 |
+
payload["options"] = {}
|
748 |
+
|
749 |
+
if (
|
750 |
+
model_info.params.get("mirostat", None)
|
751 |
+
and payload["options"].get("mirostat") is None
|
752 |
+
):
|
753 |
+
payload["options"]["mirostat"] = model_info.params.get("mirostat", None)
|
754 |
+
|
755 |
+
if (
|
756 |
+
model_info.params.get("mirostat_eta", None)
|
757 |
+
and payload["options"].get("mirostat_eta") is None
|
758 |
+
):
|
759 |
+
payload["options"]["mirostat_eta"] = model_info.params.get(
|
760 |
+
"mirostat_eta", None
|
761 |
+
)
|
762 |
+
|
763 |
+
if (
|
764 |
+
model_info.params.get("mirostat_tau", None)
|
765 |
+
and payload["options"].get("mirostat_tau") is None
|
766 |
+
):
|
767 |
+
payload["options"]["mirostat_tau"] = model_info.params.get(
|
768 |
+
"mirostat_tau", None
|
769 |
+
)
|
770 |
+
|
771 |
+
if (
|
772 |
+
model_info.params.get("num_ctx", None)
|
773 |
+
and payload["options"].get("num_ctx") is None
|
774 |
+
):
|
775 |
+
payload["options"]["num_ctx"] = model_info.params.get("num_ctx", None)
|
776 |
+
|
777 |
+
if (
|
778 |
+
model_info.params.get("num_batch", None)
|
779 |
+
and payload["options"].get("num_batch") is None
|
780 |
+
):
|
781 |
+
payload["options"]["num_batch"] = model_info.params.get(
|
782 |
+
"num_batch", None
|
783 |
+
)
|
784 |
+
|
785 |
+
if (
|
786 |
+
model_info.params.get("num_keep", None)
|
787 |
+
and payload["options"].get("num_keep") is None
|
788 |
+
):
|
789 |
+
payload["options"]["num_keep"] = model_info.params.get("num_keep", None)
|
790 |
+
|
791 |
+
if (
|
792 |
+
model_info.params.get("repeat_last_n", None)
|
793 |
+
and payload["options"].get("repeat_last_n") is None
|
794 |
+
):
|
795 |
+
payload["options"]["repeat_last_n"] = model_info.params.get(
|
796 |
+
"repeat_last_n", None
|
797 |
+
)
|
798 |
+
|
799 |
+
if (
|
800 |
+
model_info.params.get("frequency_penalty", None)
|
801 |
+
and payload["options"].get("frequency_penalty") is None
|
802 |
+
):
|
803 |
+
payload["options"]["repeat_penalty"] = model_info.params.get(
|
804 |
+
"frequency_penalty", None
|
805 |
+
)
|
806 |
+
|
807 |
+
if (
|
808 |
+
model_info.params.get("temperature", None) is not None
|
809 |
+
and payload["options"].get("temperature") is None
|
810 |
+
):
|
811 |
+
payload["options"]["temperature"] = model_info.params.get(
|
812 |
+
"temperature", None
|
813 |
+
)
|
814 |
+
|
815 |
+
if (
|
816 |
+
model_info.params.get("seed", None) is not None
|
817 |
+
and payload["options"].get("seed") is None
|
818 |
+
):
|
819 |
+
payload["options"]["seed"] = model_info.params.get("seed", None)
|
820 |
+
|
821 |
+
if (
|
822 |
+
model_info.params.get("stop", None)
|
823 |
+
and payload["options"].get("stop") is None
|
824 |
+
):
|
825 |
+
payload["options"]["stop"] = (
|
826 |
+
[
|
827 |
+
bytes(stop, "utf-8").decode("unicode_escape")
|
828 |
+
for stop in model_info.params["stop"]
|
829 |
+
]
|
830 |
+
if model_info.params.get("stop", None)
|
831 |
+
else None
|
832 |
+
)
|
833 |
+
|
834 |
+
if (
|
835 |
+
model_info.params.get("tfs_z", None)
|
836 |
+
and payload["options"].get("tfs_z") is None
|
837 |
+
):
|
838 |
+
payload["options"]["tfs_z"] = model_info.params.get("tfs_z", None)
|
839 |
+
|
840 |
+
if (
|
841 |
+
model_info.params.get("max_tokens", None)
|
842 |
+
and payload["options"].get("max_tokens") is None
|
843 |
+
):
|
844 |
+
payload["options"]["num_predict"] = model_info.params.get(
|
845 |
+
"max_tokens", None
|
846 |
+
)
|
847 |
+
|
848 |
+
if (
|
849 |
+
model_info.params.get("top_k", None)
|
850 |
+
and payload["options"].get("top_k") is None
|
851 |
+
):
|
852 |
+
payload["options"]["top_k"] = model_info.params.get("top_k", None)
|
853 |
+
|
854 |
+
if (
|
855 |
+
model_info.params.get("top_p", None)
|
856 |
+
and payload["options"].get("top_p") is None
|
857 |
+
):
|
858 |
+
payload["options"]["top_p"] = model_info.params.get("top_p", None)
|
859 |
+
|
860 |
+
if (
|
861 |
+
model_info.params.get("use_mmap", None)
|
862 |
+
and payload["options"].get("use_mmap") is None
|
863 |
+
):
|
864 |
+
payload["options"]["use_mmap"] = model_info.params.get("use_mmap", None)
|
865 |
+
|
866 |
+
if (
|
867 |
+
model_info.params.get("use_mlock", None)
|
868 |
+
and payload["options"].get("use_mlock") is None
|
869 |
+
):
|
870 |
+
payload["options"]["use_mlock"] = model_info.params.get(
|
871 |
+
"use_mlock", None
|
872 |
+
)
|
873 |
+
|
874 |
+
if (
|
875 |
+
model_info.params.get("num_thread", None)
|
876 |
+
and payload["options"].get("num_thread") is None
|
877 |
+
):
|
878 |
+
payload["options"]["num_thread"] = model_info.params.get(
|
879 |
+
"num_thread", None
|
880 |
+
)
|
881 |
+
|
882 |
+
system = model_info.params.get("system", None)
|
883 |
+
if system:
|
884 |
+
system = prompt_template(
|
885 |
+
system,
|
886 |
+
**(
|
887 |
+
{
|
888 |
+
"user_name": user.name,
|
889 |
+
"user_location": (
|
890 |
+
user.info.get("location") if user.info else None
|
891 |
+
),
|
892 |
+
}
|
893 |
+
if user
|
894 |
+
else {}
|
895 |
+
),
|
896 |
+
)
|
897 |
+
|
898 |
+
if payload.get("messages"):
|
899 |
+
payload["messages"] = add_or_update_system_message(
|
900 |
+
system, payload["messages"]
|
901 |
+
)
|
902 |
+
|
903 |
+
if url_idx == None:
|
904 |
+
if ":" not in payload["model"]:
|
905 |
+
payload["model"] = f"{payload['model']}:latest"
|
906 |
+
|
907 |
+
if payload["model"] in app.state.MODELS:
|
908 |
+
url_idx = random.choice(app.state.MODELS[payload["model"]]["urls"])
|
909 |
+
else:
|
910 |
+
raise HTTPException(
|
911 |
+
status_code=400,
|
912 |
+
detail=ERROR_MESSAGES.MODEL_NOT_FOUND(form_data.model),
|
913 |
+
)
|
914 |
+
|
915 |
+
url = app.state.config.OLLAMA_BASE_URLS[url_idx]
|
916 |
+
log.info(f"url: {url}")
|
917 |
+
log.debug(payload)
|
918 |
+
|
919 |
+
return await post_streaming_url(f"{url}/api/chat", json.dumps(payload))
|
920 |
+
|
921 |
+
|
922 |
+
# TODO: we should update this part once Ollama supports other types
|
923 |
+
class OpenAIChatMessageContent(BaseModel):
|
924 |
+
type: str
|
925 |
+
model_config = ConfigDict(extra="allow")
|
926 |
+
|
927 |
+
|
928 |
+
class OpenAIChatMessage(BaseModel):
|
929 |
+
role: str
|
930 |
+
content: Union[str, OpenAIChatMessageContent]
|
931 |
+
|
932 |
+
model_config = ConfigDict(extra="allow")
|
933 |
+
|
934 |
+
|
935 |
+
class OpenAIChatCompletionForm(BaseModel):
|
936 |
+
model: str
|
937 |
+
messages: List[OpenAIChatMessage]
|
938 |
+
|
939 |
+
model_config = ConfigDict(extra="allow")
|
940 |
+
|
941 |
+
|
942 |
+
@app.post("/v1/chat/completions")
|
943 |
+
@app.post("/v1/chat/completions/{url_idx}")
|
944 |
+
async def generate_openai_chat_completion(
|
945 |
+
form_data: dict,
|
946 |
+
url_idx: Optional[int] = None,
|
947 |
+
user=Depends(get_verified_user),
|
948 |
+
):
|
949 |
+
form_data = OpenAIChatCompletionForm(**form_data)
|
950 |
+
payload = {**form_data.model_dump(exclude_none=True, exclude=["metadata"])}
|
951 |
+
|
952 |
+
if "metadata" in payload:
|
953 |
+
del payload["metadata"]
|
954 |
+
|
955 |
+
model_id = form_data.model
|
956 |
+
model_info = Models.get_model_by_id(model_id)
|
957 |
+
|
958 |
+
if model_info:
|
959 |
+
if model_info.base_model_id:
|
960 |
+
payload["model"] = model_info.base_model_id
|
961 |
+
|
962 |
+
model_info.params = model_info.params.model_dump()
|
963 |
+
|
964 |
+
if model_info.params:
|
965 |
+
payload["temperature"] = model_info.params.get("temperature", None)
|
966 |
+
payload["top_p"] = model_info.params.get("top_p", None)
|
967 |
+
payload["max_tokens"] = model_info.params.get("max_tokens", None)
|
968 |
+
payload["frequency_penalty"] = model_info.params.get(
|
969 |
+
"frequency_penalty", None
|
970 |
+
)
|
971 |
+
payload["seed"] = model_info.params.get("seed", None)
|
972 |
+
payload["stop"] = (
|
973 |
+
[
|
974 |
+
bytes(stop, "utf-8").decode("unicode_escape")
|
975 |
+
for stop in model_info.params["stop"]
|
976 |
+
]
|
977 |
+
if model_info.params.get("stop", None)
|
978 |
+
else None
|
979 |
+
)
|
980 |
+
|
981 |
+
system = model_info.params.get("system", None)
|
982 |
+
|
983 |
+
if system:
|
984 |
+
system = prompt_template(
|
985 |
+
system,
|
986 |
+
**(
|
987 |
+
{
|
988 |
+
"user_name": user.name,
|
989 |
+
"user_location": (
|
990 |
+
user.info.get("location") if user.info else None
|
991 |
+
),
|
992 |
+
}
|
993 |
+
if user
|
994 |
+
else {}
|
995 |
+
),
|
996 |
+
)
|
997 |
+
# Check if the payload already has a system message
|
998 |
+
# If not, add a system message to the payload
|
999 |
+
if payload.get("messages"):
|
1000 |
+
for message in payload["messages"]:
|
1001 |
+
if message.get("role") == "system":
|
1002 |
+
message["content"] = system + message["content"]
|
1003 |
+
break
|
1004 |
+
else:
|
1005 |
+
payload["messages"].insert(
|
1006 |
+
0,
|
1007 |
+
{
|
1008 |
+
"role": "system",
|
1009 |
+
"content": system,
|
1010 |
+
},
|
1011 |
+
)
|
1012 |
+
|
1013 |
+
if url_idx == None:
|
1014 |
+
if ":" not in payload["model"]:
|
1015 |
+
payload["model"] = f"{payload['model']}:latest"
|
1016 |
+
|
1017 |
+
if payload["model"] in app.state.MODELS:
|
1018 |
+
url_idx = random.choice(app.state.MODELS[payload["model"]]["urls"])
|
1019 |
+
else:
|
1020 |
+
raise HTTPException(
|
1021 |
+
status_code=400,
|
1022 |
+
detail=ERROR_MESSAGES.MODEL_NOT_FOUND(form_data.model),
|
1023 |
+
)
|
1024 |
+
|
1025 |
+
url = app.state.config.OLLAMA_BASE_URLS[url_idx]
|
1026 |
+
log.info(f"url: {url}")
|
1027 |
+
|
1028 |
+
return await post_streaming_url(
|
1029 |
+
f"{url}/v1/chat/completions",
|
1030 |
+
json.dumps(payload),
|
1031 |
+
stream=payload.get("stream", False),
|
1032 |
+
)
|
1033 |
+
|
1034 |
+
|
1035 |
+
@app.get("/v1/models")
|
1036 |
+
@app.get("/v1/models/{url_idx}")
|
1037 |
+
async def get_openai_models(
|
1038 |
+
url_idx: Optional[int] = None,
|
1039 |
+
user=Depends(get_verified_user),
|
1040 |
+
):
|
1041 |
+
if url_idx == None:
|
1042 |
+
models = await get_all_models()
|
1043 |
+
|
1044 |
+
if app.state.config.ENABLE_MODEL_FILTER:
|
1045 |
+
if user.role == "user":
|
1046 |
+
models["models"] = list(
|
1047 |
+
filter(
|
1048 |
+
lambda model: model["name"]
|
1049 |
+
in app.state.config.MODEL_FILTER_LIST,
|
1050 |
+
models["models"],
|
1051 |
+
)
|
1052 |
+
)
|
1053 |
+
|
1054 |
+
return {
|
1055 |
+
"data": [
|
1056 |
+
{
|
1057 |
+
"id": model["model"],
|
1058 |
+
"object": "model",
|
1059 |
+
"created": int(time.time()),
|
1060 |
+
"owned_by": "openai",
|
1061 |
+
}
|
1062 |
+
for model in models["models"]
|
1063 |
+
],
|
1064 |
+
"object": "list",
|
1065 |
+
}
|
1066 |
+
|
1067 |
+
else:
|
1068 |
+
url = app.state.config.OLLAMA_BASE_URLS[url_idx]
|
1069 |
+
try:
|
1070 |
+
r = requests.request(method="GET", url=f"{url}/api/tags")
|
1071 |
+
r.raise_for_status()
|
1072 |
+
|
1073 |
+
models = r.json()
|
1074 |
+
|
1075 |
+
return {
|
1076 |
+
"data": [
|
1077 |
+
{
|
1078 |
+
"id": model["model"],
|
1079 |
+
"object": "model",
|
1080 |
+
"created": int(time.time()),
|
1081 |
+
"owned_by": "openai",
|
1082 |
+
}
|
1083 |
+
for model in models["models"]
|
1084 |
+
],
|
1085 |
+
"object": "list",
|
1086 |
+
}
|
1087 |
+
|
1088 |
+
except Exception as e:
|
1089 |
+
log.exception(e)
|
1090 |
+
error_detail = "Open WebUI: Server Connection Error"
|
1091 |
+
if r is not None:
|
1092 |
+
try:
|
1093 |
+
res = r.json()
|
1094 |
+
if "error" in res:
|
1095 |
+
error_detail = f"Ollama: {res['error']}"
|
1096 |
+
except:
|
1097 |
+
error_detail = f"Ollama: {e}"
|
1098 |
+
|
1099 |
+
raise HTTPException(
|
1100 |
+
status_code=r.status_code if r else 500,
|
1101 |
+
detail=error_detail,
|
1102 |
+
)
|
1103 |
+
|
1104 |
+
|
1105 |
+
class UrlForm(BaseModel):
|
1106 |
+
url: str
|
1107 |
+
|
1108 |
+
|
1109 |
+
class UploadBlobForm(BaseModel):
|
1110 |
+
filename: str
|
1111 |
+
|
1112 |
+
|
1113 |
+
def parse_huggingface_url(hf_url):
|
1114 |
+
try:
|
1115 |
+
# Parse the URL
|
1116 |
+
parsed_url = urlparse(hf_url)
|
1117 |
+
|
1118 |
+
# Get the path and split it into components
|
1119 |
+
path_components = parsed_url.path.split("/")
|
1120 |
+
|
1121 |
+
# Extract the desired output
|
1122 |
+
user_repo = "/".join(path_components[1:3])
|
1123 |
+
model_file = path_components[-1]
|
1124 |
+
|
1125 |
+
return model_file
|
1126 |
+
except ValueError:
|
1127 |
+
return None
|
1128 |
+
|
1129 |
+
|
1130 |
+
async def download_file_stream(
|
1131 |
+
ollama_url, file_url, file_path, file_name, chunk_size=1024 * 1024
|
1132 |
+
):
|
1133 |
+
done = False
|
1134 |
+
|
1135 |
+
if os.path.exists(file_path):
|
1136 |
+
current_size = os.path.getsize(file_path)
|
1137 |
+
else:
|
1138 |
+
current_size = 0
|
1139 |
+
|
1140 |
+
headers = {"Range": f"bytes={current_size}-"} if current_size > 0 else {}
|
1141 |
+
|
1142 |
+
timeout = aiohttp.ClientTimeout(total=600) # Set the timeout
|
1143 |
+
|
1144 |
+
async with aiohttp.ClientSession(timeout=timeout, trust_env=True) as session:
|
1145 |
+
async with session.get(file_url, headers=headers) as response:
|
1146 |
+
total_size = int(response.headers.get("content-length", 0)) + current_size
|
1147 |
+
|
1148 |
+
with open(file_path, "ab+") as file:
|
1149 |
+
async for data in response.content.iter_chunked(chunk_size):
|
1150 |
+
current_size += len(data)
|
1151 |
+
file.write(data)
|
1152 |
+
|
1153 |
+
done = current_size == total_size
|
1154 |
+
progress = round((current_size / total_size) * 100, 2)
|
1155 |
+
|
1156 |
+
yield f'data: {{"progress": {progress}, "completed": {current_size}, "total": {total_size}}}\n\n'
|
1157 |
+
|
1158 |
+
if done:
|
1159 |
+
file.seek(0)
|
1160 |
+
hashed = calculate_sha256(file)
|
1161 |
+
file.seek(0)
|
1162 |
+
|
1163 |
+
url = f"{ollama_url}/api/blobs/sha256:{hashed}"
|
1164 |
+
response = requests.post(url, data=file)
|
1165 |
+
|
1166 |
+
if response.ok:
|
1167 |
+
res = {
|
1168 |
+
"done": done,
|
1169 |
+
"blob": f"sha256:{hashed}",
|
1170 |
+
"name": file_name,
|
1171 |
+
}
|
1172 |
+
os.remove(file_path)
|
1173 |
+
|
1174 |
+
yield f"data: {json.dumps(res)}\n\n"
|
1175 |
+
else:
|
1176 |
+
raise "Ollama: Could not create blob, Please try again."
|
1177 |
+
|
1178 |
+
|
1179 |
+
# url = "https://huggingface.co/TheBloke/stablelm-zephyr-3b-GGUF/resolve/main/stablelm-zephyr-3b.Q2_K.gguf"
|
1180 |
+
@app.post("/models/download")
|
1181 |
+
@app.post("/models/download/{url_idx}")
|
1182 |
+
async def download_model(
|
1183 |
+
form_data: UrlForm,
|
1184 |
+
url_idx: Optional[int] = None,
|
1185 |
+
user=Depends(get_admin_user),
|
1186 |
+
):
|
1187 |
+
|
1188 |
+
allowed_hosts = ["https://huggingface.co/", "https://github.com/"]
|
1189 |
+
|
1190 |
+
if not any(form_data.url.startswith(host) for host in allowed_hosts):
|
1191 |
+
raise HTTPException(
|
1192 |
+
status_code=400,
|
1193 |
+
detail="Invalid file_url. Only URLs from allowed hosts are permitted.",
|
1194 |
+
)
|
1195 |
+
|
1196 |
+
if url_idx == None:
|
1197 |
+
url_idx = 0
|
1198 |
+
url = app.state.config.OLLAMA_BASE_URLS[url_idx]
|
1199 |
+
|
1200 |
+
file_name = parse_huggingface_url(form_data.url)
|
1201 |
+
|
1202 |
+
if file_name:
|
1203 |
+
file_path = f"{UPLOAD_DIR}/{file_name}"
|
1204 |
+
|
1205 |
+
return StreamingResponse(
|
1206 |
+
download_file_stream(url, form_data.url, file_path, file_name),
|
1207 |
+
)
|
1208 |
+
else:
|
1209 |
+
return None
|
1210 |
+
|
1211 |
+
|
1212 |
+
@app.post("/models/upload")
|
1213 |
+
@app.post("/models/upload/{url_idx}")
|
1214 |
+
def upload_model(
|
1215 |
+
file: UploadFile = File(...),
|
1216 |
+
url_idx: Optional[int] = None,
|
1217 |
+
user=Depends(get_admin_user),
|
1218 |
+
):
|
1219 |
+
if url_idx == None:
|
1220 |
+
url_idx = 0
|
1221 |
+
ollama_url = app.state.config.OLLAMA_BASE_URLS[url_idx]
|
1222 |
+
|
1223 |
+
file_path = f"{UPLOAD_DIR}/{file.filename}"
|
1224 |
+
|
1225 |
+
# Save file in chunks
|
1226 |
+
with open(file_path, "wb+") as f:
|
1227 |
+
for chunk in file.file:
|
1228 |
+
f.write(chunk)
|
1229 |
+
|
1230 |
+
def file_process_stream():
|
1231 |
+
nonlocal ollama_url
|
1232 |
+
total_size = os.path.getsize(file_path)
|
1233 |
+
chunk_size = 1024 * 1024
|
1234 |
+
try:
|
1235 |
+
with open(file_path, "rb") as f:
|
1236 |
+
total = 0
|
1237 |
+
done = False
|
1238 |
+
|
1239 |
+
while not done:
|
1240 |
+
chunk = f.read(chunk_size)
|
1241 |
+
if not chunk:
|
1242 |
+
done = True
|
1243 |
+
continue
|
1244 |
+
|
1245 |
+
total += len(chunk)
|
1246 |
+
progress = round((total / total_size) * 100, 2)
|
1247 |
+
|
1248 |
+
res = {
|
1249 |
+
"progress": progress,
|
1250 |
+
"total": total_size,
|
1251 |
+
"completed": total,
|
1252 |
+
}
|
1253 |
+
yield f"data: {json.dumps(res)}\n\n"
|
1254 |
+
|
1255 |
+
if done:
|
1256 |
+
f.seek(0)
|
1257 |
+
hashed = calculate_sha256(f)
|
1258 |
+
f.seek(0)
|
1259 |
+
|
1260 |
+
url = f"{ollama_url}/api/blobs/sha256:{hashed}"
|
1261 |
+
response = requests.post(url, data=f)
|
1262 |
+
|
1263 |
+
if response.ok:
|
1264 |
+
res = {
|
1265 |
+
"done": done,
|
1266 |
+
"blob": f"sha256:{hashed}",
|
1267 |
+
"name": file.filename,
|
1268 |
+
}
|
1269 |
+
os.remove(file_path)
|
1270 |
+
yield f"data: {json.dumps(res)}\n\n"
|
1271 |
+
else:
|
1272 |
+
raise Exception(
|
1273 |
+
"Ollama: Could not create blob, Please try again."
|
1274 |
+
)
|
1275 |
+
|
1276 |
+
except Exception as e:
|
1277 |
+
res = {"error": str(e)}
|
1278 |
+
yield f"data: {json.dumps(res)}\n\n"
|
1279 |
+
|
1280 |
+
return StreamingResponse(file_process_stream(), media_type="text/event-stream")
|
backend/apps/openai/main.py
ADDED
@@ -0,0 +1,576 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 |
+
from starlette.background import BackgroundTask
|
13 |
+
|
14 |
+
from apps.webui.models.models import Models
|
15 |
+
from apps.webui.models.users import Users
|
16 |
+
from constants import ERROR_MESSAGES
|
17 |
+
from utils.utils import (
|
18 |
+
decode_token,
|
19 |
+
get_verified_user,
|
20 |
+
get_verified_user,
|
21 |
+
get_admin_user,
|
22 |
+
)
|
23 |
+
from utils.task import prompt_template
|
24 |
+
from utils.misc import add_or_update_system_message
|
25 |
+
|
26 |
+
from config import (
|
27 |
+
SRC_LOG_LEVELS,
|
28 |
+
ENABLE_OPENAI_API,
|
29 |
+
AIOHTTP_CLIENT_TIMEOUT,
|
30 |
+
OPENAI_API_BASE_URLS,
|
31 |
+
OPENAI_API_KEYS,
|
32 |
+
CACHE_DIR,
|
33 |
+
ENABLE_MODEL_FILTER,
|
34 |
+
MODEL_FILTER_LIST,
|
35 |
+
AppConfig,
|
36 |
+
)
|
37 |
+
from typing import List, Optional
|
38 |
+
|
39 |
+
|
40 |
+
import hashlib
|
41 |
+
from pathlib import Path
|
42 |
+
|
43 |
+
log = logging.getLogger(__name__)
|
44 |
+
log.setLevel(SRC_LOG_LEVELS["OPENAI"])
|
45 |
+
|
46 |
+
app = FastAPI()
|
47 |
+
app.add_middleware(
|
48 |
+
CORSMiddleware,
|
49 |
+
allow_origins=["*"],
|
50 |
+
allow_credentials=True,
|
51 |
+
allow_methods=["*"],
|
52 |
+
allow_headers=["*"],
|
53 |
+
)
|
54 |
+
|
55 |
+
|
56 |
+
app.state.config = AppConfig()
|
57 |
+
|
58 |
+
app.state.config.ENABLE_MODEL_FILTER = ENABLE_MODEL_FILTER
|
59 |
+
app.state.config.MODEL_FILTER_LIST = MODEL_FILTER_LIST
|
60 |
+
|
61 |
+
app.state.config.ENABLE_OPENAI_API = ENABLE_OPENAI_API
|
62 |
+
app.state.config.OPENAI_API_BASE_URLS = OPENAI_API_BASE_URLS
|
63 |
+
app.state.config.OPENAI_API_KEYS = OPENAI_API_KEYS
|
64 |
+
|
65 |
+
app.state.MODELS = {}
|
66 |
+
|
67 |
+
|
68 |
+
@app.middleware("http")
|
69 |
+
async def check_url(request: Request, call_next):
|
70 |
+
if len(app.state.MODELS) == 0:
|
71 |
+
await get_all_models()
|
72 |
+
else:
|
73 |
+
pass
|
74 |
+
|
75 |
+
response = await call_next(request)
|
76 |
+
return response
|
77 |
+
|
78 |
+
|
79 |
+
@app.get("/config")
|
80 |
+
async def get_config(user=Depends(get_admin_user)):
|
81 |
+
return {"ENABLE_OPENAI_API": app.state.config.ENABLE_OPENAI_API}
|
82 |
+
|
83 |
+
|
84 |
+
class OpenAIConfigForm(BaseModel):
|
85 |
+
enable_openai_api: Optional[bool] = None
|
86 |
+
|
87 |
+
|
88 |
+
@app.post("/config/update")
|
89 |
+
async def update_config(form_data: OpenAIConfigForm, user=Depends(get_admin_user)):
|
90 |
+
app.state.config.ENABLE_OPENAI_API = form_data.enable_openai_api
|
91 |
+
return {"ENABLE_OPENAI_API": app.state.config.ENABLE_OPENAI_API}
|
92 |
+
|
93 |
+
|
94 |
+
class UrlsUpdateForm(BaseModel):
|
95 |
+
urls: List[str]
|
96 |
+
|
97 |
+
|
98 |
+
class KeysUpdateForm(BaseModel):
|
99 |
+
keys: List[str]
|
100 |
+
|
101 |
+
|
102 |
+
@app.get("/urls")
|
103 |
+
async def get_openai_urls(user=Depends(get_admin_user)):
|
104 |
+
return {"OPENAI_API_BASE_URLS": app.state.config.OPENAI_API_BASE_URLS}
|
105 |
+
|
106 |
+
|
107 |
+
@app.post("/urls/update")
|
108 |
+
async def update_openai_urls(form_data: UrlsUpdateForm, user=Depends(get_admin_user)):
|
109 |
+
await get_all_models()
|
110 |
+
app.state.config.OPENAI_API_BASE_URLS = form_data.urls
|
111 |
+
return {"OPENAI_API_BASE_URLS": app.state.config.OPENAI_API_BASE_URLS}
|
112 |
+
|
113 |
+
|
114 |
+
@app.get("/keys")
|
115 |
+
async def get_openai_keys(user=Depends(get_admin_user)):
|
116 |
+
return {"OPENAI_API_KEYS": app.state.config.OPENAI_API_KEYS}
|
117 |
+
|
118 |
+
|
119 |
+
@app.post("/keys/update")
|
120 |
+
async def update_openai_key(form_data: KeysUpdateForm, user=Depends(get_admin_user)):
|
121 |
+
app.state.config.OPENAI_API_KEYS = form_data.keys
|
122 |
+
return {"OPENAI_API_KEYS": app.state.config.OPENAI_API_KEYS}
|
123 |
+
|
124 |
+
|
125 |
+
@app.post("/audio/speech")
|
126 |
+
async def speech(request: Request, user=Depends(get_verified_user)):
|
127 |
+
idx = None
|
128 |
+
try:
|
129 |
+
idx = app.state.config.OPENAI_API_BASE_URLS.index("https://api.openai.com/v1")
|
130 |
+
body = await request.body()
|
131 |
+
name = hashlib.sha256(body).hexdigest()
|
132 |
+
|
133 |
+
SPEECH_CACHE_DIR = Path(CACHE_DIR).joinpath("./audio/speech/")
|
134 |
+
SPEECH_CACHE_DIR.mkdir(parents=True, exist_ok=True)
|
135 |
+
file_path = SPEECH_CACHE_DIR.joinpath(f"{name}.mp3")
|
136 |
+
file_body_path = SPEECH_CACHE_DIR.joinpath(f"{name}.json")
|
137 |
+
|
138 |
+
# Check if the file already exists in the cache
|
139 |
+
if file_path.is_file():
|
140 |
+
return FileResponse(file_path)
|
141 |
+
|
142 |
+
headers = {}
|
143 |
+
headers["Authorization"] = f"Bearer {app.state.config.OPENAI_API_KEYS[idx]}"
|
144 |
+
headers["Content-Type"] = "application/json"
|
145 |
+
if "openrouter.ai" in app.state.config.OPENAI_API_BASE_URLS[idx]:
|
146 |
+
headers["HTTP-Referer"] = "https://openwebui.com/"
|
147 |
+
headers["X-Title"] = "Open WebUI"
|
148 |
+
r = None
|
149 |
+
try:
|
150 |
+
r = requests.post(
|
151 |
+
url=f"{app.state.config.OPENAI_API_BASE_URLS[idx]}/audio/speech",
|
152 |
+
data=body,
|
153 |
+
headers=headers,
|
154 |
+
stream=True,
|
155 |
+
)
|
156 |
+
|
157 |
+
r.raise_for_status()
|
158 |
+
|
159 |
+
# Save the streaming content to a file
|
160 |
+
with open(file_path, "wb") as f:
|
161 |
+
for chunk in r.iter_content(chunk_size=8192):
|
162 |
+
f.write(chunk)
|
163 |
+
|
164 |
+
with open(file_body_path, "w") as f:
|
165 |
+
json.dump(json.loads(body.decode("utf-8")), f)
|
166 |
+
|
167 |
+
# Return the saved file
|
168 |
+
return FileResponse(file_path)
|
169 |
+
|
170 |
+
except Exception as e:
|
171 |
+
log.exception(e)
|
172 |
+
error_detail = "Open WebUI: Server Connection Error"
|
173 |
+
if r is not None:
|
174 |
+
try:
|
175 |
+
res = r.json()
|
176 |
+
if "error" in res:
|
177 |
+
error_detail = f"External: {res['error']}"
|
178 |
+
except:
|
179 |
+
error_detail = f"External: {e}"
|
180 |
+
|
181 |
+
raise HTTPException(
|
182 |
+
status_code=r.status_code if r else 500, detail=error_detail
|
183 |
+
)
|
184 |
+
|
185 |
+
except ValueError:
|
186 |
+
raise HTTPException(status_code=401, detail=ERROR_MESSAGES.OPENAI_NOT_FOUND)
|
187 |
+
|
188 |
+
|
189 |
+
async def fetch_url(url, key):
|
190 |
+
timeout = aiohttp.ClientTimeout(total=5)
|
191 |
+
try:
|
192 |
+
headers = {"Authorization": f"Bearer {key}"}
|
193 |
+
async with aiohttp.ClientSession(timeout=timeout, trust_env=True) as session:
|
194 |
+
async with session.get(url, headers=headers) as response:
|
195 |
+
return await response.json()
|
196 |
+
except Exception as e:
|
197 |
+
# Handle connection error here
|
198 |
+
log.error(f"Connection error: {e}")
|
199 |
+
return None
|
200 |
+
|
201 |
+
|
202 |
+
async def cleanup_response(
|
203 |
+
response: Optional[aiohttp.ClientResponse],
|
204 |
+
session: Optional[aiohttp.ClientSession],
|
205 |
+
):
|
206 |
+
if response:
|
207 |
+
response.close()
|
208 |
+
if session:
|
209 |
+
await session.close()
|
210 |
+
|
211 |
+
|
212 |
+
def merge_models_lists(model_lists):
|
213 |
+
log.debug(f"merge_models_lists {model_lists}")
|
214 |
+
merged_list = []
|
215 |
+
|
216 |
+
for idx, models in enumerate(model_lists):
|
217 |
+
if models is not None and "error" not in models:
|
218 |
+
merged_list.extend(
|
219 |
+
[
|
220 |
+
{
|
221 |
+
**model,
|
222 |
+
"name": model.get("name", model["id"]),
|
223 |
+
"owned_by": "openai",
|
224 |
+
"openai": model,
|
225 |
+
"urlIdx": idx,
|
226 |
+
}
|
227 |
+
for model in models
|
228 |
+
if "api.openai.com"
|
229 |
+
not in app.state.config.OPENAI_API_BASE_URLS[idx]
|
230 |
+
or "gpt" in model["id"]
|
231 |
+
]
|
232 |
+
)
|
233 |
+
|
234 |
+
return merged_list
|
235 |
+
|
236 |
+
|
237 |
+
async def get_all_models(raw: bool = False):
|
238 |
+
log.info("get_all_models()")
|
239 |
+
|
240 |
+
if (
|
241 |
+
len(app.state.config.OPENAI_API_KEYS) == 1
|
242 |
+
and app.state.config.OPENAI_API_KEYS[0] == ""
|
243 |
+
) or not app.state.config.ENABLE_OPENAI_API:
|
244 |
+
models = {"data": []}
|
245 |
+
else:
|
246 |
+
# Check if API KEYS length is same than API URLS length
|
247 |
+
if len(app.state.config.OPENAI_API_KEYS) != len(
|
248 |
+
app.state.config.OPENAI_API_BASE_URLS
|
249 |
+
):
|
250 |
+
# if there are more keys than urls, remove the extra keys
|
251 |
+
if len(app.state.config.OPENAI_API_KEYS) > len(
|
252 |
+
app.state.config.OPENAI_API_BASE_URLS
|
253 |
+
):
|
254 |
+
app.state.config.OPENAI_API_KEYS = app.state.config.OPENAI_API_KEYS[
|
255 |
+
: len(app.state.config.OPENAI_API_BASE_URLS)
|
256 |
+
]
|
257 |
+
# if there are more urls than keys, add empty keys
|
258 |
+
else:
|
259 |
+
app.state.config.OPENAI_API_KEYS += [
|
260 |
+
""
|
261 |
+
for _ in range(
|
262 |
+
len(app.state.config.OPENAI_API_BASE_URLS)
|
263 |
+
- len(app.state.config.OPENAI_API_KEYS)
|
264 |
+
)
|
265 |
+
]
|
266 |
+
|
267 |
+
tasks = [
|
268 |
+
fetch_url(f"{url}/models", app.state.config.OPENAI_API_KEYS[idx])
|
269 |
+
for idx, url in enumerate(app.state.config.OPENAI_API_BASE_URLS)
|
270 |
+
]
|
271 |
+
|
272 |
+
responses = await asyncio.gather(*tasks)
|
273 |
+
log.debug(f"get_all_models:responses() {responses}")
|
274 |
+
|
275 |
+
if raw:
|
276 |
+
return responses
|
277 |
+
|
278 |
+
models = {
|
279 |
+
"data": merge_models_lists(
|
280 |
+
list(
|
281 |
+
map(
|
282 |
+
lambda response: (
|
283 |
+
response["data"]
|
284 |
+
if (response and "data" in response)
|
285 |
+
else (response if isinstance(response, list) else None)
|
286 |
+
),
|
287 |
+
responses,
|
288 |
+
)
|
289 |
+
)
|
290 |
+
)
|
291 |
+
}
|
292 |
+
|
293 |
+
log.debug(f"models: {models}")
|
294 |
+
app.state.MODELS = {model["id"]: model for model in models["data"]}
|
295 |
+
|
296 |
+
return models
|
297 |
+
|
298 |
+
|
299 |
+
@app.get("/models")
|
300 |
+
@app.get("/models/{url_idx}")
|
301 |
+
async def get_models(url_idx: Optional[int] = None, user=Depends(get_verified_user)):
|
302 |
+
if url_idx == None:
|
303 |
+
models = await get_all_models()
|
304 |
+
if app.state.config.ENABLE_MODEL_FILTER:
|
305 |
+
if user.role == "user":
|
306 |
+
models["data"] = list(
|
307 |
+
filter(
|
308 |
+
lambda model: model["id"] in app.state.config.MODEL_FILTER_LIST,
|
309 |
+
models["data"],
|
310 |
+
)
|
311 |
+
)
|
312 |
+
return models
|
313 |
+
return models
|
314 |
+
else:
|
315 |
+
url = app.state.config.OPENAI_API_BASE_URLS[url_idx]
|
316 |
+
key = app.state.config.OPENAI_API_KEYS[url_idx]
|
317 |
+
|
318 |
+
headers = {}
|
319 |
+
headers["Authorization"] = f"Bearer {key}"
|
320 |
+
headers["Content-Type"] = "application/json"
|
321 |
+
|
322 |
+
r = None
|
323 |
+
|
324 |
+
try:
|
325 |
+
r = requests.request(method="GET", url=f"{url}/models", headers=headers)
|
326 |
+
r.raise_for_status()
|
327 |
+
|
328 |
+
response_data = r.json()
|
329 |
+
if "api.openai.com" in url:
|
330 |
+
response_data["data"] = list(
|
331 |
+
filter(lambda model: "gpt" in model["id"], response_data["data"])
|
332 |
+
)
|
333 |
+
|
334 |
+
return response_data
|
335 |
+
except Exception as e:
|
336 |
+
log.exception(e)
|
337 |
+
error_detail = "Open WebUI: Server Connection Error"
|
338 |
+
if r is not None:
|
339 |
+
try:
|
340 |
+
res = r.json()
|
341 |
+
if "error" in res:
|
342 |
+
error_detail = f"External: {res['error']}"
|
343 |
+
except:
|
344 |
+
error_detail = f"External: {e}"
|
345 |
+
|
346 |
+
raise HTTPException(
|
347 |
+
status_code=r.status_code if r else 500,
|
348 |
+
detail=error_detail,
|
349 |
+
)
|
350 |
+
|
351 |
+
|
352 |
+
@app.post("/chat/completions")
|
353 |
+
@app.post("/chat/completions/{url_idx}")
|
354 |
+
async def generate_chat_completion(
|
355 |
+
form_data: dict,
|
356 |
+
url_idx: Optional[int] = None,
|
357 |
+
user=Depends(get_verified_user),
|
358 |
+
):
|
359 |
+
idx = 0
|
360 |
+
payload = {**form_data}
|
361 |
+
if "metadata" in payload:
|
362 |
+
del payload["metadata"]
|
363 |
+
|
364 |
+
model_id = form_data.get("model")
|
365 |
+
model_info = Models.get_model_by_id(model_id)
|
366 |
+
|
367 |
+
if model_info:
|
368 |
+
if model_info.base_model_id:
|
369 |
+
payload["model"] = model_info.base_model_id
|
370 |
+
|
371 |
+
model_info.params = model_info.params.model_dump()
|
372 |
+
|
373 |
+
if model_info.params:
|
374 |
+
if (
|
375 |
+
model_info.params.get("temperature", None) is not None
|
376 |
+
and payload.get("temperature") is None
|
377 |
+
):
|
378 |
+
payload["temperature"] = float(model_info.params.get("temperature"))
|
379 |
+
|
380 |
+
if model_info.params.get("top_p", None) and payload.get("top_p") is None:
|
381 |
+
payload["top_p"] = int(model_info.params.get("top_p", None))
|
382 |
+
|
383 |
+
if (
|
384 |
+
model_info.params.get("max_tokens", None)
|
385 |
+
and payload.get("max_tokens") is None
|
386 |
+
):
|
387 |
+
payload["max_tokens"] = int(model_info.params.get("max_tokens", None))
|
388 |
+
|
389 |
+
if (
|
390 |
+
model_info.params.get("frequency_penalty", None)
|
391 |
+
and payload.get("frequency_penalty") is None
|
392 |
+
):
|
393 |
+
payload["frequency_penalty"] = int(
|
394 |
+
model_info.params.get("frequency_penalty", None)
|
395 |
+
)
|
396 |
+
|
397 |
+
if (
|
398 |
+
model_info.params.get("seed", None) is not None
|
399 |
+
and payload.get("seed") is None
|
400 |
+
):
|
401 |
+
payload["seed"] = model_info.params.get("seed", None)
|
402 |
+
|
403 |
+
if model_info.params.get("stop", None) and payload.get("stop") is None:
|
404 |
+
payload["stop"] = (
|
405 |
+
[
|
406 |
+
bytes(stop, "utf-8").decode("unicode_escape")
|
407 |
+
for stop in model_info.params["stop"]
|
408 |
+
]
|
409 |
+
if model_info.params.get("stop", None)
|
410 |
+
else None
|
411 |
+
)
|
412 |
+
|
413 |
+
system = model_info.params.get("system", None)
|
414 |
+
if system:
|
415 |
+
system = prompt_template(
|
416 |
+
system,
|
417 |
+
**(
|
418 |
+
{
|
419 |
+
"user_name": user.name,
|
420 |
+
"user_location": (
|
421 |
+
user.info.get("location") if user.info else None
|
422 |
+
),
|
423 |
+
}
|
424 |
+
if user
|
425 |
+
else {}
|
426 |
+
),
|
427 |
+
)
|
428 |
+
if payload.get("messages"):
|
429 |
+
payload["messages"] = add_or_update_system_message(
|
430 |
+
system, payload["messages"]
|
431 |
+
)
|
432 |
+
|
433 |
+
else:
|
434 |
+
pass
|
435 |
+
|
436 |
+
model = app.state.MODELS[payload.get("model")]
|
437 |
+
idx = model["urlIdx"]
|
438 |
+
|
439 |
+
if "pipeline" in model and model.get("pipeline"):
|
440 |
+
payload["user"] = {
|
441 |
+
"name": user.name,
|
442 |
+
"id": user.id,
|
443 |
+
"email": user.email,
|
444 |
+
"role": user.role,
|
445 |
+
}
|
446 |
+
|
447 |
+
# Check if the model is "gpt-4-vision-preview" and set "max_tokens" to 4000
|
448 |
+
# This is a workaround until OpenAI fixes the issue with this model
|
449 |
+
if payload.get("model") == "gpt-4-vision-preview":
|
450 |
+
if "max_tokens" not in payload:
|
451 |
+
payload["max_tokens"] = 4000
|
452 |
+
log.debug("Modified payload:", payload)
|
453 |
+
|
454 |
+
# Convert the modified body back to JSON
|
455 |
+
payload = json.dumps(payload)
|
456 |
+
|
457 |
+
log.debug(payload)
|
458 |
+
|
459 |
+
url = app.state.config.OPENAI_API_BASE_URLS[idx]
|
460 |
+
key = app.state.config.OPENAI_API_KEYS[idx]
|
461 |
+
|
462 |
+
headers = {}
|
463 |
+
headers["Authorization"] = f"Bearer {key}"
|
464 |
+
headers["Content-Type"] = "application/json"
|
465 |
+
|
466 |
+
r = None
|
467 |
+
session = None
|
468 |
+
streaming = False
|
469 |
+
|
470 |
+
try:
|
471 |
+
session = aiohttp.ClientSession(
|
472 |
+
trust_env=True, timeout=aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT)
|
473 |
+
)
|
474 |
+
r = await session.request(
|
475 |
+
method="POST",
|
476 |
+
url=f"{url}/chat/completions",
|
477 |
+
data=payload,
|
478 |
+
headers=headers,
|
479 |
+
)
|
480 |
+
|
481 |
+
r.raise_for_status()
|
482 |
+
|
483 |
+
# Check if response is SSE
|
484 |
+
if "text/event-stream" in r.headers.get("Content-Type", ""):
|
485 |
+
streaming = True
|
486 |
+
return StreamingResponse(
|
487 |
+
r.content,
|
488 |
+
status_code=r.status,
|
489 |
+
headers=dict(r.headers),
|
490 |
+
background=BackgroundTask(
|
491 |
+
cleanup_response, response=r, session=session
|
492 |
+
),
|
493 |
+
)
|
494 |
+
else:
|
495 |
+
response_data = await r.json()
|
496 |
+
return response_data
|
497 |
+
except Exception as e:
|
498 |
+
log.exception(e)
|
499 |
+
error_detail = "Open WebUI: Server Connection Error"
|
500 |
+
if r is not None:
|
501 |
+
try:
|
502 |
+
res = await r.json()
|
503 |
+
print(res)
|
504 |
+
if "error" in res:
|
505 |
+
error_detail = f"External: {res['error']['message'] if 'message' in res['error'] else res['error']}"
|
506 |
+
except:
|
507 |
+
error_detail = f"External: {e}"
|
508 |
+
raise HTTPException(status_code=r.status if r else 500, detail=error_detail)
|
509 |
+
finally:
|
510 |
+
if not streaming and session:
|
511 |
+
if r:
|
512 |
+
r.close()
|
513 |
+
await session.close()
|
514 |
+
|
515 |
+
|
516 |
+
@app.api_route("/{path:path}", methods=["GET", "POST", "PUT", "DELETE"])
|
517 |
+
async def proxy(path: str, request: Request, user=Depends(get_verified_user)):
|
518 |
+
idx = 0
|
519 |
+
|
520 |
+
body = await request.body()
|
521 |
+
|
522 |
+
url = app.state.config.OPENAI_API_BASE_URLS[idx]
|
523 |
+
key = app.state.config.OPENAI_API_KEYS[idx]
|
524 |
+
|
525 |
+
target_url = f"{url}/{path}"
|
526 |
+
|
527 |
+
headers = {}
|
528 |
+
headers["Authorization"] = f"Bearer {key}"
|
529 |
+
headers["Content-Type"] = "application/json"
|
530 |
+
|
531 |
+
r = None
|
532 |
+
session = None
|
533 |
+
streaming = False
|
534 |
+
|
535 |
+
try:
|
536 |
+
session = aiohttp.ClientSession(trust_env=True)
|
537 |
+
r = await session.request(
|
538 |
+
method=request.method,
|
539 |
+
url=target_url,
|
540 |
+
data=body,
|
541 |
+
headers=headers,
|
542 |
+
)
|
543 |
+
|
544 |
+
r.raise_for_status()
|
545 |
+
|
546 |
+
# Check if response is SSE
|
547 |
+
if "text/event-stream" in r.headers.get("Content-Type", ""):
|
548 |
+
streaming = True
|
549 |
+
return StreamingResponse(
|
550 |
+
r.content,
|
551 |
+
status_code=r.status,
|
552 |
+
headers=dict(r.headers),
|
553 |
+
background=BackgroundTask(
|
554 |
+
cleanup_response, response=r, session=session
|
555 |
+
),
|
556 |
+
)
|
557 |
+
else:
|
558 |
+
response_data = await r.json()
|
559 |
+
return response_data
|
560 |
+
except Exception as e:
|
561 |
+
log.exception(e)
|
562 |
+
error_detail = "Open WebUI: Server Connection Error"
|
563 |
+
if r is not None:
|
564 |
+
try:
|
565 |
+
res = await r.json()
|
566 |
+
print(res)
|
567 |
+
if "error" in res:
|
568 |
+
error_detail = f"External: {res['error']['message'] if 'message' in res['error'] else res['error']}"
|
569 |
+
except:
|
570 |
+
error_detail = f"External: {e}"
|
571 |
+
raise HTTPException(status_code=r.status if r else 500, detail=error_detail)
|
572 |
+
finally:
|
573 |
+
if not streaming and session:
|
574 |
+
if r:
|
575 |
+
r.close()
|
576 |
+
await session.close()
|
backend/apps/rag/main.py
ADDED
@@ -0,0 +1,1463 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 requests
|
12 |
+
import os, shutil, logging, re
|
13 |
+
from datetime import datetime
|
14 |
+
|
15 |
+
from pathlib import Path
|
16 |
+
from typing import List, Union, Sequence, Iterator, Any
|
17 |
+
|
18 |
+
from chromadb.utils.batch_utils import create_batches
|
19 |
+
from langchain_core.documents import Document
|
20 |
+
|
21 |
+
from langchain_community.document_loaders import (
|
22 |
+
WebBaseLoader,
|
23 |
+
TextLoader,
|
24 |
+
PyPDFLoader,
|
25 |
+
CSVLoader,
|
26 |
+
BSHTMLLoader,
|
27 |
+
Docx2txtLoader,
|
28 |
+
UnstructuredEPubLoader,
|
29 |
+
UnstructuredWordDocumentLoader,
|
30 |
+
UnstructuredMarkdownLoader,
|
31 |
+
UnstructuredXMLLoader,
|
32 |
+
UnstructuredRSTLoader,
|
33 |
+
UnstructuredExcelLoader,
|
34 |
+
UnstructuredPowerPointLoader,
|
35 |
+
YoutubeLoader,
|
36 |
+
OutlookMessageLoader,
|
37 |
+
)
|
38 |
+
from langchain.text_splitter import RecursiveCharacterTextSplitter
|
39 |
+
|
40 |
+
import validators
|
41 |
+
import urllib.parse
|
42 |
+
import socket
|
43 |
+
|
44 |
+
|
45 |
+
from pydantic import BaseModel
|
46 |
+
from typing import Optional
|
47 |
+
import mimetypes
|
48 |
+
import uuid
|
49 |
+
import json
|
50 |
+
|
51 |
+
from apps.webui.models.documents import (
|
52 |
+
Documents,
|
53 |
+
DocumentForm,
|
54 |
+
DocumentResponse,
|
55 |
+
)
|
56 |
+
from apps.webui.models.files import (
|
57 |
+
Files,
|
58 |
+
)
|
59 |
+
|
60 |
+
from apps.rag.utils import (
|
61 |
+
get_model_path,
|
62 |
+
get_embedding_function,
|
63 |
+
query_doc,
|
64 |
+
query_doc_with_hybrid_search,
|
65 |
+
query_collection,
|
66 |
+
query_collection_with_hybrid_search,
|
67 |
+
)
|
68 |
+
|
69 |
+
from apps.rag.search.brave import search_brave
|
70 |
+
from apps.rag.search.google_pse import search_google_pse
|
71 |
+
from apps.rag.search.main import SearchResult
|
72 |
+
from apps.rag.search.searxng import search_searxng
|
73 |
+
from apps.rag.search.serper import search_serper
|
74 |
+
from apps.rag.search.serpstack import search_serpstack
|
75 |
+
from apps.rag.search.serply import search_serply
|
76 |
+
from apps.rag.search.duckduckgo import search_duckduckgo
|
77 |
+
from apps.rag.search.tavily import search_tavily
|
78 |
+
from apps.rag.search.jina_search import search_jina
|
79 |
+
|
80 |
+
from utils.misc import (
|
81 |
+
calculate_sha256,
|
82 |
+
calculate_sha256_string,
|
83 |
+
sanitize_filename,
|
84 |
+
extract_folders_after_data_docs,
|
85 |
+
)
|
86 |
+
from utils.utils import get_verified_user, get_admin_user
|
87 |
+
|
88 |
+
from config import (
|
89 |
+
AppConfig,
|
90 |
+
ENV,
|
91 |
+
SRC_LOG_LEVELS,
|
92 |
+
UPLOAD_DIR,
|
93 |
+
DOCS_DIR,
|
94 |
+
CONTENT_EXTRACTION_ENGINE,
|
95 |
+
TIKA_SERVER_URL,
|
96 |
+
RAG_TOP_K,
|
97 |
+
RAG_RELEVANCE_THRESHOLD,
|
98 |
+
RAG_EMBEDDING_ENGINE,
|
99 |
+
RAG_EMBEDDING_MODEL,
|
100 |
+
RAG_EMBEDDING_MODEL_AUTO_UPDATE,
|
101 |
+
RAG_EMBEDDING_MODEL_TRUST_REMOTE_CODE,
|
102 |
+
ENABLE_RAG_HYBRID_SEARCH,
|
103 |
+
ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION,
|
104 |
+
RAG_RERANKING_MODEL,
|
105 |
+
PDF_EXTRACT_IMAGES,
|
106 |
+
RAG_RERANKING_MODEL_AUTO_UPDATE,
|
107 |
+
RAG_RERANKING_MODEL_TRUST_REMOTE_CODE,
|
108 |
+
RAG_OPENAI_API_BASE_URL,
|
109 |
+
RAG_OPENAI_API_KEY,
|
110 |
+
DEVICE_TYPE,
|
111 |
+
CHROMA_CLIENT,
|
112 |
+
CHUNK_SIZE,
|
113 |
+
CHUNK_OVERLAP,
|
114 |
+
RAG_TEMPLATE,
|
115 |
+
ENABLE_RAG_LOCAL_WEB_FETCH,
|
116 |
+
YOUTUBE_LOADER_LANGUAGE,
|
117 |
+
ENABLE_RAG_WEB_SEARCH,
|
118 |
+
RAG_WEB_SEARCH_ENGINE,
|
119 |
+
RAG_WEB_SEARCH_DOMAIN_FILTER_LIST,
|
120 |
+
SEARXNG_QUERY_URL,
|
121 |
+
GOOGLE_PSE_API_KEY,
|
122 |
+
GOOGLE_PSE_ENGINE_ID,
|
123 |
+
BRAVE_SEARCH_API_KEY,
|
124 |
+
SERPSTACK_API_KEY,
|
125 |
+
SERPSTACK_HTTPS,
|
126 |
+
SERPER_API_KEY,
|
127 |
+
SERPLY_API_KEY,
|
128 |
+
TAVILY_API_KEY,
|
129 |
+
RAG_WEB_SEARCH_RESULT_COUNT,
|
130 |
+
RAG_WEB_SEARCH_CONCURRENT_REQUESTS,
|
131 |
+
RAG_EMBEDDING_OPENAI_BATCH_SIZE,
|
132 |
+
)
|
133 |
+
|
134 |
+
from constants import ERROR_MESSAGES
|
135 |
+
|
136 |
+
log = logging.getLogger(__name__)
|
137 |
+
log.setLevel(SRC_LOG_LEVELS["RAG"])
|
138 |
+
|
139 |
+
app = FastAPI()
|
140 |
+
|
141 |
+
app.state.config = AppConfig()
|
142 |
+
|
143 |
+
app.state.config.TOP_K = RAG_TOP_K
|
144 |
+
app.state.config.RELEVANCE_THRESHOLD = RAG_RELEVANCE_THRESHOLD
|
145 |
+
|
146 |
+
app.state.config.ENABLE_RAG_HYBRID_SEARCH = ENABLE_RAG_HYBRID_SEARCH
|
147 |
+
app.state.config.ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION = (
|
148 |
+
ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION
|
149 |
+
)
|
150 |
+
|
151 |
+
app.state.config.CONTENT_EXTRACTION_ENGINE = CONTENT_EXTRACTION_ENGINE
|
152 |
+
app.state.config.TIKA_SERVER_URL = TIKA_SERVER_URL
|
153 |
+
|
154 |
+
app.state.config.CHUNK_SIZE = CHUNK_SIZE
|
155 |
+
app.state.config.CHUNK_OVERLAP = CHUNK_OVERLAP
|
156 |
+
|
157 |
+
app.state.config.RAG_EMBEDDING_ENGINE = RAG_EMBEDDING_ENGINE
|
158 |
+
app.state.config.RAG_EMBEDDING_MODEL = RAG_EMBEDDING_MODEL
|
159 |
+
app.state.config.RAG_EMBEDDING_OPENAI_BATCH_SIZE = RAG_EMBEDDING_OPENAI_BATCH_SIZE
|
160 |
+
app.state.config.RAG_RERANKING_MODEL = RAG_RERANKING_MODEL
|
161 |
+
app.state.config.RAG_TEMPLATE = RAG_TEMPLATE
|
162 |
+
|
163 |
+
|
164 |
+
app.state.config.OPENAI_API_BASE_URL = RAG_OPENAI_API_BASE_URL
|
165 |
+
app.state.config.OPENAI_API_KEY = RAG_OPENAI_API_KEY
|
166 |
+
|
167 |
+
app.state.config.PDF_EXTRACT_IMAGES = PDF_EXTRACT_IMAGES
|
168 |
+
|
169 |
+
|
170 |
+
app.state.config.YOUTUBE_LOADER_LANGUAGE = YOUTUBE_LOADER_LANGUAGE
|
171 |
+
app.state.YOUTUBE_LOADER_TRANSLATION = None
|
172 |
+
|
173 |
+
|
174 |
+
app.state.config.ENABLE_RAG_WEB_SEARCH = ENABLE_RAG_WEB_SEARCH
|
175 |
+
app.state.config.RAG_WEB_SEARCH_ENGINE = RAG_WEB_SEARCH_ENGINE
|
176 |
+
app.state.config.RAG_WEB_SEARCH_DOMAIN_FILTER_LIST = RAG_WEB_SEARCH_DOMAIN_FILTER_LIST
|
177 |
+
|
178 |
+
app.state.config.SEARXNG_QUERY_URL = SEARXNG_QUERY_URL
|
179 |
+
app.state.config.GOOGLE_PSE_API_KEY = GOOGLE_PSE_API_KEY
|
180 |
+
app.state.config.GOOGLE_PSE_ENGINE_ID = GOOGLE_PSE_ENGINE_ID
|
181 |
+
app.state.config.BRAVE_SEARCH_API_KEY = BRAVE_SEARCH_API_KEY
|
182 |
+
app.state.config.SERPSTACK_API_KEY = SERPSTACK_API_KEY
|
183 |
+
app.state.config.SERPSTACK_HTTPS = SERPSTACK_HTTPS
|
184 |
+
app.state.config.SERPER_API_KEY = SERPER_API_KEY
|
185 |
+
app.state.config.SERPLY_API_KEY = SERPLY_API_KEY
|
186 |
+
app.state.config.TAVILY_API_KEY = TAVILY_API_KEY
|
187 |
+
app.state.config.RAG_WEB_SEARCH_RESULT_COUNT = RAG_WEB_SEARCH_RESULT_COUNT
|
188 |
+
app.state.config.RAG_WEB_SEARCH_CONCURRENT_REQUESTS = RAG_WEB_SEARCH_CONCURRENT_REQUESTS
|
189 |
+
|
190 |
+
|
191 |
+
def update_embedding_model(
|
192 |
+
embedding_model: str,
|
193 |
+
update_model: bool = False,
|
194 |
+
):
|
195 |
+
if embedding_model and app.state.config.RAG_EMBEDDING_ENGINE == "":
|
196 |
+
import sentence_transformers
|
197 |
+
|
198 |
+
app.state.sentence_transformer_ef = sentence_transformers.SentenceTransformer(
|
199 |
+
get_model_path(embedding_model, update_model),
|
200 |
+
device=DEVICE_TYPE,
|
201 |
+
trust_remote_code=RAG_EMBEDDING_MODEL_TRUST_REMOTE_CODE,
|
202 |
+
)
|
203 |
+
else:
|
204 |
+
app.state.sentence_transformer_ef = None
|
205 |
+
|
206 |
+
|
207 |
+
def update_reranking_model(
|
208 |
+
reranking_model: str,
|
209 |
+
update_model: bool = False,
|
210 |
+
):
|
211 |
+
if reranking_model:
|
212 |
+
import sentence_transformers
|
213 |
+
|
214 |
+
app.state.sentence_transformer_rf = sentence_transformers.CrossEncoder(
|
215 |
+
get_model_path(reranking_model, update_model),
|
216 |
+
device=DEVICE_TYPE,
|
217 |
+
trust_remote_code=RAG_RERANKING_MODEL_TRUST_REMOTE_CODE,
|
218 |
+
)
|
219 |
+
else:
|
220 |
+
app.state.sentence_transformer_rf = None
|
221 |
+
|
222 |
+
|
223 |
+
update_embedding_model(
|
224 |
+
app.state.config.RAG_EMBEDDING_MODEL,
|
225 |
+
RAG_EMBEDDING_MODEL_AUTO_UPDATE,
|
226 |
+
)
|
227 |
+
|
228 |
+
update_reranking_model(
|
229 |
+
app.state.config.RAG_RERANKING_MODEL,
|
230 |
+
RAG_RERANKING_MODEL_AUTO_UPDATE,
|
231 |
+
)
|
232 |
+
|
233 |
+
|
234 |
+
app.state.EMBEDDING_FUNCTION = get_embedding_function(
|
235 |
+
app.state.config.RAG_EMBEDDING_ENGINE,
|
236 |
+
app.state.config.RAG_EMBEDDING_MODEL,
|
237 |
+
app.state.sentence_transformer_ef,
|
238 |
+
app.state.config.OPENAI_API_KEY,
|
239 |
+
app.state.config.OPENAI_API_BASE_URL,
|
240 |
+
app.state.config.RAG_EMBEDDING_OPENAI_BATCH_SIZE,
|
241 |
+
)
|
242 |
+
|
243 |
+
origins = ["*"]
|
244 |
+
|
245 |
+
|
246 |
+
app.add_middleware(
|
247 |
+
CORSMiddleware,
|
248 |
+
allow_origins=origins,
|
249 |
+
allow_credentials=True,
|
250 |
+
allow_methods=["*"],
|
251 |
+
allow_headers=["*"],
|
252 |
+
)
|
253 |
+
|
254 |
+
|
255 |
+
class CollectionNameForm(BaseModel):
|
256 |
+
collection_name: Optional[str] = "test"
|
257 |
+
|
258 |
+
|
259 |
+
class UrlForm(CollectionNameForm):
|
260 |
+
url: str
|
261 |
+
|
262 |
+
|
263 |
+
class SearchForm(CollectionNameForm):
|
264 |
+
query: str
|
265 |
+
|
266 |
+
|
267 |
+
@app.get("/")
|
268 |
+
async def get_status():
|
269 |
+
return {
|
270 |
+
"status": True,
|
271 |
+
"chunk_size": app.state.config.CHUNK_SIZE,
|
272 |
+
"chunk_overlap": app.state.config.CHUNK_OVERLAP,
|
273 |
+
"template": app.state.config.RAG_TEMPLATE,
|
274 |
+
"embedding_engine": app.state.config.RAG_EMBEDDING_ENGINE,
|
275 |
+
"embedding_model": app.state.config.RAG_EMBEDDING_MODEL,
|
276 |
+
"reranking_model": app.state.config.RAG_RERANKING_MODEL,
|
277 |
+
"openai_batch_size": app.state.config.RAG_EMBEDDING_OPENAI_BATCH_SIZE,
|
278 |
+
}
|
279 |
+
|
280 |
+
|
281 |
+
@app.get("/embedding")
|
282 |
+
async def get_embedding_config(user=Depends(get_admin_user)):
|
283 |
+
return {
|
284 |
+
"status": True,
|
285 |
+
"embedding_engine": app.state.config.RAG_EMBEDDING_ENGINE,
|
286 |
+
"embedding_model": app.state.config.RAG_EMBEDDING_MODEL,
|
287 |
+
"openai_config": {
|
288 |
+
"url": app.state.config.OPENAI_API_BASE_URL,
|
289 |
+
"key": app.state.config.OPENAI_API_KEY,
|
290 |
+
"batch_size": app.state.config.RAG_EMBEDDING_OPENAI_BATCH_SIZE,
|
291 |
+
},
|
292 |
+
}
|
293 |
+
|
294 |
+
|
295 |
+
@app.get("/reranking")
|
296 |
+
async def get_reraanking_config(user=Depends(get_admin_user)):
|
297 |
+
return {
|
298 |
+
"status": True,
|
299 |
+
"reranking_model": app.state.config.RAG_RERANKING_MODEL,
|
300 |
+
}
|
301 |
+
|
302 |
+
|
303 |
+
class OpenAIConfigForm(BaseModel):
|
304 |
+
url: str
|
305 |
+
key: str
|
306 |
+
batch_size: Optional[int] = None
|
307 |
+
|
308 |
+
|
309 |
+
class EmbeddingModelUpdateForm(BaseModel):
|
310 |
+
openai_config: Optional[OpenAIConfigForm] = None
|
311 |
+
embedding_engine: str
|
312 |
+
embedding_model: str
|
313 |
+
|
314 |
+
|
315 |
+
@app.post("/embedding/update")
|
316 |
+
async def update_embedding_config(
|
317 |
+
form_data: EmbeddingModelUpdateForm, user=Depends(get_admin_user)
|
318 |
+
):
|
319 |
+
log.info(
|
320 |
+
f"Updating embedding model: {app.state.config.RAG_EMBEDDING_MODEL} to {form_data.embedding_model}"
|
321 |
+
)
|
322 |
+
try:
|
323 |
+
app.state.config.RAG_EMBEDDING_ENGINE = form_data.embedding_engine
|
324 |
+
app.state.config.RAG_EMBEDDING_MODEL = form_data.embedding_model
|
325 |
+
|
326 |
+
if app.state.config.RAG_EMBEDDING_ENGINE in ["ollama", "openai"]:
|
327 |
+
if form_data.openai_config is not None:
|
328 |
+
app.state.config.OPENAI_API_BASE_URL = form_data.openai_config.url
|
329 |
+
app.state.config.OPENAI_API_KEY = form_data.openai_config.key
|
330 |
+
app.state.config.RAG_EMBEDDING_OPENAI_BATCH_SIZE = (
|
331 |
+
form_data.openai_config.batch_size
|
332 |
+
if form_data.openai_config.batch_size
|
333 |
+
else 1
|
334 |
+
)
|
335 |
+
|
336 |
+
update_embedding_model(app.state.config.RAG_EMBEDDING_MODEL)
|
337 |
+
|
338 |
+
app.state.EMBEDDING_FUNCTION = get_embedding_function(
|
339 |
+
app.state.config.RAG_EMBEDDING_ENGINE,
|
340 |
+
app.state.config.RAG_EMBEDDING_MODEL,
|
341 |
+
app.state.sentence_transformer_ef,
|
342 |
+
app.state.config.OPENAI_API_KEY,
|
343 |
+
app.state.config.OPENAI_API_BASE_URL,
|
344 |
+
app.state.config.RAG_EMBEDDING_OPENAI_BATCH_SIZE,
|
345 |
+
)
|
346 |
+
|
347 |
+
return {
|
348 |
+
"status": True,
|
349 |
+
"embedding_engine": app.state.config.RAG_EMBEDDING_ENGINE,
|
350 |
+
"embedding_model": app.state.config.RAG_EMBEDDING_MODEL,
|
351 |
+
"openai_config": {
|
352 |
+
"url": app.state.config.OPENAI_API_BASE_URL,
|
353 |
+
"key": app.state.config.OPENAI_API_KEY,
|
354 |
+
"batch_size": app.state.config.RAG_EMBEDDING_OPENAI_BATCH_SIZE,
|
355 |
+
},
|
356 |
+
}
|
357 |
+
except Exception as e:
|
358 |
+
log.exception(f"Problem updating embedding model: {e}")
|
359 |
+
raise HTTPException(
|
360 |
+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
361 |
+
detail=ERROR_MESSAGES.DEFAULT(e),
|
362 |
+
)
|
363 |
+
|
364 |
+
|
365 |
+
class RerankingModelUpdateForm(BaseModel):
|
366 |
+
reranking_model: str
|
367 |
+
|
368 |
+
|
369 |
+
@app.post("/reranking/update")
|
370 |
+
async def update_reranking_config(
|
371 |
+
form_data: RerankingModelUpdateForm, user=Depends(get_admin_user)
|
372 |
+
):
|
373 |
+
log.info(
|
374 |
+
f"Updating reranking model: {app.state.config.RAG_RERANKING_MODEL} to {form_data.reranking_model}"
|
375 |
+
)
|
376 |
+
try:
|
377 |
+
app.state.config.RAG_RERANKING_MODEL = form_data.reranking_model
|
378 |
+
|
379 |
+
update_reranking_model(app.state.config.RAG_RERANKING_MODEL), True
|
380 |
+
|
381 |
+
return {
|
382 |
+
"status": True,
|
383 |
+
"reranking_model": app.state.config.RAG_RERANKING_MODEL,
|
384 |
+
}
|
385 |
+
except Exception as e:
|
386 |
+
log.exception(f"Problem updating reranking model: {e}")
|
387 |
+
raise HTTPException(
|
388 |
+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
389 |
+
detail=ERROR_MESSAGES.DEFAULT(e),
|
390 |
+
)
|
391 |
+
|
392 |
+
|
393 |
+
@app.get("/config")
|
394 |
+
async def get_rag_config(user=Depends(get_admin_user)):
|
395 |
+
return {
|
396 |
+
"status": True,
|
397 |
+
"pdf_extract_images": app.state.config.PDF_EXTRACT_IMAGES,
|
398 |
+
"content_extraction": {
|
399 |
+
"engine": app.state.config.CONTENT_EXTRACTION_ENGINE,
|
400 |
+
"tika_server_url": app.state.config.TIKA_SERVER_URL,
|
401 |
+
},
|
402 |
+
"chunk": {
|
403 |
+
"chunk_size": app.state.config.CHUNK_SIZE,
|
404 |
+
"chunk_overlap": app.state.config.CHUNK_OVERLAP,
|
405 |
+
},
|
406 |
+
"youtube": {
|
407 |
+
"language": app.state.config.YOUTUBE_LOADER_LANGUAGE,
|
408 |
+
"translation": app.state.YOUTUBE_LOADER_TRANSLATION,
|
409 |
+
},
|
410 |
+
"web": {
|
411 |
+
"ssl_verification": app.state.config.ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION,
|
412 |
+
"search": {
|
413 |
+
"enabled": app.state.config.ENABLE_RAG_WEB_SEARCH,
|
414 |
+
"engine": app.state.config.RAG_WEB_SEARCH_ENGINE,
|
415 |
+
"searxng_query_url": app.state.config.SEARXNG_QUERY_URL,
|
416 |
+
"google_pse_api_key": app.state.config.GOOGLE_PSE_API_KEY,
|
417 |
+
"google_pse_engine_id": app.state.config.GOOGLE_PSE_ENGINE_ID,
|
418 |
+
"brave_search_api_key": app.state.config.BRAVE_SEARCH_API_KEY,
|
419 |
+
"serpstack_api_key": app.state.config.SERPSTACK_API_KEY,
|
420 |
+
"serpstack_https": app.state.config.SERPSTACK_HTTPS,
|
421 |
+
"serper_api_key": app.state.config.SERPER_API_KEY,
|
422 |
+
"serply_api_key": app.state.config.SERPLY_API_KEY,
|
423 |
+
"tavily_api_key": app.state.config.TAVILY_API_KEY,
|
424 |
+
"result_count": app.state.config.RAG_WEB_SEARCH_RESULT_COUNT,
|
425 |
+
"concurrent_requests": app.state.config.RAG_WEB_SEARCH_CONCURRENT_REQUESTS,
|
426 |
+
},
|
427 |
+
},
|
428 |
+
}
|
429 |
+
|
430 |
+
|
431 |
+
class ContentExtractionConfig(BaseModel):
|
432 |
+
engine: str = ""
|
433 |
+
tika_server_url: Optional[str] = None
|
434 |
+
|
435 |
+
|
436 |
+
class ChunkParamUpdateForm(BaseModel):
|
437 |
+
chunk_size: int
|
438 |
+
chunk_overlap: int
|
439 |
+
|
440 |
+
|
441 |
+
class YoutubeLoaderConfig(BaseModel):
|
442 |
+
language: List[str]
|
443 |
+
translation: Optional[str] = None
|
444 |
+
|
445 |
+
|
446 |
+
class WebSearchConfig(BaseModel):
|
447 |
+
enabled: bool
|
448 |
+
engine: Optional[str] = None
|
449 |
+
searxng_query_url: Optional[str] = None
|
450 |
+
google_pse_api_key: Optional[str] = None
|
451 |
+
google_pse_engine_id: Optional[str] = None
|
452 |
+
brave_search_api_key: Optional[str] = None
|
453 |
+
serpstack_api_key: Optional[str] = None
|
454 |
+
serpstack_https: Optional[bool] = None
|
455 |
+
serper_api_key: Optional[str] = None
|
456 |
+
serply_api_key: Optional[str] = None
|
457 |
+
tavily_api_key: Optional[str] = None
|
458 |
+
result_count: Optional[int] = None
|
459 |
+
concurrent_requests: Optional[int] = None
|
460 |
+
|
461 |
+
|
462 |
+
class WebConfig(BaseModel):
|
463 |
+
search: WebSearchConfig
|
464 |
+
web_loader_ssl_verification: Optional[bool] = None
|
465 |
+
|
466 |
+
|
467 |
+
class ConfigUpdateForm(BaseModel):
|
468 |
+
pdf_extract_images: Optional[bool] = None
|
469 |
+
content_extraction: Optional[ContentExtractionConfig] = None
|
470 |
+
chunk: Optional[ChunkParamUpdateForm] = None
|
471 |
+
youtube: Optional[YoutubeLoaderConfig] = None
|
472 |
+
web: Optional[WebConfig] = None
|
473 |
+
|
474 |
+
|
475 |
+
@app.post("/config/update")
|
476 |
+
async def update_rag_config(form_data: ConfigUpdateForm, user=Depends(get_admin_user)):
|
477 |
+
app.state.config.PDF_EXTRACT_IMAGES = (
|
478 |
+
form_data.pdf_extract_images
|
479 |
+
if form_data.pdf_extract_images is not None
|
480 |
+
else app.state.config.PDF_EXTRACT_IMAGES
|
481 |
+
)
|
482 |
+
|
483 |
+
if form_data.content_extraction is not None:
|
484 |
+
log.info(f"Updating text settings: {form_data.content_extraction}")
|
485 |
+
app.state.config.CONTENT_EXTRACTION_ENGINE = form_data.content_extraction.engine
|
486 |
+
app.state.config.TIKA_SERVER_URL = form_data.content_extraction.tika_server_url
|
487 |
+
|
488 |
+
if form_data.chunk is not None:
|
489 |
+
app.state.config.CHUNK_SIZE = form_data.chunk.chunk_size
|
490 |
+
app.state.config.CHUNK_OVERLAP = form_data.chunk.chunk_overlap
|
491 |
+
|
492 |
+
if form_data.youtube is not None:
|
493 |
+
app.state.config.YOUTUBE_LOADER_LANGUAGE = form_data.youtube.language
|
494 |
+
app.state.YOUTUBE_LOADER_TRANSLATION = form_data.youtube.translation
|
495 |
+
|
496 |
+
if form_data.web is not None:
|
497 |
+
app.state.config.ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION = (
|
498 |
+
form_data.web.web_loader_ssl_verification
|
499 |
+
)
|
500 |
+
|
501 |
+
app.state.config.ENABLE_RAG_WEB_SEARCH = form_data.web.search.enabled
|
502 |
+
app.state.config.RAG_WEB_SEARCH_ENGINE = form_data.web.search.engine
|
503 |
+
app.state.config.SEARXNG_QUERY_URL = form_data.web.search.searxng_query_url
|
504 |
+
app.state.config.GOOGLE_PSE_API_KEY = form_data.web.search.google_pse_api_key
|
505 |
+
app.state.config.GOOGLE_PSE_ENGINE_ID = (
|
506 |
+
form_data.web.search.google_pse_engine_id
|
507 |
+
)
|
508 |
+
app.state.config.BRAVE_SEARCH_API_KEY = (
|
509 |
+
form_data.web.search.brave_search_api_key
|
510 |
+
)
|
511 |
+
app.state.config.SERPSTACK_API_KEY = form_data.web.search.serpstack_api_key
|
512 |
+
app.state.config.SERPSTACK_HTTPS = form_data.web.search.serpstack_https
|
513 |
+
app.state.config.SERPER_API_KEY = form_data.web.search.serper_api_key
|
514 |
+
app.state.config.SERPLY_API_KEY = form_data.web.search.serply_api_key
|
515 |
+
app.state.config.TAVILY_API_KEY = form_data.web.search.tavily_api_key
|
516 |
+
app.state.config.RAG_WEB_SEARCH_RESULT_COUNT = form_data.web.search.result_count
|
517 |
+
app.state.config.RAG_WEB_SEARCH_CONCURRENT_REQUESTS = (
|
518 |
+
form_data.web.search.concurrent_requests
|
519 |
+
)
|
520 |
+
|
521 |
+
return {
|
522 |
+
"status": True,
|
523 |
+
"pdf_extract_images": app.state.config.PDF_EXTRACT_IMAGES,
|
524 |
+
"content_extraction": {
|
525 |
+
"engine": app.state.config.CONTENT_EXTRACTION_ENGINE,
|
526 |
+
"tika_server_url": app.state.config.TIKA_SERVER_URL,
|
527 |
+
},
|
528 |
+
"chunk": {
|
529 |
+
"chunk_size": app.state.config.CHUNK_SIZE,
|
530 |
+
"chunk_overlap": app.state.config.CHUNK_OVERLAP,
|
531 |
+
},
|
532 |
+
"youtube": {
|
533 |
+
"language": app.state.config.YOUTUBE_LOADER_LANGUAGE,
|
534 |
+
"translation": app.state.YOUTUBE_LOADER_TRANSLATION,
|
535 |
+
},
|
536 |
+
"web": {
|
537 |
+
"ssl_verification": app.state.config.ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION,
|
538 |
+
"search": {
|
539 |
+
"enabled": app.state.config.ENABLE_RAG_WEB_SEARCH,
|
540 |
+
"engine": app.state.config.RAG_WEB_SEARCH_ENGINE,
|
541 |
+
"searxng_query_url": app.state.config.SEARXNG_QUERY_URL,
|
542 |
+
"google_pse_api_key": app.state.config.GOOGLE_PSE_API_KEY,
|
543 |
+
"google_pse_engine_id": app.state.config.GOOGLE_PSE_ENGINE_ID,
|
544 |
+
"brave_search_api_key": app.state.config.BRAVE_SEARCH_API_KEY,
|
545 |
+
"serpstack_api_key": app.state.config.SERPSTACK_API_KEY,
|
546 |
+
"serpstack_https": app.state.config.SERPSTACK_HTTPS,
|
547 |
+
"serper_api_key": app.state.config.SERPER_API_KEY,
|
548 |
+
"serply_api_key": app.state.config.SERPLY_API_KEY,
|
549 |
+
"tavily_api_key": app.state.config.TAVILY_API_KEY,
|
550 |
+
"result_count": app.state.config.RAG_WEB_SEARCH_RESULT_COUNT,
|
551 |
+
"concurrent_requests": app.state.config.RAG_WEB_SEARCH_CONCURRENT_REQUESTS,
|
552 |
+
},
|
553 |
+
},
|
554 |
+
}
|
555 |
+
|
556 |
+
|
557 |
+
@app.get("/template")
|
558 |
+
async def get_rag_template(user=Depends(get_verified_user)):
|
559 |
+
return {
|
560 |
+
"status": True,
|
561 |
+
"template": app.state.config.RAG_TEMPLATE,
|
562 |
+
}
|
563 |
+
|
564 |
+
|
565 |
+
@app.get("/query/settings")
|
566 |
+
async def get_query_settings(user=Depends(get_admin_user)):
|
567 |
+
return {
|
568 |
+
"status": True,
|
569 |
+
"template": app.state.config.RAG_TEMPLATE,
|
570 |
+
"k": app.state.config.TOP_K,
|
571 |
+
"r": app.state.config.RELEVANCE_THRESHOLD,
|
572 |
+
"hybrid": app.state.config.ENABLE_RAG_HYBRID_SEARCH,
|
573 |
+
}
|
574 |
+
|
575 |
+
|
576 |
+
class QuerySettingsForm(BaseModel):
|
577 |
+
k: Optional[int] = None
|
578 |
+
r: Optional[float] = None
|
579 |
+
template: Optional[str] = None
|
580 |
+
hybrid: Optional[bool] = None
|
581 |
+
|
582 |
+
|
583 |
+
@app.post("/query/settings/update")
|
584 |
+
async def update_query_settings(
|
585 |
+
form_data: QuerySettingsForm, user=Depends(get_admin_user)
|
586 |
+
):
|
587 |
+
app.state.config.RAG_TEMPLATE = (
|
588 |
+
form_data.template if form_data.template else RAG_TEMPLATE
|
589 |
+
)
|
590 |
+
app.state.config.TOP_K = form_data.k if form_data.k else 4
|
591 |
+
app.state.config.RELEVANCE_THRESHOLD = form_data.r if form_data.r else 0.0
|
592 |
+
app.state.config.ENABLE_RAG_HYBRID_SEARCH = (
|
593 |
+
form_data.hybrid if form_data.hybrid else False
|
594 |
+
)
|
595 |
+
return {
|
596 |
+
"status": True,
|
597 |
+
"template": app.state.config.RAG_TEMPLATE,
|
598 |
+
"k": app.state.config.TOP_K,
|
599 |
+
"r": app.state.config.RELEVANCE_THRESHOLD,
|
600 |
+
"hybrid": app.state.config.ENABLE_RAG_HYBRID_SEARCH,
|
601 |
+
}
|
602 |
+
|
603 |
+
|
604 |
+
class QueryDocForm(BaseModel):
|
605 |
+
collection_name: str
|
606 |
+
query: str
|
607 |
+
k: Optional[int] = None
|
608 |
+
r: Optional[float] = None
|
609 |
+
hybrid: Optional[bool] = None
|
610 |
+
|
611 |
+
|
612 |
+
@app.post("/query/doc")
|
613 |
+
def query_doc_handler(
|
614 |
+
form_data: QueryDocForm,
|
615 |
+
user=Depends(get_verified_user),
|
616 |
+
):
|
617 |
+
try:
|
618 |
+
if app.state.config.ENABLE_RAG_HYBRID_SEARCH:
|
619 |
+
return query_doc_with_hybrid_search(
|
620 |
+
collection_name=form_data.collection_name,
|
621 |
+
query=form_data.query,
|
622 |
+
embedding_function=app.state.EMBEDDING_FUNCTION,
|
623 |
+
k=form_data.k if form_data.k else app.state.config.TOP_K,
|
624 |
+
reranking_function=app.state.sentence_transformer_rf,
|
625 |
+
r=(
|
626 |
+
form_data.r if form_data.r else app.state.config.RELEVANCE_THRESHOLD
|
627 |
+
),
|
628 |
+
)
|
629 |
+
else:
|
630 |
+
return query_doc(
|
631 |
+
collection_name=form_data.collection_name,
|
632 |
+
query=form_data.query,
|
633 |
+
embedding_function=app.state.EMBEDDING_FUNCTION,
|
634 |
+
k=form_data.k if form_data.k else app.state.config.TOP_K,
|
635 |
+
)
|
636 |
+
except Exception as e:
|
637 |
+
log.exception(e)
|
638 |
+
raise HTTPException(
|
639 |
+
status_code=status.HTTP_400_BAD_REQUEST,
|
640 |
+
detail=ERROR_MESSAGES.DEFAULT(e),
|
641 |
+
)
|
642 |
+
|
643 |
+
|
644 |
+
class QueryCollectionsForm(BaseModel):
|
645 |
+
collection_names: List[str]
|
646 |
+
query: str
|
647 |
+
k: Optional[int] = None
|
648 |
+
r: Optional[float] = None
|
649 |
+
hybrid: Optional[bool] = None
|
650 |
+
|
651 |
+
|
652 |
+
@app.post("/query/collection")
|
653 |
+
def query_collection_handler(
|
654 |
+
form_data: QueryCollectionsForm,
|
655 |
+
user=Depends(get_verified_user),
|
656 |
+
):
|
657 |
+
try:
|
658 |
+
if app.state.config.ENABLE_RAG_HYBRID_SEARCH:
|
659 |
+
return query_collection_with_hybrid_search(
|
660 |
+
collection_names=form_data.collection_names,
|
661 |
+
query=form_data.query,
|
662 |
+
embedding_function=app.state.EMBEDDING_FUNCTION,
|
663 |
+
k=form_data.k if form_data.k else app.state.config.TOP_K,
|
664 |
+
reranking_function=app.state.sentence_transformer_rf,
|
665 |
+
r=(
|
666 |
+
form_data.r if form_data.r else app.state.config.RELEVANCE_THRESHOLD
|
667 |
+
),
|
668 |
+
)
|
669 |
+
else:
|
670 |
+
return query_collection(
|
671 |
+
collection_names=form_data.collection_names,
|
672 |
+
query=form_data.query,
|
673 |
+
embedding_function=app.state.EMBEDDING_FUNCTION,
|
674 |
+
k=form_data.k if form_data.k else app.state.config.TOP_K,
|
675 |
+
)
|
676 |
+
|
677 |
+
except Exception as e:
|
678 |
+
log.exception(e)
|
679 |
+
raise HTTPException(
|
680 |
+
status_code=status.HTTP_400_BAD_REQUEST,
|
681 |
+
detail=ERROR_MESSAGES.DEFAULT(e),
|
682 |
+
)
|
683 |
+
|
684 |
+
|
685 |
+
@app.post("/youtube")
|
686 |
+
def store_youtube_video(form_data: UrlForm, user=Depends(get_verified_user)):
|
687 |
+
try:
|
688 |
+
loader = YoutubeLoader.from_youtube_url(
|
689 |
+
form_data.url,
|
690 |
+
add_video_info=True,
|
691 |
+
language=app.state.config.YOUTUBE_LOADER_LANGUAGE,
|
692 |
+
translation=app.state.YOUTUBE_LOADER_TRANSLATION,
|
693 |
+
)
|
694 |
+
data = loader.load()
|
695 |
+
|
696 |
+
collection_name = form_data.collection_name
|
697 |
+
if collection_name == "":
|
698 |
+
collection_name = calculate_sha256_string(form_data.url)[:63]
|
699 |
+
|
700 |
+
store_data_in_vector_db(data, collection_name, overwrite=True)
|
701 |
+
return {
|
702 |
+
"status": True,
|
703 |
+
"collection_name": collection_name,
|
704 |
+
"filename": form_data.url,
|
705 |
+
}
|
706 |
+
except Exception as e:
|
707 |
+
log.exception(e)
|
708 |
+
raise HTTPException(
|
709 |
+
status_code=status.HTTP_400_BAD_REQUEST,
|
710 |
+
detail=ERROR_MESSAGES.DEFAULT(e),
|
711 |
+
)
|
712 |
+
|
713 |
+
|
714 |
+
@app.post("/web")
|
715 |
+
def store_web(form_data: UrlForm, user=Depends(get_verified_user)):
|
716 |
+
# "https://www.gutenberg.org/files/1727/1727-h/1727-h.htm"
|
717 |
+
try:
|
718 |
+
loader = get_web_loader(
|
719 |
+
form_data.url,
|
720 |
+
verify_ssl=app.state.config.ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION,
|
721 |
+
)
|
722 |
+
data = loader.load()
|
723 |
+
|
724 |
+
collection_name = form_data.collection_name
|
725 |
+
if collection_name == "":
|
726 |
+
collection_name = calculate_sha256_string(form_data.url)[:63]
|
727 |
+
|
728 |
+
store_data_in_vector_db(data, collection_name, overwrite=True)
|
729 |
+
return {
|
730 |
+
"status": True,
|
731 |
+
"collection_name": collection_name,
|
732 |
+
"filename": form_data.url,
|
733 |
+
}
|
734 |
+
except Exception as e:
|
735 |
+
log.exception(e)
|
736 |
+
raise HTTPException(
|
737 |
+
status_code=status.HTTP_400_BAD_REQUEST,
|
738 |
+
detail=ERROR_MESSAGES.DEFAULT(e),
|
739 |
+
)
|
740 |
+
|
741 |
+
|
742 |
+
def get_web_loader(url: Union[str, Sequence[str]], verify_ssl: bool = True):
|
743 |
+
# Check if the URL is valid
|
744 |
+
if not validate_url(url):
|
745 |
+
raise ValueError(ERROR_MESSAGES.INVALID_URL)
|
746 |
+
return SafeWebBaseLoader(
|
747 |
+
url,
|
748 |
+
verify_ssl=verify_ssl,
|
749 |
+
requests_per_second=RAG_WEB_SEARCH_CONCURRENT_REQUESTS,
|
750 |
+
continue_on_failure=True,
|
751 |
+
)
|
752 |
+
|
753 |
+
|
754 |
+
def validate_url(url: Union[str, Sequence[str]]):
|
755 |
+
if isinstance(url, str):
|
756 |
+
if isinstance(validators.url(url), validators.ValidationError):
|
757 |
+
raise ValueError(ERROR_MESSAGES.INVALID_URL)
|
758 |
+
if not ENABLE_RAG_LOCAL_WEB_FETCH:
|
759 |
+
# Local web fetch is disabled, filter out any URLs that resolve to private IP addresses
|
760 |
+
parsed_url = urllib.parse.urlparse(url)
|
761 |
+
# Get IPv4 and IPv6 addresses
|
762 |
+
ipv4_addresses, ipv6_addresses = resolve_hostname(parsed_url.hostname)
|
763 |
+
# Check if any of the resolved addresses are private
|
764 |
+
# This is technically still vulnerable to DNS rebinding attacks, as we don't control WebBaseLoader
|
765 |
+
for ip in ipv4_addresses:
|
766 |
+
if validators.ipv4(ip, private=True):
|
767 |
+
raise ValueError(ERROR_MESSAGES.INVALID_URL)
|
768 |
+
for ip in ipv6_addresses:
|
769 |
+
if validators.ipv6(ip, private=True):
|
770 |
+
raise ValueError(ERROR_MESSAGES.INVALID_URL)
|
771 |
+
return True
|
772 |
+
elif isinstance(url, Sequence):
|
773 |
+
return all(validate_url(u) for u in url)
|
774 |
+
else:
|
775 |
+
return False
|
776 |
+
|
777 |
+
|
778 |
+
def resolve_hostname(hostname):
|
779 |
+
# Get address information
|
780 |
+
addr_info = socket.getaddrinfo(hostname, None)
|
781 |
+
|
782 |
+
# Extract IP addresses from address information
|
783 |
+
ipv4_addresses = [info[4][0] for info in addr_info if info[0] == socket.AF_INET]
|
784 |
+
ipv6_addresses = [info[4][0] for info in addr_info if info[0] == socket.AF_INET6]
|
785 |
+
|
786 |
+
return ipv4_addresses, ipv6_addresses
|
787 |
+
|
788 |
+
|
789 |
+
def search_web(engine: str, query: str) -> list[SearchResult]:
|
790 |
+
"""Search the web using a search engine and return the results as a list of SearchResult objects.
|
791 |
+
Will look for a search engine API key in environment variables in the following order:
|
792 |
+
- SEARXNG_QUERY_URL
|
793 |
+
- GOOGLE_PSE_API_KEY + GOOGLE_PSE_ENGINE_ID
|
794 |
+
- BRAVE_SEARCH_API_KEY
|
795 |
+
- SERPSTACK_API_KEY
|
796 |
+
- SERPER_API_KEY
|
797 |
+
- SERPLY_API_KEY
|
798 |
+
- TAVILY_API_KEY
|
799 |
+
Args:
|
800 |
+
query (str): The query to search for
|
801 |
+
"""
|
802 |
+
|
803 |
+
# TODO: add playwright to search the web
|
804 |
+
if engine == "searxng":
|
805 |
+
if app.state.config.SEARXNG_QUERY_URL:
|
806 |
+
return search_searxng(
|
807 |
+
app.state.config.SEARXNG_QUERY_URL,
|
808 |
+
query,
|
809 |
+
app.state.config.RAG_WEB_SEARCH_RESULT_COUNT,
|
810 |
+
app.state.config.RAG_WEB_SEARCH_DOMAIN_FILTER_LIST,
|
811 |
+
)
|
812 |
+
else:
|
813 |
+
raise Exception("No SEARXNG_QUERY_URL found in environment variables")
|
814 |
+
elif engine == "google_pse":
|
815 |
+
if (
|
816 |
+
app.state.config.GOOGLE_PSE_API_KEY
|
817 |
+
and app.state.config.GOOGLE_PSE_ENGINE_ID
|
818 |
+
):
|
819 |
+
return search_google_pse(
|
820 |
+
app.state.config.GOOGLE_PSE_API_KEY,
|
821 |
+
app.state.config.GOOGLE_PSE_ENGINE_ID,
|
822 |
+
query,
|
823 |
+
app.state.config.RAG_WEB_SEARCH_RESULT_COUNT,
|
824 |
+
app.state.config.RAG_WEB_SEARCH_DOMAIN_FILTER_LIST,
|
825 |
+
)
|
826 |
+
else:
|
827 |
+
raise Exception(
|
828 |
+
"No GOOGLE_PSE_API_KEY or GOOGLE_PSE_ENGINE_ID found in environment variables"
|
829 |
+
)
|
830 |
+
elif engine == "brave":
|
831 |
+
if app.state.config.BRAVE_SEARCH_API_KEY:
|
832 |
+
return search_brave(
|
833 |
+
app.state.config.BRAVE_SEARCH_API_KEY,
|
834 |
+
query,
|
835 |
+
app.state.config.RAG_WEB_SEARCH_RESULT_COUNT,
|
836 |
+
app.state.config.RAG_WEB_SEARCH_DOMAIN_FILTER_LIST,
|
837 |
+
)
|
838 |
+
else:
|
839 |
+
raise Exception("No BRAVE_SEARCH_API_KEY found in environment variables")
|
840 |
+
elif engine == "serpstack":
|
841 |
+
if app.state.config.SERPSTACK_API_KEY:
|
842 |
+
return search_serpstack(
|
843 |
+
app.state.config.SERPSTACK_API_KEY,
|
844 |
+
query,
|
845 |
+
app.state.config.RAG_WEB_SEARCH_RESULT_COUNT,
|
846 |
+
app.state.config.RAG_WEB_SEARCH_DOMAIN_FILTER_LIST,
|
847 |
+
https_enabled=app.state.config.SERPSTACK_HTTPS,
|
848 |
+
)
|
849 |
+
else:
|
850 |
+
raise Exception("No SERPSTACK_API_KEY found in environment variables")
|
851 |
+
elif engine == "serper":
|
852 |
+
if app.state.config.SERPER_API_KEY:
|
853 |
+
return search_serper(
|
854 |
+
app.state.config.SERPER_API_KEY,
|
855 |
+
query,
|
856 |
+
app.state.config.RAG_WEB_SEARCH_RESULT_COUNT,
|
857 |
+
app.state.config.RAG_WEB_SEARCH_DOMAIN_FILTER_LIST,
|
858 |
+
)
|
859 |
+
else:
|
860 |
+
raise Exception("No SERPER_API_KEY found in environment variables")
|
861 |
+
elif engine == "serply":
|
862 |
+
if app.state.config.SERPLY_API_KEY:
|
863 |
+
return search_serply(
|
864 |
+
app.state.config.SERPLY_API_KEY,
|
865 |
+
query,
|
866 |
+
app.state.config.RAG_WEB_SEARCH_RESULT_COUNT,
|
867 |
+
app.state.config.RAG_WEB_SEARCH_DOMAIN_FILTER_LIST,
|
868 |
+
)
|
869 |
+
else:
|
870 |
+
raise Exception("No SERPLY_API_KEY found in environment variables")
|
871 |
+
elif engine == "duckduckgo":
|
872 |
+
return search_duckduckgo(
|
873 |
+
query,
|
874 |
+
app.state.config.RAG_WEB_SEARCH_RESULT_COUNT,
|
875 |
+
app.state.config.RAG_WEB_SEARCH_DOMAIN_FILTER_LIST,
|
876 |
+
)
|
877 |
+
elif engine == "tavily":
|
878 |
+
if app.state.config.TAVILY_API_KEY:
|
879 |
+
return search_tavily(
|
880 |
+
app.state.config.TAVILY_API_KEY,
|
881 |
+
query,
|
882 |
+
app.state.config.RAG_WEB_SEARCH_RESULT_COUNT,
|
883 |
+
)
|
884 |
+
else:
|
885 |
+
raise Exception("No TAVILY_API_KEY found in environment variables")
|
886 |
+
elif engine == "jina":
|
887 |
+
return search_jina(query, app.state.config.RAG_WEB_SEARCH_RESULT_COUNT)
|
888 |
+
else:
|
889 |
+
raise Exception("No search engine API key found in environment variables")
|
890 |
+
|
891 |
+
|
892 |
+
@app.post("/web/search")
|
893 |
+
def store_web_search(form_data: SearchForm, user=Depends(get_verified_user)):
|
894 |
+
try:
|
895 |
+
logging.info(
|
896 |
+
f"trying to web search with {app.state.config.RAG_WEB_SEARCH_ENGINE, form_data.query}"
|
897 |
+
)
|
898 |
+
web_results = search_web(
|
899 |
+
app.state.config.RAG_WEB_SEARCH_ENGINE, form_data.query
|
900 |
+
)
|
901 |
+
except Exception as e:
|
902 |
+
log.exception(e)
|
903 |
+
|
904 |
+
print(e)
|
905 |
+
raise HTTPException(
|
906 |
+
status_code=status.HTTP_400_BAD_REQUEST,
|
907 |
+
detail=ERROR_MESSAGES.WEB_SEARCH_ERROR(e),
|
908 |
+
)
|
909 |
+
|
910 |
+
try:
|
911 |
+
urls = [result.link for result in web_results]
|
912 |
+
loader = get_web_loader(urls)
|
913 |
+
data = loader.load()
|
914 |
+
|
915 |
+
collection_name = form_data.collection_name
|
916 |
+
if collection_name == "":
|
917 |
+
collection_name = calculate_sha256_string(form_data.query)[:63]
|
918 |
+
|
919 |
+
store_data_in_vector_db(data, collection_name, overwrite=True)
|
920 |
+
return {
|
921 |
+
"status": True,
|
922 |
+
"collection_name": collection_name,
|
923 |
+
"filenames": urls,
|
924 |
+
}
|
925 |
+
except Exception as e:
|
926 |
+
log.exception(e)
|
927 |
+
raise HTTPException(
|
928 |
+
status_code=status.HTTP_400_BAD_REQUEST,
|
929 |
+
detail=ERROR_MESSAGES.DEFAULT(e),
|
930 |
+
)
|
931 |
+
|
932 |
+
|
933 |
+
def store_data_in_vector_db(
|
934 |
+
data, collection_name, metadata: Optional[dict] = None, overwrite: bool = False
|
935 |
+
) -> bool:
|
936 |
+
|
937 |
+
text_splitter = RecursiveCharacterTextSplitter(
|
938 |
+
chunk_size=app.state.config.CHUNK_SIZE,
|
939 |
+
chunk_overlap=app.state.config.CHUNK_OVERLAP,
|
940 |
+
add_start_index=True,
|
941 |
+
)
|
942 |
+
|
943 |
+
docs = text_splitter.split_documents(data)
|
944 |
+
|
945 |
+
if len(docs) > 0:
|
946 |
+
log.info(f"store_data_in_vector_db {docs}")
|
947 |
+
return store_docs_in_vector_db(docs, collection_name, metadata, overwrite), None
|
948 |
+
else:
|
949 |
+
raise ValueError(ERROR_MESSAGES.EMPTY_CONTENT)
|
950 |
+
|
951 |
+
|
952 |
+
def store_text_in_vector_db(
|
953 |
+
text, metadata, collection_name, overwrite: bool = False
|
954 |
+
) -> bool:
|
955 |
+
text_splitter = RecursiveCharacterTextSplitter(
|
956 |
+
chunk_size=app.state.config.CHUNK_SIZE,
|
957 |
+
chunk_overlap=app.state.config.CHUNK_OVERLAP,
|
958 |
+
add_start_index=True,
|
959 |
+
)
|
960 |
+
docs = text_splitter.create_documents([text], metadatas=[metadata])
|
961 |
+
return store_docs_in_vector_db(docs, collection_name, overwrite=overwrite)
|
962 |
+
|
963 |
+
|
964 |
+
def store_docs_in_vector_db(
|
965 |
+
docs, collection_name, metadata: Optional[dict] = None, overwrite: bool = False
|
966 |
+
) -> bool:
|
967 |
+
log.info(f"store_docs_in_vector_db {docs} {collection_name}")
|
968 |
+
|
969 |
+
texts = [doc.page_content for doc in docs]
|
970 |
+
metadatas = [{**doc.metadata, **(metadata if metadata else {})} for doc in docs]
|
971 |
+
|
972 |
+
# ChromaDB does not like datetime formats
|
973 |
+
# for meta-data so convert them to string.
|
974 |
+
for metadata in metadatas:
|
975 |
+
for key, value in metadata.items():
|
976 |
+
if isinstance(value, datetime):
|
977 |
+
metadata[key] = str(value)
|
978 |
+
|
979 |
+
try:
|
980 |
+
if overwrite:
|
981 |
+
for collection in CHROMA_CLIENT.list_collections():
|
982 |
+
if collection_name == collection.name:
|
983 |
+
log.info(f"deleting existing collection {collection_name}")
|
984 |
+
CHROMA_CLIENT.delete_collection(name=collection_name)
|
985 |
+
|
986 |
+
collection = CHROMA_CLIENT.create_collection(name=collection_name)
|
987 |
+
|
988 |
+
embedding_func = get_embedding_function(
|
989 |
+
app.state.config.RAG_EMBEDDING_ENGINE,
|
990 |
+
app.state.config.RAG_EMBEDDING_MODEL,
|
991 |
+
app.state.sentence_transformer_ef,
|
992 |
+
app.state.config.OPENAI_API_KEY,
|
993 |
+
app.state.config.OPENAI_API_BASE_URL,
|
994 |
+
app.state.config.RAG_EMBEDDING_OPENAI_BATCH_SIZE,
|
995 |
+
)
|
996 |
+
|
997 |
+
embedding_texts = list(map(lambda x: x.replace("\n", " "), texts))
|
998 |
+
embeddings = embedding_func(embedding_texts)
|
999 |
+
|
1000 |
+
for batch in create_batches(
|
1001 |
+
api=CHROMA_CLIENT,
|
1002 |
+
ids=[str(uuid.uuid4()) for _ in texts],
|
1003 |
+
metadatas=metadatas,
|
1004 |
+
embeddings=embeddings,
|
1005 |
+
documents=texts,
|
1006 |
+
):
|
1007 |
+
collection.add(*batch)
|
1008 |
+
|
1009 |
+
return True
|
1010 |
+
except Exception as e:
|
1011 |
+
if e.__class__.__name__ == "UniqueConstraintError":
|
1012 |
+
return True
|
1013 |
+
|
1014 |
+
log.exception(e)
|
1015 |
+
|
1016 |
+
return False
|
1017 |
+
|
1018 |
+
|
1019 |
+
class TikaLoader:
|
1020 |
+
def __init__(self, file_path, mime_type=None):
|
1021 |
+
self.file_path = file_path
|
1022 |
+
self.mime_type = mime_type
|
1023 |
+
|
1024 |
+
def load(self) -> List[Document]:
|
1025 |
+
with open(self.file_path, "rb") as f:
|
1026 |
+
data = f.read()
|
1027 |
+
|
1028 |
+
if self.mime_type is not None:
|
1029 |
+
headers = {"Content-Type": self.mime_type}
|
1030 |
+
else:
|
1031 |
+
headers = {}
|
1032 |
+
|
1033 |
+
endpoint = app.state.config.TIKA_SERVER_URL
|
1034 |
+
if not endpoint.endswith("/"):
|
1035 |
+
endpoint += "/"
|
1036 |
+
endpoint += "tika/text"
|
1037 |
+
|
1038 |
+
r = requests.put(endpoint, data=data, headers=headers)
|
1039 |
+
|
1040 |
+
if r.ok:
|
1041 |
+
raw_metadata = r.json()
|
1042 |
+
text = raw_metadata.get("X-TIKA:content", "<No text content found>")
|
1043 |
+
|
1044 |
+
if "Content-Type" in raw_metadata:
|
1045 |
+
headers["Content-Type"] = raw_metadata["Content-Type"]
|
1046 |
+
|
1047 |
+
log.info("Tika extracted text: %s", text)
|
1048 |
+
|
1049 |
+
return [Document(page_content=text, metadata=headers)]
|
1050 |
+
else:
|
1051 |
+
raise Exception(f"Error calling Tika: {r.reason}")
|
1052 |
+
|
1053 |
+
|
1054 |
+
def get_loader(filename: str, file_content_type: str, file_path: str):
|
1055 |
+
file_ext = filename.split(".")[-1].lower()
|
1056 |
+
known_type = True
|
1057 |
+
|
1058 |
+
known_source_ext = [
|
1059 |
+
"go",
|
1060 |
+
"py",
|
1061 |
+
"java",
|
1062 |
+
"sh",
|
1063 |
+
"bat",
|
1064 |
+
"ps1",
|
1065 |
+
"cmd",
|
1066 |
+
"js",
|
1067 |
+
"ts",
|
1068 |
+
"css",
|
1069 |
+
"cpp",
|
1070 |
+
"hpp",
|
1071 |
+
"h",
|
1072 |
+
"c",
|
1073 |
+
"cs",
|
1074 |
+
"sql",
|
1075 |
+
"log",
|
1076 |
+
"ini",
|
1077 |
+
"pl",
|
1078 |
+
"pm",
|
1079 |
+
"r",
|
1080 |
+
"dart",
|
1081 |
+
"dockerfile",
|
1082 |
+
"env",
|
1083 |
+
"php",
|
1084 |
+
"hs",
|
1085 |
+
"hsc",
|
1086 |
+
"lua",
|
1087 |
+
"nginxconf",
|
1088 |
+
"conf",
|
1089 |
+
"m",
|
1090 |
+
"mm",
|
1091 |
+
"plsql",
|
1092 |
+
"perl",
|
1093 |
+
"rb",
|
1094 |
+
"rs",
|
1095 |
+
"db2",
|
1096 |
+
"scala",
|
1097 |
+
"bash",
|
1098 |
+
"swift",
|
1099 |
+
"vue",
|
1100 |
+
"svelte",
|
1101 |
+
"msg",
|
1102 |
+
"ex",
|
1103 |
+
"exs",
|
1104 |
+
"erl",
|
1105 |
+
"tsx",
|
1106 |
+
"jsx",
|
1107 |
+
"hs",
|
1108 |
+
"lhs",
|
1109 |
+
]
|
1110 |
+
|
1111 |
+
if (
|
1112 |
+
app.state.config.CONTENT_EXTRACTION_ENGINE == "tika"
|
1113 |
+
and app.state.config.TIKA_SERVER_URL
|
1114 |
+
):
|
1115 |
+
if file_ext in known_source_ext or (
|
1116 |
+
file_content_type and file_content_type.find("text/") >= 0
|
1117 |
+
):
|
1118 |
+
loader = TextLoader(file_path, autodetect_encoding=True)
|
1119 |
+
else:
|
1120 |
+
loader = TikaLoader(file_path, file_content_type)
|
1121 |
+
else:
|
1122 |
+
if file_ext == "pdf":
|
1123 |
+
loader = PyPDFLoader(
|
1124 |
+
file_path, extract_images=app.state.config.PDF_EXTRACT_IMAGES
|
1125 |
+
)
|
1126 |
+
elif file_ext == "csv":
|
1127 |
+
loader = CSVLoader(file_path)
|
1128 |
+
elif file_ext == "rst":
|
1129 |
+
loader = UnstructuredRSTLoader(file_path, mode="elements")
|
1130 |
+
elif file_ext == "xml":
|
1131 |
+
loader = UnstructuredXMLLoader(file_path)
|
1132 |
+
elif file_ext in ["htm", "html"]:
|
1133 |
+
loader = BSHTMLLoader(file_path, open_encoding="unicode_escape")
|
1134 |
+
elif file_ext == "md":
|
1135 |
+
loader = UnstructuredMarkdownLoader(file_path)
|
1136 |
+
elif file_content_type == "application/epub+zip":
|
1137 |
+
loader = UnstructuredEPubLoader(file_path)
|
1138 |
+
elif (
|
1139 |
+
file_content_type
|
1140 |
+
== "application/vnd.openxmlformats-officedocument.wordprocessingml.document"
|
1141 |
+
or file_ext in ["doc", "docx"]
|
1142 |
+
):
|
1143 |
+
loader = Docx2txtLoader(file_path)
|
1144 |
+
elif file_content_type in [
|
1145 |
+
"application/vnd.ms-excel",
|
1146 |
+
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
1147 |
+
] or file_ext in ["xls", "xlsx"]:
|
1148 |
+
loader = UnstructuredExcelLoader(file_path)
|
1149 |
+
elif file_content_type in [
|
1150 |
+
"application/vnd.ms-powerpoint",
|
1151 |
+
"application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
1152 |
+
] or file_ext in ["ppt", "pptx"]:
|
1153 |
+
loader = UnstructuredPowerPointLoader(file_path)
|
1154 |
+
elif file_ext == "msg":
|
1155 |
+
loader = OutlookMessageLoader(file_path)
|
1156 |
+
elif file_ext in known_source_ext or (
|
1157 |
+
file_content_type and file_content_type.find("text/") >= 0
|
1158 |
+
):
|
1159 |
+
loader = TextLoader(file_path, autodetect_encoding=True)
|
1160 |
+
else:
|
1161 |
+
loader = TextLoader(file_path, autodetect_encoding=True)
|
1162 |
+
known_type = False
|
1163 |
+
|
1164 |
+
return loader, known_type
|
1165 |
+
|
1166 |
+
|
1167 |
+
@app.post("/doc")
|
1168 |
+
def store_doc(
|
1169 |
+
collection_name: Optional[str] = Form(None),
|
1170 |
+
file: UploadFile = File(...),
|
1171 |
+
user=Depends(get_verified_user),
|
1172 |
+
):
|
1173 |
+
# "https://www.gutenberg.org/files/1727/1727-h/1727-h.htm"
|
1174 |
+
|
1175 |
+
log.info(f"file.content_type: {file.content_type}")
|
1176 |
+
try:
|
1177 |
+
unsanitized_filename = file.filename
|
1178 |
+
filename = os.path.basename(unsanitized_filename)
|
1179 |
+
|
1180 |
+
file_path = f"{UPLOAD_DIR}/{filename}"
|
1181 |
+
|
1182 |
+
contents = file.file.read()
|
1183 |
+
with open(file_path, "wb") as f:
|
1184 |
+
f.write(contents)
|
1185 |
+
f.close()
|
1186 |
+
|
1187 |
+
f = open(file_path, "rb")
|
1188 |
+
if collection_name == None:
|
1189 |
+
collection_name = calculate_sha256(f)[:63]
|
1190 |
+
f.close()
|
1191 |
+
|
1192 |
+
loader, known_type = get_loader(filename, file.content_type, file_path)
|
1193 |
+
data = loader.load()
|
1194 |
+
|
1195 |
+
try:
|
1196 |
+
result = store_data_in_vector_db(data, collection_name)
|
1197 |
+
|
1198 |
+
if result:
|
1199 |
+
return {
|
1200 |
+
"status": True,
|
1201 |
+
"collection_name": collection_name,
|
1202 |
+
"filename": filename,
|
1203 |
+
"known_type": known_type,
|
1204 |
+
}
|
1205 |
+
except Exception as e:
|
1206 |
+
raise HTTPException(
|
1207 |
+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
1208 |
+
detail=e,
|
1209 |
+
)
|
1210 |
+
except Exception as e:
|
1211 |
+
log.exception(e)
|
1212 |
+
if "No pandoc was found" in str(e):
|
1213 |
+
raise HTTPException(
|
1214 |
+
status_code=status.HTTP_400_BAD_REQUEST,
|
1215 |
+
detail=ERROR_MESSAGES.PANDOC_NOT_INSTALLED,
|
1216 |
+
)
|
1217 |
+
else:
|
1218 |
+
raise HTTPException(
|
1219 |
+
status_code=status.HTTP_400_BAD_REQUEST,
|
1220 |
+
detail=ERROR_MESSAGES.DEFAULT(e),
|
1221 |
+
)
|
1222 |
+
|
1223 |
+
|
1224 |
+
class ProcessDocForm(BaseModel):
|
1225 |
+
file_id: str
|
1226 |
+
collection_name: Optional[str] = None
|
1227 |
+
|
1228 |
+
|
1229 |
+
@app.post("/process/doc")
|
1230 |
+
def process_doc(
|
1231 |
+
form_data: ProcessDocForm,
|
1232 |
+
user=Depends(get_verified_user),
|
1233 |
+
):
|
1234 |
+
try:
|
1235 |
+
file = Files.get_file_by_id(form_data.file_id)
|
1236 |
+
file_path = file.meta.get("path", f"{UPLOAD_DIR}/{file.filename}")
|
1237 |
+
|
1238 |
+
f = open(file_path, "rb")
|
1239 |
+
|
1240 |
+
collection_name = form_data.collection_name
|
1241 |
+
if collection_name == None:
|
1242 |
+
collection_name = calculate_sha256(f)[:63]
|
1243 |
+
f.close()
|
1244 |
+
|
1245 |
+
loader, known_type = get_loader(
|
1246 |
+
file.filename, file.meta.get("content_type"), file_path
|
1247 |
+
)
|
1248 |
+
data = loader.load()
|
1249 |
+
|
1250 |
+
try:
|
1251 |
+
result = store_data_in_vector_db(
|
1252 |
+
data,
|
1253 |
+
collection_name,
|
1254 |
+
{
|
1255 |
+
"file_id": form_data.file_id,
|
1256 |
+
"name": file.meta.get("name", file.filename),
|
1257 |
+
},
|
1258 |
+
)
|
1259 |
+
|
1260 |
+
if result:
|
1261 |
+
return {
|
1262 |
+
"status": True,
|
1263 |
+
"collection_name": collection_name,
|
1264 |
+
"known_type": known_type,
|
1265 |
+
"filename": file.meta.get("name", file.filename),
|
1266 |
+
}
|
1267 |
+
except Exception as e:
|
1268 |
+
raise HTTPException(
|
1269 |
+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
1270 |
+
detail=e,
|
1271 |
+
)
|
1272 |
+
except Exception as e:
|
1273 |
+
log.exception(e)
|
1274 |
+
if "No pandoc was found" in str(e):
|
1275 |
+
raise HTTPException(
|
1276 |
+
status_code=status.HTTP_400_BAD_REQUEST,
|
1277 |
+
detail=ERROR_MESSAGES.PANDOC_NOT_INSTALLED,
|
1278 |
+
)
|
1279 |
+
else:
|
1280 |
+
raise HTTPException(
|
1281 |
+
status_code=status.HTTP_400_BAD_REQUEST,
|
1282 |
+
detail=ERROR_MESSAGES.DEFAULT(e),
|
1283 |
+
)
|
1284 |
+
|
1285 |
+
|
1286 |
+
class TextRAGForm(BaseModel):
|
1287 |
+
name: str
|
1288 |
+
content: str
|
1289 |
+
collection_name: Optional[str] = None
|
1290 |
+
|
1291 |
+
|
1292 |
+
@app.post("/text")
|
1293 |
+
def store_text(
|
1294 |
+
form_data: TextRAGForm,
|
1295 |
+
user=Depends(get_verified_user),
|
1296 |
+
):
|
1297 |
+
|
1298 |
+
collection_name = form_data.collection_name
|
1299 |
+
if collection_name == None:
|
1300 |
+
collection_name = calculate_sha256_string(form_data.content)
|
1301 |
+
|
1302 |
+
result = store_text_in_vector_db(
|
1303 |
+
form_data.content,
|
1304 |
+
metadata={"name": form_data.name, "created_by": user.id},
|
1305 |
+
collection_name=collection_name,
|
1306 |
+
)
|
1307 |
+
|
1308 |
+
if result:
|
1309 |
+
return {"status": True, "collection_name": collection_name}
|
1310 |
+
else:
|
1311 |
+
raise HTTPException(
|
1312 |
+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
1313 |
+
detail=ERROR_MESSAGES.DEFAULT(),
|
1314 |
+
)
|
1315 |
+
|
1316 |
+
|
1317 |
+
@app.get("/scan")
|
1318 |
+
def scan_docs_dir(user=Depends(get_admin_user)):
|
1319 |
+
for path in Path(DOCS_DIR).rglob("./**/*"):
|
1320 |
+
try:
|
1321 |
+
if path.is_file() and not path.name.startswith("."):
|
1322 |
+
tags = extract_folders_after_data_docs(path)
|
1323 |
+
filename = path.name
|
1324 |
+
file_content_type = mimetypes.guess_type(path)
|
1325 |
+
|
1326 |
+
f = open(path, "rb")
|
1327 |
+
collection_name = calculate_sha256(f)[:63]
|
1328 |
+
f.close()
|
1329 |
+
|
1330 |
+
loader, known_type = get_loader(
|
1331 |
+
filename, file_content_type[0], str(path)
|
1332 |
+
)
|
1333 |
+
data = loader.load()
|
1334 |
+
|
1335 |
+
try:
|
1336 |
+
result = store_data_in_vector_db(data, collection_name)
|
1337 |
+
|
1338 |
+
if result:
|
1339 |
+
sanitized_filename = sanitize_filename(filename)
|
1340 |
+
doc = Documents.get_doc_by_name(sanitized_filename)
|
1341 |
+
|
1342 |
+
if doc == None:
|
1343 |
+
doc = Documents.insert_new_doc(
|
1344 |
+
user.id,
|
1345 |
+
DocumentForm(
|
1346 |
+
**{
|
1347 |
+
"name": sanitized_filename,
|
1348 |
+
"title": filename,
|
1349 |
+
"collection_name": collection_name,
|
1350 |
+
"filename": filename,
|
1351 |
+
"content": (
|
1352 |
+
json.dumps(
|
1353 |
+
{
|
1354 |
+
"tags": list(
|
1355 |
+
map(
|
1356 |
+
lambda name: {"name": name},
|
1357 |
+
tags,
|
1358 |
+
)
|
1359 |
+
)
|
1360 |
+
}
|
1361 |
+
)
|
1362 |
+
if len(tags)
|
1363 |
+
else "{}"
|
1364 |
+
),
|
1365 |
+
}
|
1366 |
+
),
|
1367 |
+
)
|
1368 |
+
except Exception as e:
|
1369 |
+
log.exception(e)
|
1370 |
+
pass
|
1371 |
+
|
1372 |
+
except Exception as e:
|
1373 |
+
log.exception(e)
|
1374 |
+
|
1375 |
+
return True
|
1376 |
+
|
1377 |
+
|
1378 |
+
@app.get("/reset/db")
|
1379 |
+
def reset_vector_db(user=Depends(get_admin_user)):
|
1380 |
+
CHROMA_CLIENT.reset()
|
1381 |
+
|
1382 |
+
|
1383 |
+
@app.get("/reset/uploads")
|
1384 |
+
def reset_upload_dir(user=Depends(get_admin_user)) -> bool:
|
1385 |
+
folder = f"{UPLOAD_DIR}"
|
1386 |
+
try:
|
1387 |
+
# Check if the directory exists
|
1388 |
+
if os.path.exists(folder):
|
1389 |
+
# Iterate over all the files and directories in the specified directory
|
1390 |
+
for filename in os.listdir(folder):
|
1391 |
+
file_path = os.path.join(folder, filename)
|
1392 |
+
try:
|
1393 |
+
if os.path.isfile(file_path) or os.path.islink(file_path):
|
1394 |
+
os.unlink(file_path) # Remove the file or link
|
1395 |
+
elif os.path.isdir(file_path):
|
1396 |
+
shutil.rmtree(file_path) # Remove the directory
|
1397 |
+
except Exception as e:
|
1398 |
+
print(f"Failed to delete {file_path}. Reason: {e}")
|
1399 |
+
else:
|
1400 |
+
print(f"The directory {folder} does not exist")
|
1401 |
+
except Exception as e:
|
1402 |
+
print(f"Failed to process the directory {folder}. Reason: {e}")
|
1403 |
+
|
1404 |
+
return True
|
1405 |
+
|
1406 |
+
|
1407 |
+
@app.get("/reset")
|
1408 |
+
def reset(user=Depends(get_admin_user)) -> bool:
|
1409 |
+
folder = f"{UPLOAD_DIR}"
|
1410 |
+
for filename in os.listdir(folder):
|
1411 |
+
file_path = os.path.join(folder, filename)
|
1412 |
+
try:
|
1413 |
+
if os.path.isfile(file_path) or os.path.islink(file_path):
|
1414 |
+
os.unlink(file_path)
|
1415 |
+
elif os.path.isdir(file_path):
|
1416 |
+
shutil.rmtree(file_path)
|
1417 |
+
except Exception as e:
|
1418 |
+
log.error("Failed to delete %s. Reason: %s" % (file_path, e))
|
1419 |
+
|
1420 |
+
try:
|
1421 |
+
CHROMA_CLIENT.reset()
|
1422 |
+
except Exception as e:
|
1423 |
+
log.exception(e)
|
1424 |
+
|
1425 |
+
return True
|
1426 |
+
|
1427 |
+
|
1428 |
+
class SafeWebBaseLoader(WebBaseLoader):
|
1429 |
+
"""WebBaseLoader with enhanced error handling for URLs."""
|
1430 |
+
|
1431 |
+
def lazy_load(self) -> Iterator[Document]:
|
1432 |
+
"""Lazy load text from the url(s) in web_path with error handling."""
|
1433 |
+
for path in self.web_paths:
|
1434 |
+
try:
|
1435 |
+
soup = self._scrape(path, bs_kwargs=self.bs_kwargs)
|
1436 |
+
text = soup.get_text(**self.bs_get_text_kwargs)
|
1437 |
+
|
1438 |
+
# Build metadata
|
1439 |
+
metadata = {"source": path}
|
1440 |
+
if title := soup.find("title"):
|
1441 |
+
metadata["title"] = title.get_text()
|
1442 |
+
if description := soup.find("meta", attrs={"name": "description"}):
|
1443 |
+
metadata["description"] = description.get(
|
1444 |
+
"content", "No description found."
|
1445 |
+
)
|
1446 |
+
if html := soup.find("html"):
|
1447 |
+
metadata["language"] = html.get("lang", "No language found.")
|
1448 |
+
|
1449 |
+
yield Document(page_content=text, metadata=metadata)
|
1450 |
+
except Exception as e:
|
1451 |
+
# Log the error and continue with the next URL
|
1452 |
+
log.error(f"Error loading {path}: {e}")
|
1453 |
+
|
1454 |
+
|
1455 |
+
if ENV == "dev":
|
1456 |
+
|
1457 |
+
@app.get("/ef")
|
1458 |
+
async def get_embeddings():
|
1459 |
+
return {"result": app.state.EMBEDDING_FUNCTION("hello world")}
|
1460 |
+
|
1461 |
+
@app.get("/ef/{text}")
|
1462 |
+
async def get_embeddings_text(text: str):
|
1463 |
+
return {"result": app.state.EMBEDDING_FUNCTION(text)}
|
backend/apps/rag/search/brave.py
ADDED
@@ -0,0 +1,42 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import logging
|
2 |
+
from typing import List, Optional
|
3 |
+
import requests
|
4 |
+
|
5 |
+
from apps.rag.search.main import SearchResult, get_filtered_results
|
6 |
+
from config import SRC_LOG_LEVELS
|
7 |
+
|
8 |
+
log = logging.getLogger(__name__)
|
9 |
+
log.setLevel(SRC_LOG_LEVELS["RAG"])
|
10 |
+
|
11 |
+
|
12 |
+
def search_brave(
|
13 |
+
api_key: str, query: str, count: int, filter_list: Optional[List[str]] = None
|
14 |
+
) -> list[SearchResult]:
|
15 |
+
"""Search using Brave's Search API and return the results as a list of SearchResult objects.
|
16 |
+
|
17 |
+
Args:
|
18 |
+
api_key (str): A Brave Search API key
|
19 |
+
query (str): The query to search for
|
20 |
+
"""
|
21 |
+
url = "https://api.search.brave.com/res/v1/web/search"
|
22 |
+
headers = {
|
23 |
+
"Accept": "application/json",
|
24 |
+
"Accept-Encoding": "gzip",
|
25 |
+
"X-Subscription-Token": api_key,
|
26 |
+
}
|
27 |
+
params = {"q": query, "count": count}
|
28 |
+
|
29 |
+
response = requests.get(url, headers=headers, params=params)
|
30 |
+
response.raise_for_status()
|
31 |
+
|
32 |
+
json_response = response.json()
|
33 |
+
results = json_response.get("web", {}).get("results", [])
|
34 |
+
if filter_list:
|
35 |
+
results = get_filtered_results(results, filter_list)
|
36 |
+
|
37 |
+
return [
|
38 |
+
SearchResult(
|
39 |
+
link=result["url"], title=result.get("title"), snippet=result.get("snippet")
|
40 |
+
)
|
41 |
+
for result in results[:count]
|
42 |
+
]
|
backend/apps/rag/search/duckduckgo.py
ADDED
@@ -0,0 +1,49 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import logging
|
2 |
+
from typing import List, Optional
|
3 |
+
from apps.rag.search.main import SearchResult, get_filtered_results
|
4 |
+
from duckduckgo_search import DDGS
|
5 |
+
from config import SRC_LOG_LEVELS
|
6 |
+
|
7 |
+
log = logging.getLogger(__name__)
|
8 |
+
log.setLevel(SRC_LOG_LEVELS["RAG"])
|
9 |
+
|
10 |
+
|
11 |
+
def search_duckduckgo(
|
12 |
+
query: str, count: int, filter_list: Optional[List[str]] = None
|
13 |
+
) -> list[SearchResult]:
|
14 |
+
"""
|
15 |
+
Search using DuckDuckGo's Search API and return the results as a list of SearchResult objects.
|
16 |
+
Args:
|
17 |
+
query (str): The query to search for
|
18 |
+
count (int): The number of results to return
|
19 |
+
|
20 |
+
Returns:
|
21 |
+
List[SearchResult]: A list of search results
|
22 |
+
"""
|
23 |
+
# Use the DDGS context manager to create a DDGS object
|
24 |
+
with DDGS() as ddgs:
|
25 |
+
# Use the ddgs.text() method to perform the search
|
26 |
+
ddgs_gen = ddgs.text(
|
27 |
+
query, safesearch="moderate", max_results=count, backend="api"
|
28 |
+
)
|
29 |
+
# Check if there are search results
|
30 |
+
if ddgs_gen:
|
31 |
+
# Convert the search results into a list
|
32 |
+
search_results = [r for r in ddgs_gen]
|
33 |
+
|
34 |
+
# Create an empty list to store the SearchResult objects
|
35 |
+
results = []
|
36 |
+
# Iterate over each search result
|
37 |
+
for result in search_results:
|
38 |
+
# Create a SearchResult object and append it to the results list
|
39 |
+
results.append(
|
40 |
+
SearchResult(
|
41 |
+
link=result["href"],
|
42 |
+
title=result.get("title"),
|
43 |
+
snippet=result.get("body"),
|
44 |
+
)
|
45 |
+
)
|
46 |
+
if filter_list:
|
47 |
+
results = get_filtered_results(results, filter_list)
|
48 |
+
# Return the list of search results
|
49 |
+
return results
|
backend/apps/rag/search/google_pse.py
ADDED
@@ -0,0 +1,51 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import json
|
2 |
+
import logging
|
3 |
+
from typing import List, Optional
|
4 |
+
import requests
|
5 |
+
|
6 |
+
from apps.rag.search.main import SearchResult, get_filtered_results
|
7 |
+
from config import SRC_LOG_LEVELS
|
8 |
+
|
9 |
+
log = logging.getLogger(__name__)
|
10 |
+
log.setLevel(SRC_LOG_LEVELS["RAG"])
|
11 |
+
|
12 |
+
|
13 |
+
def search_google_pse(
|
14 |
+
api_key: str,
|
15 |
+
search_engine_id: str,
|
16 |
+
query: str,
|
17 |
+
count: int,
|
18 |
+
filter_list: Optional[List[str]] = None,
|
19 |
+
) -> list[SearchResult]:
|
20 |
+
"""Search using Google's Programmable Search Engine API and return the results as a list of SearchResult objects.
|
21 |
+
|
22 |
+
Args:
|
23 |
+
api_key (str): A Programmable Search Engine API key
|
24 |
+
search_engine_id (str): A Programmable Search Engine ID
|
25 |
+
query (str): The query to search for
|
26 |
+
"""
|
27 |
+
url = "https://www.googleapis.com/customsearch/v1"
|
28 |
+
|
29 |
+
headers = {"Content-Type": "application/json"}
|
30 |
+
params = {
|
31 |
+
"cx": search_engine_id,
|
32 |
+
"q": query,
|
33 |
+
"key": api_key,
|
34 |
+
"num": count,
|
35 |
+
}
|
36 |
+
|
37 |
+
response = requests.request("GET", url, headers=headers, params=params)
|
38 |
+
response.raise_for_status()
|
39 |
+
|
40 |
+
json_response = response.json()
|
41 |
+
results = json_response.get("items", [])
|
42 |
+
if filter_list:
|
43 |
+
results = get_filtered_results(results, filter_list)
|
44 |
+
return [
|
45 |
+
SearchResult(
|
46 |
+
link=result["link"],
|
47 |
+
title=result.get("title"),
|
48 |
+
snippet=result.get("snippet"),
|
49 |
+
)
|
50 |
+
for result in results
|
51 |
+
]
|
backend/apps/rag/search/jina_search.py
ADDED
@@ -0,0 +1,41 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import logging
|
2 |
+
import requests
|
3 |
+
from yarl import URL
|
4 |
+
|
5 |
+
from apps.rag.search.main import SearchResult
|
6 |
+
from config import SRC_LOG_LEVELS
|
7 |
+
|
8 |
+
log = logging.getLogger(__name__)
|
9 |
+
log.setLevel(SRC_LOG_LEVELS["RAG"])
|
10 |
+
|
11 |
+
|
12 |
+
def search_jina(query: str, count: int) -> list[SearchResult]:
|
13 |
+
"""
|
14 |
+
Search using Jina's Search API and return the results as a list of SearchResult objects.
|
15 |
+
Args:
|
16 |
+
query (str): The query to search for
|
17 |
+
count (int): The number of results to return
|
18 |
+
|
19 |
+
Returns:
|
20 |
+
List[SearchResult]: A list of search results
|
21 |
+
"""
|
22 |
+
jina_search_endpoint = "https://s.jina.ai/"
|
23 |
+
headers = {
|
24 |
+
"Accept": "application/json",
|
25 |
+
}
|
26 |
+
url = str(URL(jina_search_endpoint + query))
|
27 |
+
response = requests.get(url, headers=headers)
|
28 |
+
response.raise_for_status()
|
29 |
+
data = response.json()
|
30 |
+
|
31 |
+
results = []
|
32 |
+
for result in data["data"][:count]:
|
33 |
+
results.append(
|
34 |
+
SearchResult(
|
35 |
+
link=result["url"],
|
36 |
+
title=result.get("title"),
|
37 |
+
snippet=result.get("content"),
|
38 |
+
)
|
39 |
+
)
|
40 |
+
|
41 |
+
return results
|
backend/apps/rag/search/main.py
ADDED
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from typing import Optional
|
2 |
+
from urllib.parse import urlparse
|
3 |
+
from pydantic import BaseModel
|
4 |
+
|
5 |
+
|
6 |
+
def get_filtered_results(results, filter_list):
|
7 |
+
if not filter_list:
|
8 |
+
return results
|
9 |
+
filtered_results = []
|
10 |
+
for result in results:
|
11 |
+
domain = urlparse(result["url"]).netloc
|
12 |
+
if any(domain.endswith(filtered_domain) for filtered_domain in filter_list):
|
13 |
+
filtered_results.append(result)
|
14 |
+
return filtered_results
|
15 |
+
|
16 |
+
|
17 |
+
class SearchResult(BaseModel):
|
18 |
+
link: str
|
19 |
+
title: Optional[str]
|
20 |
+
snippet: Optional[str]
|
backend/apps/rag/search/searxng.py
ADDED
@@ -0,0 +1,92 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import logging
|
2 |
+
import requests
|
3 |
+
|
4 |
+
from typing import List, Optional
|
5 |
+
|
6 |
+
from apps.rag.search.main import SearchResult, get_filtered_results
|
7 |
+
from config import SRC_LOG_LEVELS
|
8 |
+
|
9 |
+
log = logging.getLogger(__name__)
|
10 |
+
log.setLevel(SRC_LOG_LEVELS["RAG"])
|
11 |
+
|
12 |
+
|
13 |
+
def search_searxng(
|
14 |
+
query_url: str,
|
15 |
+
query: str,
|
16 |
+
count: int,
|
17 |
+
filter_list: Optional[List[str]] = None,
|
18 |
+
**kwargs,
|
19 |
+
) -> List[SearchResult]:
|
20 |
+
"""
|
21 |
+
Search a SearXNG instance for a given query and return the results as a list of SearchResult objects.
|
22 |
+
|
23 |
+
The function allows passing additional parameters such as language or time_range to tailor the search result.
|
24 |
+
|
25 |
+
Args:
|
26 |
+
query_url (str): The base URL of the SearXNG server.
|
27 |
+
query (str): The search term or question to find in the SearXNG database.
|
28 |
+
count (int): The maximum number of results to retrieve from the search.
|
29 |
+
|
30 |
+
Keyword Args:
|
31 |
+
language (str): Language filter for the search results; e.g., "en-US". Defaults to an empty string.
|
32 |
+
safesearch (int): Safe search filter for safer web results; 0 = off, 1 = moderate, 2 = strict. Defaults to 1 (moderate).
|
33 |
+
time_range (str): Time range for filtering results by date; e.g., "2023-04-05..today" or "all-time". Defaults to ''.
|
34 |
+
categories: (Optional[List[str]]): Specific categories within which the search should be performed, defaulting to an empty string if not provided.
|
35 |
+
|
36 |
+
Returns:
|
37 |
+
List[SearchResult]: A list of SearchResults sorted by relevance score in descending order.
|
38 |
+
|
39 |
+
Raise:
|
40 |
+
requests.exceptions.RequestException: If a request error occurs during the search process.
|
41 |
+
"""
|
42 |
+
|
43 |
+
# Default values for optional parameters are provided as empty strings or None when not specified.
|
44 |
+
language = kwargs.get("language", "en-US")
|
45 |
+
safesearch = kwargs.get("safesearch", "1")
|
46 |
+
time_range = kwargs.get("time_range", "")
|
47 |
+
categories = "".join(kwargs.get("categories", []))
|
48 |
+
|
49 |
+
params = {
|
50 |
+
"q": query,
|
51 |
+
"format": "json",
|
52 |
+
"pageno": 1,
|
53 |
+
"safesearch": safesearch,
|
54 |
+
"language": language,
|
55 |
+
"time_range": time_range,
|
56 |
+
"categories": categories,
|
57 |
+
"theme": "simple",
|
58 |
+
"image_proxy": 0,
|
59 |
+
}
|
60 |
+
|
61 |
+
# Legacy query format
|
62 |
+
if "<query>" in query_url:
|
63 |
+
# Strip all query parameters from the URL
|
64 |
+
query_url = query_url.split("?")[0]
|
65 |
+
|
66 |
+
log.debug(f"searching {query_url}")
|
67 |
+
|
68 |
+
response = requests.get(
|
69 |
+
query_url,
|
70 |
+
headers={
|
71 |
+
"User-Agent": "Open WebUI (https://github.com/open-webui/open-webui) RAG Bot",
|
72 |
+
"Accept": "text/html",
|
73 |
+
"Accept-Encoding": "gzip, deflate",
|
74 |
+
"Accept-Language": "en-US,en;q=0.5",
|
75 |
+
"Connection": "keep-alive",
|
76 |
+
},
|
77 |
+
params=params,
|
78 |
+
)
|
79 |
+
|
80 |
+
response.raise_for_status() # Raise an exception for HTTP errors.
|
81 |
+
|
82 |
+
json_response = response.json()
|
83 |
+
results = json_response.get("results", [])
|
84 |
+
sorted_results = sorted(results, key=lambda x: x.get("score", 0), reverse=True)
|
85 |
+
if filter_list:
|
86 |
+
sorted_results = get_filtered_results(sorted_results, filter_list)
|
87 |
+
return [
|
88 |
+
SearchResult(
|
89 |
+
link=result["url"], title=result.get("title"), snippet=result.get("content")
|
90 |
+
)
|
91 |
+
for result in sorted_results[:count]
|
92 |
+
]
|
backend/apps/rag/search/serper.py
ADDED
@@ -0,0 +1,43 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import json
|
2 |
+
import logging
|
3 |
+
from typing import List, Optional
|
4 |
+
import requests
|
5 |
+
|
6 |
+
from apps.rag.search.main import SearchResult, get_filtered_results
|
7 |
+
from config import SRC_LOG_LEVELS
|
8 |
+
|
9 |
+
log = logging.getLogger(__name__)
|
10 |
+
log.setLevel(SRC_LOG_LEVELS["RAG"])
|
11 |
+
|
12 |
+
|
13 |
+
def search_serper(
|
14 |
+
api_key: str, query: str, count: int, filter_list: Optional[List[str]] = None
|
15 |
+
) -> list[SearchResult]:
|
16 |
+
"""Search using serper.dev's API and return the results as a list of SearchResult objects.
|
17 |
+
|
18 |
+
Args:
|
19 |
+
api_key (str): A serper.dev API key
|
20 |
+
query (str): The query to search for
|
21 |
+
"""
|
22 |
+
url = "https://google.serper.dev/search"
|
23 |
+
|
24 |
+
payload = json.dumps({"q": query})
|
25 |
+
headers = {"X-API-KEY": api_key, "Content-Type": "application/json"}
|
26 |
+
|
27 |
+
response = requests.request("POST", url, headers=headers, data=payload)
|
28 |
+
response.raise_for_status()
|
29 |
+
|
30 |
+
json_response = response.json()
|
31 |
+
results = sorted(
|
32 |
+
json_response.get("organic", []), key=lambda x: x.get("position", 0)
|
33 |
+
)
|
34 |
+
if filter_list:
|
35 |
+
results = get_filtered_results(results, filter_list)
|
36 |
+
return [
|
37 |
+
SearchResult(
|
38 |
+
link=result["link"],
|
39 |
+
title=result.get("title"),
|
40 |
+
snippet=result.get("description"),
|
41 |
+
)
|
42 |
+
for result in results[:count]
|
43 |
+
]
|
backend/apps/rag/search/serply.py
ADDED
@@ -0,0 +1,70 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import json
|
2 |
+
import logging
|
3 |
+
from typing import List, Optional
|
4 |
+
import requests
|
5 |
+
from urllib.parse import urlencode
|
6 |
+
|
7 |
+
from apps.rag.search.main import SearchResult, get_filtered_results
|
8 |
+
from config import SRC_LOG_LEVELS
|
9 |
+
|
10 |
+
log = logging.getLogger(__name__)
|
11 |
+
log.setLevel(SRC_LOG_LEVELS["RAG"])
|
12 |
+
|
13 |
+
|
14 |
+
def search_serply(
|
15 |
+
api_key: str,
|
16 |
+
query: str,
|
17 |
+
count: int,
|
18 |
+
hl: str = "us",
|
19 |
+
limit: int = 10,
|
20 |
+
device_type: str = "desktop",
|
21 |
+
proxy_location: str = "US",
|
22 |
+
filter_list: Optional[List[str]] = None,
|
23 |
+
) -> list[SearchResult]:
|
24 |
+
"""Search using serper.dev's API and return the results as a list of SearchResult objects.
|
25 |
+
|
26 |
+
Args:
|
27 |
+
api_key (str): A serply.io API key
|
28 |
+
query (str): The query to search for
|
29 |
+
hl (str): Host Language code to display results in (reference https://developers.google.com/custom-search/docs/xml_results?hl=en#wsInterfaceLanguages)
|
30 |
+
limit (int): The maximum number of results to return [10-100, defaults to 10]
|
31 |
+
"""
|
32 |
+
log.info("Searching with Serply")
|
33 |
+
|
34 |
+
url = "https://api.serply.io/v1/search/"
|
35 |
+
|
36 |
+
query_payload = {
|
37 |
+
"q": query,
|
38 |
+
"language": "en",
|
39 |
+
"num": limit,
|
40 |
+
"gl": proxy_location.upper(),
|
41 |
+
"hl": hl.lower(),
|
42 |
+
}
|
43 |
+
|
44 |
+
url = f"{url}{urlencode(query_payload)}"
|
45 |
+
headers = {
|
46 |
+
"X-API-KEY": api_key,
|
47 |
+
"X-User-Agent": device_type,
|
48 |
+
"User-Agent": "open-webui",
|
49 |
+
"X-Proxy-Location": proxy_location,
|
50 |
+
}
|
51 |
+
|
52 |
+
response = requests.request("GET", url, headers=headers)
|
53 |
+
response.raise_for_status()
|
54 |
+
|
55 |
+
json_response = response.json()
|
56 |
+
log.info(f"results from serply search: {json_response}")
|
57 |
+
|
58 |
+
results = sorted(
|
59 |
+
json_response.get("results", []), key=lambda x: x.get("realPosition", 0)
|
60 |
+
)
|
61 |
+
if filter_list:
|
62 |
+
results = get_filtered_results(results, filter_list)
|
63 |
+
return [
|
64 |
+
SearchResult(
|
65 |
+
link=result["link"],
|
66 |
+
title=result.get("title"),
|
67 |
+
snippet=result.get("description"),
|
68 |
+
)
|
69 |
+
for result in results[:count]
|
70 |
+
]
|
backend/apps/rag/search/serpstack.py
ADDED
@@ -0,0 +1,49 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import json
|
2 |
+
import logging
|
3 |
+
from typing import List, Optional
|
4 |
+
import requests
|
5 |
+
|
6 |
+
from apps.rag.search.main import SearchResult, get_filtered_results
|
7 |
+
from config import SRC_LOG_LEVELS
|
8 |
+
|
9 |
+
log = logging.getLogger(__name__)
|
10 |
+
log.setLevel(SRC_LOG_LEVELS["RAG"])
|
11 |
+
|
12 |
+
|
13 |
+
def search_serpstack(
|
14 |
+
api_key: str,
|
15 |
+
query: str,
|
16 |
+
count: int,
|
17 |
+
filter_list: Optional[List[str]] = None,
|
18 |
+
https_enabled: bool = True,
|
19 |
+
) -> list[SearchResult]:
|
20 |
+
"""Search using serpstack.com's and return the results as a list of SearchResult objects.
|
21 |
+
|
22 |
+
Args:
|
23 |
+
api_key (str): A serpstack.com API key
|
24 |
+
query (str): The query to search for
|
25 |
+
https_enabled (bool): Whether to use HTTPS or HTTP for the API request
|
26 |
+
"""
|
27 |
+
url = f"{'https' if https_enabled else 'http'}://api.serpstack.com/search"
|
28 |
+
|
29 |
+
headers = {"Content-Type": "application/json"}
|
30 |
+
params = {
|
31 |
+
"access_key": api_key,
|
32 |
+
"query": query,
|
33 |
+
}
|
34 |
+
|
35 |
+
response = requests.request("POST", url, headers=headers, params=params)
|
36 |
+
response.raise_for_status()
|
37 |
+
|
38 |
+
json_response = response.json()
|
39 |
+
results = sorted(
|
40 |
+
json_response.get("organic_results", []), key=lambda x: x.get("position", 0)
|
41 |
+
)
|
42 |
+
if filter_list:
|
43 |
+
results = get_filtered_results(results, filter_list)
|
44 |
+
return [
|
45 |
+
SearchResult(
|
46 |
+
link=result["url"], title=result.get("title"), snippet=result.get("snippet")
|
47 |
+
)
|
48 |
+
for result in results[:count]
|
49 |
+
]
|