github-actions[bot] commited on
Commit
6dd477f
0 Parent(s):

GitHub deploy: 54420f530001a038286f162143e7239a92a97ddb

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .dockerignore +18 -0
  2. .env.example +13 -0
  3. .eslintignore +13 -0
  4. .eslintrc.cjs +31 -0
  5. .gitattributes +2 -0
  6. .github/FUNDING.yml +1 -0
  7. .github/ISSUE_TEMPLATE/bug_report.md +63 -0
  8. .github/ISSUE_TEMPLATE/feature_request.md +19 -0
  9. .github/dependabot.disabled +11 -0
  10. .github/pull_request_template.md +72 -0
  11. .github/workflows/build-release.yml +70 -0
  12. .github/workflows/deploy-to-hf-spaces.yml +59 -0
  13. .github/workflows/docker-build.yaml +426 -0
  14. .github/workflows/format-backend.yaml +39 -0
  15. .github/workflows/format-build-frontend.yaml +57 -0
  16. .github/workflows/integration-test.yml +199 -0
  17. .github/workflows/lint-backend.disabled +27 -0
  18. .github/workflows/lint-frontend.disabled +21 -0
  19. .github/workflows/release-pypi.yml +31 -0
  20. .gitignore +308 -0
  21. .npmrc +1 -0
  22. .prettierignore +316 -0
  23. .prettierrc +9 -0
  24. CHANGELOG.md +533 -0
  25. CODE_OF_CONDUCT.md +77 -0
  26. Caddyfile.localhost +64 -0
  27. Dockerfile +159 -0
  28. INSTALLATION.md +35 -0
  29. LICENSE +21 -0
  30. Makefile +33 -0
  31. README.md +198 -0
  32. TROUBLESHOOTING.md +32 -0
  33. backend/.dockerignore +14 -0
  34. backend/.gitignore +16 -0
  35. backend/apps/audio/main.py +238 -0
  36. backend/apps/images/main.py +527 -0
  37. backend/apps/images/utils/comfyui.py +234 -0
  38. backend/apps/ollama/main.py +1321 -0
  39. backend/apps/openai/main.py +505 -0
  40. backend/apps/rag/main.py +1220 -0
  41. backend/apps/rag/search/brave.py +37 -0
  42. backend/apps/rag/search/google_pse.py +45 -0
  43. backend/apps/rag/search/main.py +9 -0
  44. backend/apps/rag/search/searxng.py +83 -0
  45. backend/apps/rag/search/serper.py +39 -0
  46. backend/apps/rag/search/serpstack.py +43 -0
  47. backend/apps/rag/search/testdata/brave.json +998 -0
  48. backend/apps/rag/search/testdata/google_pse.json +442 -0
  49. backend/apps/rag/search/testdata/searxng.json +476 -0
  50. backend/apps/rag/search/testdata/serper.json +190 -0
.dockerignore ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .github
2
+ .DS_Store
3
+ docs
4
+ kubernetes
5
+ node_modules
6
+ /.svelte-kit
7
+ /package
8
+ .env
9
+ .env.*
10
+ vite.config.js.timestamp-*
11
+ vite.config.ts.timestamp-*
12
+ __pycache__
13
+ .env
14
+ _old
15
+ uploads
16
+ .ipynb_checkpoints
17
+ **/*.db
18
+ _test
.env.example ADDED
@@ -0,0 +1,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.disabled ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ version: 2
2
+ updates:
3
+ - package-ecosystem: pip
4
+ directory: '/backend'
5
+ schedule:
6
+ interval: weekly
7
+ - package-ecosystem: 'github-actions'
8
+ directory: '/'
9
+ schedule:
10
+ # Check for updates to GitHub Actions every week
11
+ interval: 'weekly'
.github/pull_request_template.md ADDED
@@ -0,0 +1,72 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Pull Request Checklist
2
+
3
+ ### Note to first-time contributors: Please open a discussion post in [Discussions](https://github.com/open-webui/open-webui/discussions) and describe your changes before submitting a pull request.
4
+
5
+ **Before submitting, make sure you've checked the following:**
6
+
7
+ - [ ] **Target branch:** Please verify that the pull request targets the `dev` branch.
8
+ - [ ] **Description:** Provide a concise description of the changes made in this pull request.
9
+ - [ ] **Changelog:** Ensure a changelog entry following the format of [Keep a Changelog](https://keepachangelog.com/) is added at the bottom of the PR description.
10
+ - [ ] **Documentation:** Have you updated relevant documentation [Open WebUI Docs](https://github.com/open-webui/docs), or other documentation sources?
11
+ - [ ] **Dependencies:** Are there any new dependencies? Have you updated the dependency versions in the documentation?
12
+ - [ ] **Testing:** Have you written and run sufficient tests for validating the changes?
13
+ - [ ] **Code review:** Have you performed a self-review of your code, addressing any coding standard issues and ensuring adherence to the project's coding standards?
14
+ - [ ] **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,426 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: Create and publish Docker images with specific build args
2
+
3
+ on:
4
+ workflow_dispatch:
5
+ push:
6
+ branches:
7
+ - main
8
+ - dev
9
+ tags:
10
+ - v*
11
+
12
+ env:
13
+ REGISTRY: ghcr.io
14
+ IMAGE_NAME: ${{ github.repository }}
15
+ FULL_IMAGE_NAME: ghcr.io/${{ github.repository }}
16
+
17
+ jobs:
18
+ build-main-image:
19
+ runs-on: ubuntu-latest
20
+ permissions:
21
+ contents: read
22
+ packages: write
23
+ strategy:
24
+ fail-fast: false
25
+ matrix:
26
+ platform:
27
+ - linux/amd64
28
+ - linux/arm64
29
+
30
+ steps:
31
+ - name: Prepare
32
+ run: |
33
+ platform=${{ matrix.platform }}
34
+ echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV
35
+
36
+ - name: Checkout repository
37
+ uses: actions/checkout@v4
38
+
39
+ - name: Set up QEMU
40
+ uses: docker/setup-qemu-action@v3
41
+
42
+ - name: Set up Docker Buildx
43
+ uses: docker/setup-buildx-action@v3
44
+
45
+ - name: Log in to the Container registry
46
+ uses: docker/login-action@v3
47
+ with:
48
+ registry: ${{ env.REGISTRY }}
49
+ username: ${{ github.actor }}
50
+ password: ${{ secrets.GITHUB_TOKEN }}
51
+
52
+ - name: Extract metadata for Docker images (default latest tag)
53
+ id: meta
54
+ uses: docker/metadata-action@v5
55
+ with:
56
+ images: ${{ env.FULL_IMAGE_NAME }}
57
+ tags: |
58
+ type=ref,event=branch
59
+ type=ref,event=tag
60
+ type=sha,prefix=git-
61
+ type=semver,pattern={{version}}
62
+ type=semver,pattern={{major}}.{{minor}}
63
+ flavor: |
64
+ latest=${{ github.ref == 'refs/heads/main' }}
65
+
66
+ - name: Extract metadata for Docker cache
67
+ id: cache-meta
68
+ uses: docker/metadata-action@v5
69
+ with:
70
+ images: ${{ env.FULL_IMAGE_NAME }}
71
+ tags: |
72
+ type=ref,event=branch
73
+ ${{ github.ref_type == 'tag' && 'type=raw,value=main' || '' }}
74
+ flavor: |
75
+ prefix=cache-${{ matrix.platform }}-
76
+ latest=false
77
+
78
+ - name: Build Docker image (latest)
79
+ uses: docker/build-push-action@v5
80
+ id: build
81
+ with:
82
+ context: .
83
+ push: true
84
+ platforms: ${{ matrix.platform }}
85
+ labels: ${{ steps.meta.outputs.labels }}
86
+ outputs: type=image,name=${{ env.FULL_IMAGE_NAME }},push-by-digest=true,name-canonical=true,push=true
87
+ cache-from: type=registry,ref=${{ steps.cache-meta.outputs.tags }}
88
+ cache-to: type=registry,ref=${{ steps.cache-meta.outputs.tags }},mode=max
89
+ build-args: |
90
+ BUILD_HASH=${{ github.sha }}
91
+
92
+ - name: Export digest
93
+ run: |
94
+ mkdir -p /tmp/digests
95
+ digest="${{ steps.build.outputs.digest }}"
96
+ touch "/tmp/digests/${digest#sha256:}"
97
+
98
+ - name: Upload digest
99
+ uses: actions/upload-artifact@v4
100
+ with:
101
+ name: digests-main-${{ env.PLATFORM_PAIR }}
102
+ path: /tmp/digests/*
103
+ if-no-files-found: error
104
+ retention-days: 1
105
+
106
+ build-cuda-image:
107
+ runs-on: ubuntu-latest
108
+ permissions:
109
+ contents: read
110
+ packages: write
111
+ strategy:
112
+ fail-fast: false
113
+ matrix:
114
+ platform:
115
+ - linux/amd64
116
+ - linux/arm64
117
+
118
+ steps:
119
+ - name: Prepare
120
+ run: |
121
+ platform=${{ matrix.platform }}
122
+ echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV
123
+
124
+ - name: Checkout repository
125
+ uses: actions/checkout@v4
126
+
127
+ - name: Set up QEMU
128
+ uses: docker/setup-qemu-action@v3
129
+
130
+ - name: Set up Docker Buildx
131
+ uses: docker/setup-buildx-action@v3
132
+
133
+ - name: Log in to the Container registry
134
+ uses: docker/login-action@v3
135
+ with:
136
+ registry: ${{ env.REGISTRY }}
137
+ username: ${{ github.actor }}
138
+ password: ${{ secrets.GITHUB_TOKEN }}
139
+
140
+ - name: Extract metadata for Docker images (cuda tag)
141
+ id: meta
142
+ uses: docker/metadata-action@v5
143
+ with:
144
+ images: ${{ env.FULL_IMAGE_NAME }}
145
+ tags: |
146
+ type=ref,event=branch
147
+ type=ref,event=tag
148
+ type=sha,prefix=git-
149
+ type=semver,pattern={{version}}
150
+ type=semver,pattern={{major}}.{{minor}}
151
+ type=raw,enable=${{ github.ref == 'refs/heads/main' }},prefix=,suffix=,value=cuda
152
+ flavor: |
153
+ latest=${{ github.ref == 'refs/heads/main' }}
154
+ suffix=-cuda,onlatest=true
155
+
156
+ - name: Extract metadata for Docker cache
157
+ id: cache-meta
158
+ uses: docker/metadata-action@v5
159
+ with:
160
+ images: ${{ env.FULL_IMAGE_NAME }}
161
+ tags: |
162
+ type=ref,event=branch
163
+ ${{ github.ref_type == 'tag' && 'type=raw,value=main' || '' }}
164
+ flavor: |
165
+ prefix=cache-cuda-${{ matrix.platform }}-
166
+ latest=false
167
+
168
+ - name: Build Docker image (cuda)
169
+ uses: docker/build-push-action@v5
170
+ id: build
171
+ with:
172
+ context: .
173
+ push: true
174
+ platforms: ${{ matrix.platform }}
175
+ labels: ${{ steps.meta.outputs.labels }}
176
+ outputs: type=image,name=${{ env.FULL_IMAGE_NAME }},push-by-digest=true,name-canonical=true,push=true
177
+ cache-from: type=registry,ref=${{ steps.cache-meta.outputs.tags }}
178
+ cache-to: type=registry,ref=${{ steps.cache-meta.outputs.tags }},mode=max
179
+ build-args: |
180
+ BUILD_HASH=${{ github.sha }}
181
+ USE_CUDA=true
182
+
183
+ - name: Export digest
184
+ run: |
185
+ mkdir -p /tmp/digests
186
+ digest="${{ steps.build.outputs.digest }}"
187
+ touch "/tmp/digests/${digest#sha256:}"
188
+
189
+ - name: Upload digest
190
+ uses: actions/upload-artifact@v4
191
+ with:
192
+ name: digests-cuda-${{ env.PLATFORM_PAIR }}
193
+ path: /tmp/digests/*
194
+ if-no-files-found: error
195
+ retention-days: 1
196
+
197
+ build-ollama-image:
198
+ runs-on: ubuntu-latest
199
+ permissions:
200
+ contents: read
201
+ packages: write
202
+ strategy:
203
+ fail-fast: false
204
+ matrix:
205
+ platform:
206
+ - linux/amd64
207
+ - linux/arm64
208
+
209
+ steps:
210
+ - name: Prepare
211
+ run: |
212
+ platform=${{ matrix.platform }}
213
+ echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV
214
+
215
+ - name: Checkout repository
216
+ uses: actions/checkout@v4
217
+
218
+ - name: Set up QEMU
219
+ uses: docker/setup-qemu-action@v3
220
+
221
+ - name: Set up Docker Buildx
222
+ uses: docker/setup-buildx-action@v3
223
+
224
+ - name: Log in to the Container registry
225
+ uses: docker/login-action@v3
226
+ with:
227
+ registry: ${{ env.REGISTRY }}
228
+ username: ${{ github.actor }}
229
+ password: ${{ secrets.GITHUB_TOKEN }}
230
+
231
+ - name: Extract metadata for Docker images (ollama tag)
232
+ id: meta
233
+ uses: docker/metadata-action@v5
234
+ with:
235
+ images: ${{ env.FULL_IMAGE_NAME }}
236
+ tags: |
237
+ type=ref,event=branch
238
+ type=ref,event=tag
239
+ type=sha,prefix=git-
240
+ type=semver,pattern={{version}}
241
+ type=semver,pattern={{major}}.{{minor}}
242
+ type=raw,enable=${{ github.ref == 'refs/heads/main' }},prefix=,suffix=,value=ollama
243
+ flavor: |
244
+ latest=${{ github.ref == 'refs/heads/main' }}
245
+ suffix=-ollama,onlatest=true
246
+
247
+ - name: Extract metadata for Docker cache
248
+ id: cache-meta
249
+ uses: docker/metadata-action@v5
250
+ with:
251
+ images: ${{ env.FULL_IMAGE_NAME }}
252
+ tags: |
253
+ type=ref,event=branch
254
+ ${{ github.ref_type == 'tag' && 'type=raw,value=main' || '' }}
255
+ flavor: |
256
+ prefix=cache-ollama-${{ matrix.platform }}-
257
+ latest=false
258
+
259
+ - name: Build Docker image (ollama)
260
+ uses: docker/build-push-action@v5
261
+ id: build
262
+ with:
263
+ context: .
264
+ push: true
265
+ platforms: ${{ matrix.platform }}
266
+ labels: ${{ steps.meta.outputs.labels }}
267
+ outputs: type=image,name=${{ env.FULL_IMAGE_NAME }},push-by-digest=true,name-canonical=true,push=true
268
+ cache-from: type=registry,ref=${{ steps.cache-meta.outputs.tags }}
269
+ cache-to: type=registry,ref=${{ steps.cache-meta.outputs.tags }},mode=max
270
+ build-args: |
271
+ BUILD_HASH=${{ github.sha }}
272
+ USE_OLLAMA=true
273
+
274
+ - name: Export digest
275
+ run: |
276
+ mkdir -p /tmp/digests
277
+ digest="${{ steps.build.outputs.digest }}"
278
+ touch "/tmp/digests/${digest#sha256:}"
279
+
280
+ - name: Upload digest
281
+ uses: actions/upload-artifact@v4
282
+ with:
283
+ name: digests-ollama-${{ env.PLATFORM_PAIR }}
284
+ path: /tmp/digests/*
285
+ if-no-files-found: error
286
+ retention-days: 1
287
+
288
+ merge-main-images:
289
+ runs-on: ubuntu-latest
290
+ needs: [ build-main-image ]
291
+ steps:
292
+ - name: Download digests
293
+ uses: actions/download-artifact@v4
294
+ with:
295
+ pattern: digests-main-*
296
+ path: /tmp/digests
297
+ merge-multiple: true
298
+
299
+ - name: Set up Docker Buildx
300
+ uses: docker/setup-buildx-action@v3
301
+
302
+ - name: Log in to the Container registry
303
+ uses: docker/login-action@v3
304
+ with:
305
+ registry: ${{ env.REGISTRY }}
306
+ username: ${{ github.actor }}
307
+ password: ${{ secrets.GITHUB_TOKEN }}
308
+
309
+ - name: Extract metadata for Docker images (default latest tag)
310
+ id: meta
311
+ uses: docker/metadata-action@v5
312
+ with:
313
+ images: ${{ env.FULL_IMAGE_NAME }}
314
+ tags: |
315
+ type=ref,event=branch
316
+ type=ref,event=tag
317
+ type=sha,prefix=git-
318
+ type=semver,pattern={{version}}
319
+ type=semver,pattern={{major}}.{{minor}}
320
+ flavor: |
321
+ latest=${{ github.ref == 'refs/heads/main' }}
322
+
323
+ - name: Create manifest list and push
324
+ working-directory: /tmp/digests
325
+ run: |
326
+ docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
327
+ $(printf '${{ env.FULL_IMAGE_NAME }}@sha256:%s ' *)
328
+
329
+ - name: Inspect image
330
+ run: |
331
+ docker buildx imagetools inspect ${{ env.FULL_IMAGE_NAME }}:${{ steps.meta.outputs.version }}
332
+
333
+
334
+ merge-cuda-images:
335
+ runs-on: ubuntu-latest
336
+ needs: [ build-cuda-image ]
337
+ steps:
338
+ - name: Download digests
339
+ uses: actions/download-artifact@v4
340
+ with:
341
+ pattern: digests-cuda-*
342
+ path: /tmp/digests
343
+ merge-multiple: true
344
+
345
+ - name: Set up Docker Buildx
346
+ uses: docker/setup-buildx-action@v3
347
+
348
+ - name: Log in to the Container registry
349
+ uses: docker/login-action@v3
350
+ with:
351
+ registry: ${{ env.REGISTRY }}
352
+ username: ${{ github.actor }}
353
+ password: ${{ secrets.GITHUB_TOKEN }}
354
+
355
+ - name: Extract metadata for Docker images (default latest tag)
356
+ id: meta
357
+ uses: docker/metadata-action@v5
358
+ with:
359
+ images: ${{ env.FULL_IMAGE_NAME }}
360
+ tags: |
361
+ type=ref,event=branch
362
+ type=ref,event=tag
363
+ type=sha,prefix=git-
364
+ type=semver,pattern={{version}}
365
+ type=semver,pattern={{major}}.{{minor}}
366
+ type=raw,enable=${{ github.ref == 'refs/heads/main' }},prefix=,suffix=,value=cuda
367
+ flavor: |
368
+ latest=${{ github.ref == 'refs/heads/main' }}
369
+ suffix=-cuda,onlatest=true
370
+
371
+ - name: Create manifest list and push
372
+ working-directory: /tmp/digests
373
+ run: |
374
+ docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
375
+ $(printf '${{ env.FULL_IMAGE_NAME }}@sha256:%s ' *)
376
+
377
+ - name: Inspect image
378
+ run: |
379
+ docker buildx imagetools inspect ${{ env.FULL_IMAGE_NAME }}:${{ steps.meta.outputs.version }}
380
+
381
+ merge-ollama-images:
382
+ runs-on: ubuntu-latest
383
+ needs: [ build-ollama-image ]
384
+ steps:
385
+ - name: Download digests
386
+ uses: actions/download-artifact@v4
387
+ with:
388
+ pattern: digests-ollama-*
389
+ path: /tmp/digests
390
+ merge-multiple: true
391
+
392
+ - name: Set up Docker Buildx
393
+ uses: docker/setup-buildx-action@v3
394
+
395
+ - name: Log in to the Container registry
396
+ uses: docker/login-action@v3
397
+ with:
398
+ registry: ${{ env.REGISTRY }}
399
+ username: ${{ github.actor }}
400
+ password: ${{ secrets.GITHUB_TOKEN }}
401
+
402
+ - name: Extract metadata for Docker images (default ollama tag)
403
+ id: meta
404
+ uses: docker/metadata-action@v5
405
+ with:
406
+ images: ${{ env.FULL_IMAGE_NAME }}
407
+ tags: |
408
+ type=ref,event=branch
409
+ type=ref,event=tag
410
+ type=sha,prefix=git-
411
+ type=semver,pattern={{version}}
412
+ type=semver,pattern={{major}}.{{minor}}
413
+ type=raw,enable=${{ github.ref == 'refs/heads/main' }},prefix=,suffix=,value=ollama
414
+ flavor: |
415
+ latest=${{ github.ref == 'refs/heads/main' }}
416
+ suffix=-ollama,onlatest=true
417
+
418
+ - name: Create manifest list and push
419
+ working-directory: /tmp/digests
420
+ run: |
421
+ docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
422
+ $(printf '${{ env.FULL_IMAGE_NAME }}@sha256:%s ' *)
423
+
424
+ - name: Inspect image
425
+ run: |
426
+ docker buildx imagetools inspect ${{ env.FULL_IMAGE_NAME }}:${{ steps.meta.outputs.version }}
.github/workflows/format-backend.yaml ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: Python CI
2
+
3
+ on:
4
+ push:
5
+ branches:
6
+ - main
7
+ - dev
8
+ pull_request:
9
+ branches:
10
+ - main
11
+ - dev
12
+
13
+ jobs:
14
+ build:
15
+ name: 'Format Backend'
16
+ runs-on: ubuntu-latest
17
+
18
+ strategy:
19
+ matrix:
20
+ python-version: [3.11]
21
+
22
+ steps:
23
+ - uses: actions/checkout@v4
24
+
25
+ - name: Set up Python
26
+ uses: actions/setup-python@v4
27
+ with:
28
+ python-version: ${{ matrix.python-version }}
29
+
30
+ - name: Install dependencies
31
+ run: |
32
+ python -m pip install --upgrade pip
33
+ pip install black
34
+
35
+ - name: Format backend
36
+ run: npm run format:backend
37
+
38
+ - name: Check for changes after format
39
+ run: git diff --exit-code
.github/workflows/format-build-frontend.yaml ADDED
@@ -0,0 +1,57 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: Frontend Build
2
+
3
+ on:
4
+ push:
5
+ branches:
6
+ - main
7
+ - dev
8
+ pull_request:
9
+ branches:
10
+ - main
11
+ - dev
12
+
13
+ jobs:
14
+ build:
15
+ name: 'Format & Build Frontend'
16
+ runs-on: ubuntu-latest
17
+ steps:
18
+ - name: Checkout Repository
19
+ uses: actions/checkout@v4
20
+
21
+ - name: Setup Node.js
22
+ uses: actions/setup-node@v4
23
+ with:
24
+ node-version: '20' # Or specify any other version you want to use
25
+
26
+ - name: Install Dependencies
27
+ run: npm install
28
+
29
+ - name: Format Frontend
30
+ run: npm run format
31
+
32
+ - name: Run i18next
33
+ run: npm run i18n:parse
34
+
35
+ - name: Check for Changes After Format
36
+ run: git diff --exit-code
37
+
38
+ - name: Build Frontend
39
+ run: npm run build
40
+
41
+ test-frontend:
42
+ name: 'Frontend Unit Tests'
43
+ runs-on: ubuntu-latest
44
+ steps:
45
+ - name: Checkout Repository
46
+ uses: actions/checkout@v4
47
+
48
+ - name: Setup Node.js
49
+ uses: actions/setup-node@v4
50
+ with:
51
+ node-version: '20'
52
+
53
+ - name: Install Dependencies
54
+ run: npm ci
55
+
56
+ - name: Run vitest
57
+ run: npm run test:frontend
.github/workflows/integration-test.yml ADDED
@@ -0,0 +1,199 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: Integration Test
2
+
3
+ on:
4
+ push:
5
+ branches:
6
+ - main
7
+ - dev
8
+ pull_request:
9
+ branches:
10
+ - main
11
+ - dev
12
+
13
+ jobs:
14
+ cypress-run:
15
+ name: Run Cypress Integration Tests
16
+ runs-on: ubuntu-latest
17
+ steps:
18
+ - name: Checkout Repository
19
+ uses: actions/checkout@v4
20
+
21
+ - name: Build and run Compose Stack
22
+ run: |
23
+ docker compose \
24
+ --file docker-compose.yaml \
25
+ --file docker-compose.api.yaml \
26
+ --file docker-compose.a1111-test.yaml \
27
+ up --detach --build
28
+
29
+ - name: Wait for Ollama to be up
30
+ timeout-minutes: 5
31
+ run: |
32
+ until curl --output /dev/null --silent --fail http://localhost:11434; do
33
+ printf '.'
34
+ sleep 1
35
+ done
36
+ echo "Service is up!"
37
+
38
+ - name: Preload Ollama model
39
+ run: |
40
+ docker exec ollama ollama pull qwen:0.5b-chat-v1.5-q2_K
41
+
42
+ - name: Cypress run
43
+ uses: cypress-io/github-action@v6
44
+ with:
45
+ browser: chrome
46
+ wait-on: 'http://localhost:3000'
47
+ config: baseUrl=http://localhost:3000
48
+
49
+ - uses: actions/upload-artifact@v4
50
+ if: always()
51
+ name: Upload Cypress videos
52
+ with:
53
+ name: cypress-videos
54
+ path: cypress/videos
55
+ if-no-files-found: ignore
56
+
57
+ - name: Extract Compose logs
58
+ if: always()
59
+ run: |
60
+ docker compose logs > compose-logs.txt
61
+
62
+ - uses: actions/upload-artifact@v4
63
+ if: always()
64
+ name: Upload Compose logs
65
+ with:
66
+ name: compose-logs
67
+ path: compose-logs.txt
68
+ if-no-files-found: ignore
69
+
70
+ migration_test:
71
+ name: Run Migration Tests
72
+ runs-on: ubuntu-latest
73
+ services:
74
+ postgres:
75
+ image: postgres
76
+ env:
77
+ POSTGRES_PASSWORD: postgres
78
+ options: >-
79
+ --health-cmd pg_isready
80
+ --health-interval 10s
81
+ --health-timeout 5s
82
+ --health-retries 5
83
+ ports:
84
+ - 5432:5432
85
+ # mysql:
86
+ # image: mysql
87
+ # env:
88
+ # MYSQL_ROOT_PASSWORD: mysql
89
+ # MYSQL_DATABASE: mysql
90
+ # options: >-
91
+ # --health-cmd "mysqladmin ping -h localhost"
92
+ # --health-interval 10s
93
+ # --health-timeout 5s
94
+ # --health-retries 5
95
+ # ports:
96
+ # - 3306:3306
97
+ steps:
98
+ - name: Checkout Repository
99
+ uses: actions/checkout@v4
100
+
101
+ - name: Set up Python
102
+ uses: actions/setup-python@v5
103
+ with:
104
+ python-version: ${{ matrix.python-version }}
105
+
106
+ - name: Set up uv
107
+ uses: yezz123/setup-uv@v4
108
+ with:
109
+ uv-venv: venv
110
+
111
+ - name: Activate virtualenv
112
+ run: |
113
+ . venv/bin/activate
114
+ echo PATH=$PATH >> $GITHUB_ENV
115
+
116
+ - name: Install dependencies
117
+ run: |
118
+ uv pip install -r backend/requirements.txt
119
+
120
+ - name: Test backend with SQLite
121
+ id: sqlite
122
+ env:
123
+ WEBUI_SECRET_KEY: secret-key
124
+ GLOBAL_LOG_LEVEL: debug
125
+ run: |
126
+ cd backend
127
+ uvicorn main:app --port "8080" --forwarded-allow-ips '*' &
128
+ UVICORN_PID=$!
129
+ # Wait up to 20 seconds for the server to start
130
+ for i in {1..20}; do
131
+ curl -s http://localhost:8080/api/config > /dev/null && break
132
+ sleep 1
133
+ if [ $i -eq 20 ]; then
134
+ echo "Server failed to start"
135
+ kill -9 $UVICORN_PID
136
+ exit 1
137
+ fi
138
+ done
139
+ # Check that the server is still running after 5 seconds
140
+ sleep 5
141
+ if ! kill -0 $UVICORN_PID; then
142
+ echo "Server has stopped"
143
+ exit 1
144
+ fi
145
+
146
+
147
+ - name: Test backend with Postgres
148
+ if: success() || steps.sqlite.conclusion == 'failure'
149
+ env:
150
+ WEBUI_SECRET_KEY: secret-key
151
+ GLOBAL_LOG_LEVEL: debug
152
+ DATABASE_URL: postgresql://postgres:postgres@localhost:5432/postgres
153
+ run: |
154
+ cd backend
155
+ uvicorn main:app --port "8081" --forwarded-allow-ips '*' &
156
+ UVICORN_PID=$!
157
+ # Wait up to 20 seconds for the server to start
158
+ for i in {1..20}; do
159
+ curl -s http://localhost:8081/api/config > /dev/null && break
160
+ sleep 1
161
+ if [ $i -eq 20 ]; then
162
+ echo "Server failed to start"
163
+ kill -9 $UVICORN_PID
164
+ exit 1
165
+ fi
166
+ done
167
+ # Check that the server is still running after 5 seconds
168
+ sleep 5
169
+ if ! kill -0 $UVICORN_PID; then
170
+ echo "Server has stopped"
171
+ exit 1
172
+ fi
173
+
174
+ # - name: Test backend with MySQL
175
+ # if: success() || steps.sqlite.conclusion == 'failure' || steps.postgres.conclusion == 'failure'
176
+ # env:
177
+ # WEBUI_SECRET_KEY: secret-key
178
+ # GLOBAL_LOG_LEVEL: debug
179
+ # DATABASE_URL: mysql://root:mysql@localhost:3306/mysql
180
+ # run: |
181
+ # cd backend
182
+ # uvicorn main:app --port "8083" --forwarded-allow-ips '*' &
183
+ # UVICORN_PID=$!
184
+ # # Wait up to 20 seconds for the server to start
185
+ # for i in {1..20}; do
186
+ # curl -s http://localhost:8083/api/config > /dev/null && break
187
+ # sleep 1
188
+ # if [ $i -eq 20 ]; then
189
+ # echo "Server failed to start"
190
+ # kill -9 $UVICORN_PID
191
+ # exit 1
192
+ # fi
193
+ # done
194
+ # # Check that the server is still running after 5 seconds
195
+ # sleep 5
196
+ # if ! kill -0 $UVICORN_PID; then
197
+ # echo "Server has stopped"
198
+ # exit 1
199
+ # fi
.github/workflows/lint-backend.disabled ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: Python CI
2
+ on:
3
+ push:
4
+ branches: ['main']
5
+ pull_request:
6
+ jobs:
7
+ build:
8
+ name: 'Lint Backend'
9
+ env:
10
+ PUBLIC_API_BASE_URL: ''
11
+ runs-on: ubuntu-latest
12
+ strategy:
13
+ matrix:
14
+ node-version:
15
+ - latest
16
+ steps:
17
+ - uses: actions/checkout@v4
18
+ - name: Use Python
19
+ uses: actions/setup-python@v4
20
+ - name: Use Bun
21
+ uses: oven-sh/setup-bun@v1
22
+ - name: Install dependencies
23
+ run: |
24
+ python -m pip install --upgrade pip
25
+ pip install pylint
26
+ - name: Lint backend
27
+ run: bun run lint:backend
.github/workflows/lint-frontend.disabled ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: Bun CI
2
+ on:
3
+ push:
4
+ branches: ['main']
5
+ pull_request:
6
+ jobs:
7
+ build:
8
+ name: 'Lint Frontend'
9
+ env:
10
+ PUBLIC_API_BASE_URL: ''
11
+ runs-on: ubuntu-latest
12
+ steps:
13
+ - uses: actions/checkout@v4
14
+ - name: Use Bun
15
+ uses: oven-sh/setup-bun@v1
16
+ - run: bun --version
17
+ - name: Install frontend dependencies
18
+ run: bun install --frozen-lockfile
19
+ - run: bun run lint:frontend
20
+ - run: bun run lint:types
21
+ if: success() || failure()
.github/workflows/release-pypi.yml ADDED
@@ -0,0 +1,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,308 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .DS_Store
2
+ node_modules
3
+ /build
4
+ /.svelte-kit
5
+ /package
6
+ .env
7
+ .env.*
8
+ !.env.example
9
+ vite.config.js.timestamp-*
10
+ vite.config.ts.timestamp-*
11
+ # Byte-compiled / optimized / DLL files
12
+ __pycache__/
13
+ *.py[cod]
14
+ *$py.class
15
+
16
+ # C extensions
17
+ *.so
18
+
19
+ # Pyodide distribution
20
+ static/pyodide/*
21
+ !static/pyodide/pyodide-lock.json
22
+
23
+ # Distribution / packaging
24
+ .Python
25
+ build/
26
+ develop-eggs/
27
+ dist/
28
+ downloads/
29
+ eggs/
30
+ .eggs/
31
+ lib64/
32
+ parts/
33
+ sdist/
34
+ var/
35
+ wheels/
36
+ share/python-wheels/
37
+ *.egg-info/
38
+ .installed.cfg
39
+ *.egg
40
+ MANIFEST
41
+
42
+ # PyInstaller
43
+ # Usually these files are written by a python script from a template
44
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
45
+ *.manifest
46
+ *.spec
47
+
48
+ # Installer logs
49
+ pip-log.txt
50
+ pip-delete-this-directory.txt
51
+
52
+ # Unit test / coverage reports
53
+ htmlcov/
54
+ .tox/
55
+ .nox/
56
+ .coverage
57
+ .coverage.*
58
+ .cache
59
+ nosetests.xml
60
+ coverage.xml
61
+ *.cover
62
+ *.py,cover
63
+ .hypothesis/
64
+ .pytest_cache/
65
+ cover/
66
+
67
+ # Translations
68
+ *.mo
69
+ *.pot
70
+
71
+ # Django stuff:
72
+ *.log
73
+ local_settings.py
74
+ db.sqlite3
75
+ db.sqlite3-journal
76
+
77
+ # Flask stuff:
78
+ instance/
79
+ .webassets-cache
80
+
81
+ # Scrapy stuff:
82
+ .scrapy
83
+
84
+ # Sphinx documentation
85
+ docs/_build/
86
+
87
+ # PyBuilder
88
+ .pybuilder/
89
+ target/
90
+
91
+ # Jupyter Notebook
92
+ .ipynb_checkpoints
93
+
94
+ # IPython
95
+ profile_default/
96
+ ipython_config.py
97
+
98
+ # pyenv
99
+ # For a library or package, you might want to ignore these files since the code is
100
+ # intended to run in multiple environments; otherwise, check them in:
101
+ # .python-version
102
+
103
+ # pipenv
104
+ # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
105
+ # However, in case of collaboration, if having platform-specific dependencies or dependencies
106
+ # having no cross-platform support, pipenv may install dependencies that don't work, or not
107
+ # install all needed dependencies.
108
+ #Pipfile.lock
109
+
110
+ # poetry
111
+ # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
112
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
113
+ # commonly ignored for libraries.
114
+ # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
115
+ #poetry.lock
116
+
117
+ # pdm
118
+ # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
119
+ #pdm.lock
120
+ # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
121
+ # in version control.
122
+ # https://pdm.fming.dev/#use-with-ide
123
+ .pdm.toml
124
+
125
+ # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
126
+ __pypackages__/
127
+
128
+ # Celery stuff
129
+ celerybeat-schedule
130
+ celerybeat.pid
131
+
132
+ # SageMath parsed files
133
+ *.sage.py
134
+
135
+ # Environments
136
+ .env
137
+ .venv
138
+ env/
139
+ venv/
140
+ ENV/
141
+ env.bak/
142
+ venv.bak/
143
+
144
+ # Spyder project settings
145
+ .spyderproject
146
+ .spyproject
147
+
148
+ # Rope project settings
149
+ .ropeproject
150
+
151
+ # mkdocs documentation
152
+ /site
153
+
154
+ # mypy
155
+ .mypy_cache/
156
+ .dmypy.json
157
+ dmypy.json
158
+
159
+ # Pyre type checker
160
+ .pyre/
161
+
162
+ # pytype static type analyzer
163
+ .pytype/
164
+
165
+ # Cython debug symbols
166
+ cython_debug/
167
+
168
+ # PyCharm
169
+ # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
170
+ # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
171
+ # and can be added to the global gitignore or merged into this file. For a more nuclear
172
+ # option (not recommended) you can uncomment the following to ignore the entire idea folder.
173
+ .idea/
174
+
175
+ # Logs
176
+ logs
177
+ *.log
178
+ npm-debug.log*
179
+ yarn-debug.log*
180
+ yarn-error.log*
181
+ lerna-debug.log*
182
+ .pnpm-debug.log*
183
+
184
+ # Diagnostic reports (https://nodejs.org/api/report.html)
185
+ report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
186
+
187
+ # Runtime data
188
+ pids
189
+ *.pid
190
+ *.seed
191
+ *.pid.lock
192
+
193
+ # Directory for instrumented libs generated by jscoverage/JSCover
194
+ lib-cov
195
+
196
+ # Coverage directory used by tools like istanbul
197
+ coverage
198
+ *.lcov
199
+
200
+ # nyc test coverage
201
+ .nyc_output
202
+
203
+ # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
204
+ .grunt
205
+
206
+ # Bower dependency directory (https://bower.io/)
207
+ bower_components
208
+
209
+ # node-waf configuration
210
+ .lock-wscript
211
+
212
+ # Compiled binary addons (https://nodejs.org/api/addons.html)
213
+ build/Release
214
+
215
+ # Dependency directories
216
+ node_modules/
217
+ jspm_packages/
218
+
219
+ # Snowpack dependency directory (https://snowpack.dev/)
220
+ web_modules/
221
+
222
+ # TypeScript cache
223
+ *.tsbuildinfo
224
+
225
+ # Optional npm cache directory
226
+ .npm
227
+
228
+ # Optional eslint cache
229
+ .eslintcache
230
+
231
+ # Optional stylelint cache
232
+ .stylelintcache
233
+
234
+ # Microbundle cache
235
+ .rpt2_cache/
236
+ .rts2_cache_cjs/
237
+ .rts2_cache_es/
238
+ .rts2_cache_umd/
239
+
240
+ # Optional REPL history
241
+ .node_repl_history
242
+
243
+ # Output of 'npm pack'
244
+ *.tgz
245
+
246
+ # Yarn Integrity file
247
+ .yarn-integrity
248
+
249
+ # dotenv environment variable files
250
+ .env
251
+ .env.development.local
252
+ .env.test.local
253
+ .env.production.local
254
+ .env.local
255
+
256
+ # parcel-bundler cache (https://parceljs.org/)
257
+ .cache
258
+ .parcel-cache
259
+
260
+ # Next.js build output
261
+ .next
262
+ out
263
+
264
+ # Nuxt.js build / generate output
265
+ .nuxt
266
+ dist
267
+
268
+ # Gatsby files
269
+ .cache/
270
+ # Comment in the public line in if your project uses Gatsby and not Next.js
271
+ # https://nextjs.org/blog/next-9-1#public-directory-support
272
+ # public
273
+
274
+ # vuepress build output
275
+ .vuepress/dist
276
+
277
+ # vuepress v2.x temp and cache directory
278
+ .temp
279
+ .cache
280
+
281
+ # Docusaurus cache and generated files
282
+ .docusaurus
283
+
284
+ # Serverless directories
285
+ .serverless/
286
+
287
+ # FuseBox cache
288
+ .fusebox/
289
+
290
+ # DynamoDB Local files
291
+ .dynamodb/
292
+
293
+ # TernJS port file
294
+ .tern-port
295
+
296
+ # Stores VSCode versions used for testing VSCode extensions
297
+ .vscode-test
298
+
299
+ # yarn v2
300
+ .yarn/cache
301
+ .yarn/unplugged
302
+ .yarn/build-state.yml
303
+ .yarn/install-state.gz
304
+ .pnp.*
305
+
306
+ # cypress artifacts
307
+ cypress/videos
308
+ cypress/screenshots
.npmrc ADDED
@@ -0,0 +1 @@
 
 
1
+ engine-strict=true
.prettierignore ADDED
@@ -0,0 +1,316 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Ignore files for PNPM, NPM and YARN
2
+ pnpm-lock.yaml
3
+ package-lock.json
4
+ yarn.lock
5
+
6
+ kubernetes/
7
+
8
+ # Copy of .gitignore
9
+ .DS_Store
10
+ node_modules
11
+ /build
12
+ /.svelte-kit
13
+ /package
14
+ .env
15
+ .env.*
16
+ !.env.example
17
+ vite.config.js.timestamp-*
18
+ vite.config.ts.timestamp-*
19
+ # Byte-compiled / optimized / DLL files
20
+ __pycache__/
21
+ *.py[cod]
22
+ *$py.class
23
+
24
+ # C extensions
25
+ *.so
26
+
27
+ # Distribution / packaging
28
+ .Python
29
+ build/
30
+ develop-eggs/
31
+ dist/
32
+ downloads/
33
+ eggs/
34
+ .eggs/
35
+ lib64/
36
+ parts/
37
+ sdist/
38
+ var/
39
+ wheels/
40
+ share/python-wheels/
41
+ *.egg-info/
42
+ .installed.cfg
43
+ *.egg
44
+ MANIFEST
45
+
46
+ # PyInstaller
47
+ # Usually these files are written by a python script from a template
48
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
49
+ *.manifest
50
+ *.spec
51
+
52
+ # Installer logs
53
+ pip-log.txt
54
+ pip-delete-this-directory.txt
55
+
56
+ # Unit test / coverage reports
57
+ htmlcov/
58
+ .tox/
59
+ .nox/
60
+ .coverage
61
+ .coverage.*
62
+ .cache
63
+ nosetests.xml
64
+ coverage.xml
65
+ *.cover
66
+ *.py,cover
67
+ .hypothesis/
68
+ .pytest_cache/
69
+ cover/
70
+
71
+ # Translations
72
+ *.mo
73
+ *.pot
74
+
75
+ # Django stuff:
76
+ *.log
77
+ local_settings.py
78
+ db.sqlite3
79
+ db.sqlite3-journal
80
+
81
+ # Flask stuff:
82
+ instance/
83
+ .webassets-cache
84
+
85
+ # Scrapy stuff:
86
+ .scrapy
87
+
88
+ # Sphinx documentation
89
+ docs/_build/
90
+
91
+ # PyBuilder
92
+ .pybuilder/
93
+ target/
94
+
95
+ # Jupyter Notebook
96
+ .ipynb_checkpoints
97
+
98
+ # IPython
99
+ profile_default/
100
+ ipython_config.py
101
+
102
+ # pyenv
103
+ # For a library or package, you might want to ignore these files since the code is
104
+ # intended to run in multiple environments; otherwise, check them in:
105
+ # .python-version
106
+
107
+ # pipenv
108
+ # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
109
+ # However, in case of collaboration, if having platform-specific dependencies or dependencies
110
+ # having no cross-platform support, pipenv may install dependencies that don't work, or not
111
+ # install all needed dependencies.
112
+ #Pipfile.lock
113
+
114
+ # poetry
115
+ # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
116
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
117
+ # commonly ignored for libraries.
118
+ # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
119
+ #poetry.lock
120
+
121
+ # pdm
122
+ # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
123
+ #pdm.lock
124
+ # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
125
+ # in version control.
126
+ # https://pdm.fming.dev/#use-with-ide
127
+ .pdm.toml
128
+
129
+ # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
130
+ __pypackages__/
131
+
132
+ # Celery stuff
133
+ celerybeat-schedule
134
+ celerybeat.pid
135
+
136
+ # SageMath parsed files
137
+ *.sage.py
138
+
139
+ # Environments
140
+ .env
141
+ .venv
142
+ env/
143
+ venv/
144
+ ENV/
145
+ env.bak/
146
+ venv.bak/
147
+
148
+ # Spyder project settings
149
+ .spyderproject
150
+ .spyproject
151
+
152
+ # Rope project settings
153
+ .ropeproject
154
+
155
+ # mkdocs documentation
156
+ /site
157
+
158
+ # mypy
159
+ .mypy_cache/
160
+ .dmypy.json
161
+ dmypy.json
162
+
163
+ # Pyre type checker
164
+ .pyre/
165
+
166
+ # pytype static type analyzer
167
+ .pytype/
168
+
169
+ # Cython debug symbols
170
+ cython_debug/
171
+
172
+ # PyCharm
173
+ # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
174
+ # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
175
+ # and can be added to the global gitignore or merged into this file. For a more nuclear
176
+ # option (not recommended) you can uncomment the following to ignore the entire idea folder.
177
+ .idea/
178
+
179
+ # Logs
180
+ logs
181
+ *.log
182
+ npm-debug.log*
183
+ yarn-debug.log*
184
+ yarn-error.log*
185
+ lerna-debug.log*
186
+ .pnpm-debug.log*
187
+
188
+ # Diagnostic reports (https://nodejs.org/api/report.html)
189
+ report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
190
+
191
+ # Runtime data
192
+ pids
193
+ *.pid
194
+ *.seed
195
+ *.pid.lock
196
+
197
+ # Directory for instrumented libs generated by jscoverage/JSCover
198
+ lib-cov
199
+
200
+ # Coverage directory used by tools like istanbul
201
+ coverage
202
+ *.lcov
203
+
204
+ # nyc test coverage
205
+ .nyc_output
206
+
207
+ # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
208
+ .grunt
209
+
210
+ # Bower dependency directory (https://bower.io/)
211
+ bower_components
212
+
213
+ # node-waf configuration
214
+ .lock-wscript
215
+
216
+ # Compiled binary addons (https://nodejs.org/api/addons.html)
217
+ build/Release
218
+
219
+ # Dependency directories
220
+ node_modules/
221
+ jspm_packages/
222
+
223
+ # Snowpack dependency directory (https://snowpack.dev/)
224
+ web_modules/
225
+
226
+ # TypeScript cache
227
+ *.tsbuildinfo
228
+
229
+ # Optional npm cache directory
230
+ .npm
231
+
232
+ # Optional eslint cache
233
+ .eslintcache
234
+
235
+ # Optional stylelint cache
236
+ .stylelintcache
237
+
238
+ # Microbundle cache
239
+ .rpt2_cache/
240
+ .rts2_cache_cjs/
241
+ .rts2_cache_es/
242
+ .rts2_cache_umd/
243
+
244
+ # Optional REPL history
245
+ .node_repl_history
246
+
247
+ # Output of 'npm pack'
248
+ *.tgz
249
+
250
+ # Yarn Integrity file
251
+ .yarn-integrity
252
+
253
+ # dotenv environment variable files
254
+ .env
255
+ .env.development.local
256
+ .env.test.local
257
+ .env.production.local
258
+ .env.local
259
+
260
+ # parcel-bundler cache (https://parceljs.org/)
261
+ .cache
262
+ .parcel-cache
263
+
264
+ # Next.js build output
265
+ .next
266
+ out
267
+
268
+ # Nuxt.js build / generate output
269
+ .nuxt
270
+ dist
271
+
272
+ # Gatsby files
273
+ .cache/
274
+ # Comment in the public line in if your project uses Gatsby and not Next.js
275
+ # https://nextjs.org/blog/next-9-1#public-directory-support
276
+ # public
277
+
278
+ # vuepress build output
279
+ .vuepress/dist
280
+
281
+ # vuepress v2.x temp and cache directory
282
+ .temp
283
+ .cache
284
+
285
+ # Docusaurus cache and generated files
286
+ .docusaurus
287
+
288
+ # Serverless directories
289
+ .serverless/
290
+
291
+ # FuseBox cache
292
+ .fusebox/
293
+
294
+ # DynamoDB Local files
295
+ .dynamodb/
296
+
297
+ # TernJS port file
298
+ .tern-port
299
+
300
+ # Stores VSCode versions used for testing VSCode extensions
301
+ .vscode-test
302
+
303
+ # yarn v2
304
+ .yarn/cache
305
+ .yarn/unplugged
306
+ .yarn/build-state.yml
307
+ .yarn/install-state.gz
308
+ .pnp.*
309
+
310
+ # cypress artifacts
311
+ cypress/videos
312
+ cypress/screenshots
313
+
314
+
315
+
316
+ /static/*
.prettierrc ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "useTabs": true,
3
+ "singleQuote": true,
4
+ "trailingComma": "none",
5
+ "printWidth": 100,
6
+ "plugins": ["prettier-plugin-svelte"],
7
+ "pluginSearchDirs": ["."],
8
+ "overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }]
9
+ }
CHANGELOG.md ADDED
@@ -0,0 +1,533 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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.2.5] - 2024-06-05
9
+
10
+ ### Added
11
+
12
+ - **👥 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.
13
+ - **🗂️ 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.
14
+ - **⚙️ 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.
15
+ - **🌐 Enhanced Translations**: We've improved the Chinese translations and added support for Turkmen and Norwegian languages to make the interface more accessible globally.
16
+
17
+ ### Fixed
18
+
19
+ - **📱 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.
20
+
21
+ ## [0.2.4] - 2024-06-03
22
+
23
+ ### Added
24
+
25
+ - **👤 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.
26
+ - **🌐 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.
27
+ - **❓ 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).
28
+ - **🌍 Enhanced Translation**: Improvements have been made to translations.
29
+
30
+ ### Fixed
31
+
32
+ - **🔍 SearxNG Web Search**: Fixed the issue where the SearxNG web search functionality was not working properly.
33
+
34
+ ## [0.2.3] - 2024-06-03
35
+
36
+ ### Added
37
+
38
+ - **📁 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.
39
+ - **✏️ Edit Titles with Double Click**: Double-click on titles to rename them quickly and efficiently.
40
+ - **🧩 Batch Multiple Embeddings**: Introduced 'RAG_EMBEDDING_OPENAI_BATCH_SIZE' to process multiple embeddings in a batch, enhancing performance for large datasets.
41
+ - **🌍 Improved Translations**: Enhanced the translation quality across various languages for a better user experience.
42
+
43
+ ### Fixed
44
+
45
+ - **🛠️ Modelfile Migration Script**: Fixed an issue where the modelfile migration script would fail if an invalid modelfile was encountered.
46
+ - **💬 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.
47
+ - **🔊 Local TTS Voice Selection**: Fixed the issue where the selected local Text-to-Speech (TTS) voice was not being displayed in settings.
48
+
49
+ ## [0.2.2] - 2024-06-02
50
+
51
+ ### Added
52
+
53
+ - **🌊 Mermaid Rendering Support**: We've included support for Mermaid rendering. This allows you to create beautiful diagrams and flowcharts directly within Open WebUI.
54
+ - **🔄 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.
55
+
56
+ ### Fixed
57
+
58
+ - **🔧 Pipelines Filter Issue**: We've addressed an issue with the pipelines where filters were not functioning as expected.
59
+
60
+ ## [0.2.1] - 2024-06-02
61
+
62
+ ### Added
63
+
64
+ - **🖱️ Single Model Export Button**: Easily export models with just one click using the new single model export button.
65
+ - **🖥️ Advanced Parameters Support**: Added support for 'num_thread', 'use_mmap', and 'use_mlock' parameters for Ollama.
66
+ - **🌐 Improved Vietnamese Translation**: Enhanced Vietnamese language support for a better user experience for our Vietnamese-speaking community.
67
+
68
+ ### Fixed
69
+
70
+ - **🔧 OpenAI URL API Save Issue**: Corrected a problem preventing the saving of OpenAI URL API settings.
71
+ - **🚫 Display Issue with Disabled Ollama API**: Fixed the display bug causing models to appear in settings when the Ollama API was disabled.
72
+
73
+ ### Changed
74
+
75
+ - **💡 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.
76
+
77
+ ## [0.2.0] - 2024-06-01
78
+
79
+ ### Added
80
+
81
+ - **🔧 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.
82
+ - **🔗 Function Calling via Pipelines**: Integrate function calling seamlessly through Pipelines.
83
+ - **⚖️ User Rate Limiting via Pipelines**: Implement user-specific rate limits to manage API usage efficiently.
84
+ - **📊 Usage Monitoring with Langfuse**: Track and analyze usage statistics with Langfuse integration through Pipelines.
85
+ - **🕒 Conversation Turn Limits**: Set limits on conversation turns to manage interactions better through Pipelines.
86
+ - **🛡️ Toxic Message Filtering**: Automatically filter out toxic messages to maintain a safe environment using Pipelines.
87
+ - **🔍 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.
88
+ - **🗂️ Models Workspace**: Create and manage model presets for both Ollama/OpenAI API. Note: The old Modelfiles workspace is deprecated.
89
+ - **🛠️ Model Builder Feature**: Build and edit all models with persistent builder mode.
90
+ - **🏷️ Model Tagging Support**: Organize models with tagging features in the models workspace.
91
+ - **📋 Model Ordering Support**: Effortlessly organize models by dragging and dropping them into the desired positions within the models workspace.
92
+ - **📈 OpenAI Generation Stats**: Access detailed generation statistics for OpenAI models.
93
+ - **📅 System Prompt Variables**: New variables added: '{{CURRENT_DATE}}' and '{{USER_NAME}}' for dynamic prompts.
94
+ - **📢 Global Banner Support**: Manage global banners from admin settings > banners.
95
+ - **🗃️ Enhanced Archived Chats Modal**: Search and export archived chats easily.
96
+ - **📂 Archive All Button**: Quickly archive all chats from settings > chats.
97
+ - **🌐 Improved Translations**: Added and improved translations for French, Croatian, Cebuano, and Vietnamese.
98
+
99
+ ### Fixed
100
+
101
+ - **🔍 Archived Chats Visibility**: Resolved issue with archived chats not showing in the admin panel.
102
+ - **💬 Message Styling**: Fixed styling issues affecting message appearance.
103
+ - **🔗 Shared Chat Responses**: Corrected the issue where shared chat response messages were not readonly.
104
+ - **🖥️ UI Enhancement**: Fixed the scrollbar overlapping issue with the message box in the user interface.
105
+
106
+ ### Changed
107
+
108
+ - **💾 User Settings Storage**: User settings are now saved on the backend, ensuring consistency across all devices.
109
+ - **📡 Unified API Requests**: The API request for getting models is now unified to '/api/models' for easier usage.
110
+ - **🔄 Versioning Update**: Our versioning will now follow the format 0.x for major updates and 0.x.y for patches.
111
+ - **📦 Export All Chats (All Users)**: Moved this functionality to the Admin Panel settings for better organization and accessibility.
112
+
113
+ ### Removed
114
+
115
+ - **🚫 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.
116
+
117
+ ## [0.1.125] - 2024-05-19
118
+
119
+ ### Added
120
+
121
+ - **🔄 Updated UI**: Chat interface revamped with chat bubbles. Easily switch back to the old style via settings > interface > chat bubble UI.
122
+ - **📂 Enhanced Sidebar UI**: Model files, documents, prompts, and playground merged into Workspace for streamlined access.
123
+ - **🚀 Improved Many Model Interaction**: All responses now displayed simultaneously for a smoother experience.
124
+ - **🐍 Python Code Execution**: Execute Python code locally in the browser with libraries like 'requests', 'beautifulsoup4', 'numpy', 'pandas', 'seaborn', 'matplotlib', 'scikit-learn', 'scipy', 'regex'.
125
+ - **🧠 Experimental Memory Feature**: Manually input personal information you want LLMs to remember via settings > personalization > memory.
126
+ - **💾 Persistent Settings**: Settings now saved as config.json for convenience.
127
+ - **🩺 Health Check Endpoint**: Added for Docker deployment.
128
+ - **↕️ RTL Support**: Toggle chat direction via settings > interface > chat direction.
129
+ - **🖥️ PowerPoint Support**: RAG pipeline now supports PowerPoint documents.
130
+ - **🌐 Language Updates**: Ukrainian, Turkish, Arabic, Chinese, Serbian, Vietnamese updated; Punjabi added.
131
+
132
+ ### Changed
133
+
134
+ - **👤 Shared Chat Update**: Shared chat now includes creator user information.
135
+
136
+ ## [0.1.124] - 2024-05-08
137
+
138
+ ### Added
139
+
140
+ - **🖼️ Improved Chat Sidebar**: Now conveniently displays time ranges and organizes chats by today, yesterday, and more.
141
+ - **📜 Citations in RAG Feature**: Easily track the context fed to the LLM with added citations in the RAG feature.
142
+ - **🔒 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.
143
+ - **📹 Enhanced YouTube RAG Pipeline**: Now supports non-English videos for an enriched experience.
144
+ - **🔊 Specify OpenAI TTS Models**: Customize your TTS experience by specifying OpenAI TTS models.
145
+ - **🔧 Additional Environment Variables**: Discover more environment variables in our comprehensive documentation at Open WebUI Documentation (https://docs.openwebui.com).
146
+ - **🌐 Language Support**: Arabic, Finnish, and Hindi added; Improved support for German, Vietnamese, and Chinese.
147
+
148
+ ### Fixed
149
+
150
+ - **🛠️ Model Selector Styling**: Addressed styling issues for improved user experience.
151
+ - **⚠️ Warning Messages**: Resolved backend warning messages.
152
+
153
+ ### Changed
154
+
155
+ - **📝 Title Generation**: Limited output to 50 tokens.
156
+ - **📦 Helm Charts**: Removed Helm charts, now available in a separate repository (https://github.com/open-webui/helm-charts).
157
+
158
+ ## [0.1.123] - 2024-05-02
159
+
160
+ ### Added
161
+
162
+ - **🎨 New Landing Page Design**: Refreshed design for a more modern look and optimized use of screen space.
163
+ - **📹 Youtube RAG Pipeline**: Introduces dedicated RAG pipeline for Youtube videos, enabling interaction with video transcriptions directly.
164
+ - **🔧 Enhanced Admin Panel**: Streamlined user management with options to add users directly or in bulk via CSV import.
165
+ - **👥 '@' Model Integration**: Easily switch to specific models during conversations; old collaborative chat feature phased out.
166
+ - **🌐 Language Enhancements**: Swedish translation added, plus improvements to German, Spanish, and the addition of Doge translation.
167
+
168
+ ### Fixed
169
+
170
+ - **🗑️ Delete Chat Shortcut**: Addressed issue where shortcut wasn't functioning.
171
+ - **🖼️ Modal Closing Bug**: Resolved unexpected closure of modal when dragging from within.
172
+ - **✏️ Edit Button Styling**: Fixed styling inconsistency with edit buttons.
173
+ - **🌐 Image Generation Compatibility Issue**: Rectified image generation compatibility issue with third-party APIs.
174
+ - **📱 iOS PWA Icon Fix**: Corrected iOS PWA home screen icon shape.
175
+ - **🔍 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.
176
+
177
+ ### Changed
178
+
179
+ - **🔄 Unlimited Context Length**: Advanced settings now allow unlimited max context length (previously limited to 16000).
180
+ - **👑 Super Admin Assignment**: The first signup is automatically assigned a super admin role, unchangeable by other admins.
181
+ - **🛡️ Admin User Restrictions**: User action buttons from the admin panel are now disabled for users with admin roles.
182
+ - **🔝 Default Model Selector**: Set as default model option now exclusively available on the landing page.
183
+
184
+ ## [0.1.122] - 2024-04-27
185
+
186
+ ### Added
187
+
188
+ - **🌟 Enhanced RAG Pipeline**: Now with hybrid searching via 'BM25', reranking powered by 'CrossEncoder', and configurable relevance score thresholds.
189
+ - **🛢️ External Database Support**: Seamlessly connect to custom SQLite or Postgres databases using the 'DATABASE_URL' environment variable.
190
+ - **🌐 Remote ChromaDB Support**: Introducing the capability to connect to remote ChromaDB servers.
191
+ - **👨‍💼 Improved Admin Panel**: Admins can now conveniently check users' chat lists and last active status directly from the admin panel.
192
+ - **🎨 Splash Screen**: Introducing a loading splash screen for a smoother user experience.
193
+ - **🌍 Language Support Expansion**: Added support for Bangla (bn-BD), along with enhancements to Chinese, Spanish, and Ukrainian translations.
194
+ - **💻 Improved LaTeX Rendering Performance**: Enjoy faster rendering times for LaTeX equations.
195
+ - **🔧 More Environment Variables**: Explore additional environment variables in our documentation (https://docs.openwebui.com), including the 'ENABLE_LITELLM' option to manage memory usage.
196
+
197
+ ### Fixed
198
+
199
+ - **🔧 Ollama Compatibility**: Resolved errors occurring when Ollama server version isn't an integer, such as SHA builds or RCs.
200
+ - **🐛 Various OpenAI API Issues**: Addressed several issues related to the OpenAI API.
201
+ - **🛑 Stop Sequence Issue**: Fixed the problem where the stop sequence with a backslash '\' was not functioning.
202
+ - **🔤 Font Fallback**: Corrected font fallback issue.
203
+
204
+ ### Changed
205
+
206
+ - **⌨️ Prompt Input Behavior on Mobile**: Enter key prompt submission disabled on mobile devices for improved user experience.
207
+
208
+ ## [0.1.121] - 2024-04-24
209
+
210
+ ### Fixed
211
+
212
+ - **🔧 Translation Issues**: Addressed various translation discrepancies.
213
+ - **🔒 LiteLLM Security Fix**: Updated LiteLLM version to resolve a security vulnerability.
214
+ - **🖥️ HTML Tag Display**: Rectified the issue where the '< br >' tag wasn't displaying correctly.
215
+ - **🔗 WebSocket Connection**: Resolved the failure of WebSocket connection under HTTPS security for ComfyUI server.
216
+ - **📜 FileReader Optimization**: Implemented FileReader initialization per image in multi-file drag & drop to ensure reusability.
217
+ - **🏷️ Tag Display**: Corrected tag display inconsistencies.
218
+ - **📦 Archived Chat Styling**: Fixed styling issues in archived chat.
219
+ - **🔖 Safari Copy Button Bug**: Addressed the bug where the copy button failed to copy links in Safari.
220
+
221
+ ## [0.1.120] - 2024-04-20
222
+
223
+ ### Added
224
+
225
+ - **📦 Archive Chat Feature**: Easily archive chats with a new sidebar button, and access archived chats via the profile button > archived chats.
226
+ - **🔊 Configurable Text-to-Speech Endpoint**: Customize your Text-to-Speech experience with configurable OpenAI endpoints.
227
+ - **🛠️ Improved Error Handling**: Enhanced error message handling for connection failures.
228
+ - **⌨️ Enhanced Shortcut**: When editing messages, use ctrl/cmd+enter to save and submit, and esc to close.
229
+ - **🌐 Language Support**: Added support for Georgian and enhanced translations for Portuguese and Vietnamese.
230
+
231
+ ### Fixed
232
+
233
+ - **🔧 Model Selector**: Resolved issue where default model selection was not saving.
234
+ - **🔗 Share Link Copy Button**: Fixed bug where the copy button wasn't copying links in Safari.
235
+ - **🎨 Light Theme Styling**: Addressed styling issue with the light theme.
236
+
237
+ ## [0.1.119] - 2024-04-16
238
+
239
+ ### Added
240
+
241
+ - **🌟 Enhanced RAG Embedding Support**: Ollama, and OpenAI models can now be used for RAG embedding model.
242
+ - **🔄 Seamless Integration**: Copy 'ollama run <model name>' directly from Ollama page to easily select and pull models.
243
+ - **🏷️ Tagging Feature**: Add tags to chats directly via the sidebar chat menu.
244
+ - **📱 Mobile Accessibility**: Swipe left and right on mobile to effortlessly open and close the sidebar.
245
+ - **🔍 Improved Navigation**: Admin panel now supports pagination for user list.
246
+ - **🌍 Additional Language Support**: Added Polish language support.
247
+
248
+ ### Fixed
249
+
250
+ - **🌍 Language Enhancements**: Vietnamese and Spanish translations have been improved.
251
+ - **🔧 Helm Fixes**: Resolved issues with Helm trailing slash and manifest.json.
252
+
253
+ ### Changed
254
+
255
+ - **🐳 Docker Optimization**: Updated docker image build process to utilize 'uv' for significantly faster builds compared to 'pip3'.
256
+
257
+ ## [0.1.118] - 2024-04-10
258
+
259
+ ### Added
260
+
261
+ - **🦙 Ollama and CUDA Images**: Added support for ':ollama' and ':cuda' tagged images.
262
+ - **👍 Enhanced Response Rating**: Now you can annotate your ratings for better feedback.
263
+ - **👤 User Initials Profile Photo**: User initials are now the default profile photo.
264
+ - **🔍 Update RAG Embedding Model**: Customize RAG embedding model directly in document settings.
265
+ - **🌍 Additional Language Support**: Added Turkish language support.
266
+
267
+ ### Fixed
268
+
269
+ - **🔒 Share Chat Permission**: Resolved issue with chat sharing permissions.
270
+ - **🛠 Modal Close**: Modals can now be closed using the Esc key.
271
+
272
+ ### Changed
273
+
274
+ - **🎨 Admin Panel Styling**: Refreshed styling for the admin panel.
275
+ - **🐳 Docker Image Build**: Updated docker image build process for improved efficiency.
276
+
277
+ ## [0.1.117] - 2024-04-03
278
+
279
+ ### Added
280
+
281
+ - 🗨️ **Local Chat Sharing**: Share chat links seamlessly between users.
282
+ - 🔑 **API Key Generation Support**: Generate secret keys to leverage Open WebUI with OpenAI libraries.
283
+ - 📄 **Chat Download as PDF**: Easily download chats in PDF format.
284
+ - 📝 **Improved Logging**: Enhancements to logging functionality.
285
+ - 📧 **Trusted Email Authentication**: Authenticate using a trusted email header.
286
+
287
+ ### Fixed
288
+
289
+ - 🌷 **Enhanced Dutch Translation**: Improved translation for Dutch users.
290
+ - ⚪ **White Theme Styling**: Resolved styling issue with the white theme.
291
+ - 📜 **LaTeX Chat Screen Overflow**: Fixed screen overflow issue with LaTeX rendering.
292
+ - 🔒 **Security Patches**: Applied necessary security patches.
293
+
294
+ ## [0.1.116] - 2024-03-31
295
+
296
+ ### Added
297
+
298
+ - **🔄 Enhanced UI**: Model selector now conveniently located in the navbar, enabling seamless switching between multiple models during conversations.
299
+ - **🔍 Improved Model Selector**: Directly pull a model from the selector/Models now display detailed information for better understanding.
300
+ - **💬 Webhook Support**: Now compatible with Google Chat and Microsoft Teams.
301
+ - **🌐 Localization**: Korean translation (I18n) now available.
302
+ - **🌑 Dark Theme**: OLED dark theme introduced for reduced strain during prolonged usage.
303
+ - **🏷️ Tag Autocomplete**: Dropdown feature added for effortless chat tagging.
304
+
305
+ ### Fixed
306
+
307
+ - **🔽 Auto-Scrolling**: Addressed OpenAI auto-scrolling issue.
308
+ - **🏷️ Tag Validation**: Implemented tag validation to prevent empty string tags.
309
+ - **🚫 Model Whitelisting**: Resolved LiteLLM model whitelisting issue.
310
+ - **✅ Spelling**: Corrected various spelling issues for improved readability.
311
+
312
+ ## [0.1.115] - 2024-03-24
313
+
314
+ ### Added
315
+
316
+ - **🔍 Custom Model Selector**: Easily find and select custom models with the new search filter feature.
317
+ - **🛑 Cancel Model Download**: Added the ability to cancel model downloads.
318
+ - **🎨 Image Generation ComfyUI**: Image generation now supports ComfyUI.
319
+ - **🌟 Updated Light Theme**: Updated the light theme for a fresh look.
320
+ - **🌍 Additional Language Support**: Now supporting Bulgarian, Italian, Portuguese, Japanese, and Dutch.
321
+
322
+ ### Fixed
323
+
324
+ - **🔧 Fixed Broken Experimental GGUF Upload**: Resolved issues with experimental GGUF upload functionality.
325
+
326
+ ### Changed
327
+
328
+ - **🔄 Vector Storage Reset Button**: Moved the reset vector storage button to document settings.
329
+
330
+ ## [0.1.114] - 2024-03-20
331
+
332
+ ### Added
333
+
334
+ - **🔗 Webhook Integration**: Now you can subscribe to new user sign-up events via webhook. Simply navigate to the admin panel > admin settings > webhook URL.
335
+ - **🛡️ Enhanced Model Filtering**: Alongside Ollama, OpenAI proxy model whitelisting, we've added model filtering functionality for LiteLLM proxy.
336
+ - **🌍 Expanded Language Support**: Spanish, Catalan, and Vietnamese languages are now available, with improvements made to others.
337
+
338
+ ### Fixed
339
+
340
+ - **🔧 Input Field Spelling**: Resolved issue with spelling mistakes in input fields.
341
+ - **🖊️ Light Mode Styling**: Fixed styling issue with light mode in document adding.
342
+
343
+ ### Changed
344
+
345
+ - **🔄 Language Sorting**: Languages are now sorted alphabetically by their code for improved organization.
346
+
347
+ ## [0.1.113] - 2024-03-18
348
+
349
+ ### Added
350
+
351
+ - 🌍 **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).
352
+ - 🎨 **System-wide Theme**: Introducing a new system-wide theme for enhanced visual experience.
353
+
354
+ ### Fixed
355
+
356
+ - 🌑 **Dark Background on Select Fields**: Improved readability by adding a dark background to select fields, addressing issues on certain browsers/devices.
357
+ - **Multiple OPENAI_API_BASE_URLS Issue**: Resolved issue where multiple base URLs caused conflicts when one wasn't functioning.
358
+ - **RAG Encoding Issue**: Fixed encoding problem in RAG.
359
+ - **npm Audit Fix**: Addressed npm audit findings.
360
+ - **Reduced Scroll Threshold**: Improved auto-scroll experience by reducing the scroll threshold from 50px to 5px.
361
+
362
+ ### Changed
363
+
364
+ - 🔄 **Sidebar UI Update**: Updated sidebar UI to feature a chat menu dropdown, replacing two icons for improved navigation.
365
+
366
+ ## [0.1.112] - 2024-03-15
367
+
368
+ ### Fixed
369
+
370
+ - 🗨️ Resolved chat malfunction after image generation.
371
+ - 🎨 Fixed various RAG issues.
372
+ - 🧪 Rectified experimental broken GGUF upload logic.
373
+
374
+ ## [0.1.111] - 2024-03-10
375
+
376
+ ### Added
377
+
378
+ - 🛡️ **Model Whitelisting**: Admins now have the ability to whitelist models for users with the 'user' role.
379
+ - 🔄 **Update All Models**: Added a convenient button to update all models at once.
380
+ - 📄 **Toggle PDF OCR**: Users can now toggle PDF OCR option for improved parsing performance.
381
+ - 🎨 **DALL-E Integration**: Introduced DALL-E integration for image generation alongside automatic1111.
382
+ - 🛠️ **RAG API Refactoring**: Refactored RAG logic and exposed its API, with additional documentation to follow.
383
+
384
+ ### Fixed
385
+
386
+ - 🔒 **Max Token Settings**: Added max token settings for anthropic/claude-3-sonnet-20240229 (Issue #1094).
387
+ - 🔧 **Misalignment Issue**: Corrected misalignment of Edit and Delete Icons when Chat Title is Empty (Issue #1104).
388
+ - 🔄 **Context Loss Fix**: Resolved RAG losing context on model response regeneration with Groq models via API key (Issue #1105).
389
+ - 📁 **File Handling Bug**: Addressed File Not Found Notification when Dropping a Conversation Element (Issue #1098).
390
+ - 🖱️ **Dragged File Styling**: Fixed dragged file layover styling issue.
391
+
392
+ ## [0.1.110] - 2024-03-06
393
+
394
+ ### Added
395
+
396
+ - **🌐 Multiple OpenAI Servers Support**: Enjoy seamless integration with multiple OpenAI-compatible APIs, now supported natively.
397
+
398
+ ### Fixed
399
+
400
+ - **🔍 OCR Issue**: Resolved PDF parsing issue caused by OCR malfunction.
401
+ - **🚫 RAG Issue**: Fixed the RAG functionality, ensuring it operates smoothly.
402
+ - **📄 "Add Docs" Model Button**: Addressed the non-functional behavior of the "Add Docs" model button.
403
+
404
+ ## [0.1.109] - 2024-03-06
405
+
406
+ ### Added
407
+
408
+ - **🔄 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).
409
+ - **🔧 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).
410
+ - **🔍 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).
411
+
412
+ ### Fixed
413
+
414
+ - **🛠️ RAG Collection**: Implemented a dynamic mechanism to recreate RAG collections, ensuring users have up-to-date and accurate data (#1031).
415
+ - **📝 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).
416
+ - **⏹️ Playground Cancel Functionality**: Introducing a new "Cancel" option for stopping Ollama generation in the Playground, enhancing user control and usability (#1006).
417
+ - **🔤 Typographical Error in 'ASSISTANT' Field**: Corrected a typographical error in the 'ASSISTANT' field within the GGUF model upload template for accuracy and consistency (#1061).
418
+
419
+ ### Changed
420
+
421
+ - **🔄 Refactored Message Deletion Logic**: Streamlined message deletion process for improved efficiency and user experience, simplifying interactions within the platform (#1004).
422
+ - **⚠️ 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.
423
+
424
+ ## [0.1.108] - 2024-03-02
425
+
426
+ ### Added
427
+
428
+ - **🎮 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.
429
+ - **🛠️ Direct Database Download for Admins**: Admins can now download the database directly from the WebUI via the admin settings.
430
+ - **🎨 Additional RAG Settings**: Customize your RAG process with the ability to edit the TOP K value. Navigate to Documents > Settings > General to make changes.
431
+ - **🖥️ UI Improvements**: Tooltips now available in the input area and sidebar handle. More tooltips will be added across other parts of the UI.
432
+
433
+ ### Fixed
434
+
435
+ - Resolved input autofocus issue on mobile when the sidebar is open, making it easier to use.
436
+ - Corrected numbered list display issue in Safari (#963).
437
+ - Restricted user ability to delete chats without proper permissions (#993).
438
+
439
+ ### Changed
440
+
441
+ - **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.
442
+ - **Database Renaming**: Starting from this release, `ollama.db` will be automatically renamed to `webui.db`.
443
+
444
+ ## [0.1.107] - 2024-03-01
445
+
446
+ ### Added
447
+
448
+ - **🚀 Makefile and LLM Update Script**: Included Makefile and a script for LLM updates in the repository.
449
+
450
+ ### Fixed
451
+
452
+ - Corrected issue where links in the settings modal didn't appear clickable (#960).
453
+ - Fixed problem with web UI port not taking effect due to incorrect environment variable name in run-compose.sh (#996).
454
+ - Enhanced user experience by displaying chat in browser title and enabling automatic scrolling to the bottom (#992).
455
+
456
+ ### Changed
457
+
458
+ - Upgraded toast library from `svelte-french-toast` to `svelte-sonner` for a more polished UI.
459
+ - Enhanced accessibility with the addition of dark mode on the authentication page.
460
+
461
+ ## [0.1.106] - 2024-02-27
462
+
463
+ ### Added
464
+
465
+ - **🎯 Auto-focus Feature**: The input area now automatically focuses when initiating or opening a chat conversation.
466
+
467
+ ### Fixed
468
+
469
+ - Corrected typo from "HuggingFace" to "Hugging Face" (Issue #924).
470
+ - Resolved bug causing errors in chat completion API calls to OpenAI due to missing "num_ctx" parameter (Issue #927).
471
+ - Fixed issues preventing text editing, selection, and cursor retention in the input field (Issue #940).
472
+ - 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)
473
+
474
+ ## [0.1.105] - 2024-02-25
475
+
476
+ ### Added
477
+
478
+ - **📄 Document Selection**: Now you can select and delete multiple documents at once for easier management.
479
+
480
+ ### Changed
481
+
482
+ - **🏷️ 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.
483
+
484
+ ## [0.1.104] - 2024-02-25
485
+
486
+ ### Added
487
+
488
+ - **🔄 Check for Updates**: Keep your system current by checking for updates conveniently located in Settings > About.
489
+ - **🗑️ Automatic Tag Deletion**: Unused tags on the sidebar will now be deleted automatically with just a click.
490
+
491
+ ### Changed
492
+
493
+ - **🎨 Modernized Styling**: Enjoy a refreshed look with updated styling for a more contemporary experience.
494
+
495
+ ## [0.1.103] - 2024-02-25
496
+
497
+ ### Added
498
+
499
+ - **🔗 Built-in LiteLLM Proxy**: Now includes LiteLLM proxy within Open WebUI for enhanced functionality.
500
+
501
+ - Easily integrate existing LiteLLM configurations using `-v /path/to/config.yaml:/app/backend/data/litellm/config.yaml` flag.
502
+ - When utilizing Docker container to run Open WebUI, ensure connections to localhost use `host.docker.internal`.
503
+
504
+ - **🖼️ Image Generation Enhancements**: Introducing Advanced Settings with Image Preview Feature.
505
+ - Customize image generation by setting the number of steps; defaults to A1111 value.
506
+
507
+ ### Fixed
508
+
509
+ - Resolved issue with RAG scan halting document loading upon encountering unsupported MIME types or exceptions (Issue #866).
510
+
511
+ ### Changed
512
+
513
+ - Ollama is no longer required to run Open WebUI.
514
+ - Access our comprehensive documentation at [Open WebUI Documentation](https://docs.openwebui.com/).
515
+
516
+ ## [0.1.102] - 2024-02-22
517
+
518
+ ### Added
519
+
520
+ - **🖼️ Image Generation**: Generate Images using the AUTOMATIC1111/stable-diffusion-webui API. You can set this up in Settings > Images.
521
+ - **📝 Change title generation prompt**: Change the prompt used to generate titles for your chats. You can set this up in the Settings > Interface.
522
+ - **🤖 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.
523
+ - **📢 CHANGELOG.md/Popup**: This popup will show you the latest changes.
524
+
525
+ ## [0.1.101] - 2024-02-22
526
+
527
+ ### Fixed
528
+
529
+ - LaTex output formatting issue (#828)
530
+
531
+ ### Changed
532
+
533
+ - 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,159 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ # for RAG OCR
101
+ apt-get install -y --no-install-recommends ffmpeg libsm6 libxext6 && \
102
+ # install helper tools
103
+ apt-get install -y --no-install-recommends curl jq && \
104
+ # install ollama
105
+ curl -fsSL https://ollama.com/install.sh | sh && \
106
+ # cleanup
107
+ rm -rf /var/lib/apt/lists/*; \
108
+ else \
109
+ apt-get update && \
110
+ # Install pandoc and netcat
111
+ apt-get install -y --no-install-recommends pandoc netcat-openbsd curl jq && \
112
+ # for RAG OCR
113
+ apt-get install -y --no-install-recommends ffmpeg libsm6 libxext6 && \
114
+ # cleanup
115
+ rm -rf /var/lib/apt/lists/*; \
116
+ fi
117
+
118
+ # install python dependencies
119
+ COPY --chown=$UID:$GID ./backend/requirements.txt ./requirements.txt
120
+
121
+ RUN pip3 install uv && \
122
+ if [ "$USE_CUDA" = "true" ]; then \
123
+ # If you use CUDA the whisper and embedding model will be downloaded on first use
124
+ pip3 install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/$USE_CUDA_DOCKER_VER --no-cache-dir && \
125
+ uv pip install --system -r requirements.txt --no-cache-dir && \
126
+ python -c "import os; from sentence_transformers import SentenceTransformer; SentenceTransformer(os.environ['RAG_EMBEDDING_MODEL'], device='cpu')" && \
127
+ 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'])"; \
128
+ else \
129
+ pip3 install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cpu --no-cache-dir && \
130
+ uv pip install --system -r requirements.txt --no-cache-dir && \
131
+ python -c "import os; from sentence_transformers import SentenceTransformer; SentenceTransformer(os.environ['RAG_EMBEDDING_MODEL'], device='cpu')" && \
132
+ 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'])"; \
133
+ fi; \
134
+ chown -R $UID:$GID /app/backend/data/
135
+
136
+
137
+
138
+ # copy embedding weight from build
139
+ # RUN mkdir -p /root/.cache/chroma/onnx_models/all-MiniLM-L6-v2
140
+ # COPY --from=build /app/onnx /root/.cache/chroma/onnx_models/all-MiniLM-L6-v2/onnx
141
+
142
+ # copy built frontend files
143
+ COPY --chown=$UID:$GID --from=build /app/build /app/build
144
+ COPY --chown=$UID:$GID --from=build /app/CHANGELOG.md /app/CHANGELOG.md
145
+ COPY --chown=$UID:$GID --from=build /app/package.json /app/package.json
146
+
147
+ # copy backend files
148
+ COPY --chown=$UID:$GID ./backend .
149
+
150
+ EXPOSE 8080
151
+
152
+ HEALTHCHECK CMD curl --silent --fail http://localhost:8080/health | jq -e '.status == true' || exit 1
153
+
154
+ USER $UID:$GID
155
+
156
+ ARG BUILD_HASH
157
+ ENV WEBUI_BUILD_VERSION=${BUILD_HASH}
158
+
159
+ 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,198 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ - 🛠️ **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.
41
+
42
+ - 📚 **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.
43
+
44
+ - 🔍 **Web Search for RAG**: Perform web searches using providers like `SearXNG`, `Google PSE`, `Brave Search`, `serpstack`, and `serper`, and inject the results directly into your chat experience.
45
+
46
+ - 🌐 **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.
47
+
48
+ - 🎨 **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.
49
+
50
+ - ⚙️ **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.
51
+
52
+ - 🔐 **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.
53
+
54
+ - 🌐🌍 **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!
55
+
56
+ - 🌟 **Continuous Updates**: We are committed to improving Open WebUI with regular updates, fixes, and new features.
57
+
58
+ Want to learn more about Open WebUI's features? Check out our [Open WebUI documentation](https://docs.openwebui.com/features) for a comprehensive overview!
59
+
60
+ ## 🔗 Also Check Out Open WebUI Community!
61
+
62
+ 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! 🚀
63
+
64
+ ## How to Install 🚀
65
+
66
+ > [!NOTE]
67
+ > 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.
68
+
69
+ ### Quick Start with Docker 🐳
70
+
71
+ > [!WARNING]
72
+ > 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.
73
+
74
+ > [!TIP]
75
+ > 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.
76
+
77
+ ### Installation with Default Configuration
78
+
79
+ - **If Ollama is on your computer**, use this command:
80
+
81
+ ```bash
82
+ 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
83
+ ```
84
+
85
+ - **If Ollama is on a Different Server**, use this command:
86
+
87
+ To connect to Ollama on another server, change the `OLLAMA_BASE_URL` to the server's URL:
88
+
89
+ ```bash
90
+ 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
91
+ ```
92
+
93
+ - **To run Open WebUI with Nvidia GPU support**, use this command:
94
+
95
+ ```bash
96
+ 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
97
+ ```
98
+
99
+ ### Installation for OpenAI API Usage Only
100
+
101
+ - **If you're only using OpenAI API**, use this command:
102
+
103
+ ```bash
104
+ 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
105
+ ```
106
+
107
+ ### Installing Open WebUI with Bundled Ollama Support
108
+
109
+ 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:
110
+
111
+ - **With GPU Support**:
112
+ Utilize GPU resources by running the following command:
113
+
114
+ ```bash
115
+ 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
116
+ ```
117
+
118
+ - **For CPU Only**:
119
+ If you're not using a GPU, use this command instead:
120
+
121
+ ```bash
122
+ 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
123
+ ```
124
+
125
+ 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.
126
+
127
+ After installation, you can access Open WebUI at [http://localhost:3000](http://localhost:3000). Enjoy! 😄
128
+
129
+ ### Other Installation Methods
130
+
131
+ 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.
132
+
133
+ ### Troubleshooting
134
+
135
+ 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).
136
+
137
+ #### Open WebUI: Server Connection Error
138
+
139
+ 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`.
140
+
141
+ **Example Docker Command**:
142
+
143
+ ```bash
144
+ 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
145
+ ```
146
+
147
+ ### Keeping Your Docker Installation Up-to-Date
148
+
149
+ In case you want to update your local Docker installation to the latest version, you can do it with [Watchtower](https://containrrr.dev/watchtower/):
150
+
151
+ ```bash
152
+ docker run --rm --volume /var/run/docker.sock:/var/run/docker.sock containrrr/watchtower --run-once open-webui
153
+ ```
154
+
155
+ In the last part of the command, replace `open-webui` with your container name if it is different.
156
+
157
+ ### Moving from Ollama WebUI to Open WebUI
158
+
159
+ Check our Migration Guide available in our [Open WebUI Documentation](https://docs.openwebui.com/migration/).
160
+
161
+ ## What's Next? 🌟
162
+
163
+ Discover upcoming features on our roadmap in the [Open WebUI Documentation](https://docs.openwebui.com/roadmap/).
164
+
165
+ ## Supporters ✨
166
+
167
+ A big shoutout to our amazing supporters who's helping to make this project possible! 🙏
168
+
169
+ ### Platinum Sponsors 🤍
170
+
171
+ - We're looking for Sponsors!
172
+
173
+ ### Acknowledgments
174
+
175
+ 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! 🙌
176
+
177
+ ## License 📜
178
+
179
+ This project is licensed under the [MIT License](LICENSE) - see the [LICENSE](LICENSE) file for details. 📄
180
+
181
+ ## Support 💬
182
+
183
+ If you have any questions, suggestions, or need assistance, please open an issue or join our
184
+ [Open WebUI Discord community](https://discord.gg/5rJgQTnV4s) to connect with us! 🤝
185
+
186
+ ## Star History
187
+
188
+ <a href="https://star-history.com/#open-webui/open-webui&Date">
189
+ <picture>
190
+ <source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=open-webui/open-webui&type=Date&theme=dark" />
191
+ <source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=open-webui/open-webui&type=Date" />
192
+ <img alt="Star History Chart" src="https://api.star-history.com/svg?repos=open-webui/open-webui&type=Date" />
193
+ </picture>
194
+ </a>
195
+
196
+ ---
197
+
198
+ Created by [Timothy J. Baek](https://github.com/tjbck) - Let's make Open WebUI even more amazing together! 💪
TROUBLESHOOTING.md ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Open WebUI Troubleshooting Guide
2
+
3
+ ## Understanding the Open WebUI Architecture
4
+
5
+ The Open WebUI system is designed to streamline interactions between the client (your browser) and the Ollama API. At the heart of this design is a backend reverse proxy, enhancing security and resolving CORS issues.
6
+
7
+ - **How it Works**: The Open WebUI is designed to interact with the Ollama API through a specific route. When a request is made from the WebUI to Ollama, it is not directly sent to the Ollama API. Initially, the request is sent to the Open WebUI backend via `/ollama` route. From there, the backend is responsible for forwarding the request to the Ollama API. This forwarding is accomplished by using the route specified in the `OLLAMA_BASE_URL` environment variable. Therefore, a request made to `/ollama` in the WebUI is effectively the same as making a request to `OLLAMA_BASE_URL` in the backend. For instance, a request to `/ollama/api/tags` in the WebUI is equivalent to `OLLAMA_BASE_URL/api/tags` in the backend.
8
+
9
+ - **Security Benefits**: This design prevents direct exposure of the Ollama API to the frontend, safeguarding against potential CORS (Cross-Origin Resource Sharing) issues and unauthorized access. Requiring authentication to access the Ollama API further enhances this security layer.
10
+
11
+ ## Open WebUI: Server Connection Error
12
+
13
+ If you're experiencing connection issues, it’s often due to the WebUI docker container not being able to reach the Ollama server at 127.0.0.1:11434 (host.docker.internal:11434) inside the container . Use the `--network=host` flag in your docker command to resolve this. Note that the port changes from 3000 to 8080, resulting in the link: `http://localhost:8080`.
14
+
15
+ **Example Docker Command**:
16
+
17
+ ```bash
18
+ docker run -d --network=host -v open-webui:/app/backend/data -e OLLAMA_BASE_URL=http://127.0.0.1:11434 --name open-webui --restart always ghcr.io/open-webui/open-webui:main
19
+ ```
20
+
21
+ ### General Connection Errors
22
+
23
+ **Ensure Ollama Version is Up-to-Date**: Always start by checking that you have the latest version of Ollama. Visit [Ollama's official site](https://ollama.com/) for the latest updates.
24
+
25
+ **Troubleshooting Steps**:
26
+
27
+ 1. **Verify Ollama URL Format**:
28
+ - When running the Web UI container, ensure the `OLLAMA_BASE_URL` is correctly set. (e.g., `http://192.168.1.1:11434` for different host setups).
29
+ - In the Open WebUI, navigate to "Settings" > "General".
30
+ - Confirm that the Ollama Server URL is correctly set to `[OLLAMA URL]` (e.g., `http://localhost:11434`).
31
+
32
+ By following these enhanced troubleshooting steps, connection issues should be effectively resolved. For further assistance or queries, feel free to reach out to us on our community Discord.
backend/.dockerignore ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ __pycache__
2
+ .env
3
+ _old
4
+ uploads
5
+ .ipynb_checkpoints
6
+ *.db
7
+ _test
8
+ !/data
9
+ /data/*
10
+ !/data/litellm
11
+ /data/litellm/*
12
+ !data/litellm/config.yaml
13
+
14
+ !data/config.json
backend/.gitignore ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ __pycache__
2
+ .env
3
+ _old
4
+ uploads
5
+ .ipynb_checkpoints
6
+ *.db
7
+ _test
8
+ Pipfile
9
+ !/data
10
+ /data/*
11
+ !/data/litellm
12
+ /data/litellm/*
13
+ !data/litellm/config.yaml
14
+
15
+ !data/config.json
16
+ .webui_secret_key
backend/apps/audio/main.py ADDED
@@ -0,0 +1,238 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import logging
3
+ from fastapi import (
4
+ FastAPI,
5
+ Request,
6
+ Depends,
7
+ HTTPException,
8
+ status,
9
+ UploadFile,
10
+ File,
11
+ Form,
12
+ )
13
+
14
+ from fastapi.responses import StreamingResponse, JSONResponse, FileResponse
15
+
16
+ from fastapi.middleware.cors import CORSMiddleware
17
+ from faster_whisper import WhisperModel
18
+ from pydantic import BaseModel
19
+
20
+ import uuid
21
+ import requests
22
+ import hashlib
23
+ from pathlib import Path
24
+ import json
25
+
26
+
27
+ from constants import ERROR_MESSAGES
28
+ from utils.utils import (
29
+ decode_token,
30
+ get_current_user,
31
+ get_verified_user,
32
+ get_admin_user,
33
+ )
34
+ from utils.misc import calculate_sha256
35
+
36
+ from config import (
37
+ SRC_LOG_LEVELS,
38
+ CACHE_DIR,
39
+ UPLOAD_DIR,
40
+ WHISPER_MODEL,
41
+ WHISPER_MODEL_DIR,
42
+ WHISPER_MODEL_AUTO_UPDATE,
43
+ DEVICE_TYPE,
44
+ AUDIO_OPENAI_API_BASE_URL,
45
+ AUDIO_OPENAI_API_KEY,
46
+ AUDIO_OPENAI_API_MODEL,
47
+ AUDIO_OPENAI_API_VOICE,
48
+ AppConfig,
49
+ )
50
+
51
+ log = logging.getLogger(__name__)
52
+ log.setLevel(SRC_LOG_LEVELS["AUDIO"])
53
+
54
+ app = FastAPI()
55
+ app.add_middleware(
56
+ CORSMiddleware,
57
+ allow_origins=["*"],
58
+ allow_credentials=True,
59
+ allow_methods=["*"],
60
+ allow_headers=["*"],
61
+ )
62
+
63
+ app.state.config = AppConfig()
64
+ app.state.config.OPENAI_API_BASE_URL = AUDIO_OPENAI_API_BASE_URL
65
+ app.state.config.OPENAI_API_KEY = AUDIO_OPENAI_API_KEY
66
+ app.state.config.OPENAI_API_MODEL = AUDIO_OPENAI_API_MODEL
67
+ app.state.config.OPENAI_API_VOICE = AUDIO_OPENAI_API_VOICE
68
+
69
+ # setting device type for whisper model
70
+ whisper_device_type = DEVICE_TYPE if DEVICE_TYPE and DEVICE_TYPE == "cuda" else "cpu"
71
+ log.info(f"whisper_device_type: {whisper_device_type}")
72
+
73
+ SPEECH_CACHE_DIR = Path(CACHE_DIR).joinpath("./audio/speech/")
74
+ SPEECH_CACHE_DIR.mkdir(parents=True, exist_ok=True)
75
+
76
+
77
+ class OpenAIConfigUpdateForm(BaseModel):
78
+ url: str
79
+ key: str
80
+ model: str
81
+ speaker: str
82
+
83
+
84
+ @app.get("/config")
85
+ async def get_openai_config(user=Depends(get_admin_user)):
86
+ return {
87
+ "OPENAI_API_BASE_URL": app.state.config.OPENAI_API_BASE_URL,
88
+ "OPENAI_API_KEY": app.state.config.OPENAI_API_KEY,
89
+ "OPENAI_API_MODEL": app.state.config.OPENAI_API_MODEL,
90
+ "OPENAI_API_VOICE": app.state.config.OPENAI_API_VOICE,
91
+ }
92
+
93
+
94
+ @app.post("/config/update")
95
+ async def update_openai_config(
96
+ form_data: OpenAIConfigUpdateForm, user=Depends(get_admin_user)
97
+ ):
98
+ if form_data.key == "":
99
+ raise HTTPException(status_code=400, detail=ERROR_MESSAGES.API_KEY_NOT_FOUND)
100
+
101
+ app.state.config.OPENAI_API_BASE_URL = form_data.url
102
+ app.state.config.OPENAI_API_KEY = form_data.key
103
+ app.state.config.OPENAI_API_MODEL = form_data.model
104
+ app.state.config.OPENAI_API_VOICE = form_data.speaker
105
+
106
+ return {
107
+ "status": True,
108
+ "OPENAI_API_BASE_URL": app.state.config.OPENAI_API_BASE_URL,
109
+ "OPENAI_API_KEY": app.state.config.OPENAI_API_KEY,
110
+ "OPENAI_API_MODEL": app.state.config.OPENAI_API_MODEL,
111
+ "OPENAI_API_VOICE": app.state.config.OPENAI_API_VOICE,
112
+ }
113
+
114
+
115
+ @app.post("/speech")
116
+ async def speech(request: Request, user=Depends(get_verified_user)):
117
+ body = await request.body()
118
+ name = hashlib.sha256(body).hexdigest()
119
+
120
+ file_path = SPEECH_CACHE_DIR.joinpath(f"{name}.mp3")
121
+ file_body_path = SPEECH_CACHE_DIR.joinpath(f"{name}.json")
122
+
123
+ # Check if the file already exists in the cache
124
+ if file_path.is_file():
125
+ return FileResponse(file_path)
126
+
127
+ headers = {}
128
+ headers["Authorization"] = f"Bearer {app.state.config.OPENAI_API_KEY}"
129
+ headers["Content-Type"] = "application/json"
130
+
131
+ r = None
132
+ try:
133
+ r = requests.post(
134
+ url=f"{app.state.config.OPENAI_API_BASE_URL}/audio/speech",
135
+ data=body,
136
+ headers=headers,
137
+ stream=True,
138
+ )
139
+
140
+ r.raise_for_status()
141
+
142
+ # Save the streaming content to a file
143
+ with open(file_path, "wb") as f:
144
+ for chunk in r.iter_content(chunk_size=8192):
145
+ f.write(chunk)
146
+
147
+ with open(file_body_path, "w") as f:
148
+ json.dump(json.loads(body.decode("utf-8")), f)
149
+
150
+ # Return the saved file
151
+ return FileResponse(file_path)
152
+
153
+ except Exception as e:
154
+ log.exception(e)
155
+ error_detail = "Open WebUI: Server Connection Error"
156
+ if r is not None:
157
+ try:
158
+ res = r.json()
159
+ if "error" in res:
160
+ error_detail = f"External: {res['error']['message']}"
161
+ except:
162
+ error_detail = f"External: {e}"
163
+
164
+ raise HTTPException(
165
+ status_code=r.status_code if r != None else 500,
166
+ detail=error_detail,
167
+ )
168
+
169
+
170
+ @app.post("/transcriptions")
171
+ def transcribe(
172
+ file: UploadFile = File(...),
173
+ user=Depends(get_current_user),
174
+ ):
175
+ log.info(f"file.content_type: {file.content_type}")
176
+
177
+ if file.content_type not in ["audio/mpeg", "audio/wav"]:
178
+ raise HTTPException(
179
+ status_code=status.HTTP_400_BAD_REQUEST,
180
+ detail=ERROR_MESSAGES.FILE_NOT_SUPPORTED,
181
+ )
182
+
183
+ try:
184
+ ext = file.filename.split(".")[-1]
185
+
186
+ id = uuid.uuid4()
187
+ filename = f"{id}.{ext}"
188
+
189
+ file_dir = f"{CACHE_DIR}/audio/transcriptions"
190
+ os.makedirs(file_dir, exist_ok=True)
191
+ file_path = f"{file_dir}/{filename}"
192
+
193
+ contents = file.file.read()
194
+ with open(file_path, "wb") as f:
195
+ f.write(contents)
196
+ f.close()
197
+
198
+ whisper_kwargs = {
199
+ "model_size_or_path": WHISPER_MODEL,
200
+ "device": whisper_device_type,
201
+ "compute_type": "int8",
202
+ "download_root": WHISPER_MODEL_DIR,
203
+ "local_files_only": not WHISPER_MODEL_AUTO_UPDATE,
204
+ }
205
+
206
+ log.debug(f"whisper_kwargs: {whisper_kwargs}")
207
+
208
+ try:
209
+ model = WhisperModel(**whisper_kwargs)
210
+ except:
211
+ log.warning(
212
+ "WhisperModel initialization failed, attempting download with local_files_only=False"
213
+ )
214
+ whisper_kwargs["local_files_only"] = False
215
+ model = WhisperModel(**whisper_kwargs)
216
+
217
+ segments, info = model.transcribe(file_path, beam_size=5)
218
+ log.info(
219
+ "Detected language '%s' with probability %f"
220
+ % (info.language, info.language_probability)
221
+ )
222
+
223
+ transcript = "".join([segment.text for segment in list(segments)])
224
+
225
+ # save the transcript to a json file
226
+ transcript_file = f"{file_dir}/{id}.json"
227
+ with open(transcript_file, "w") as f:
228
+ json.dump({"transcript": transcript}, f)
229
+
230
+ return {"text": transcript.strip()}
231
+
232
+ except Exception as e:
233
+ log.exception(e)
234
+
235
+ raise HTTPException(
236
+ status_code=status.HTTP_400_BAD_REQUEST,
237
+ detail=ERROR_MESSAGES.DEFAULT(e),
238
+ )
backend/apps/images/main.py ADDED
@@ -0,0 +1,527 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import re
2
+ import requests
3
+ from fastapi import (
4
+ FastAPI,
5
+ Request,
6
+ Depends,
7
+ HTTPException,
8
+ status,
9
+ UploadFile,
10
+ File,
11
+ Form,
12
+ )
13
+ from fastapi.middleware.cors import CORSMiddleware
14
+ from faster_whisper import WhisperModel
15
+
16
+ from constants import ERROR_MESSAGES
17
+ from utils.utils import (
18
+ get_current_user,
19
+ get_admin_user,
20
+ )
21
+
22
+ from apps.images.utils.comfyui import ImageGenerationPayload, comfyui_generate_image
23
+ from utils.misc import calculate_sha256
24
+ from typing import Optional
25
+ from pydantic import BaseModel
26
+ from pathlib import Path
27
+ import mimetypes
28
+ import uuid
29
+ import base64
30
+ import json
31
+ import logging
32
+
33
+ from config import (
34
+ SRC_LOG_LEVELS,
35
+ CACHE_DIR,
36
+ IMAGE_GENERATION_ENGINE,
37
+ ENABLE_IMAGE_GENERATION,
38
+ AUTOMATIC1111_BASE_URL,
39
+ COMFYUI_BASE_URL,
40
+ IMAGES_OPENAI_API_BASE_URL,
41
+ IMAGES_OPENAI_API_KEY,
42
+ IMAGE_GENERATION_MODEL,
43
+ IMAGE_SIZE,
44
+ IMAGE_STEPS,
45
+ AppConfig,
46
+ )
47
+
48
+
49
+ log = logging.getLogger(__name__)
50
+ log.setLevel(SRC_LOG_LEVELS["IMAGES"])
51
+
52
+ IMAGE_CACHE_DIR = Path(CACHE_DIR).joinpath("./image/generations/")
53
+ IMAGE_CACHE_DIR.mkdir(parents=True, exist_ok=True)
54
+
55
+ app = FastAPI()
56
+ app.add_middleware(
57
+ CORSMiddleware,
58
+ allow_origins=["*"],
59
+ allow_credentials=True,
60
+ allow_methods=["*"],
61
+ allow_headers=["*"],
62
+ )
63
+
64
+ app.state.config = AppConfig()
65
+
66
+ app.state.config.ENGINE = IMAGE_GENERATION_ENGINE
67
+ app.state.config.ENABLED = ENABLE_IMAGE_GENERATION
68
+
69
+ app.state.config.OPENAI_API_BASE_URL = IMAGES_OPENAI_API_BASE_URL
70
+ app.state.config.OPENAI_API_KEY = IMAGES_OPENAI_API_KEY
71
+
72
+ app.state.config.MODEL = IMAGE_GENERATION_MODEL
73
+
74
+
75
+ app.state.config.AUTOMATIC1111_BASE_URL = AUTOMATIC1111_BASE_URL
76
+ app.state.config.COMFYUI_BASE_URL = COMFYUI_BASE_URL
77
+
78
+
79
+ app.state.config.IMAGE_SIZE = IMAGE_SIZE
80
+ app.state.config.IMAGE_STEPS = IMAGE_STEPS
81
+
82
+
83
+ @app.get("/config")
84
+ async def get_config(request: Request, user=Depends(get_admin_user)):
85
+ return {
86
+ "engine": app.state.config.ENGINE,
87
+ "enabled": app.state.config.ENABLED,
88
+ }
89
+
90
+
91
+ class ConfigUpdateForm(BaseModel):
92
+ engine: str
93
+ enabled: bool
94
+
95
+
96
+ @app.post("/config/update")
97
+ async def update_config(form_data: ConfigUpdateForm, user=Depends(get_admin_user)):
98
+ app.state.config.ENGINE = form_data.engine
99
+ app.state.config.ENABLED = form_data.enabled
100
+ return {
101
+ "engine": app.state.config.ENGINE,
102
+ "enabled": app.state.config.ENABLED,
103
+ }
104
+
105
+
106
+ class EngineUrlUpdateForm(BaseModel):
107
+ AUTOMATIC1111_BASE_URL: Optional[str] = None
108
+ COMFYUI_BASE_URL: Optional[str] = None
109
+
110
+
111
+ @app.get("/url")
112
+ async def get_engine_url(user=Depends(get_admin_user)):
113
+ return {
114
+ "AUTOMATIC1111_BASE_URL": app.state.config.AUTOMATIC1111_BASE_URL,
115
+ "COMFYUI_BASE_URL": app.state.config.COMFYUI_BASE_URL,
116
+ }
117
+
118
+
119
+ @app.post("/url/update")
120
+ async def update_engine_url(
121
+ form_data: EngineUrlUpdateForm, user=Depends(get_admin_user)
122
+ ):
123
+
124
+ if form_data.AUTOMATIC1111_BASE_URL == None:
125
+ app.state.config.AUTOMATIC1111_BASE_URL = AUTOMATIC1111_BASE_URL
126
+ else:
127
+ url = form_data.AUTOMATIC1111_BASE_URL.strip("/")
128
+ try:
129
+ r = requests.head(url)
130
+ app.state.config.AUTOMATIC1111_BASE_URL = url
131
+ except Exception as e:
132
+ raise HTTPException(status_code=400, detail=ERROR_MESSAGES.DEFAULT(e))
133
+
134
+ if form_data.COMFYUI_BASE_URL == None:
135
+ app.state.config.COMFYUI_BASE_URL = COMFYUI_BASE_URL
136
+ else:
137
+ url = form_data.COMFYUI_BASE_URL.strip("/")
138
+
139
+ try:
140
+ r = requests.head(url)
141
+ app.state.config.COMFYUI_BASE_URL = url
142
+ except Exception as e:
143
+ raise HTTPException(status_code=400, detail=ERROR_MESSAGES.DEFAULT(e))
144
+
145
+ return {
146
+ "AUTOMATIC1111_BASE_URL": app.state.config.AUTOMATIC1111_BASE_URL,
147
+ "COMFYUI_BASE_URL": app.state.config.COMFYUI_BASE_URL,
148
+ "status": True,
149
+ }
150
+
151
+
152
+ class OpenAIConfigUpdateForm(BaseModel):
153
+ url: str
154
+ key: str
155
+
156
+
157
+ @app.get("/openai/config")
158
+ async def get_openai_config(user=Depends(get_admin_user)):
159
+ return {
160
+ "OPENAI_API_BASE_URL": app.state.config.OPENAI_API_BASE_URL,
161
+ "OPENAI_API_KEY": app.state.config.OPENAI_API_KEY,
162
+ }
163
+
164
+
165
+ @app.post("/openai/config/update")
166
+ async def update_openai_config(
167
+ form_data: OpenAIConfigUpdateForm, user=Depends(get_admin_user)
168
+ ):
169
+ if form_data.key == "":
170
+ raise HTTPException(status_code=400, detail=ERROR_MESSAGES.API_KEY_NOT_FOUND)
171
+
172
+ app.state.config.OPENAI_API_BASE_URL = form_data.url
173
+ app.state.config.OPENAI_API_KEY = form_data.key
174
+
175
+ return {
176
+ "status": True,
177
+ "OPENAI_API_BASE_URL": app.state.config.OPENAI_API_BASE_URL,
178
+ "OPENAI_API_KEY": app.state.config.OPENAI_API_KEY,
179
+ }
180
+
181
+
182
+ class ImageSizeUpdateForm(BaseModel):
183
+ size: str
184
+
185
+
186
+ @app.get("/size")
187
+ async def get_image_size(user=Depends(get_admin_user)):
188
+ return {"IMAGE_SIZE": app.state.config.IMAGE_SIZE}
189
+
190
+
191
+ @app.post("/size/update")
192
+ async def update_image_size(
193
+ form_data: ImageSizeUpdateForm, user=Depends(get_admin_user)
194
+ ):
195
+ pattern = r"^\d+x\d+$" # Regular expression pattern
196
+ if re.match(pattern, form_data.size):
197
+ app.state.config.IMAGE_SIZE = form_data.size
198
+ return {
199
+ "IMAGE_SIZE": app.state.config.IMAGE_SIZE,
200
+ "status": True,
201
+ }
202
+ else:
203
+ raise HTTPException(
204
+ status_code=400,
205
+ detail=ERROR_MESSAGES.INCORRECT_FORMAT(" (e.g., 512x512)."),
206
+ )
207
+
208
+
209
+ class ImageStepsUpdateForm(BaseModel):
210
+ steps: int
211
+
212
+
213
+ @app.get("/steps")
214
+ async def get_image_size(user=Depends(get_admin_user)):
215
+ return {"IMAGE_STEPS": app.state.config.IMAGE_STEPS}
216
+
217
+
218
+ @app.post("/steps/update")
219
+ async def update_image_size(
220
+ form_data: ImageStepsUpdateForm, user=Depends(get_admin_user)
221
+ ):
222
+ if form_data.steps >= 0:
223
+ app.state.config.IMAGE_STEPS = form_data.steps
224
+ return {
225
+ "IMAGE_STEPS": app.state.config.IMAGE_STEPS,
226
+ "status": True,
227
+ }
228
+ else:
229
+ raise HTTPException(
230
+ status_code=400,
231
+ detail=ERROR_MESSAGES.INCORRECT_FORMAT(" (e.g., 50)."),
232
+ )
233
+
234
+
235
+ @app.get("/models")
236
+ def get_models(user=Depends(get_current_user)):
237
+ try:
238
+ if app.state.config.ENGINE == "openai":
239
+ return [
240
+ {"id": "dall-e-2", "name": "DALL·E 2"},
241
+ {"id": "dall-e-3", "name": "DALL·E 3"},
242
+ ]
243
+ elif app.state.config.ENGINE == "comfyui":
244
+
245
+ r = requests.get(url=f"{app.state.config.COMFYUI_BASE_URL}/object_info")
246
+ info = r.json()
247
+
248
+ return list(
249
+ map(
250
+ lambda model: {"id": model, "name": model},
251
+ info["CheckpointLoaderSimple"]["input"]["required"]["ckpt_name"][0],
252
+ )
253
+ )
254
+
255
+ else:
256
+ r = requests.get(
257
+ url=f"{app.state.config.AUTOMATIC1111_BASE_URL}/sdapi/v1/sd-models"
258
+ )
259
+ models = r.json()
260
+ return list(
261
+ map(
262
+ lambda model: {"id": model["title"], "name": model["model_name"]},
263
+ models,
264
+ )
265
+ )
266
+ except Exception as e:
267
+ app.state.config.ENABLED = False
268
+ raise HTTPException(status_code=400, detail=ERROR_MESSAGES.DEFAULT(e))
269
+
270
+
271
+ @app.get("/models/default")
272
+ async def get_default_model(user=Depends(get_admin_user)):
273
+ try:
274
+ if app.state.config.ENGINE == "openai":
275
+ return {
276
+ "model": (
277
+ app.state.config.MODEL if app.state.config.MODEL else "dall-e-2"
278
+ )
279
+ }
280
+ elif app.state.config.ENGINE == "comfyui":
281
+ return {"model": (app.state.config.MODEL if app.state.config.MODEL else "")}
282
+ else:
283
+ r = requests.get(
284
+ url=f"{app.state.config.AUTOMATIC1111_BASE_URL}/sdapi/v1/options"
285
+ )
286
+ options = r.json()
287
+ return {"model": options["sd_model_checkpoint"]}
288
+ except Exception as e:
289
+ app.state.config.ENABLED = False
290
+ raise HTTPException(status_code=400, detail=ERROR_MESSAGES.DEFAULT(e))
291
+
292
+
293
+ class UpdateModelForm(BaseModel):
294
+ model: str
295
+
296
+
297
+ def set_model_handler(model: str):
298
+ if app.state.config.ENGINE in ["openai", "comfyui"]:
299
+ app.state.config.MODEL = model
300
+ return app.state.config.MODEL
301
+ else:
302
+ r = requests.get(
303
+ url=f"{app.state.config.AUTOMATIC1111_BASE_URL}/sdapi/v1/options"
304
+ )
305
+ options = r.json()
306
+
307
+ if model != options["sd_model_checkpoint"]:
308
+ options["sd_model_checkpoint"] = model
309
+ r = requests.post(
310
+ url=f"{app.state.config.AUTOMATIC1111_BASE_URL}/sdapi/v1/options",
311
+ json=options,
312
+ )
313
+
314
+ return options
315
+
316
+
317
+ @app.post("/models/default/update")
318
+ def update_default_model(
319
+ form_data: UpdateModelForm,
320
+ user=Depends(get_current_user),
321
+ ):
322
+ return set_model_handler(form_data.model)
323
+
324
+
325
+ class GenerateImageForm(BaseModel):
326
+ model: Optional[str] = None
327
+ prompt: str
328
+ n: int = 1
329
+ size: Optional[str] = None
330
+ negative_prompt: Optional[str] = None
331
+
332
+
333
+ def save_b64_image(b64_str):
334
+ try:
335
+ image_id = str(uuid.uuid4())
336
+
337
+ if "," in b64_str:
338
+ header, encoded = b64_str.split(",", 1)
339
+ mime_type = header.split(";")[0]
340
+
341
+ img_data = base64.b64decode(encoded)
342
+ image_format = mimetypes.guess_extension(mime_type)
343
+
344
+ image_filename = f"{image_id}{image_format}"
345
+ file_path = IMAGE_CACHE_DIR / f"{image_filename}"
346
+ with open(file_path, "wb") as f:
347
+ f.write(img_data)
348
+ return image_filename
349
+ else:
350
+ image_filename = f"{image_id}.png"
351
+ file_path = IMAGE_CACHE_DIR.joinpath(image_filename)
352
+
353
+ img_data = base64.b64decode(b64_str)
354
+
355
+ # Write the image data to a file
356
+ with open(file_path, "wb") as f:
357
+ f.write(img_data)
358
+ return image_filename
359
+
360
+ except Exception as e:
361
+ log.exception(f"Error saving image: {e}")
362
+ return None
363
+
364
+
365
+ def save_url_image(url):
366
+ image_id = str(uuid.uuid4())
367
+ try:
368
+ r = requests.get(url)
369
+ r.raise_for_status()
370
+ if r.headers["content-type"].split("/")[0] == "image":
371
+
372
+ mime_type = r.headers["content-type"]
373
+ image_format = mimetypes.guess_extension(mime_type)
374
+
375
+ if not image_format:
376
+ raise ValueError("Could not determine image type from MIME type")
377
+
378
+ image_filename = f"{image_id}{image_format}"
379
+
380
+ file_path = IMAGE_CACHE_DIR.joinpath(f"{image_filename}")
381
+ with open(file_path, "wb") as image_file:
382
+ for chunk in r.iter_content(chunk_size=8192):
383
+ image_file.write(chunk)
384
+ return image_filename
385
+ else:
386
+ log.error(f"Url does not point to an image.")
387
+ return None
388
+
389
+ except Exception as e:
390
+ log.exception(f"Error saving image: {e}")
391
+ return None
392
+
393
+
394
+ @app.post("/generations")
395
+ def generate_image(
396
+ form_data: GenerateImageForm,
397
+ user=Depends(get_current_user),
398
+ ):
399
+
400
+ width, height = tuple(map(int, app.state.config.IMAGE_SIZE.split("x")))
401
+
402
+ r = None
403
+ try:
404
+ if app.state.config.ENGINE == "openai":
405
+
406
+ headers = {}
407
+ headers["Authorization"] = f"Bearer {app.state.config.OPENAI_API_KEY}"
408
+ headers["Content-Type"] = "application/json"
409
+
410
+ data = {
411
+ "model": (
412
+ app.state.config.MODEL
413
+ if app.state.config.MODEL != ""
414
+ else "dall-e-2"
415
+ ),
416
+ "prompt": form_data.prompt,
417
+ "n": form_data.n,
418
+ "size": (
419
+ form_data.size if form_data.size else app.state.config.IMAGE_SIZE
420
+ ),
421
+ "response_format": "b64_json",
422
+ }
423
+
424
+ r = requests.post(
425
+ url=f"{app.state.config.OPENAI_API_BASE_URL}/images/generations",
426
+ json=data,
427
+ headers=headers,
428
+ )
429
+
430
+ r.raise_for_status()
431
+ res = r.json()
432
+
433
+ images = []
434
+
435
+ for image in res["data"]:
436
+ image_filename = save_b64_image(image["b64_json"])
437
+ images.append({"url": f"/cache/image/generations/{image_filename}"})
438
+ file_body_path = IMAGE_CACHE_DIR.joinpath(f"{image_filename}.json")
439
+
440
+ with open(file_body_path, "w") as f:
441
+ json.dump(data, f)
442
+
443
+ return images
444
+
445
+ elif app.state.config.ENGINE == "comfyui":
446
+
447
+ data = {
448
+ "prompt": form_data.prompt,
449
+ "width": width,
450
+ "height": height,
451
+ "n": form_data.n,
452
+ }
453
+
454
+ if app.state.config.IMAGE_STEPS is not None:
455
+ data["steps"] = app.state.config.IMAGE_STEPS
456
+
457
+ if form_data.negative_prompt is not None:
458
+ data["negative_prompt"] = form_data.negative_prompt
459
+
460
+ data = ImageGenerationPayload(**data)
461
+
462
+ res = comfyui_generate_image(
463
+ app.state.config.MODEL,
464
+ data,
465
+ user.id,
466
+ app.state.config.COMFYUI_BASE_URL,
467
+ )
468
+ log.debug(f"res: {res}")
469
+
470
+ images = []
471
+
472
+ for image in res["data"]:
473
+ image_filename = save_url_image(image["url"])
474
+ images.append({"url": f"/cache/image/generations/{image_filename}"})
475
+ file_body_path = IMAGE_CACHE_DIR.joinpath(f"{image_filename}.json")
476
+
477
+ with open(file_body_path, "w") as f:
478
+ json.dump(data.model_dump(exclude_none=True), f)
479
+
480
+ log.debug(f"images: {images}")
481
+ return images
482
+ else:
483
+ if form_data.model:
484
+ set_model_handler(form_data.model)
485
+
486
+ data = {
487
+ "prompt": form_data.prompt,
488
+ "batch_size": form_data.n,
489
+ "width": width,
490
+ "height": height,
491
+ }
492
+
493
+ if app.state.config.IMAGE_STEPS is not None:
494
+ data["steps"] = app.state.config.IMAGE_STEPS
495
+
496
+ if form_data.negative_prompt is not None:
497
+ data["negative_prompt"] = form_data.negative_prompt
498
+
499
+ r = requests.post(
500
+ url=f"{app.state.config.AUTOMATIC1111_BASE_URL}/sdapi/v1/txt2img",
501
+ json=data,
502
+ )
503
+
504
+ res = r.json()
505
+
506
+ log.debug(f"res: {res}")
507
+
508
+ images = []
509
+
510
+ for image in res["images"]:
511
+ image_filename = save_b64_image(image)
512
+ images.append({"url": f"/cache/image/generations/{image_filename}"})
513
+ file_body_path = IMAGE_CACHE_DIR.joinpath(f"{image_filename}.json")
514
+
515
+ with open(file_body_path, "w") as f:
516
+ json.dump({**data, "info": res["info"]}, f)
517
+
518
+ return images
519
+
520
+ except Exception as e:
521
+ error = e
522
+
523
+ if r != None:
524
+ data = r.json()
525
+ if "error" in data:
526
+ error = data["error"]["message"]
527
+ raise HTTPException(status_code=400, detail=ERROR_MESSAGES.DEFAULT(error))
backend/apps/images/utils/comfyui.py ADDED
@@ -0,0 +1,234 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import websocket # NOTE: websocket-client (https://github.com/websocket-client/websocket-client)
2
+ import uuid
3
+ import json
4
+ import urllib.request
5
+ import urllib.parse
6
+ import random
7
+ import logging
8
+
9
+ from config import SRC_LOG_LEVELS
10
+
11
+ log = logging.getLogger(__name__)
12
+ log.setLevel(SRC_LOG_LEVELS["COMFYUI"])
13
+
14
+ from pydantic import BaseModel
15
+
16
+ from typing import Optional
17
+
18
+ COMFYUI_DEFAULT_PROMPT = """
19
+ {
20
+ "3": {
21
+ "inputs": {
22
+ "seed": 0,
23
+ "steps": 20,
24
+ "cfg": 8,
25
+ "sampler_name": "euler",
26
+ "scheduler": "normal",
27
+ "denoise": 1,
28
+ "model": [
29
+ "4",
30
+ 0
31
+ ],
32
+ "positive": [
33
+ "6",
34
+ 0
35
+ ],
36
+ "negative": [
37
+ "7",
38
+ 0
39
+ ],
40
+ "latent_image": [
41
+ "5",
42
+ 0
43
+ ]
44
+ },
45
+ "class_type": "KSampler",
46
+ "_meta": {
47
+ "title": "KSampler"
48
+ }
49
+ },
50
+ "4": {
51
+ "inputs": {
52
+ "ckpt_name": "model.safetensors"
53
+ },
54
+ "class_type": "CheckpointLoaderSimple",
55
+ "_meta": {
56
+ "title": "Load Checkpoint"
57
+ }
58
+ },
59
+ "5": {
60
+ "inputs": {
61
+ "width": 512,
62
+ "height": 512,
63
+ "batch_size": 1
64
+ },
65
+ "class_type": "EmptyLatentImage",
66
+ "_meta": {
67
+ "title": "Empty Latent Image"
68
+ }
69
+ },
70
+ "6": {
71
+ "inputs": {
72
+ "text": "Prompt",
73
+ "clip": [
74
+ "4",
75
+ 1
76
+ ]
77
+ },
78
+ "class_type": "CLIPTextEncode",
79
+ "_meta": {
80
+ "title": "CLIP Text Encode (Prompt)"
81
+ }
82
+ },
83
+ "7": {
84
+ "inputs": {
85
+ "text": "Negative Prompt",
86
+ "clip": [
87
+ "4",
88
+ 1
89
+ ]
90
+ },
91
+ "class_type": "CLIPTextEncode",
92
+ "_meta": {
93
+ "title": "CLIP Text Encode (Prompt)"
94
+ }
95
+ },
96
+ "8": {
97
+ "inputs": {
98
+ "samples": [
99
+ "3",
100
+ 0
101
+ ],
102
+ "vae": [
103
+ "4",
104
+ 2
105
+ ]
106
+ },
107
+ "class_type": "VAEDecode",
108
+ "_meta": {
109
+ "title": "VAE Decode"
110
+ }
111
+ },
112
+ "9": {
113
+ "inputs": {
114
+ "filename_prefix": "ComfyUI",
115
+ "images": [
116
+ "8",
117
+ 0
118
+ ]
119
+ },
120
+ "class_type": "SaveImage",
121
+ "_meta": {
122
+ "title": "Save Image"
123
+ }
124
+ }
125
+ }
126
+ """
127
+
128
+
129
+ def queue_prompt(prompt, client_id, base_url):
130
+ log.info("queue_prompt")
131
+ p = {"prompt": prompt, "client_id": client_id}
132
+ data = json.dumps(p).encode("utf-8")
133
+ req = urllib.request.Request(f"{base_url}/prompt", data=data)
134
+ return json.loads(urllib.request.urlopen(req).read())
135
+
136
+
137
+ def get_image(filename, subfolder, folder_type, base_url):
138
+ log.info("get_image")
139
+ data = {"filename": filename, "subfolder": subfolder, "type": folder_type}
140
+ url_values = urllib.parse.urlencode(data)
141
+ with urllib.request.urlopen(f"{base_url}/view?{url_values}") as response:
142
+ return response.read()
143
+
144
+
145
+ def get_image_url(filename, subfolder, folder_type, base_url):
146
+ log.info("get_image")
147
+ data = {"filename": filename, "subfolder": subfolder, "type": folder_type}
148
+ url_values = urllib.parse.urlencode(data)
149
+ return f"{base_url}/view?{url_values}"
150
+
151
+
152
+ def get_history(prompt_id, base_url):
153
+ log.info("get_history")
154
+ with urllib.request.urlopen(f"{base_url}/history/{prompt_id}") as response:
155
+ return json.loads(response.read())
156
+
157
+
158
+ def get_images(ws, prompt, client_id, base_url):
159
+ prompt_id = queue_prompt(prompt, client_id, base_url)["prompt_id"]
160
+ output_images = []
161
+ while True:
162
+ out = ws.recv()
163
+ if isinstance(out, str):
164
+ message = json.loads(out)
165
+ if message["type"] == "executing":
166
+ data = message["data"]
167
+ if data["node"] is None and data["prompt_id"] == prompt_id:
168
+ break # Execution is done
169
+ else:
170
+ continue # previews are binary data
171
+
172
+ history = get_history(prompt_id, base_url)[prompt_id]
173
+ for o in history["outputs"]:
174
+ for node_id in history["outputs"]:
175
+ node_output = history["outputs"][node_id]
176
+ if "images" in node_output:
177
+ for image in node_output["images"]:
178
+ url = get_image_url(
179
+ image["filename"], image["subfolder"], image["type"], base_url
180
+ )
181
+ output_images.append({"url": url})
182
+ return {"data": output_images}
183
+
184
+
185
+ class ImageGenerationPayload(BaseModel):
186
+ prompt: str
187
+ negative_prompt: Optional[str] = ""
188
+ steps: Optional[int] = None
189
+ seed: Optional[int] = None
190
+ width: int
191
+ height: int
192
+ n: int = 1
193
+
194
+
195
+ def comfyui_generate_image(
196
+ model: str, payload: ImageGenerationPayload, client_id, base_url
197
+ ):
198
+ ws_url = base_url.replace("http://", "ws://").replace("https://", "wss://")
199
+
200
+ comfyui_prompt = json.loads(COMFYUI_DEFAULT_PROMPT)
201
+
202
+ comfyui_prompt["4"]["inputs"]["ckpt_name"] = model
203
+ comfyui_prompt["5"]["inputs"]["batch_size"] = payload.n
204
+ comfyui_prompt["5"]["inputs"]["width"] = payload.width
205
+ comfyui_prompt["5"]["inputs"]["height"] = payload.height
206
+
207
+ # set the text prompt for our positive CLIPTextEncode
208
+ comfyui_prompt["6"]["inputs"]["text"] = payload.prompt
209
+ comfyui_prompt["7"]["inputs"]["text"] = payload.negative_prompt
210
+
211
+ if payload.steps:
212
+ comfyui_prompt["3"]["inputs"]["steps"] = payload.steps
213
+
214
+ comfyui_prompt["3"]["inputs"]["seed"] = (
215
+ payload.seed if payload.seed else random.randint(0, 18446744073709551614)
216
+ )
217
+
218
+ try:
219
+ ws = websocket.WebSocket()
220
+ ws.connect(f"{ws_url}/ws?clientId={client_id}")
221
+ log.info("WebSocket connection established.")
222
+ except Exception as e:
223
+ log.exception(f"Failed to connect to WebSocket server: {e}")
224
+ return None
225
+
226
+ try:
227
+ images = get_images(ws, comfyui_prompt, client_id, base_url)
228
+ except Exception as e:
229
+ log.exception(f"Error while receiving images: {e}")
230
+ images = None
231
+
232
+ ws.close()
233
+
234
+ return images
backend/apps/ollama/main.py ADDED
@@ -0,0 +1,1321 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+
44
+ from utils.models import get_model_id_from_custom_model_id
45
+
46
+
47
+ from config import (
48
+ SRC_LOG_LEVELS,
49
+ OLLAMA_BASE_URLS,
50
+ ENABLE_OLLAMA_API,
51
+ ENABLE_MODEL_FILTER,
52
+ MODEL_FILTER_LIST,
53
+ UPLOAD_DIR,
54
+ AppConfig,
55
+ )
56
+ from utils.misc import calculate_sha256
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):
157
+ r = None
158
+ try:
159
+ session = aiohttp.ClientSession(trust_env=True)
160
+ r = await session.post(url, data=payload)
161
+ r.raise_for_status()
162
+
163
+ return StreamingResponse(
164
+ r.content,
165
+ status_code=r.status,
166
+ headers=dict(r.headers),
167
+ background=BackgroundTask(cleanup_response, response=r, session=session),
168
+ )
169
+ except Exception as e:
170
+ error_detail = "Open WebUI: Server Connection Error"
171
+ if r is not None:
172
+ try:
173
+ res = await r.json()
174
+ if "error" in res:
175
+ error_detail = f"Ollama: {res['error']}"
176
+ except:
177
+ error_detail = f"Ollama: {e}"
178
+
179
+ raise HTTPException(
180
+ status_code=r.status if r else 500,
181
+ detail=error_detail,
182
+ )
183
+
184
+
185
+ def merge_models_lists(model_lists):
186
+ merged_models = {}
187
+
188
+ for idx, model_list in enumerate(model_lists):
189
+ if model_list is not None:
190
+ for model in model_list:
191
+ digest = model["digest"]
192
+ if digest not in merged_models:
193
+ model["urls"] = [idx]
194
+ merged_models[digest] = model
195
+ else:
196
+ merged_models[digest]["urls"].append(idx)
197
+
198
+ return list(merged_models.values())
199
+
200
+
201
+ # user=Depends(get_current_user)
202
+
203
+
204
+ async def get_all_models():
205
+ log.info("get_all_models()")
206
+
207
+ if app.state.config.ENABLE_OLLAMA_API:
208
+ tasks = [
209
+ fetch_url(f"{url}/api/tags") for url in app.state.config.OLLAMA_BASE_URLS
210
+ ]
211
+ responses = await asyncio.gather(*tasks)
212
+
213
+ models = {
214
+ "models": merge_models_lists(
215
+ map(
216
+ lambda response: response["models"] if response else None, responses
217
+ )
218
+ )
219
+ }
220
+
221
+ else:
222
+ models = {"models": []}
223
+
224
+ app.state.MODELS = {model["model"]: model for model in models["models"]}
225
+
226
+ return models
227
+
228
+
229
+ @app.get("/api/tags")
230
+ @app.get("/api/tags/{url_idx}")
231
+ async def get_ollama_tags(
232
+ url_idx: Optional[int] = None, user=Depends(get_verified_user)
233
+ ):
234
+ if url_idx == None:
235
+ models = await get_all_models()
236
+
237
+ if app.state.config.ENABLE_MODEL_FILTER:
238
+ if user.role == "user":
239
+ models["models"] = list(
240
+ filter(
241
+ lambda model: model["name"]
242
+ in app.state.config.MODEL_FILTER_LIST,
243
+ models["models"],
244
+ )
245
+ )
246
+ return models
247
+ return models
248
+ else:
249
+ url = app.state.config.OLLAMA_BASE_URLS[url_idx]
250
+
251
+ r = None
252
+ try:
253
+ r = requests.request(method="GET", url=f"{url}/api/tags")
254
+ r.raise_for_status()
255
+
256
+ return r.json()
257
+ except Exception as e:
258
+ log.exception(e)
259
+ error_detail = "Open WebUI: Server Connection Error"
260
+ if r is not None:
261
+ try:
262
+ res = r.json()
263
+ if "error" in res:
264
+ error_detail = f"Ollama: {res['error']}"
265
+ except:
266
+ error_detail = f"Ollama: {e}"
267
+
268
+ raise HTTPException(
269
+ status_code=r.status_code if r else 500,
270
+ detail=error_detail,
271
+ )
272
+
273
+
274
+ @app.get("/api/version")
275
+ @app.get("/api/version/{url_idx}")
276
+ async def get_ollama_versions(url_idx: Optional[int] = None):
277
+ if app.state.config.ENABLE_OLLAMA_API:
278
+ if url_idx == None:
279
+
280
+ # returns lowest version
281
+ tasks = [
282
+ fetch_url(f"{url}/api/version")
283
+ for url in app.state.config.OLLAMA_BASE_URLS
284
+ ]
285
+ responses = await asyncio.gather(*tasks)
286
+ responses = list(filter(lambda x: x is not None, responses))
287
+
288
+ if len(responses) > 0:
289
+ lowest_version = min(
290
+ responses,
291
+ key=lambda x: tuple(
292
+ map(int, re.sub(r"^v|-.*", "", x["version"]).split("."))
293
+ ),
294
+ )
295
+
296
+ return {"version": lowest_version["version"]}
297
+ else:
298
+ raise HTTPException(
299
+ status_code=500,
300
+ detail=ERROR_MESSAGES.OLLAMA_NOT_FOUND,
301
+ )
302
+ else:
303
+ url = app.state.config.OLLAMA_BASE_URLS[url_idx]
304
+
305
+ r = None
306
+ try:
307
+ r = requests.request(method="GET", url=f"{url}/api/version")
308
+ r.raise_for_status()
309
+
310
+ return r.json()
311
+ except Exception as e:
312
+ log.exception(e)
313
+ error_detail = "Open WebUI: Server Connection Error"
314
+ if r is not None:
315
+ try:
316
+ res = r.json()
317
+ if "error" in res:
318
+ error_detail = f"Ollama: {res['error']}"
319
+ except:
320
+ error_detail = f"Ollama: {e}"
321
+
322
+ raise HTTPException(
323
+ status_code=r.status_code if r else 500,
324
+ detail=error_detail,
325
+ )
326
+ else:
327
+ return {"version": False}
328
+
329
+
330
+ class ModelNameForm(BaseModel):
331
+ name: str
332
+
333
+
334
+ @app.post("/api/pull")
335
+ @app.post("/api/pull/{url_idx}")
336
+ async def pull_model(
337
+ form_data: ModelNameForm, url_idx: int = 0, user=Depends(get_admin_user)
338
+ ):
339
+ url = app.state.config.OLLAMA_BASE_URLS[url_idx]
340
+ log.info(f"url: {url}")
341
+
342
+ r = None
343
+
344
+ # Admin should be able to pull models from any source
345
+ payload = {**form_data.model_dump(exclude_none=True), "insecure": True}
346
+
347
+ return await post_streaming_url(f"{url}/api/pull", json.dumps(payload))
348
+
349
+
350
+ class PushModelForm(BaseModel):
351
+ name: str
352
+ insecure: Optional[bool] = None
353
+ stream: Optional[bool] = None
354
+
355
+
356
+ @app.delete("/api/push")
357
+ @app.delete("/api/push/{url_idx}")
358
+ async def push_model(
359
+ form_data: PushModelForm,
360
+ url_idx: Optional[int] = None,
361
+ user=Depends(get_admin_user),
362
+ ):
363
+ if url_idx == None:
364
+ if form_data.name in app.state.MODELS:
365
+ url_idx = app.state.MODELS[form_data.name]["urls"][0]
366
+ else:
367
+ raise HTTPException(
368
+ status_code=400,
369
+ detail=ERROR_MESSAGES.MODEL_NOT_FOUND(form_data.name),
370
+ )
371
+
372
+ url = app.state.config.OLLAMA_BASE_URLS[url_idx]
373
+ log.debug(f"url: {url}")
374
+
375
+ return await post_streaming_url(
376
+ f"{url}/api/push", form_data.model_dump_json(exclude_none=True).encode()
377
+ )
378
+
379
+
380
+ class CreateModelForm(BaseModel):
381
+ name: str
382
+ modelfile: Optional[str] = None
383
+ stream: Optional[bool] = None
384
+ path: Optional[str] = None
385
+
386
+
387
+ @app.post("/api/create")
388
+ @app.post("/api/create/{url_idx}")
389
+ async def create_model(
390
+ form_data: CreateModelForm, url_idx: int = 0, user=Depends(get_admin_user)
391
+ ):
392
+ log.debug(f"form_data: {form_data}")
393
+ url = app.state.config.OLLAMA_BASE_URLS[url_idx]
394
+ log.info(f"url: {url}")
395
+
396
+ return await post_streaming_url(
397
+ f"{url}/api/create", form_data.model_dump_json(exclude_none=True).encode()
398
+ )
399
+
400
+
401
+ class CopyModelForm(BaseModel):
402
+ source: str
403
+ destination: str
404
+
405
+
406
+ @app.post("/api/copy")
407
+ @app.post("/api/copy/{url_idx}")
408
+ async def copy_model(
409
+ form_data: CopyModelForm,
410
+ url_idx: Optional[int] = None,
411
+ user=Depends(get_admin_user),
412
+ ):
413
+ if url_idx == None:
414
+ if form_data.source in app.state.MODELS:
415
+ url_idx = app.state.MODELS[form_data.source]["urls"][0]
416
+ else:
417
+ raise HTTPException(
418
+ status_code=400,
419
+ detail=ERROR_MESSAGES.MODEL_NOT_FOUND(form_data.source),
420
+ )
421
+
422
+ url = app.state.config.OLLAMA_BASE_URLS[url_idx]
423
+ log.info(f"url: {url}")
424
+
425
+ try:
426
+ r = requests.request(
427
+ method="POST",
428
+ url=f"{url}/api/copy",
429
+ data=form_data.model_dump_json(exclude_none=True).encode(),
430
+ )
431
+ r.raise_for_status()
432
+
433
+ log.debug(f"r.text: {r.text}")
434
+
435
+ return True
436
+ except Exception as e:
437
+ log.exception(e)
438
+ error_detail = "Open WebUI: Server Connection Error"
439
+ if r is not None:
440
+ try:
441
+ res = r.json()
442
+ if "error" in res:
443
+ error_detail = f"Ollama: {res['error']}"
444
+ except:
445
+ error_detail = f"Ollama: {e}"
446
+
447
+ raise HTTPException(
448
+ status_code=r.status_code if r else 500,
449
+ detail=error_detail,
450
+ )
451
+
452
+
453
+ @app.delete("/api/delete")
454
+ @app.delete("/api/delete/{url_idx}")
455
+ async def delete_model(
456
+ form_data: ModelNameForm,
457
+ url_idx: Optional[int] = None,
458
+ user=Depends(get_admin_user),
459
+ ):
460
+ if url_idx == None:
461
+ if form_data.name in app.state.MODELS:
462
+ url_idx = app.state.MODELS[form_data.name]["urls"][0]
463
+ else:
464
+ raise HTTPException(
465
+ status_code=400,
466
+ detail=ERROR_MESSAGES.MODEL_NOT_FOUND(form_data.name),
467
+ )
468
+
469
+ url = app.state.config.OLLAMA_BASE_URLS[url_idx]
470
+ log.info(f"url: {url}")
471
+
472
+ try:
473
+ r = requests.request(
474
+ method="DELETE",
475
+ url=f"{url}/api/delete",
476
+ data=form_data.model_dump_json(exclude_none=True).encode(),
477
+ )
478
+ r.raise_for_status()
479
+
480
+ log.debug(f"r.text: {r.text}")
481
+
482
+ return True
483
+ except Exception as e:
484
+ log.exception(e)
485
+ error_detail = "Open WebUI: Server Connection Error"
486
+ if r is not None:
487
+ try:
488
+ res = r.json()
489
+ if "error" in res:
490
+ error_detail = f"Ollama: {res['error']}"
491
+ except:
492
+ error_detail = f"Ollama: {e}"
493
+
494
+ raise HTTPException(
495
+ status_code=r.status_code if r else 500,
496
+ detail=error_detail,
497
+ )
498
+
499
+
500
+ @app.post("/api/show")
501
+ async def show_model_info(form_data: ModelNameForm, user=Depends(get_verified_user)):
502
+ if form_data.name not in app.state.MODELS:
503
+ raise HTTPException(
504
+ status_code=400,
505
+ detail=ERROR_MESSAGES.MODEL_NOT_FOUND(form_data.name),
506
+ )
507
+
508
+ url_idx = random.choice(app.state.MODELS[form_data.name]["urls"])
509
+ url = app.state.config.OLLAMA_BASE_URLS[url_idx]
510
+ log.info(f"url: {url}")
511
+
512
+ try:
513
+ r = requests.request(
514
+ method="POST",
515
+ url=f"{url}/api/show",
516
+ data=form_data.model_dump_json(exclude_none=True).encode(),
517
+ )
518
+ r.raise_for_status()
519
+
520
+ return r.json()
521
+ except Exception as e:
522
+ log.exception(e)
523
+ error_detail = "Open WebUI: Server Connection Error"
524
+ if r is not None:
525
+ try:
526
+ res = r.json()
527
+ if "error" in res:
528
+ error_detail = f"Ollama: {res['error']}"
529
+ except:
530
+ error_detail = f"Ollama: {e}"
531
+
532
+ raise HTTPException(
533
+ status_code=r.status_code if r else 500,
534
+ detail=error_detail,
535
+ )
536
+
537
+
538
+ class GenerateEmbeddingsForm(BaseModel):
539
+ model: str
540
+ prompt: str
541
+ options: Optional[dict] = None
542
+ keep_alive: Optional[Union[int, str]] = None
543
+
544
+
545
+ @app.post("/api/embeddings")
546
+ @app.post("/api/embeddings/{url_idx}")
547
+ async def generate_embeddings(
548
+ form_data: GenerateEmbeddingsForm,
549
+ url_idx: Optional[int] = None,
550
+ user=Depends(get_verified_user),
551
+ ):
552
+ if url_idx == None:
553
+ model = form_data.model
554
+
555
+ if ":" not in model:
556
+ model = f"{model}:latest"
557
+
558
+ if model in app.state.MODELS:
559
+ url_idx = random.choice(app.state.MODELS[model]["urls"])
560
+ else:
561
+ raise HTTPException(
562
+ status_code=400,
563
+ detail=ERROR_MESSAGES.MODEL_NOT_FOUND(form_data.model),
564
+ )
565
+
566
+ url = app.state.config.OLLAMA_BASE_URLS[url_idx]
567
+ log.info(f"url: {url}")
568
+
569
+ try:
570
+ r = requests.request(
571
+ method="POST",
572
+ url=f"{url}/api/embeddings",
573
+ data=form_data.model_dump_json(exclude_none=True).encode(),
574
+ )
575
+ r.raise_for_status()
576
+
577
+ return r.json()
578
+ except Exception as e:
579
+ log.exception(e)
580
+ error_detail = "Open WebUI: Server Connection Error"
581
+ if r is not None:
582
+ try:
583
+ res = r.json()
584
+ if "error" in res:
585
+ error_detail = f"Ollama: {res['error']}"
586
+ except:
587
+ error_detail = f"Ollama: {e}"
588
+
589
+ raise HTTPException(
590
+ status_code=r.status_code if r else 500,
591
+ detail=error_detail,
592
+ )
593
+
594
+
595
+ def generate_ollama_embeddings(
596
+ form_data: GenerateEmbeddingsForm,
597
+ url_idx: Optional[int] = None,
598
+ ):
599
+
600
+ log.info(f"generate_ollama_embeddings {form_data}")
601
+
602
+ if url_idx == None:
603
+ model = form_data.model
604
+
605
+ if ":" not in model:
606
+ model = f"{model}:latest"
607
+
608
+ if model in app.state.MODELS:
609
+ url_idx = random.choice(app.state.MODELS[model]["urls"])
610
+ else:
611
+ raise HTTPException(
612
+ status_code=400,
613
+ detail=ERROR_MESSAGES.MODEL_NOT_FOUND(form_data.model),
614
+ )
615
+
616
+ url = app.state.config.OLLAMA_BASE_URLS[url_idx]
617
+ log.info(f"url: {url}")
618
+
619
+ try:
620
+ r = requests.request(
621
+ method="POST",
622
+ url=f"{url}/api/embeddings",
623
+ data=form_data.model_dump_json(exclude_none=True).encode(),
624
+ )
625
+ r.raise_for_status()
626
+
627
+ data = r.json()
628
+
629
+ log.info(f"generate_ollama_embeddings {data}")
630
+
631
+ if "embedding" in data:
632
+ return data["embedding"]
633
+ else:
634
+ raise "Something went wrong :/"
635
+ except Exception as e:
636
+ log.exception(e)
637
+ error_detail = "Open WebUI: Server Connection Error"
638
+ if r is not None:
639
+ try:
640
+ res = r.json()
641
+ if "error" in res:
642
+ error_detail = f"Ollama: {res['error']}"
643
+ except:
644
+ error_detail = f"Ollama: {e}"
645
+
646
+ raise error_detail
647
+
648
+
649
+ class GenerateCompletionForm(BaseModel):
650
+ model: str
651
+ prompt: str
652
+ images: Optional[List[str]] = None
653
+ format: Optional[str] = None
654
+ options: Optional[dict] = None
655
+ system: Optional[str] = None
656
+ template: Optional[str] = None
657
+ context: Optional[str] = None
658
+ stream: Optional[bool] = True
659
+ raw: Optional[bool] = None
660
+ keep_alive: Optional[Union[int, str]] = None
661
+
662
+
663
+ @app.post("/api/generate")
664
+ @app.post("/api/generate/{url_idx}")
665
+ async def generate_completion(
666
+ form_data: GenerateCompletionForm,
667
+ url_idx: Optional[int] = None,
668
+ user=Depends(get_verified_user),
669
+ ):
670
+
671
+ if url_idx == None:
672
+ model = form_data.model
673
+
674
+ if ":" not in model:
675
+ model = f"{model}:latest"
676
+
677
+ if model in app.state.MODELS:
678
+ url_idx = random.choice(app.state.MODELS[model]["urls"])
679
+ else:
680
+ raise HTTPException(
681
+ status_code=400,
682
+ detail=ERROR_MESSAGES.MODEL_NOT_FOUND(form_data.model),
683
+ )
684
+
685
+ url = app.state.config.OLLAMA_BASE_URLS[url_idx]
686
+ log.info(f"url: {url}")
687
+
688
+ return await post_streaming_url(
689
+ f"{url}/api/generate", form_data.model_dump_json(exclude_none=True).encode()
690
+ )
691
+
692
+
693
+ class ChatMessage(BaseModel):
694
+ role: str
695
+ content: str
696
+ images: Optional[List[str]] = None
697
+
698
+
699
+ class GenerateChatCompletionForm(BaseModel):
700
+ model: str
701
+ messages: List[ChatMessage]
702
+ format: Optional[str] = None
703
+ options: Optional[dict] = None
704
+ template: Optional[str] = None
705
+ stream: Optional[bool] = None
706
+ keep_alive: Optional[Union[int, str]] = None
707
+
708
+
709
+ @app.post("/api/chat")
710
+ @app.post("/api/chat/{url_idx}")
711
+ async def generate_chat_completion(
712
+ form_data: GenerateChatCompletionForm,
713
+ url_idx: Optional[int] = None,
714
+ user=Depends(get_verified_user),
715
+ ):
716
+
717
+ log.debug(
718
+ "form_data.model_dump_json(exclude_none=True).encode(): {0} ".format(
719
+ form_data.model_dump_json(exclude_none=True).encode()
720
+ )
721
+ )
722
+
723
+ payload = {
724
+ **form_data.model_dump(exclude_none=True),
725
+ }
726
+
727
+ model_id = form_data.model
728
+ model_info = Models.get_model_by_id(model_id)
729
+
730
+ if model_info:
731
+ print(model_info)
732
+ if model_info.base_model_id:
733
+ payload["model"] = model_info.base_model_id
734
+
735
+ model_info.params = model_info.params.model_dump()
736
+
737
+ if model_info.params:
738
+ payload["options"] = {}
739
+
740
+ if model_info.params.get("mirostat", None):
741
+ payload["options"]["mirostat"] = model_info.params.get("mirostat", None)
742
+
743
+ if model_info.params.get("mirostat_eta", None):
744
+ payload["options"]["mirostat_eta"] = model_info.params.get(
745
+ "mirostat_eta", None
746
+ )
747
+
748
+ if model_info.params.get("mirostat_tau", None):
749
+
750
+ payload["options"]["mirostat_tau"] = model_info.params.get(
751
+ "mirostat_tau", None
752
+ )
753
+
754
+ if model_info.params.get("num_ctx", None):
755
+ payload["options"]["num_ctx"] = model_info.params.get("num_ctx", None)
756
+
757
+ if model_info.params.get("repeat_last_n", None):
758
+ payload["options"]["repeat_last_n"] = model_info.params.get(
759
+ "repeat_last_n", None
760
+ )
761
+
762
+ if model_info.params.get("frequency_penalty", None):
763
+ payload["options"]["repeat_penalty"] = model_info.params.get(
764
+ "frequency_penalty", None
765
+ )
766
+
767
+ if model_info.params.get("temperature", None):
768
+ payload["options"]["temperature"] = model_info.params.get(
769
+ "temperature", None
770
+ )
771
+
772
+ if model_info.params.get("seed", None):
773
+ payload["options"]["seed"] = model_info.params.get("seed", None)
774
+
775
+ if model_info.params.get("stop", None):
776
+ payload["options"]["stop"] = (
777
+ [
778
+ bytes(stop, "utf-8").decode("unicode_escape")
779
+ for stop in model_info.params["stop"]
780
+ ]
781
+ if model_info.params.get("stop", None)
782
+ else None
783
+ )
784
+
785
+ if model_info.params.get("tfs_z", None):
786
+ payload["options"]["tfs_z"] = model_info.params.get("tfs_z", None)
787
+
788
+ if model_info.params.get("max_tokens", None):
789
+ payload["options"]["num_predict"] = model_info.params.get(
790
+ "max_tokens", None
791
+ )
792
+
793
+ if model_info.params.get("top_k", None):
794
+ payload["options"]["top_k"] = model_info.params.get("top_k", None)
795
+
796
+ if model_info.params.get("top_p", None):
797
+ payload["options"]["top_p"] = model_info.params.get("top_p", None)
798
+
799
+ if model_info.params.get("use_mmap", None):
800
+ payload["options"]["use_mmap"] = model_info.params.get("use_mmap", None)
801
+
802
+ if model_info.params.get("use_mlock", None):
803
+ payload["options"]["use_mlock"] = model_info.params.get(
804
+ "use_mlock", None
805
+ )
806
+
807
+ if model_info.params.get("num_thread", None):
808
+ payload["options"]["num_thread"] = model_info.params.get(
809
+ "num_thread", None
810
+ )
811
+
812
+ if model_info.params.get("system", None):
813
+ # Check if the payload already has a system message
814
+ # If not, add a system message to the payload
815
+ if payload.get("messages"):
816
+ for message in payload["messages"]:
817
+ if message.get("role") == "system":
818
+ message["content"] = (
819
+ model_info.params.get("system", None) + message["content"]
820
+ )
821
+ break
822
+ else:
823
+ payload["messages"].insert(
824
+ 0,
825
+ {
826
+ "role": "system",
827
+ "content": model_info.params.get("system", None),
828
+ },
829
+ )
830
+
831
+ if url_idx == None:
832
+ if ":" not in payload["model"]:
833
+ payload["model"] = f"{payload['model']}:latest"
834
+
835
+ if payload["model"] in app.state.MODELS:
836
+ url_idx = random.choice(app.state.MODELS[payload["model"]]["urls"])
837
+ else:
838
+ raise HTTPException(
839
+ status_code=400,
840
+ detail=ERROR_MESSAGES.MODEL_NOT_FOUND(form_data.model),
841
+ )
842
+
843
+ url = app.state.config.OLLAMA_BASE_URLS[url_idx]
844
+ log.info(f"url: {url}")
845
+
846
+ print(payload)
847
+
848
+ return await post_streaming_url(f"{url}/api/chat", json.dumps(payload))
849
+
850
+
851
+ # TODO: we should update this part once Ollama supports other types
852
+ class OpenAIChatMessage(BaseModel):
853
+ role: str
854
+ content: str
855
+
856
+ model_config = ConfigDict(extra="allow")
857
+
858
+
859
+ class OpenAIChatCompletionForm(BaseModel):
860
+ model: str
861
+ messages: List[OpenAIChatMessage]
862
+
863
+ model_config = ConfigDict(extra="allow")
864
+
865
+
866
+ @app.post("/v1/chat/completions")
867
+ @app.post("/v1/chat/completions/{url_idx}")
868
+ async def generate_openai_chat_completion(
869
+ form_data: OpenAIChatCompletionForm,
870
+ url_idx: Optional[int] = None,
871
+ user=Depends(get_verified_user),
872
+ ):
873
+
874
+ payload = {
875
+ **form_data.model_dump(exclude_none=True),
876
+ }
877
+
878
+ model_id = form_data.model
879
+ model_info = Models.get_model_by_id(model_id)
880
+
881
+ if model_info:
882
+ print(model_info)
883
+ if model_info.base_model_id:
884
+ payload["model"] = model_info.base_model_id
885
+
886
+ model_info.params = model_info.params.model_dump()
887
+
888
+ if model_info.params:
889
+ payload["temperature"] = model_info.params.get("temperature", None)
890
+ payload["top_p"] = model_info.params.get("top_p", None)
891
+ payload["max_tokens"] = model_info.params.get("max_tokens", None)
892
+ payload["frequency_penalty"] = model_info.params.get(
893
+ "frequency_penalty", None
894
+ )
895
+ payload["seed"] = model_info.params.get("seed", None)
896
+ payload["stop"] = (
897
+ [
898
+ bytes(stop, "utf-8").decode("unicode_escape")
899
+ for stop in model_info.params["stop"]
900
+ ]
901
+ if model_info.params.get("stop", None)
902
+ else None
903
+ )
904
+
905
+ if model_info.params.get("system", None):
906
+ # Check if the payload already has a system message
907
+ # If not, add a system message to the payload
908
+ if payload.get("messages"):
909
+ for message in payload["messages"]:
910
+ if message.get("role") == "system":
911
+ message["content"] = (
912
+ model_info.params.get("system", None) + message["content"]
913
+ )
914
+ break
915
+ else:
916
+ payload["messages"].insert(
917
+ 0,
918
+ {
919
+ "role": "system",
920
+ "content": model_info.params.get("system", None),
921
+ },
922
+ )
923
+
924
+ if url_idx == None:
925
+ if ":" not in payload["model"]:
926
+ payload["model"] = f"{payload['model']}:latest"
927
+
928
+ if payload["model"] in app.state.MODELS:
929
+ url_idx = random.choice(app.state.MODELS[payload["model"]]["urls"])
930
+ else:
931
+ raise HTTPException(
932
+ status_code=400,
933
+ detail=ERROR_MESSAGES.MODEL_NOT_FOUND(form_data.model),
934
+ )
935
+
936
+ url = app.state.config.OLLAMA_BASE_URLS[url_idx]
937
+ log.info(f"url: {url}")
938
+
939
+ return await post_streaming_url(f"{url}/v1/chat/completions", json.dumps(payload))
940
+
941
+
942
+ @app.get("/v1/models")
943
+ @app.get("/v1/models/{url_idx}")
944
+ async def get_openai_models(
945
+ url_idx: Optional[int] = None,
946
+ user=Depends(get_verified_user),
947
+ ):
948
+ if url_idx == None:
949
+ models = await get_all_models()
950
+
951
+ if app.state.config.ENABLE_MODEL_FILTER:
952
+ if user.role == "user":
953
+ models["models"] = list(
954
+ filter(
955
+ lambda model: model["name"]
956
+ in app.state.config.MODEL_FILTER_LIST,
957
+ models["models"],
958
+ )
959
+ )
960
+
961
+ return {
962
+ "data": [
963
+ {
964
+ "id": model["model"],
965
+ "object": "model",
966
+ "created": int(time.time()),
967
+ "owned_by": "openai",
968
+ }
969
+ for model in models["models"]
970
+ ],
971
+ "object": "list",
972
+ }
973
+
974
+ else:
975
+ url = app.state.config.OLLAMA_BASE_URLS[url_idx]
976
+ try:
977
+ r = requests.request(method="GET", url=f"{url}/api/tags")
978
+ r.raise_for_status()
979
+
980
+ models = r.json()
981
+
982
+ return {
983
+ "data": [
984
+ {
985
+ "id": model["model"],
986
+ "object": "model",
987
+ "created": int(time.time()),
988
+ "owned_by": "openai",
989
+ }
990
+ for model in models["models"]
991
+ ],
992
+ "object": "list",
993
+ }
994
+
995
+ except Exception as e:
996
+ log.exception(e)
997
+ error_detail = "Open WebUI: Server Connection Error"
998
+ if r is not None:
999
+ try:
1000
+ res = r.json()
1001
+ if "error" in res:
1002
+ error_detail = f"Ollama: {res['error']}"
1003
+ except:
1004
+ error_detail = f"Ollama: {e}"
1005
+
1006
+ raise HTTPException(
1007
+ status_code=r.status_code if r else 500,
1008
+ detail=error_detail,
1009
+ )
1010
+
1011
+
1012
+ class UrlForm(BaseModel):
1013
+ url: str
1014
+
1015
+
1016
+ class UploadBlobForm(BaseModel):
1017
+ filename: str
1018
+
1019
+
1020
+ def parse_huggingface_url(hf_url):
1021
+ try:
1022
+ # Parse the URL
1023
+ parsed_url = urlparse(hf_url)
1024
+
1025
+ # Get the path and split it into components
1026
+ path_components = parsed_url.path.split("/")
1027
+
1028
+ # Extract the desired output
1029
+ user_repo = "/".join(path_components[1:3])
1030
+ model_file = path_components[-1]
1031
+
1032
+ return model_file
1033
+ except ValueError:
1034
+ return None
1035
+
1036
+
1037
+ async def download_file_stream(
1038
+ ollama_url, file_url, file_path, file_name, chunk_size=1024 * 1024
1039
+ ):
1040
+ done = False
1041
+
1042
+ if os.path.exists(file_path):
1043
+ current_size = os.path.getsize(file_path)
1044
+ else:
1045
+ current_size = 0
1046
+
1047
+ headers = {"Range": f"bytes={current_size}-"} if current_size > 0 else {}
1048
+
1049
+ timeout = aiohttp.ClientTimeout(total=600) # Set the timeout
1050
+
1051
+ async with aiohttp.ClientSession(timeout=timeout, trust_env=True) as session:
1052
+ async with session.get(file_url, headers=headers) as response:
1053
+ total_size = int(response.headers.get("content-length", 0)) + current_size
1054
+
1055
+ with open(file_path, "ab+") as file:
1056
+ async for data in response.content.iter_chunked(chunk_size):
1057
+ current_size += len(data)
1058
+ file.write(data)
1059
+
1060
+ done = current_size == total_size
1061
+ progress = round((current_size / total_size) * 100, 2)
1062
+
1063
+ yield f'data: {{"progress": {progress}, "completed": {current_size}, "total": {total_size}}}\n\n'
1064
+
1065
+ if done:
1066
+ file.seek(0)
1067
+ hashed = calculate_sha256(file)
1068
+ file.seek(0)
1069
+
1070
+ url = f"{ollama_url}/api/blobs/sha256:{hashed}"
1071
+ response = requests.post(url, data=file)
1072
+
1073
+ if response.ok:
1074
+ res = {
1075
+ "done": done,
1076
+ "blob": f"sha256:{hashed}",
1077
+ "name": file_name,
1078
+ }
1079
+ os.remove(file_path)
1080
+
1081
+ yield f"data: {json.dumps(res)}\n\n"
1082
+ else:
1083
+ raise "Ollama: Could not create blob, Please try again."
1084
+
1085
+
1086
+ # def number_generator():
1087
+ # for i in range(1, 101):
1088
+ # yield f"data: {i}\n"
1089
+
1090
+
1091
+ # url = "https://huggingface.co/TheBloke/stablelm-zephyr-3b-GGUF/resolve/main/stablelm-zephyr-3b.Q2_K.gguf"
1092
+ @app.post("/models/download")
1093
+ @app.post("/models/download/{url_idx}")
1094
+ async def download_model(
1095
+ form_data: UrlForm,
1096
+ url_idx: Optional[int] = None,
1097
+ ):
1098
+
1099
+ allowed_hosts = ["https://huggingface.co/", "https://github.com/"]
1100
+
1101
+ if not any(form_data.url.startswith(host) for host in allowed_hosts):
1102
+ raise HTTPException(
1103
+ status_code=400,
1104
+ detail="Invalid file_url. Only URLs from allowed hosts are permitted.",
1105
+ )
1106
+
1107
+ if url_idx == None:
1108
+ url_idx = 0
1109
+ url = app.state.config.OLLAMA_BASE_URLS[url_idx]
1110
+
1111
+ file_name = parse_huggingface_url(form_data.url)
1112
+
1113
+ if file_name:
1114
+ file_path = f"{UPLOAD_DIR}/{file_name}"
1115
+
1116
+ return StreamingResponse(
1117
+ download_file_stream(url, form_data.url, file_path, file_name),
1118
+ )
1119
+ else:
1120
+ return None
1121
+
1122
+
1123
+ @app.post("/models/upload")
1124
+ @app.post("/models/upload/{url_idx}")
1125
+ def upload_model(file: UploadFile = File(...), url_idx: Optional[int] = None):
1126
+ if url_idx == None:
1127
+ url_idx = 0
1128
+ ollama_url = app.state.config.OLLAMA_BASE_URLS[url_idx]
1129
+
1130
+ file_path = f"{UPLOAD_DIR}/{file.filename}"
1131
+
1132
+ # Save file in chunks
1133
+ with open(file_path, "wb+") as f:
1134
+ for chunk in file.file:
1135
+ f.write(chunk)
1136
+
1137
+ def file_process_stream():
1138
+ nonlocal ollama_url
1139
+ total_size = os.path.getsize(file_path)
1140
+ chunk_size = 1024 * 1024
1141
+ try:
1142
+ with open(file_path, "rb") as f:
1143
+ total = 0
1144
+ done = False
1145
+
1146
+ while not done:
1147
+ chunk = f.read(chunk_size)
1148
+ if not chunk:
1149
+ done = True
1150
+ continue
1151
+
1152
+ total += len(chunk)
1153
+ progress = round((total / total_size) * 100, 2)
1154
+
1155
+ res = {
1156
+ "progress": progress,
1157
+ "total": total_size,
1158
+ "completed": total,
1159
+ }
1160
+ yield f"data: {json.dumps(res)}\n\n"
1161
+
1162
+ if done:
1163
+ f.seek(0)
1164
+ hashed = calculate_sha256(f)
1165
+ f.seek(0)
1166
+
1167
+ url = f"{ollama_url}/api/blobs/sha256:{hashed}"
1168
+ response = requests.post(url, data=f)
1169
+
1170
+ if response.ok:
1171
+ res = {
1172
+ "done": done,
1173
+ "blob": f"sha256:{hashed}",
1174
+ "name": file.filename,
1175
+ }
1176
+ os.remove(file_path)
1177
+ yield f"data: {json.dumps(res)}\n\n"
1178
+ else:
1179
+ raise Exception(
1180
+ "Ollama: Could not create blob, Please try again."
1181
+ )
1182
+
1183
+ except Exception as e:
1184
+ res = {"error": str(e)}
1185
+ yield f"data: {json.dumps(res)}\n\n"
1186
+
1187
+ return StreamingResponse(file_process_stream(), media_type="text/event-stream")
1188
+
1189
+
1190
+ # async def upload_model(file: UploadFile = File(), url_idx: Optional[int] = None):
1191
+ # if url_idx == None:
1192
+ # url_idx = 0
1193
+ # url = app.state.config.OLLAMA_BASE_URLS[url_idx]
1194
+
1195
+ # file_location = os.path.join(UPLOAD_DIR, file.filename)
1196
+ # total_size = file.size
1197
+
1198
+ # async def file_upload_generator(file):
1199
+ # print(file)
1200
+ # try:
1201
+ # async with aiofiles.open(file_location, "wb") as f:
1202
+ # completed_size = 0
1203
+ # while True:
1204
+ # chunk = await file.read(1024*1024)
1205
+ # if not chunk:
1206
+ # break
1207
+ # await f.write(chunk)
1208
+ # completed_size += len(chunk)
1209
+ # progress = (completed_size / total_size) * 100
1210
+
1211
+ # print(progress)
1212
+ # yield f'data: {json.dumps({"status": "uploading", "percentage": progress, "total": total_size, "completed": completed_size, "done": False})}\n'
1213
+ # except Exception as e:
1214
+ # print(e)
1215
+ # yield f"data: {json.dumps({'status': 'error', 'message': str(e)})}\n"
1216
+ # finally:
1217
+ # await file.close()
1218
+ # print("done")
1219
+ # yield f'data: {json.dumps({"status": "completed", "percentage": 100, "total": total_size, "completed": completed_size, "done": True})}\n'
1220
+
1221
+ # return StreamingResponse(
1222
+ # file_upload_generator(copy.deepcopy(file)), media_type="text/event-stream"
1223
+ # )
1224
+
1225
+
1226
+ @app.api_route("/{path:path}", methods=["GET", "POST", "PUT", "DELETE"])
1227
+ async def deprecated_proxy(
1228
+ path: str, request: Request, user=Depends(get_verified_user)
1229
+ ):
1230
+ url = app.state.config.OLLAMA_BASE_URLS[0]
1231
+ target_url = f"{url}/{path}"
1232
+
1233
+ body = await request.body()
1234
+ headers = dict(request.headers)
1235
+
1236
+ if user.role in ["user", "admin"]:
1237
+ if path in ["pull", "delete", "push", "copy", "create"]:
1238
+ if user.role != "admin":
1239
+ raise HTTPException(
1240
+ status_code=status.HTTP_401_UNAUTHORIZED,
1241
+ detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
1242
+ )
1243
+ else:
1244
+ raise HTTPException(
1245
+ status_code=status.HTTP_401_UNAUTHORIZED,
1246
+ detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
1247
+ )
1248
+
1249
+ headers.pop("host", None)
1250
+ headers.pop("authorization", None)
1251
+ headers.pop("origin", None)
1252
+ headers.pop("referer", None)
1253
+
1254
+ r = None
1255
+
1256
+ def get_request():
1257
+ nonlocal r
1258
+
1259
+ request_id = str(uuid.uuid4())
1260
+ try:
1261
+ REQUEST_POOL.append(request_id)
1262
+
1263
+ def stream_content():
1264
+ try:
1265
+ if path == "generate":
1266
+ data = json.loads(body.decode("utf-8"))
1267
+
1268
+ if data.get("stream", True):
1269
+ yield json.dumps({"id": request_id, "done": False}) + "\n"
1270
+
1271
+ elif path == "chat":
1272
+ yield json.dumps({"id": request_id, "done": False}) + "\n"
1273
+
1274
+ for chunk in r.iter_content(chunk_size=8192):
1275
+ if request_id in REQUEST_POOL:
1276
+ yield chunk
1277
+ else:
1278
+ log.warning("User: canceled request")
1279
+ break
1280
+ finally:
1281
+ if hasattr(r, "close"):
1282
+ r.close()
1283
+ if request_id in REQUEST_POOL:
1284
+ REQUEST_POOL.remove(request_id)
1285
+
1286
+ r = requests.request(
1287
+ method=request.method,
1288
+ url=target_url,
1289
+ data=body,
1290
+ headers=headers,
1291
+ stream=True,
1292
+ )
1293
+
1294
+ r.raise_for_status()
1295
+
1296
+ # r.close()
1297
+
1298
+ return StreamingResponse(
1299
+ stream_content(),
1300
+ status_code=r.status_code,
1301
+ headers=dict(r.headers),
1302
+ )
1303
+ except Exception as e:
1304
+ raise e
1305
+
1306
+ try:
1307
+ return await run_in_threadpool(get_request)
1308
+ except Exception as e:
1309
+ error_detail = "Open WebUI: Server Connection Error"
1310
+ if r is not None:
1311
+ try:
1312
+ res = r.json()
1313
+ if "error" in res:
1314
+ error_detail = f"Ollama: {res['error']}"
1315
+ except:
1316
+ error_detail = f"Ollama: {e}"
1317
+
1318
+ raise HTTPException(
1319
+ status_code=r.status_code if r else 500,
1320
+ detail=error_detail,
1321
+ )
backend/apps/openai/main.py ADDED
@@ -0,0 +1,505 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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_current_user,
20
+ get_verified_user,
21
+ get_admin_user,
22
+ )
23
+ from config import (
24
+ SRC_LOG_LEVELS,
25
+ ENABLE_OPENAI_API,
26
+ OPENAI_API_BASE_URLS,
27
+ OPENAI_API_KEYS,
28
+ CACHE_DIR,
29
+ ENABLE_MODEL_FILTER,
30
+ MODEL_FILTER_LIST,
31
+ AppConfig,
32
+ )
33
+ from typing import List, Optional
34
+
35
+
36
+ import hashlib
37
+ from pathlib import Path
38
+
39
+ log = logging.getLogger(__name__)
40
+ log.setLevel(SRC_LOG_LEVELS["OPENAI"])
41
+
42
+ app = FastAPI()
43
+ app.add_middleware(
44
+ CORSMiddleware,
45
+ allow_origins=["*"],
46
+ allow_credentials=True,
47
+ allow_methods=["*"],
48
+ allow_headers=["*"],
49
+ )
50
+
51
+
52
+ app.state.config = AppConfig()
53
+
54
+ app.state.config.ENABLE_MODEL_FILTER = ENABLE_MODEL_FILTER
55
+ app.state.config.MODEL_FILTER_LIST = MODEL_FILTER_LIST
56
+
57
+ app.state.config.ENABLE_OPENAI_API = ENABLE_OPENAI_API
58
+ app.state.config.OPENAI_API_BASE_URLS = OPENAI_API_BASE_URLS
59
+ app.state.config.OPENAI_API_KEYS = OPENAI_API_KEYS
60
+
61
+ app.state.MODELS = {}
62
+
63
+
64
+ @app.middleware("http")
65
+ async def check_url(request: Request, call_next):
66
+ if len(app.state.MODELS) == 0:
67
+ await get_all_models()
68
+ else:
69
+ pass
70
+
71
+ response = await call_next(request)
72
+ return response
73
+
74
+
75
+ @app.get("/config")
76
+ async def get_config(user=Depends(get_admin_user)):
77
+ return {"ENABLE_OPENAI_API": app.state.config.ENABLE_OPENAI_API}
78
+
79
+
80
+ class OpenAIConfigForm(BaseModel):
81
+ enable_openai_api: Optional[bool] = None
82
+
83
+
84
+ @app.post("/config/update")
85
+ async def update_config(form_data: OpenAIConfigForm, user=Depends(get_admin_user)):
86
+ app.state.config.ENABLE_OPENAI_API = form_data.enable_openai_api
87
+ return {"ENABLE_OPENAI_API": app.state.config.ENABLE_OPENAI_API}
88
+
89
+
90
+ class UrlsUpdateForm(BaseModel):
91
+ urls: List[str]
92
+
93
+
94
+ class KeysUpdateForm(BaseModel):
95
+ keys: List[str]
96
+
97
+
98
+ @app.get("/urls")
99
+ async def get_openai_urls(user=Depends(get_admin_user)):
100
+ return {"OPENAI_API_BASE_URLS": app.state.config.OPENAI_API_BASE_URLS}
101
+
102
+
103
+ @app.post("/urls/update")
104
+ async def update_openai_urls(form_data: UrlsUpdateForm, user=Depends(get_admin_user)):
105
+ await get_all_models()
106
+ app.state.config.OPENAI_API_BASE_URLS = form_data.urls
107
+ return {"OPENAI_API_BASE_URLS": app.state.config.OPENAI_API_BASE_URLS}
108
+
109
+
110
+ @app.get("/keys")
111
+ async def get_openai_keys(user=Depends(get_admin_user)):
112
+ return {"OPENAI_API_KEYS": app.state.config.OPENAI_API_KEYS}
113
+
114
+
115
+ @app.post("/keys/update")
116
+ async def update_openai_key(form_data: KeysUpdateForm, user=Depends(get_admin_user)):
117
+ app.state.config.OPENAI_API_KEYS = form_data.keys
118
+ return {"OPENAI_API_KEYS": app.state.config.OPENAI_API_KEYS}
119
+
120
+
121
+ @app.post("/audio/speech")
122
+ async def speech(request: Request, user=Depends(get_verified_user)):
123
+ idx = None
124
+ try:
125
+ idx = app.state.config.OPENAI_API_BASE_URLS.index("https://api.openai.com/v1")
126
+ body = await request.body()
127
+ name = hashlib.sha256(body).hexdigest()
128
+
129
+ SPEECH_CACHE_DIR = Path(CACHE_DIR).joinpath("./audio/speech/")
130
+ SPEECH_CACHE_DIR.mkdir(parents=True, exist_ok=True)
131
+ file_path = SPEECH_CACHE_DIR.joinpath(f"{name}.mp3")
132
+ file_body_path = SPEECH_CACHE_DIR.joinpath(f"{name}.json")
133
+
134
+ # Check if the file already exists in the cache
135
+ if file_path.is_file():
136
+ return FileResponse(file_path)
137
+
138
+ headers = {}
139
+ headers["Authorization"] = f"Bearer {app.state.config.OPENAI_API_KEYS[idx]}"
140
+ headers["Content-Type"] = "application/json"
141
+ if "openrouter.ai" in app.state.config.OPENAI_API_BASE_URLS[idx]:
142
+ headers["HTTP-Referer"] = "https://openwebui.com/"
143
+ headers["X-Title"] = "Open WebUI"
144
+ r = None
145
+ try:
146
+ r = requests.post(
147
+ url=f"{app.state.config.OPENAI_API_BASE_URLS[idx]}/audio/speech",
148
+ data=body,
149
+ headers=headers,
150
+ stream=True,
151
+ )
152
+
153
+ r.raise_for_status()
154
+
155
+ # Save the streaming content to a file
156
+ with open(file_path, "wb") as f:
157
+ for chunk in r.iter_content(chunk_size=8192):
158
+ f.write(chunk)
159
+
160
+ with open(file_body_path, "w") as f:
161
+ json.dump(json.loads(body.decode("utf-8")), f)
162
+
163
+ # Return the saved file
164
+ return FileResponse(file_path)
165
+
166
+ except Exception as e:
167
+ log.exception(e)
168
+ error_detail = "Open WebUI: Server Connection Error"
169
+ if r is not None:
170
+ try:
171
+ res = r.json()
172
+ if "error" in res:
173
+ error_detail = f"External: {res['error']}"
174
+ except:
175
+ error_detail = f"External: {e}"
176
+
177
+ raise HTTPException(
178
+ status_code=r.status_code if r else 500, detail=error_detail
179
+ )
180
+
181
+ except ValueError:
182
+ raise HTTPException(status_code=401, detail=ERROR_MESSAGES.OPENAI_NOT_FOUND)
183
+
184
+
185
+ async def fetch_url(url, key):
186
+ timeout = aiohttp.ClientTimeout(total=5)
187
+ try:
188
+ headers = {"Authorization": f"Bearer {key}"}
189
+ async with aiohttp.ClientSession(timeout=timeout, trust_env=True) as session:
190
+ async with session.get(url, headers=headers) as response:
191
+ return await response.json()
192
+ except Exception as e:
193
+ # Handle connection error here
194
+ log.error(f"Connection error: {e}")
195
+ return None
196
+
197
+
198
+ async def cleanup_response(
199
+ response: Optional[aiohttp.ClientResponse],
200
+ session: Optional[aiohttp.ClientSession],
201
+ ):
202
+ if response:
203
+ response.close()
204
+ if session:
205
+ await session.close()
206
+
207
+
208
+ def merge_models_lists(model_lists):
209
+ log.debug(f"merge_models_lists {model_lists}")
210
+ merged_list = []
211
+
212
+ for idx, models in enumerate(model_lists):
213
+ if models is not None and "error" not in models:
214
+ merged_list.extend(
215
+ [
216
+ {
217
+ **model,
218
+ "name": model.get("name", model["id"]),
219
+ "owned_by": "openai",
220
+ "openai": model,
221
+ "urlIdx": idx,
222
+ }
223
+ for model in models
224
+ if "api.openai.com"
225
+ not in app.state.config.OPENAI_API_BASE_URLS[idx]
226
+ or "gpt" in model["id"]
227
+ ]
228
+ )
229
+
230
+ return merged_list
231
+
232
+
233
+ async def get_all_models(raw: bool = False):
234
+ log.info("get_all_models()")
235
+
236
+ if (
237
+ len(app.state.config.OPENAI_API_KEYS) == 1
238
+ and app.state.config.OPENAI_API_KEYS[0] == ""
239
+ ) or not app.state.config.ENABLE_OPENAI_API:
240
+ models = {"data": []}
241
+ else:
242
+ # Check if API KEYS length is same than API URLS length
243
+ if len(app.state.config.OPENAI_API_KEYS) != len(
244
+ app.state.config.OPENAI_API_BASE_URLS
245
+ ):
246
+ # if there are more keys than urls, remove the extra keys
247
+ if len(app.state.config.OPENAI_API_KEYS) > len(
248
+ app.state.config.OPENAI_API_BASE_URLS
249
+ ):
250
+ app.state.config.OPENAI_API_KEYS = app.state.config.OPENAI_API_KEYS[
251
+ : len(app.state.config.OPENAI_API_BASE_URLS)
252
+ ]
253
+ # if there are more urls than keys, add empty keys
254
+ else:
255
+ app.state.config.OPENAI_API_KEYS += [
256
+ ""
257
+ for _ in range(
258
+ len(app.state.config.OPENAI_API_BASE_URLS)
259
+ - len(app.state.config.OPENAI_API_KEYS)
260
+ )
261
+ ]
262
+
263
+ tasks = [
264
+ fetch_url(f"{url}/models", app.state.config.OPENAI_API_KEYS[idx])
265
+ for idx, url in enumerate(app.state.config.OPENAI_API_BASE_URLS)
266
+ ]
267
+
268
+ responses = await asyncio.gather(*tasks)
269
+ log.debug(f"get_all_models:responses() {responses}")
270
+
271
+ if raw:
272
+ return responses
273
+
274
+ models = {
275
+ "data": merge_models_lists(
276
+ list(
277
+ map(
278
+ lambda response: (
279
+ response["data"]
280
+ if (response and "data" in response)
281
+ else (response if isinstance(response, list) else None)
282
+ ),
283
+ responses,
284
+ )
285
+ )
286
+ )
287
+ }
288
+
289
+ log.debug(f"models: {models}")
290
+ app.state.MODELS = {model["id"]: model for model in models["data"]}
291
+
292
+ return models
293
+
294
+
295
+ @app.get("/models")
296
+ @app.get("/models/{url_idx}")
297
+ async def get_models(url_idx: Optional[int] = None, user=Depends(get_current_user)):
298
+ if url_idx == None:
299
+ models = await get_all_models()
300
+ if app.state.config.ENABLE_MODEL_FILTER:
301
+ if user.role == "user":
302
+ models["data"] = list(
303
+ filter(
304
+ lambda model: model["id"] in app.state.config.MODEL_FILTER_LIST,
305
+ models["data"],
306
+ )
307
+ )
308
+ return models
309
+ return models
310
+ else:
311
+ url = app.state.config.OPENAI_API_BASE_URLS[url_idx]
312
+ key = app.state.config.OPENAI_API_KEYS[url_idx]
313
+
314
+ headers = {}
315
+ headers["Authorization"] = f"Bearer {key}"
316
+ headers["Content-Type"] = "application/json"
317
+
318
+ r = None
319
+
320
+ try:
321
+ r = requests.request(method="GET", url=f"{url}/models", headers=headers)
322
+ r.raise_for_status()
323
+
324
+ response_data = r.json()
325
+ if "api.openai.com" in url:
326
+ response_data["data"] = list(
327
+ filter(lambda model: "gpt" in model["id"], response_data["data"])
328
+ )
329
+
330
+ return response_data
331
+ except Exception as e:
332
+ log.exception(e)
333
+ error_detail = "Open WebUI: Server Connection Error"
334
+ if r is not None:
335
+ try:
336
+ res = r.json()
337
+ if "error" in res:
338
+ error_detail = f"External: {res['error']}"
339
+ except:
340
+ error_detail = f"External: {e}"
341
+
342
+ raise HTTPException(
343
+ status_code=r.status_code if r else 500,
344
+ detail=error_detail,
345
+ )
346
+
347
+
348
+ @app.api_route("/{path:path}", methods=["GET", "POST", "PUT", "DELETE"])
349
+ async def proxy(path: str, request: Request, user=Depends(get_verified_user)):
350
+ idx = 0
351
+
352
+ body = await request.body()
353
+ # TODO: Remove below after gpt-4-vision fix from Open AI
354
+ # Try to decode the body of the request from bytes to a UTF-8 string (Require add max_token to fix gpt-4-vision)
355
+
356
+ payload = None
357
+
358
+ try:
359
+ if "chat/completions" in path:
360
+ body = body.decode("utf-8")
361
+ body = json.loads(body)
362
+
363
+ payload = {**body}
364
+
365
+ model_id = body.get("model")
366
+ model_info = Models.get_model_by_id(model_id)
367
+
368
+ if model_info:
369
+ print(model_info)
370
+ if model_info.base_model_id:
371
+ payload["model"] = model_info.base_model_id
372
+
373
+ model_info.params = model_info.params.model_dump()
374
+
375
+ if model_info.params:
376
+ if model_info.params.get("temperature", None):
377
+ payload["temperature"] = int(
378
+ model_info.params.get("temperature")
379
+ )
380
+
381
+ if model_info.params.get("top_p", None):
382
+ payload["top_p"] = int(model_info.params.get("top_p", None))
383
+
384
+ if model_info.params.get("max_tokens", None):
385
+ payload["max_tokens"] = int(
386
+ model_info.params.get("max_tokens", None)
387
+ )
388
+
389
+ if model_info.params.get("frequency_penalty", None):
390
+ payload["frequency_penalty"] = int(
391
+ model_info.params.get("frequency_penalty", None)
392
+ )
393
+
394
+ if model_info.params.get("seed", None):
395
+ payload["seed"] = model_info.params.get("seed", None)
396
+
397
+ if model_info.params.get("stop", None):
398
+ payload["stop"] = (
399
+ [
400
+ bytes(stop, "utf-8").decode("unicode_escape")
401
+ for stop in model_info.params["stop"]
402
+ ]
403
+ if model_info.params.get("stop", None)
404
+ else None
405
+ )
406
+
407
+ if model_info.params.get("system", None):
408
+ # Check if the payload already has a system message
409
+ # If not, add a system message to the payload
410
+ if payload.get("messages"):
411
+ for message in payload["messages"]:
412
+ if message.get("role") == "system":
413
+ message["content"] = (
414
+ model_info.params.get("system", None)
415
+ + message["content"]
416
+ )
417
+ break
418
+ else:
419
+ payload["messages"].insert(
420
+ 0,
421
+ {
422
+ "role": "system",
423
+ "content": model_info.params.get("system", None),
424
+ },
425
+ )
426
+ else:
427
+ pass
428
+
429
+ model = app.state.MODELS[payload.get("model")]
430
+
431
+ idx = model["urlIdx"]
432
+
433
+ if "pipeline" in model and model.get("pipeline"):
434
+ payload["user"] = {"name": user.name, "id": user.id}
435
+
436
+ # Check if the model is "gpt-4-vision-preview" and set "max_tokens" to 4000
437
+ # This is a workaround until OpenAI fixes the issue with this model
438
+ if payload.get("model") == "gpt-4-vision-preview":
439
+ if "max_tokens" not in payload:
440
+ payload["max_tokens"] = 4000
441
+ log.debug("Modified payload:", payload)
442
+
443
+ # Convert the modified body back to JSON
444
+ payload = json.dumps(payload)
445
+
446
+ except json.JSONDecodeError as e:
447
+ log.error("Error loading request body into a dictionary:", e)
448
+
449
+ print(payload)
450
+
451
+ url = app.state.config.OPENAI_API_BASE_URLS[idx]
452
+ key = app.state.config.OPENAI_API_KEYS[idx]
453
+
454
+ target_url = f"{url}/{path}"
455
+
456
+ headers = {}
457
+ headers["Authorization"] = f"Bearer {key}"
458
+ headers["Content-Type"] = "application/json"
459
+
460
+ r = None
461
+ session = None
462
+ streaming = False
463
+
464
+ try:
465
+ session = aiohttp.ClientSession(trust_env=True)
466
+ r = await session.request(
467
+ method=request.method,
468
+ url=target_url,
469
+ data=payload if payload else body,
470
+ headers=headers,
471
+ )
472
+
473
+ r.raise_for_status()
474
+
475
+ # Check if response is SSE
476
+ if "text/event-stream" in r.headers.get("Content-Type", ""):
477
+ streaming = True
478
+ return StreamingResponse(
479
+ r.content,
480
+ status_code=r.status,
481
+ headers=dict(r.headers),
482
+ background=BackgroundTask(
483
+ cleanup_response, response=r, session=session
484
+ ),
485
+ )
486
+ else:
487
+ response_data = await r.json()
488
+ return response_data
489
+ except Exception as e:
490
+ log.exception(e)
491
+ error_detail = "Open WebUI: Server Connection Error"
492
+ if r is not None:
493
+ try:
494
+ res = await r.json()
495
+ print(res)
496
+ if "error" in res:
497
+ error_detail = f"External: {res['error']['message'] if 'message' in res['error'] else res['error']}"
498
+ except:
499
+ error_detail = f"External: {e}"
500
+ raise HTTPException(status_code=r.status if r else 500, detail=error_detail)
501
+ finally:
502
+ if not streaming and session:
503
+ if r:
504
+ r.close()
505
+ await session.close()
backend/apps/rag/main.py ADDED
@@ -0,0 +1,1220 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import (
2
+ FastAPI,
3
+ Depends,
4
+ HTTPException,
5
+ status,
6
+ UploadFile,
7
+ File,
8
+ Form,
9
+ )
10
+ from fastapi.middleware.cors import CORSMiddleware
11
+ import os, shutil, logging, re
12
+
13
+ from pathlib import Path
14
+ from typing import List, Union, Sequence
15
+
16
+ from chromadb.utils.batch_utils import create_batches
17
+
18
+ from langchain_community.document_loaders import (
19
+ WebBaseLoader,
20
+ TextLoader,
21
+ PyPDFLoader,
22
+ CSVLoader,
23
+ BSHTMLLoader,
24
+ Docx2txtLoader,
25
+ UnstructuredEPubLoader,
26
+ UnstructuredWordDocumentLoader,
27
+ UnstructuredMarkdownLoader,
28
+ UnstructuredXMLLoader,
29
+ UnstructuredRSTLoader,
30
+ UnstructuredExcelLoader,
31
+ UnstructuredPowerPointLoader,
32
+ YoutubeLoader,
33
+ )
34
+ from langchain.text_splitter import RecursiveCharacterTextSplitter
35
+
36
+ import validators
37
+ import urllib.parse
38
+ import socket
39
+
40
+
41
+ from pydantic import BaseModel
42
+ from typing import Optional
43
+ import mimetypes
44
+ import uuid
45
+ import json
46
+
47
+ import sentence_transformers
48
+
49
+ from apps.webui.models.documents import (
50
+ Documents,
51
+ DocumentForm,
52
+ DocumentResponse,
53
+ )
54
+
55
+ from apps.rag.utils import (
56
+ get_model_path,
57
+ get_embedding_function,
58
+ query_doc,
59
+ query_doc_with_hybrid_search,
60
+ query_collection,
61
+ query_collection_with_hybrid_search,
62
+ )
63
+
64
+ from apps.rag.search.brave import search_brave
65
+ from apps.rag.search.google_pse import search_google_pse
66
+ from apps.rag.search.main import SearchResult
67
+ from apps.rag.search.searxng import search_searxng
68
+ from apps.rag.search.serper import search_serper
69
+ from apps.rag.search.serpstack import search_serpstack
70
+
71
+
72
+ from utils.misc import (
73
+ calculate_sha256,
74
+ calculate_sha256_string,
75
+ sanitize_filename,
76
+ extract_folders_after_data_docs,
77
+ )
78
+ from utils.utils import get_current_user, get_admin_user
79
+
80
+ from config import (
81
+ AppConfig,
82
+ ENV,
83
+ SRC_LOG_LEVELS,
84
+ UPLOAD_DIR,
85
+ DOCS_DIR,
86
+ RAG_TOP_K,
87
+ RAG_RELEVANCE_THRESHOLD,
88
+ RAG_EMBEDDING_ENGINE,
89
+ RAG_EMBEDDING_MODEL,
90
+ RAG_EMBEDDING_MODEL_AUTO_UPDATE,
91
+ RAG_EMBEDDING_MODEL_TRUST_REMOTE_CODE,
92
+ ENABLE_RAG_HYBRID_SEARCH,
93
+ ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION,
94
+ RAG_RERANKING_MODEL,
95
+ PDF_EXTRACT_IMAGES,
96
+ RAG_RERANKING_MODEL_AUTO_UPDATE,
97
+ RAG_RERANKING_MODEL_TRUST_REMOTE_CODE,
98
+ RAG_OPENAI_API_BASE_URL,
99
+ RAG_OPENAI_API_KEY,
100
+ DEVICE_TYPE,
101
+ CHROMA_CLIENT,
102
+ CHUNK_SIZE,
103
+ CHUNK_OVERLAP,
104
+ RAG_TEMPLATE,
105
+ ENABLE_RAG_LOCAL_WEB_FETCH,
106
+ YOUTUBE_LOADER_LANGUAGE,
107
+ ENABLE_RAG_WEB_SEARCH,
108
+ RAG_WEB_SEARCH_ENGINE,
109
+ SEARXNG_QUERY_URL,
110
+ GOOGLE_PSE_API_KEY,
111
+ GOOGLE_PSE_ENGINE_ID,
112
+ BRAVE_SEARCH_API_KEY,
113
+ SERPSTACK_API_KEY,
114
+ SERPSTACK_HTTPS,
115
+ SERPER_API_KEY,
116
+ RAG_WEB_SEARCH_RESULT_COUNT,
117
+ RAG_WEB_SEARCH_CONCURRENT_REQUESTS,
118
+ RAG_EMBEDDING_OPENAI_BATCH_SIZE,
119
+ )
120
+
121
+ from constants import ERROR_MESSAGES
122
+
123
+ log = logging.getLogger(__name__)
124
+ log.setLevel(SRC_LOG_LEVELS["RAG"])
125
+
126
+ app = FastAPI()
127
+
128
+ app.state.config = AppConfig()
129
+
130
+ app.state.config.TOP_K = RAG_TOP_K
131
+ app.state.config.RELEVANCE_THRESHOLD = RAG_RELEVANCE_THRESHOLD
132
+
133
+ app.state.config.ENABLE_RAG_HYBRID_SEARCH = ENABLE_RAG_HYBRID_SEARCH
134
+ app.state.config.ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION = (
135
+ ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION
136
+ )
137
+
138
+ app.state.config.CHUNK_SIZE = CHUNK_SIZE
139
+ app.state.config.CHUNK_OVERLAP = CHUNK_OVERLAP
140
+
141
+ app.state.config.RAG_EMBEDDING_ENGINE = RAG_EMBEDDING_ENGINE
142
+ app.state.config.RAG_EMBEDDING_MODEL = RAG_EMBEDDING_MODEL
143
+ app.state.config.RAG_EMBEDDING_OPENAI_BATCH_SIZE = RAG_EMBEDDING_OPENAI_BATCH_SIZE
144
+ app.state.config.RAG_RERANKING_MODEL = RAG_RERANKING_MODEL
145
+ app.state.config.RAG_TEMPLATE = RAG_TEMPLATE
146
+
147
+
148
+ app.state.config.OPENAI_API_BASE_URL = RAG_OPENAI_API_BASE_URL
149
+ app.state.config.OPENAI_API_KEY = RAG_OPENAI_API_KEY
150
+
151
+ app.state.config.PDF_EXTRACT_IMAGES = PDF_EXTRACT_IMAGES
152
+
153
+
154
+ app.state.config.YOUTUBE_LOADER_LANGUAGE = YOUTUBE_LOADER_LANGUAGE
155
+ app.state.YOUTUBE_LOADER_TRANSLATION = None
156
+
157
+
158
+ app.state.config.ENABLE_RAG_WEB_SEARCH = ENABLE_RAG_WEB_SEARCH
159
+ app.state.config.RAG_WEB_SEARCH_ENGINE = RAG_WEB_SEARCH_ENGINE
160
+
161
+ app.state.config.SEARXNG_QUERY_URL = SEARXNG_QUERY_URL
162
+ app.state.config.GOOGLE_PSE_API_KEY = GOOGLE_PSE_API_KEY
163
+ app.state.config.GOOGLE_PSE_ENGINE_ID = GOOGLE_PSE_ENGINE_ID
164
+ app.state.config.BRAVE_SEARCH_API_KEY = BRAVE_SEARCH_API_KEY
165
+ app.state.config.SERPSTACK_API_KEY = SERPSTACK_API_KEY
166
+ app.state.config.SERPSTACK_HTTPS = SERPSTACK_HTTPS
167
+ app.state.config.SERPER_API_KEY = SERPER_API_KEY
168
+ app.state.config.RAG_WEB_SEARCH_RESULT_COUNT = RAG_WEB_SEARCH_RESULT_COUNT
169
+ app.state.config.RAG_WEB_SEARCH_CONCURRENT_REQUESTS = RAG_WEB_SEARCH_CONCURRENT_REQUESTS
170
+
171
+
172
+ def update_embedding_model(
173
+ embedding_model: str,
174
+ update_model: bool = False,
175
+ ):
176
+ if embedding_model and app.state.config.RAG_EMBEDDING_ENGINE == "":
177
+ app.state.sentence_transformer_ef = sentence_transformers.SentenceTransformer(
178
+ get_model_path(embedding_model, update_model),
179
+ device=DEVICE_TYPE,
180
+ trust_remote_code=RAG_EMBEDDING_MODEL_TRUST_REMOTE_CODE,
181
+ )
182
+ else:
183
+ app.state.sentence_transformer_ef = None
184
+
185
+
186
+ def update_reranking_model(
187
+ reranking_model: str,
188
+ update_model: bool = False,
189
+ ):
190
+ if reranking_model:
191
+ app.state.sentence_transformer_rf = sentence_transformers.CrossEncoder(
192
+ get_model_path(reranking_model, update_model),
193
+ device=DEVICE_TYPE,
194
+ trust_remote_code=RAG_RERANKING_MODEL_TRUST_REMOTE_CODE,
195
+ )
196
+ else:
197
+ app.state.sentence_transformer_rf = None
198
+
199
+
200
+ update_embedding_model(
201
+ app.state.config.RAG_EMBEDDING_MODEL,
202
+ RAG_EMBEDDING_MODEL_AUTO_UPDATE,
203
+ )
204
+
205
+ update_reranking_model(
206
+ app.state.config.RAG_RERANKING_MODEL,
207
+ RAG_RERANKING_MODEL_AUTO_UPDATE,
208
+ )
209
+
210
+
211
+ app.state.EMBEDDING_FUNCTION = get_embedding_function(
212
+ app.state.config.RAG_EMBEDDING_ENGINE,
213
+ app.state.config.RAG_EMBEDDING_MODEL,
214
+ app.state.sentence_transformer_ef,
215
+ app.state.config.OPENAI_API_KEY,
216
+ app.state.config.OPENAI_API_BASE_URL,
217
+ app.state.config.RAG_EMBEDDING_OPENAI_BATCH_SIZE,
218
+ )
219
+
220
+ origins = ["*"]
221
+
222
+
223
+ app.add_middleware(
224
+ CORSMiddleware,
225
+ allow_origins=origins,
226
+ allow_credentials=True,
227
+ allow_methods=["*"],
228
+ allow_headers=["*"],
229
+ )
230
+
231
+
232
+ class CollectionNameForm(BaseModel):
233
+ collection_name: Optional[str] = "test"
234
+
235
+
236
+ class UrlForm(CollectionNameForm):
237
+ url: str
238
+
239
+
240
+ class SearchForm(CollectionNameForm):
241
+ query: str
242
+
243
+
244
+ @app.get("/")
245
+ async def get_status():
246
+ return {
247
+ "status": True,
248
+ "chunk_size": app.state.config.CHUNK_SIZE,
249
+ "chunk_overlap": app.state.config.CHUNK_OVERLAP,
250
+ "template": app.state.config.RAG_TEMPLATE,
251
+ "embedding_engine": app.state.config.RAG_EMBEDDING_ENGINE,
252
+ "embedding_model": app.state.config.RAG_EMBEDDING_MODEL,
253
+ "reranking_model": app.state.config.RAG_RERANKING_MODEL,
254
+ "openai_batch_size": app.state.config.RAG_EMBEDDING_OPENAI_BATCH_SIZE,
255
+ }
256
+
257
+
258
+ @app.get("/embedding")
259
+ async def get_embedding_config(user=Depends(get_admin_user)):
260
+ return {
261
+ "status": True,
262
+ "embedding_engine": app.state.config.RAG_EMBEDDING_ENGINE,
263
+ "embedding_model": app.state.config.RAG_EMBEDDING_MODEL,
264
+ "openai_config": {
265
+ "url": app.state.config.OPENAI_API_BASE_URL,
266
+ "key": app.state.config.OPENAI_API_KEY,
267
+ "batch_size": app.state.config.RAG_EMBEDDING_OPENAI_BATCH_SIZE,
268
+ },
269
+ }
270
+
271
+
272
+ @app.get("/reranking")
273
+ async def get_reraanking_config(user=Depends(get_admin_user)):
274
+ return {
275
+ "status": True,
276
+ "reranking_model": app.state.config.RAG_RERANKING_MODEL,
277
+ }
278
+
279
+
280
+ class OpenAIConfigForm(BaseModel):
281
+ url: str
282
+ key: str
283
+ batch_size: Optional[int] = None
284
+
285
+
286
+ class EmbeddingModelUpdateForm(BaseModel):
287
+ openai_config: Optional[OpenAIConfigForm] = None
288
+ embedding_engine: str
289
+ embedding_model: str
290
+
291
+
292
+ @app.post("/embedding/update")
293
+ async def update_embedding_config(
294
+ form_data: EmbeddingModelUpdateForm, user=Depends(get_admin_user)
295
+ ):
296
+ log.info(
297
+ f"Updating embedding model: {app.state.config.RAG_EMBEDDING_MODEL} to {form_data.embedding_model}"
298
+ )
299
+ try:
300
+ app.state.config.RAG_EMBEDDING_ENGINE = form_data.embedding_engine
301
+ app.state.config.RAG_EMBEDDING_MODEL = form_data.embedding_model
302
+
303
+ if app.state.config.RAG_EMBEDDING_ENGINE in ["ollama", "openai"]:
304
+ if form_data.openai_config is not None:
305
+ app.state.config.OPENAI_API_BASE_URL = form_data.openai_config.url
306
+ app.state.config.OPENAI_API_KEY = form_data.openai_config.key
307
+ app.state.config.RAG_EMBEDDING_OPENAI_BATCH_SIZE = (
308
+ form_data.openai_config.batch_size
309
+ if form_data.openai_config.batch_size
310
+ else 1
311
+ )
312
+
313
+ update_embedding_model(app.state.config.RAG_EMBEDDING_MODEL)
314
+
315
+ app.state.EMBEDDING_FUNCTION = get_embedding_function(
316
+ app.state.config.RAG_EMBEDDING_ENGINE,
317
+ app.state.config.RAG_EMBEDDING_MODEL,
318
+ app.state.sentence_transformer_ef,
319
+ app.state.config.OPENAI_API_KEY,
320
+ app.state.config.OPENAI_API_BASE_URL,
321
+ app.state.config.RAG_EMBEDDING_OPENAI_BATCH_SIZE,
322
+ )
323
+
324
+ return {
325
+ "status": True,
326
+ "embedding_engine": app.state.config.RAG_EMBEDDING_ENGINE,
327
+ "embedding_model": app.state.config.RAG_EMBEDDING_MODEL,
328
+ "openai_config": {
329
+ "url": app.state.config.OPENAI_API_BASE_URL,
330
+ "key": app.state.config.OPENAI_API_KEY,
331
+ "batch_size": app.state.config.RAG_EMBEDDING_OPENAI_BATCH_SIZE,
332
+ },
333
+ }
334
+ except Exception as e:
335
+ log.exception(f"Problem updating embedding model: {e}")
336
+ raise HTTPException(
337
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
338
+ detail=ERROR_MESSAGES.DEFAULT(e),
339
+ )
340
+
341
+
342
+ class RerankingModelUpdateForm(BaseModel):
343
+ reranking_model: str
344
+
345
+
346
+ @app.post("/reranking/update")
347
+ async def update_reranking_config(
348
+ form_data: RerankingModelUpdateForm, user=Depends(get_admin_user)
349
+ ):
350
+ log.info(
351
+ f"Updating reranking model: {app.state.config.RAG_RERANKING_MODEL} to {form_data.reranking_model}"
352
+ )
353
+ try:
354
+ app.state.config.RAG_RERANKING_MODEL = form_data.reranking_model
355
+
356
+ update_reranking_model(app.state.config.RAG_RERANKING_MODEL), True
357
+
358
+ return {
359
+ "status": True,
360
+ "reranking_model": app.state.config.RAG_RERANKING_MODEL,
361
+ }
362
+ except Exception as e:
363
+ log.exception(f"Problem updating reranking model: {e}")
364
+ raise HTTPException(
365
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
366
+ detail=ERROR_MESSAGES.DEFAULT(e),
367
+ )
368
+
369
+
370
+ @app.get("/config")
371
+ async def get_rag_config(user=Depends(get_admin_user)):
372
+ return {
373
+ "status": True,
374
+ "pdf_extract_images": app.state.config.PDF_EXTRACT_IMAGES,
375
+ "chunk": {
376
+ "chunk_size": app.state.config.CHUNK_SIZE,
377
+ "chunk_overlap": app.state.config.CHUNK_OVERLAP,
378
+ },
379
+ "youtube": {
380
+ "language": app.state.config.YOUTUBE_LOADER_LANGUAGE,
381
+ "translation": app.state.YOUTUBE_LOADER_TRANSLATION,
382
+ },
383
+ "web": {
384
+ "ssl_verification": app.state.config.ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION,
385
+ "search": {
386
+ "enabled": app.state.config.ENABLE_RAG_WEB_SEARCH,
387
+ "engine": app.state.config.RAG_WEB_SEARCH_ENGINE,
388
+ "searxng_query_url": app.state.config.SEARXNG_QUERY_URL,
389
+ "google_pse_api_key": app.state.config.GOOGLE_PSE_API_KEY,
390
+ "google_pse_engine_id": app.state.config.GOOGLE_PSE_ENGINE_ID,
391
+ "brave_search_api_key": app.state.config.BRAVE_SEARCH_API_KEY,
392
+ "serpstack_api_key": app.state.config.SERPSTACK_API_KEY,
393
+ "serpstack_https": app.state.config.SERPSTACK_HTTPS,
394
+ "serper_api_key": app.state.config.SERPER_API_KEY,
395
+ "result_count": app.state.config.RAG_WEB_SEARCH_RESULT_COUNT,
396
+ "concurrent_requests": app.state.config.RAG_WEB_SEARCH_CONCURRENT_REQUESTS,
397
+ },
398
+ },
399
+ }
400
+
401
+
402
+ class ChunkParamUpdateForm(BaseModel):
403
+ chunk_size: int
404
+ chunk_overlap: int
405
+
406
+
407
+ class YoutubeLoaderConfig(BaseModel):
408
+ language: List[str]
409
+ translation: Optional[str] = None
410
+
411
+
412
+ class WebSearchConfig(BaseModel):
413
+ enabled: bool
414
+ engine: Optional[str] = None
415
+ searxng_query_url: Optional[str] = None
416
+ google_pse_api_key: Optional[str] = None
417
+ google_pse_engine_id: Optional[str] = None
418
+ brave_search_api_key: Optional[str] = None
419
+ serpstack_api_key: Optional[str] = None
420
+ serpstack_https: Optional[bool] = None
421
+ serper_api_key: Optional[str] = None
422
+ result_count: Optional[int] = None
423
+ concurrent_requests: Optional[int] = None
424
+
425
+
426
+ class WebConfig(BaseModel):
427
+ search: WebSearchConfig
428
+ web_loader_ssl_verification: Optional[bool] = None
429
+
430
+
431
+ class ConfigUpdateForm(BaseModel):
432
+ pdf_extract_images: Optional[bool] = None
433
+ chunk: Optional[ChunkParamUpdateForm] = None
434
+ youtube: Optional[YoutubeLoaderConfig] = None
435
+ web: Optional[WebConfig] = None
436
+
437
+
438
+ @app.post("/config/update")
439
+ async def update_rag_config(form_data: ConfigUpdateForm, user=Depends(get_admin_user)):
440
+ app.state.config.PDF_EXTRACT_IMAGES = (
441
+ form_data.pdf_extract_images
442
+ if form_data.pdf_extract_images is not None
443
+ else app.state.config.PDF_EXTRACT_IMAGES
444
+ )
445
+
446
+ if form_data.chunk is not None:
447
+ app.state.config.CHUNK_SIZE = form_data.chunk.chunk_size
448
+ app.state.config.CHUNK_OVERLAP = form_data.chunk.chunk_overlap
449
+
450
+ if form_data.youtube is not None:
451
+ app.state.config.YOUTUBE_LOADER_LANGUAGE = form_data.youtube.language
452
+ app.state.YOUTUBE_LOADER_TRANSLATION = form_data.youtube.translation
453
+
454
+ if form_data.web is not None:
455
+ app.state.config.ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION = (
456
+ form_data.web.web_loader_ssl_verification
457
+ )
458
+
459
+ app.state.config.ENABLE_RAG_WEB_SEARCH = form_data.web.search.enabled
460
+ app.state.config.RAG_WEB_SEARCH_ENGINE = form_data.web.search.engine
461
+ app.state.config.SEARXNG_QUERY_URL = form_data.web.search.searxng_query_url
462
+ app.state.config.GOOGLE_PSE_API_KEY = form_data.web.search.google_pse_api_key
463
+ app.state.config.GOOGLE_PSE_ENGINE_ID = (
464
+ form_data.web.search.google_pse_engine_id
465
+ )
466
+ app.state.config.BRAVE_SEARCH_API_KEY = (
467
+ form_data.web.search.brave_search_api_key
468
+ )
469
+ app.state.config.SERPSTACK_API_KEY = form_data.web.search.serpstack_api_key
470
+ app.state.config.SERPSTACK_HTTPS = form_data.web.search.serpstack_https
471
+ app.state.config.SERPER_API_KEY = form_data.web.search.serper_api_key
472
+ app.state.config.RAG_WEB_SEARCH_RESULT_COUNT = form_data.web.search.result_count
473
+ app.state.config.RAG_WEB_SEARCH_CONCURRENT_REQUESTS = (
474
+ form_data.web.search.concurrent_requests
475
+ )
476
+
477
+ return {
478
+ "status": True,
479
+ "pdf_extract_images": app.state.config.PDF_EXTRACT_IMAGES,
480
+ "chunk": {
481
+ "chunk_size": app.state.config.CHUNK_SIZE,
482
+ "chunk_overlap": app.state.config.CHUNK_OVERLAP,
483
+ },
484
+ "youtube": {
485
+ "language": app.state.config.YOUTUBE_LOADER_LANGUAGE,
486
+ "translation": app.state.YOUTUBE_LOADER_TRANSLATION,
487
+ },
488
+ "web": {
489
+ "ssl_verification": app.state.config.ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION,
490
+ "search": {
491
+ "enabled": app.state.config.ENABLE_RAG_WEB_SEARCH,
492
+ "engine": app.state.config.RAG_WEB_SEARCH_ENGINE,
493
+ "searxng_query_url": app.state.config.SEARXNG_QUERY_URL,
494
+ "google_pse_api_key": app.state.config.GOOGLE_PSE_API_KEY,
495
+ "google_pse_engine_id": app.state.config.GOOGLE_PSE_ENGINE_ID,
496
+ "brave_search_api_key": app.state.config.BRAVE_SEARCH_API_KEY,
497
+ "serpstack_api_key": app.state.config.SERPSTACK_API_KEY,
498
+ "serpstack_https": app.state.config.SERPSTACK_HTTPS,
499
+ "serper_api_key": app.state.config.SERPER_API_KEY,
500
+ "result_count": app.state.config.RAG_WEB_SEARCH_RESULT_COUNT,
501
+ "concurrent_requests": app.state.config.RAG_WEB_SEARCH_CONCURRENT_REQUESTS,
502
+ },
503
+ },
504
+ }
505
+
506
+
507
+ @app.get("/template")
508
+ async def get_rag_template(user=Depends(get_current_user)):
509
+ return {
510
+ "status": True,
511
+ "template": app.state.config.RAG_TEMPLATE,
512
+ }
513
+
514
+
515
+ @app.get("/query/settings")
516
+ async def get_query_settings(user=Depends(get_admin_user)):
517
+ return {
518
+ "status": True,
519
+ "template": app.state.config.RAG_TEMPLATE,
520
+ "k": app.state.config.TOP_K,
521
+ "r": app.state.config.RELEVANCE_THRESHOLD,
522
+ "hybrid": app.state.config.ENABLE_RAG_HYBRID_SEARCH,
523
+ }
524
+
525
+
526
+ class QuerySettingsForm(BaseModel):
527
+ k: Optional[int] = None
528
+ r: Optional[float] = None
529
+ template: Optional[str] = None
530
+ hybrid: Optional[bool] = None
531
+
532
+
533
+ @app.post("/query/settings/update")
534
+ async def update_query_settings(
535
+ form_data: QuerySettingsForm, user=Depends(get_admin_user)
536
+ ):
537
+ app.state.config.RAG_TEMPLATE = (
538
+ form_data.template if form_data.template else RAG_TEMPLATE
539
+ )
540
+ app.state.config.TOP_K = form_data.k if form_data.k else 4
541
+ app.state.config.RELEVANCE_THRESHOLD = form_data.r if form_data.r else 0.0
542
+ app.state.config.ENABLE_RAG_HYBRID_SEARCH = (
543
+ form_data.hybrid if form_data.hybrid else False
544
+ )
545
+ return {
546
+ "status": True,
547
+ "template": app.state.config.RAG_TEMPLATE,
548
+ "k": app.state.config.TOP_K,
549
+ "r": app.state.config.RELEVANCE_THRESHOLD,
550
+ "hybrid": app.state.config.ENABLE_RAG_HYBRID_SEARCH,
551
+ }
552
+
553
+
554
+ class QueryDocForm(BaseModel):
555
+ collection_name: str
556
+ query: str
557
+ k: Optional[int] = None
558
+ r: Optional[float] = None
559
+ hybrid: Optional[bool] = None
560
+
561
+
562
+ @app.post("/query/doc")
563
+ def query_doc_handler(
564
+ form_data: QueryDocForm,
565
+ user=Depends(get_current_user),
566
+ ):
567
+ try:
568
+ if app.state.config.ENABLE_RAG_HYBRID_SEARCH:
569
+ return query_doc_with_hybrid_search(
570
+ collection_name=form_data.collection_name,
571
+ query=form_data.query,
572
+ embedding_function=app.state.EMBEDDING_FUNCTION,
573
+ k=form_data.k if form_data.k else app.state.config.TOP_K,
574
+ reranking_function=app.state.sentence_transformer_rf,
575
+ r=(
576
+ form_data.r if form_data.r else app.state.config.RELEVANCE_THRESHOLD
577
+ ),
578
+ )
579
+ else:
580
+ return query_doc(
581
+ collection_name=form_data.collection_name,
582
+ query=form_data.query,
583
+ embedding_function=app.state.EMBEDDING_FUNCTION,
584
+ k=form_data.k if form_data.k else app.state.config.TOP_K,
585
+ )
586
+ except Exception as e:
587
+ log.exception(e)
588
+ raise HTTPException(
589
+ status_code=status.HTTP_400_BAD_REQUEST,
590
+ detail=ERROR_MESSAGES.DEFAULT(e),
591
+ )
592
+
593
+
594
+ class QueryCollectionsForm(BaseModel):
595
+ collection_names: List[str]
596
+ query: str
597
+ k: Optional[int] = None
598
+ r: Optional[float] = None
599
+ hybrid: Optional[bool] = None
600
+
601
+
602
+ @app.post("/query/collection")
603
+ def query_collection_handler(
604
+ form_data: QueryCollectionsForm,
605
+ user=Depends(get_current_user),
606
+ ):
607
+ try:
608
+ if app.state.config.ENABLE_RAG_HYBRID_SEARCH:
609
+ return query_collection_with_hybrid_search(
610
+ collection_names=form_data.collection_names,
611
+ query=form_data.query,
612
+ embedding_function=app.state.EMBEDDING_FUNCTION,
613
+ k=form_data.k if form_data.k else app.state.config.TOP_K,
614
+ reranking_function=app.state.sentence_transformer_rf,
615
+ r=(
616
+ form_data.r if form_data.r else app.state.config.RELEVANCE_THRESHOLD
617
+ ),
618
+ )
619
+ else:
620
+ return query_collection(
621
+ collection_names=form_data.collection_names,
622
+ query=form_data.query,
623
+ embedding_function=app.state.EMBEDDING_FUNCTION,
624
+ k=form_data.k if form_data.k else app.state.config.TOP_K,
625
+ )
626
+
627
+ except Exception as e:
628
+ log.exception(e)
629
+ raise HTTPException(
630
+ status_code=status.HTTP_400_BAD_REQUEST,
631
+ detail=ERROR_MESSAGES.DEFAULT(e),
632
+ )
633
+
634
+
635
+ @app.post("/youtube")
636
+ def store_youtube_video(form_data: UrlForm, user=Depends(get_current_user)):
637
+ try:
638
+ loader = YoutubeLoader.from_youtube_url(
639
+ form_data.url,
640
+ add_video_info=True,
641
+ language=app.state.config.YOUTUBE_LOADER_LANGUAGE,
642
+ translation=app.state.YOUTUBE_LOADER_TRANSLATION,
643
+ )
644
+ data = loader.load()
645
+
646
+ collection_name = form_data.collection_name
647
+ if collection_name == "":
648
+ collection_name = calculate_sha256_string(form_data.url)[:63]
649
+
650
+ store_data_in_vector_db(data, collection_name, overwrite=True)
651
+ return {
652
+ "status": True,
653
+ "collection_name": collection_name,
654
+ "filename": form_data.url,
655
+ }
656
+ except Exception as e:
657
+ log.exception(e)
658
+ raise HTTPException(
659
+ status_code=status.HTTP_400_BAD_REQUEST,
660
+ detail=ERROR_MESSAGES.DEFAULT(e),
661
+ )
662
+
663
+
664
+ @app.post("/web")
665
+ def store_web(form_data: UrlForm, user=Depends(get_current_user)):
666
+ # "https://www.gutenberg.org/files/1727/1727-h/1727-h.htm"
667
+ try:
668
+ loader = get_web_loader(
669
+ form_data.url,
670
+ verify_ssl=app.state.config.ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION,
671
+ )
672
+ data = loader.load()
673
+
674
+ collection_name = form_data.collection_name
675
+ if collection_name == "":
676
+ collection_name = calculate_sha256_string(form_data.url)[:63]
677
+
678
+ store_data_in_vector_db(data, collection_name, overwrite=True)
679
+ return {
680
+ "status": True,
681
+ "collection_name": collection_name,
682
+ "filename": form_data.url,
683
+ }
684
+ except Exception as e:
685
+ log.exception(e)
686
+ raise HTTPException(
687
+ status_code=status.HTTP_400_BAD_REQUEST,
688
+ detail=ERROR_MESSAGES.DEFAULT(e),
689
+ )
690
+
691
+
692
+ def get_web_loader(url: Union[str, Sequence[str]], verify_ssl: bool = True):
693
+ # Check if the URL is valid
694
+ if not validate_url(url):
695
+ raise ValueError(ERROR_MESSAGES.INVALID_URL)
696
+ return WebBaseLoader(
697
+ url,
698
+ verify_ssl=verify_ssl,
699
+ requests_per_second=RAG_WEB_SEARCH_CONCURRENT_REQUESTS,
700
+ continue_on_failure=True,
701
+ )
702
+
703
+
704
+ def validate_url(url: Union[str, Sequence[str]]):
705
+ if isinstance(url, str):
706
+ if isinstance(validators.url(url), validators.ValidationError):
707
+ raise ValueError(ERROR_MESSAGES.INVALID_URL)
708
+ if not ENABLE_RAG_LOCAL_WEB_FETCH:
709
+ # Local web fetch is disabled, filter out any URLs that resolve to private IP addresses
710
+ parsed_url = urllib.parse.urlparse(url)
711
+ # Get IPv4 and IPv6 addresses
712
+ ipv4_addresses, ipv6_addresses = resolve_hostname(parsed_url.hostname)
713
+ # Check if any of the resolved addresses are private
714
+ # This is technically still vulnerable to DNS rebinding attacks, as we don't control WebBaseLoader
715
+ for ip in ipv4_addresses:
716
+ if validators.ipv4(ip, private=True):
717
+ raise ValueError(ERROR_MESSAGES.INVALID_URL)
718
+ for ip in ipv6_addresses:
719
+ if validators.ipv6(ip, private=True):
720
+ raise ValueError(ERROR_MESSAGES.INVALID_URL)
721
+ return True
722
+ elif isinstance(url, Sequence):
723
+ return all(validate_url(u) for u in url)
724
+ else:
725
+ return False
726
+
727
+
728
+ def resolve_hostname(hostname):
729
+ # Get address information
730
+ addr_info = socket.getaddrinfo(hostname, None)
731
+
732
+ # Extract IP addresses from address information
733
+ ipv4_addresses = [info[4][0] for info in addr_info if info[0] == socket.AF_INET]
734
+ ipv6_addresses = [info[4][0] for info in addr_info if info[0] == socket.AF_INET6]
735
+
736
+ return ipv4_addresses, ipv6_addresses
737
+
738
+
739
+ def search_web(engine: str, query: str) -> list[SearchResult]:
740
+ """Search the web using a search engine and return the results as a list of SearchResult objects.
741
+ Will look for a search engine API key in environment variables in the following order:
742
+ - SEARXNG_QUERY_URL
743
+ - GOOGLE_PSE_API_KEY + GOOGLE_PSE_ENGINE_ID
744
+ - BRAVE_SEARCH_API_KEY
745
+ - SERPSTACK_API_KEY
746
+ - SERPER_API_KEY
747
+
748
+ Args:
749
+ query (str): The query to search for
750
+ """
751
+
752
+ # TODO: add playwright to search the web
753
+ if engine == "searxng":
754
+ if app.state.config.SEARXNG_QUERY_URL:
755
+ return search_searxng(
756
+ app.state.config.SEARXNG_QUERY_URL,
757
+ query,
758
+ app.state.config.RAG_WEB_SEARCH_RESULT_COUNT,
759
+ )
760
+ else:
761
+ raise Exception("No SEARXNG_QUERY_URL found in environment variables")
762
+ elif engine == "google_pse":
763
+ if (
764
+ app.state.config.GOOGLE_PSE_API_KEY
765
+ and app.state.config.GOOGLE_PSE_ENGINE_ID
766
+ ):
767
+ return search_google_pse(
768
+ app.state.config.GOOGLE_PSE_API_KEY,
769
+ app.state.config.GOOGLE_PSE_ENGINE_ID,
770
+ query,
771
+ app.state.config.RAG_WEB_SEARCH_RESULT_COUNT,
772
+ )
773
+ else:
774
+ raise Exception(
775
+ "No GOOGLE_PSE_API_KEY or GOOGLE_PSE_ENGINE_ID found in environment variables"
776
+ )
777
+ elif engine == "brave":
778
+ if app.state.config.BRAVE_SEARCH_API_KEY:
779
+ return search_brave(
780
+ app.state.config.BRAVE_SEARCH_API_KEY,
781
+ query,
782
+ app.state.config.RAG_WEB_SEARCH_RESULT_COUNT,
783
+ )
784
+ else:
785
+ raise Exception("No BRAVE_SEARCH_API_KEY found in environment variables")
786
+ elif engine == "serpstack":
787
+ if app.state.config.SERPSTACK_API_KEY:
788
+ return search_serpstack(
789
+ app.state.config.SERPSTACK_API_KEY,
790
+ query,
791
+ app.state.config.RAG_WEB_SEARCH_RESULT_COUNT,
792
+ https_enabled=app.state.config.SERPSTACK_HTTPS,
793
+ )
794
+ else:
795
+ raise Exception("No SERPSTACK_API_KEY found in environment variables")
796
+ elif engine == "serper":
797
+ if app.state.config.SERPER_API_KEY:
798
+ return search_serper(
799
+ app.state.config.SERPER_API_KEY,
800
+ query,
801
+ app.state.config.RAG_WEB_SEARCH_RESULT_COUNT,
802
+ )
803
+ else:
804
+ raise Exception("No SERPER_API_KEY found in environment variables")
805
+ else:
806
+ raise Exception("No search engine API key found in environment variables")
807
+
808
+
809
+ @app.post("/web/search")
810
+ def store_web_search(form_data: SearchForm, user=Depends(get_current_user)):
811
+ try:
812
+ web_results = search_web(
813
+ app.state.config.RAG_WEB_SEARCH_ENGINE, form_data.query
814
+ )
815
+ except Exception as e:
816
+ log.exception(e)
817
+
818
+ print(e)
819
+ raise HTTPException(
820
+ status_code=status.HTTP_400_BAD_REQUEST,
821
+ detail=ERROR_MESSAGES.WEB_SEARCH_ERROR(e),
822
+ )
823
+
824
+ try:
825
+ urls = [result.link for result in web_results]
826
+ loader = get_web_loader(urls)
827
+ data = loader.load()
828
+
829
+ collection_name = form_data.collection_name
830
+ if collection_name == "":
831
+ collection_name = calculate_sha256_string(form_data.query)[:63]
832
+
833
+ store_data_in_vector_db(data, collection_name, overwrite=True)
834
+ return {
835
+ "status": True,
836
+ "collection_name": collection_name,
837
+ "filenames": urls,
838
+ }
839
+ except Exception as e:
840
+ log.exception(e)
841
+ raise HTTPException(
842
+ status_code=status.HTTP_400_BAD_REQUEST,
843
+ detail=ERROR_MESSAGES.DEFAULT(e),
844
+ )
845
+
846
+
847
+ def store_data_in_vector_db(data, collection_name, overwrite: bool = False) -> bool:
848
+
849
+ text_splitter = RecursiveCharacterTextSplitter(
850
+ chunk_size=app.state.config.CHUNK_SIZE,
851
+ chunk_overlap=app.state.config.CHUNK_OVERLAP,
852
+ add_start_index=True,
853
+ )
854
+
855
+ docs = text_splitter.split_documents(data)
856
+
857
+ if len(docs) > 0:
858
+ log.info(f"store_data_in_vector_db {docs}")
859
+ return store_docs_in_vector_db(docs, collection_name, overwrite), None
860
+ else:
861
+ raise ValueError(ERROR_MESSAGES.EMPTY_CONTENT)
862
+
863
+
864
+ def store_text_in_vector_db(
865
+ text, metadata, collection_name, overwrite: bool = False
866
+ ) -> bool:
867
+ text_splitter = RecursiveCharacterTextSplitter(
868
+ chunk_size=app.state.config.CHUNK_SIZE,
869
+ chunk_overlap=app.state.config.CHUNK_OVERLAP,
870
+ add_start_index=True,
871
+ )
872
+ docs = text_splitter.create_documents([text], metadatas=[metadata])
873
+ return store_docs_in_vector_db(docs, collection_name, overwrite)
874
+
875
+
876
+ def store_docs_in_vector_db(docs, collection_name, overwrite: bool = False) -> bool:
877
+ log.info(f"store_docs_in_vector_db {docs} {collection_name}")
878
+
879
+ texts = [doc.page_content for doc in docs]
880
+ metadatas = [doc.metadata for doc in docs]
881
+
882
+ try:
883
+ if overwrite:
884
+ for collection in CHROMA_CLIENT.list_collections():
885
+ if collection_name == collection.name:
886
+ log.info(f"deleting existing collection {collection_name}")
887
+ CHROMA_CLIENT.delete_collection(name=collection_name)
888
+
889
+ collection = CHROMA_CLIENT.create_collection(name=collection_name)
890
+
891
+ embedding_func = get_embedding_function(
892
+ app.state.config.RAG_EMBEDDING_ENGINE,
893
+ app.state.config.RAG_EMBEDDING_MODEL,
894
+ app.state.sentence_transformer_ef,
895
+ app.state.config.OPENAI_API_KEY,
896
+ app.state.config.OPENAI_API_BASE_URL,
897
+ app.state.config.RAG_EMBEDDING_OPENAI_BATCH_SIZE,
898
+ )
899
+
900
+ embedding_texts = list(map(lambda x: x.replace("\n", " "), texts))
901
+ embeddings = embedding_func(embedding_texts)
902
+
903
+ for batch in create_batches(
904
+ api=CHROMA_CLIENT,
905
+ ids=[str(uuid.uuid4()) for _ in texts],
906
+ metadatas=metadatas,
907
+ embeddings=embeddings,
908
+ documents=texts,
909
+ ):
910
+ collection.add(*batch)
911
+
912
+ return True
913
+ except Exception as e:
914
+ log.exception(e)
915
+ if e.__class__.__name__ == "UniqueConstraintError":
916
+ return True
917
+
918
+ return False
919
+
920
+
921
+ def get_loader(filename: str, file_content_type: str, file_path: str):
922
+ file_ext = filename.split(".")[-1].lower()
923
+ known_type = True
924
+
925
+ known_source_ext = [
926
+ "go",
927
+ "py",
928
+ "java",
929
+ "sh",
930
+ "bat",
931
+ "ps1",
932
+ "cmd",
933
+ "js",
934
+ "ts",
935
+ "css",
936
+ "cpp",
937
+ "hpp",
938
+ "h",
939
+ "c",
940
+ "cs",
941
+ "sql",
942
+ "log",
943
+ "ini",
944
+ "pl",
945
+ "pm",
946
+ "r",
947
+ "dart",
948
+ "dockerfile",
949
+ "env",
950
+ "php",
951
+ "hs",
952
+ "hsc",
953
+ "lua",
954
+ "nginxconf",
955
+ "conf",
956
+ "m",
957
+ "mm",
958
+ "plsql",
959
+ "perl",
960
+ "rb",
961
+ "rs",
962
+ "db2",
963
+ "scala",
964
+ "bash",
965
+ "swift",
966
+ "vue",
967
+ "svelte",
968
+ ]
969
+
970
+ if file_ext == "pdf":
971
+ loader = PyPDFLoader(
972
+ file_path, extract_images=app.state.config.PDF_EXTRACT_IMAGES
973
+ )
974
+ elif file_ext == "csv":
975
+ loader = CSVLoader(file_path)
976
+ elif file_ext == "rst":
977
+ loader = UnstructuredRSTLoader(file_path, mode="elements")
978
+ elif file_ext == "xml":
979
+ loader = UnstructuredXMLLoader(file_path)
980
+ elif file_ext in ["htm", "html"]:
981
+ loader = BSHTMLLoader(file_path, open_encoding="unicode_escape")
982
+ elif file_ext == "md":
983
+ loader = UnstructuredMarkdownLoader(file_path)
984
+ elif file_content_type == "application/epub+zip":
985
+ loader = UnstructuredEPubLoader(file_path)
986
+ elif (
987
+ file_content_type
988
+ == "application/vnd.openxmlformats-officedocument.wordprocessingml.document"
989
+ or file_ext in ["doc", "docx"]
990
+ ):
991
+ loader = Docx2txtLoader(file_path)
992
+ elif file_content_type in [
993
+ "application/vnd.ms-excel",
994
+ "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
995
+ ] or file_ext in ["xls", "xlsx"]:
996
+ loader = UnstructuredExcelLoader(file_path)
997
+ elif file_content_type in [
998
+ "application/vnd.ms-powerpoint",
999
+ "application/vnd.openxmlformats-officedocument.presentationml.presentation",
1000
+ ] or file_ext in ["ppt", "pptx"]:
1001
+ loader = UnstructuredPowerPointLoader(file_path)
1002
+ elif file_ext in known_source_ext or (
1003
+ file_content_type and file_content_type.find("text/") >= 0
1004
+ ):
1005
+ loader = TextLoader(file_path, autodetect_encoding=True)
1006
+ else:
1007
+ loader = TextLoader(file_path, autodetect_encoding=True)
1008
+ known_type = False
1009
+
1010
+ return loader, known_type
1011
+
1012
+
1013
+ @app.post("/doc")
1014
+ def store_doc(
1015
+ collection_name: Optional[str] = Form(None),
1016
+ file: UploadFile = File(...),
1017
+ user=Depends(get_current_user),
1018
+ ):
1019
+ # "https://www.gutenberg.org/files/1727/1727-h/1727-h.htm"
1020
+
1021
+ log.info(f"file.content_type: {file.content_type}")
1022
+ try:
1023
+ unsanitized_filename = file.filename
1024
+ filename = os.path.basename(unsanitized_filename)
1025
+
1026
+ file_path = f"{UPLOAD_DIR}/{filename}"
1027
+
1028
+ contents = file.file.read()
1029
+ with open(file_path, "wb") as f:
1030
+ f.write(contents)
1031
+ f.close()
1032
+
1033
+ f = open(file_path, "rb")
1034
+ if collection_name == None:
1035
+ collection_name = calculate_sha256(f)[:63]
1036
+ f.close()
1037
+
1038
+ loader, known_type = get_loader(filename, file.content_type, file_path)
1039
+ data = loader.load()
1040
+
1041
+ try:
1042
+ result = store_data_in_vector_db(data, collection_name)
1043
+
1044
+ if result:
1045
+ return {
1046
+ "status": True,
1047
+ "collection_name": collection_name,
1048
+ "filename": filename,
1049
+ "known_type": known_type,
1050
+ }
1051
+ except Exception as e:
1052
+ raise HTTPException(
1053
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
1054
+ detail=e,
1055
+ )
1056
+ except Exception as e:
1057
+ log.exception(e)
1058
+ if "No pandoc was found" in str(e):
1059
+ raise HTTPException(
1060
+ status_code=status.HTTP_400_BAD_REQUEST,
1061
+ detail=ERROR_MESSAGES.PANDOC_NOT_INSTALLED,
1062
+ )
1063
+ else:
1064
+ raise HTTPException(
1065
+ status_code=status.HTTP_400_BAD_REQUEST,
1066
+ detail=ERROR_MESSAGES.DEFAULT(e),
1067
+ )
1068
+
1069
+
1070
+ class TextRAGForm(BaseModel):
1071
+ name: str
1072
+ content: str
1073
+ collection_name: Optional[str] = None
1074
+
1075
+
1076
+ @app.post("/text")
1077
+ def store_text(
1078
+ form_data: TextRAGForm,
1079
+ user=Depends(get_current_user),
1080
+ ):
1081
+
1082
+ collection_name = form_data.collection_name
1083
+ if collection_name == None:
1084
+ collection_name = calculate_sha256_string(form_data.content)
1085
+
1086
+ result = store_text_in_vector_db(
1087
+ form_data.content,
1088
+ metadata={"name": form_data.name, "created_by": user.id},
1089
+ collection_name=collection_name,
1090
+ )
1091
+
1092
+ if result:
1093
+ return {"status": True, "collection_name": collection_name}
1094
+ else:
1095
+ raise HTTPException(
1096
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
1097
+ detail=ERROR_MESSAGES.DEFAULT(),
1098
+ )
1099
+
1100
+
1101
+ @app.get("/scan")
1102
+ def scan_docs_dir(user=Depends(get_admin_user)):
1103
+ for path in Path(DOCS_DIR).rglob("./**/*"):
1104
+ try:
1105
+ if path.is_file() and not path.name.startswith("."):
1106
+ tags = extract_folders_after_data_docs(path)
1107
+ filename = path.name
1108
+ file_content_type = mimetypes.guess_type(path)
1109
+
1110
+ f = open(path, "rb")
1111
+ collection_name = calculate_sha256(f)[:63]
1112
+ f.close()
1113
+
1114
+ loader, known_type = get_loader(
1115
+ filename, file_content_type[0], str(path)
1116
+ )
1117
+ data = loader.load()
1118
+
1119
+ try:
1120
+ result = store_data_in_vector_db(data, collection_name)
1121
+
1122
+ if result:
1123
+ sanitized_filename = sanitize_filename(filename)
1124
+ doc = Documents.get_doc_by_name(sanitized_filename)
1125
+
1126
+ if doc == None:
1127
+ doc = Documents.insert_new_doc(
1128
+ user.id,
1129
+ DocumentForm(
1130
+ **{
1131
+ "name": sanitized_filename,
1132
+ "title": filename,
1133
+ "collection_name": collection_name,
1134
+ "filename": filename,
1135
+ "content": (
1136
+ json.dumps(
1137
+ {
1138
+ "tags": list(
1139
+ map(
1140
+ lambda name: {"name": name},
1141
+ tags,
1142
+ )
1143
+ )
1144
+ }
1145
+ )
1146
+ if len(tags)
1147
+ else "{}"
1148
+ ),
1149
+ }
1150
+ ),
1151
+ )
1152
+ except Exception as e:
1153
+ log.exception(e)
1154
+ pass
1155
+
1156
+ except Exception as e:
1157
+ log.exception(e)
1158
+
1159
+ return True
1160
+
1161
+
1162
+ @app.get("/reset/db")
1163
+ def reset_vector_db(user=Depends(get_admin_user)):
1164
+ CHROMA_CLIENT.reset()
1165
+
1166
+
1167
+ @app.get("/reset/uploads")
1168
+ def reset_upload_dir(user=Depends(get_admin_user)) -> bool:
1169
+ folder = f"{UPLOAD_DIR}"
1170
+ try:
1171
+ # Check if the directory exists
1172
+ if os.path.exists(folder):
1173
+ # Iterate over all the files and directories in the specified directory
1174
+ for filename in os.listdir(folder):
1175
+ file_path = os.path.join(folder, filename)
1176
+ try:
1177
+ if os.path.isfile(file_path) or os.path.islink(file_path):
1178
+ os.unlink(file_path) # Remove the file or link
1179
+ elif os.path.isdir(file_path):
1180
+ shutil.rmtree(file_path) # Remove the directory
1181
+ except Exception as e:
1182
+ print(f"Failed to delete {file_path}. Reason: {e}")
1183
+ else:
1184
+ print(f"The directory {folder} does not exist")
1185
+ except Exception as e:
1186
+ print(f"Failed to process the directory {folder}. Reason: {e}")
1187
+
1188
+ return True
1189
+
1190
+
1191
+ @app.get("/reset")
1192
+ def reset(user=Depends(get_admin_user)) -> bool:
1193
+ folder = f"{UPLOAD_DIR}"
1194
+ for filename in os.listdir(folder):
1195
+ file_path = os.path.join(folder, filename)
1196
+ try:
1197
+ if os.path.isfile(file_path) or os.path.islink(file_path):
1198
+ os.unlink(file_path)
1199
+ elif os.path.isdir(file_path):
1200
+ shutil.rmtree(file_path)
1201
+ except Exception as e:
1202
+ log.error("Failed to delete %s. Reason: %s" % (file_path, e))
1203
+
1204
+ try:
1205
+ CHROMA_CLIENT.reset()
1206
+ except Exception as e:
1207
+ log.exception(e)
1208
+
1209
+ return True
1210
+
1211
+
1212
+ if ENV == "dev":
1213
+
1214
+ @app.get("/ef")
1215
+ async def get_embeddings():
1216
+ return {"result": app.state.EMBEDDING_FUNCTION("hello world")}
1217
+
1218
+ @app.get("/ef/{text}")
1219
+ async def get_embeddings_text(text: str):
1220
+ return {"result": app.state.EMBEDDING_FUNCTION(text)}
backend/apps/rag/search/brave.py ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import logging
2
+
3
+ import requests
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_brave(api_key: str, query: str, count: int) -> list[SearchResult]:
13
+ """Search using Brave's Search API and return the results as a list of SearchResult objects.
14
+
15
+ Args:
16
+ api_key (str): A Brave Search API key
17
+ query (str): The query to search for
18
+ """
19
+ url = "https://api.search.brave.com/res/v1/web/search"
20
+ headers = {
21
+ "Accept": "application/json",
22
+ "Accept-Encoding": "gzip",
23
+ "X-Subscription-Token": api_key,
24
+ }
25
+ params = {"q": query, "count": count}
26
+
27
+ response = requests.get(url, headers=headers, params=params)
28
+ response.raise_for_status()
29
+
30
+ json_response = response.json()
31
+ results = json_response.get("web", {}).get("results", [])
32
+ return [
33
+ SearchResult(
34
+ link=result["url"], title=result.get("title"), snippet=result.get("snippet")
35
+ )
36
+ for result in results[:count]
37
+ ]
backend/apps/rag/search/google_pse.py ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ import logging
3
+
4
+ import requests
5
+
6
+ from apps.rag.search.main import SearchResult
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, search_engine_id: str, query: str, count: int
15
+ ) -> list[SearchResult]:
16
+ """Search using Google's Programmable Search Engine API and return the results as a list of SearchResult objects.
17
+
18
+ Args:
19
+ api_key (str): A Programmable Search Engine API key
20
+ search_engine_id (str): A Programmable Search Engine ID
21
+ query (str): The query to search for
22
+ """
23
+ url = "https://www.googleapis.com/customsearch/v1"
24
+
25
+ headers = {"Content-Type": "application/json"}
26
+ params = {
27
+ "cx": search_engine_id,
28
+ "q": query,
29
+ "key": api_key,
30
+ "num": count,
31
+ }
32
+
33
+ response = requests.request("GET", url, headers=headers, params=params)
34
+ response.raise_for_status()
35
+
36
+ json_response = response.json()
37
+ results = json_response.get("items", [])
38
+ return [
39
+ SearchResult(
40
+ link=result["link"],
41
+ title=result.get("title"),
42
+ snippet=result.get("snippet"),
43
+ )
44
+ for result in results
45
+ ]
backend/apps/rag/search/main.py ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ from typing import Optional
2
+
3
+ from pydantic import BaseModel
4
+
5
+
6
+ class SearchResult(BaseModel):
7
+ link: str
8
+ title: Optional[str]
9
+ snippet: Optional[str]
backend/apps/rag/search/searxng.py ADDED
@@ -0,0 +1,83 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import logging
2
+ import requests
3
+
4
+ from typing import List
5
+
6
+ from apps.rag.search.main import SearchResult
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, query: str, count: int, **kwargs
15
+ ) -> List[SearchResult]:
16
+ """
17
+ Search a SearXNG instance for a given query and return the results as a list of SearchResult objects.
18
+
19
+ The function allows passing additional parameters such as language or time_range to tailor the search result.
20
+
21
+ Args:
22
+ query_url (str): The base URL of the SearXNG server.
23
+ query (str): The search term or question to find in the SearXNG database.
24
+ count (int): The maximum number of results to retrieve from the search.
25
+
26
+ Keyword Args:
27
+ language (str): Language filter for the search results; e.g., "en-US". Defaults to an empty string.
28
+ time_range (str): Time range for filtering results by date; e.g., "2023-04-05..today" or "all-time". Defaults to ''.
29
+ categories: (Optional[List[str]]): Specific categories within which the search should be performed, defaulting to an empty string if not provided.
30
+
31
+ Returns:
32
+ List[SearchResult]: A list of SearchResults sorted by relevance score in descending order.
33
+
34
+ Raise:
35
+ requests.exceptions.RequestException: If a request error occurs during the search process.
36
+ """
37
+
38
+ # Default values for optional parameters are provided as empty strings or None when not specified.
39
+ language = kwargs.get("language", "en-US")
40
+ time_range = kwargs.get("time_range", "")
41
+ categories = "".join(kwargs.get("categories", []))
42
+
43
+ params = {
44
+ "q": query,
45
+ "format": "json",
46
+ "pageno": 1,
47
+ "language": language,
48
+ "time_range": time_range,
49
+ "categories": categories,
50
+ "theme": "simple",
51
+ "image_proxy": 0,
52
+ }
53
+
54
+ # Legacy query format
55
+ if "<query>" in query_url:
56
+ # Strip all query parameters from the URL
57
+ query_url = query_url.split("?")[0]
58
+
59
+ log.debug(f"searching {query_url}")
60
+
61
+ response = requests.get(
62
+ query_url,
63
+ headers={
64
+ "User-Agent": "Open WebUI (https://github.com/open-webui/open-webui) RAG Bot",
65
+ "Accept": "text/html",
66
+ "Accept-Encoding": "gzip, deflate",
67
+ "Accept-Language": "en-US,en;q=0.5",
68
+ "Connection": "keep-alive",
69
+ },
70
+ params=params,
71
+ )
72
+
73
+ response.raise_for_status() # Raise an exception for HTTP errors.
74
+
75
+ json_response = response.json()
76
+ results = json_response.get("results", [])
77
+ sorted_results = sorted(results, key=lambda x: x.get("score", 0), reverse=True)
78
+ return [
79
+ SearchResult(
80
+ link=result["url"], title=result.get("title"), snippet=result.get("content")
81
+ )
82
+ for result in sorted_results[:count]
83
+ ]
backend/apps/rag/search/serper.py ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ import logging
3
+
4
+ import requests
5
+
6
+ from apps.rag.search.main import SearchResult
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(api_key: str, query: str, count: int) -> list[SearchResult]:
14
+ """Search using serper.dev's API and return the results as a list of SearchResult objects.
15
+
16
+ Args:
17
+ api_key (str): A serper.dev API key
18
+ query (str): The query to search for
19
+ """
20
+ url = "https://google.serper.dev/search"
21
+
22
+ payload = json.dumps({"q": query})
23
+ headers = {"X-API-KEY": api_key, "Content-Type": "application/json"}
24
+
25
+ response = requests.request("POST", url, headers=headers, data=payload)
26
+ response.raise_for_status()
27
+
28
+ json_response = response.json()
29
+ results = sorted(
30
+ json_response.get("organic", []), key=lambda x: x.get("position", 0)
31
+ )
32
+ return [
33
+ SearchResult(
34
+ link=result["link"],
35
+ title=result.get("title"),
36
+ snippet=result.get("description"),
37
+ )
38
+ for result in results[:count]
39
+ ]
backend/apps/rag/search/serpstack.py ADDED
@@ -0,0 +1,43 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ import logging
3
+
4
+ import requests
5
+
6
+ from apps.rag.search.main import SearchResult
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, query: str, count: int, https_enabled: bool = True
15
+ ) -> list[SearchResult]:
16
+ """Search using serpstack.com's and return the results as a list of SearchResult objects.
17
+
18
+ Args:
19
+ api_key (str): A serpstack.com API key
20
+ query (str): The query to search for
21
+ https_enabled (bool): Whether to use HTTPS or HTTP for the API request
22
+ """
23
+ url = f"{'https' if https_enabled else 'http'}://api.serpstack.com/search"
24
+
25
+ headers = {"Content-Type": "application/json"}
26
+ params = {
27
+ "access_key": api_key,
28
+ "query": query,
29
+ }
30
+
31
+ response = requests.request("POST", url, headers=headers, params=params)
32
+ response.raise_for_status()
33
+
34
+ json_response = response.json()
35
+ results = sorted(
36
+ json_response.get("organic_results", []), key=lambda x: x.get("position", 0)
37
+ )
38
+ return [
39
+ SearchResult(
40
+ link=result["url"], title=result.get("title"), snippet=result.get("snippet")
41
+ )
42
+ for result in results[:count]
43
+ ]
backend/apps/rag/search/testdata/brave.json ADDED
@@ -0,0 +1,998 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "query": {
3
+ "original": "python",
4
+ "show_strict_warning": false,
5
+ "is_navigational": true,
6
+ "is_news_breaking": false,
7
+ "spellcheck_off": true,
8
+ "country": "us",
9
+ "bad_results": false,
10
+ "should_fallback": false,
11
+ "postal_code": "",
12
+ "city": "",
13
+ "header_country": "",
14
+ "more_results_available": true,
15
+ "state": ""
16
+ },
17
+ "mixed": {
18
+ "type": "mixed",
19
+ "main": [
20
+ {
21
+ "type": "web",
22
+ "index": 0,
23
+ "all": false
24
+ },
25
+ {
26
+ "type": "web",
27
+ "index": 1,
28
+ "all": false
29
+ },
30
+ {
31
+ "type": "news",
32
+ "all": true
33
+ },
34
+ {
35
+ "type": "web",
36
+ "index": 2,
37
+ "all": false
38
+ },
39
+ {
40
+ "type": "videos",
41
+ "all": true
42
+ },
43
+ {
44
+ "type": "web",
45
+ "index": 3,
46
+ "all": false
47
+ },
48
+ {
49
+ "type": "web",
50
+ "index": 4,
51
+ "all": false
52
+ },
53
+ {
54
+ "type": "web",
55
+ "index": 5,
56
+ "all": false
57
+ },
58
+ {
59
+ "type": "web",
60
+ "index": 6,
61
+ "all": false
62
+ },
63
+ {
64
+ "type": "web",
65
+ "index": 7,
66
+ "all": false
67
+ },
68
+ {
69
+ "type": "web",
70
+ "index": 8,
71
+ "all": false
72
+ },
73
+ {
74
+ "type": "web",
75
+ "index": 9,
76
+ "all": false
77
+ },
78
+ {
79
+ "type": "web",
80
+ "index": 10,
81
+ "all": false
82
+ },
83
+ {
84
+ "type": "web",
85
+ "index": 11,
86
+ "all": false
87
+ },
88
+ {
89
+ "type": "web",
90
+ "index": 12,
91
+ "all": false
92
+ },
93
+ {
94
+ "type": "web",
95
+ "index": 13,
96
+ "all": false
97
+ },
98
+ {
99
+ "type": "web",
100
+ "index": 14,
101
+ "all": false
102
+ },
103
+ {
104
+ "type": "web",
105
+ "index": 15,
106
+ "all": false
107
+ },
108
+ {
109
+ "type": "web",
110
+ "index": 16,
111
+ "all": false
112
+ },
113
+ {
114
+ "type": "web",
115
+ "index": 17,
116
+ "all": false
117
+ },
118
+ {
119
+ "type": "web",
120
+ "index": 18,
121
+ "all": false
122
+ },
123
+ {
124
+ "type": "web",
125
+ "index": 19,
126
+ "all": false
127
+ }
128
+ ],
129
+ "top": [],
130
+ "side": []
131
+ },
132
+ "news": {
133
+ "type": "news",
134
+ "results": [
135
+ {
136
+ "title": "Google lays off staff from Flutter, Dart and Python teams weeks before its developer conference | TechCrunch",
137
+ "url": "https://techcrunch.com/2024/05/01/google-lays-off-staff-from-flutter-dart-python-weeks-before-its-developer-conference/",
138
+ "is_source_local": false,
139
+ "is_source_both": false,
140
+ "description": "Google told TechCrunch that Flutter will have new updates to share at I/O this year.",
141
+ "page_age": "2024-05-02T17:40:05",
142
+ "family_friendly": true,
143
+ "meta_url": {
144
+ "scheme": "https",
145
+ "netloc": "techcrunch.com",
146
+ "hostname": "techcrunch.com",
147
+ "favicon": "https://imgs.search.brave.com/N6VSEVahheQOb7lqfb47dhUOB4XD-6sfQOP94sCe3Oo/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvZGI5Njk0Yzlk/YWM3ZWMwZjg1MTM1/NmIyMWEyNzBjZDZj/ZDQyNmFlNGU0NDRi/MDgyYjQwOGU0Y2Qy/ZWMwNWQ2ZC90ZWNo/Y3J1bmNoLmNvbS8",
148
+ "path": "› 2024 › 05 › 01 › google-lays-off-staff-from-flutter-dart-python-weeks-before-its-developer-conference"
149
+ },
150
+ "breaking": false,
151
+ "thumbnail": {
152
+ "src": "https://imgs.search.brave.com/gCI5UG8muOEOZDAx9vpu6L6r6R00mD7jOF08-biFoyQ/rs:fit:200:200:1/g:ce/aHR0cHM6Ly90ZWNo/Y3J1bmNoLmNvbS93/cC1jb250ZW50L3Vw/bG9hZHMvMjAxOC8x/MS9HZXR0eUltYWdl/cy0xMDAyNDg0NzQ2/LmpwZz9yZXNpemU9/MTIwMCw4MDA"
153
+ },
154
+ "age": "3 days ago",
155
+ "extra_snippets": [
156
+ "Ahead of Google’s annual I/O developer conference in May, the tech giant has laid off staff across key teams like Flutter, Dart, Python and others, according to reports from affected employees shared on social media. Google confirmed the layoffs to TechCrunch, but not the specific teams, roles or how many people were let go.",
157
+ "In a separate post on Reddit, another commenter noted the Python team affected by the layoffs were those who managed the internal Python runtimes and toolchains and worked with OSS Python. Included in this group were “multiple current and former core devs and steering council members,” they said.",
158
+ "Meanwhile, others shared on Y Combinator’s Hacker News, where a Python team member detailed their specific duties on the technical front and noted that, for years, much of the work was done with fewer than 10 people. Another Hacker News commenter said their early years on the Python team were spent paying down internal technical debt accumulated from not having a strong Python strategy.",
159
+ "CNBC reports that a total of 200 people were let go across Google’s “Core” teams, which included those working on Python, app platforms, and other engineering roles. Some jobs were being shifted to India and Mexico, it said, citing internal documents."
160
+ ]
161
+ }
162
+ ],
163
+ "mutated_by_goggles": false
164
+ },
165
+ "type": "search",
166
+ "videos": {
167
+ "type": "videos",
168
+ "results": [
169
+ {
170
+ "type": "video_result",
171
+ "url": "https://www.youtube.com/watch?v=b093aqAZiPU",
172
+ "title": "👩‍💻 Python for Beginners Tutorial - YouTube",
173
+ "description": "In this step-by-step Python for beginner's tutorial, learn how you can get started programming in Python. In this video, I assume that you are completely new...",
174
+ "age": "March 25, 2021",
175
+ "page_age": "2021-03-25T10:00:08",
176
+ "video": {},
177
+ "meta_url": {
178
+ "scheme": "https",
179
+ "netloc": "youtube.com",
180
+ "hostname": "www.youtube.com",
181
+ "favicon": "https://imgs.search.brave.com/Ux4Hee4evZhvjuTKwtapBycOGjGDci2Gvn2pbSzvbC0/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvOTkyZTZiMWU3/YzU3Nzc5YjExYzUy/N2VhZTIxOWNlYjM5/ZGVjN2MyZDY4Nzdh/ZDYzMTYxNmI5N2Rk/Y2Q3N2FkNy93d3cu/eW91dHViZS5jb20v",
182
+ "path": "› watch"
183
+ },
184
+ "thumbnail": {
185
+ "src": "https://imgs.search.brave.com/tZI4Do4_EYcTCsD_MvE3Jx8FzjIXwIJ5ZuKhwiWTyZs/rs:fit:200:200:1/g:ce/aHR0cHM6Ly9pLnl0/aW1nLmNvbS92aS9i/MDkzYXFBWmlQVS9t/YXhyZXNkZWZhdWx0/LmpwZw"
186
+ }
187
+ },
188
+ {
189
+ "type": "video_result",
190
+ "url": "https://www.youtube.com/watch?v=rfscVS0vtbw",
191
+ "title": "Learn Python - Full Course for Beginners [Tutorial] - YouTube",
192
+ "description": "This course will give you a full introduction into all of the core concepts in python. Follow along with the videos and you'll be a python programmer in no t...",
193
+ "age": "July 11, 2018",
194
+ "page_age": "2018-07-11T18:00:42",
195
+ "video": {},
196
+ "meta_url": {
197
+ "scheme": "https",
198
+ "netloc": "youtube.com",
199
+ "hostname": "www.youtube.com",
200
+ "favicon": "https://imgs.search.brave.com/Ux4Hee4evZhvjuTKwtapBycOGjGDci2Gvn2pbSzvbC0/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvOTkyZTZiMWU3/YzU3Nzc5YjExYzUy/N2VhZTIxOWNlYjM5/ZGVjN2MyZDY4Nzdh/ZDYzMTYxNmI5N2Rk/Y2Q3N2FkNy93d3cu/eW91dHViZS5jb20v",
201
+ "path": "› watch"
202
+ },
203
+ "thumbnail": {
204
+ "src": "https://imgs.search.brave.com/65zkx_kPU_zJb-4nmvvY-q5-ZZwzceChz-N00V8cqvk/rs:fit:200:200:1/g:ce/aHR0cHM6Ly9pLnl0/aW1nLmNvbS92aS9y/ZnNjVlMwdnRidy9t/YXhyZXNkZWZhdWx0/LmpwZw"
205
+ }
206
+ },
207
+ {
208
+ "type": "video_result",
209
+ "url": "https://www.youtube.com/watch?v=_uQrJ0TkZlc",
210
+ "title": "Python Tutorial - Python Full Course for Beginners - YouTube",
211
+ "description": "Become a Python pro! 🚀 This comprehensive tutorial takes you from beginner to hero, covering the basics, machine learning, and web development projects.🚀 W...",
212
+ "age": "February 18, 2019",
213
+ "page_age": "2019-02-18T15:00:08",
214
+ "video": {},
215
+ "meta_url": {
216
+ "scheme": "https",
217
+ "netloc": "youtube.com",
218
+ "hostname": "www.youtube.com",
219
+ "favicon": "https://imgs.search.brave.com/Ux4Hee4evZhvjuTKwtapBycOGjGDci2Gvn2pbSzvbC0/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvOTkyZTZiMWU3/YzU3Nzc5YjExYzUy/N2VhZTIxOWNlYjM5/ZGVjN2MyZDY4Nzdh/ZDYzMTYxNmI5N2Rk/Y2Q3N2FkNy93d3cu/eW91dHViZS5jb20v",
220
+ "path": "› watch"
221
+ },
222
+ "thumbnail": {
223
+ "src": "https://imgs.search.brave.com/Djiv1pXLq1ClqBSE_86jQnEYR8bW8UJP6Cs7LrgyQzQ/rs:fit:200:200:1/g:ce/aHR0cHM6Ly9pLnl0/aW1nLmNvbS92aS9f/dVFySjBUa1psYy9t/YXhyZXNkZWZhdWx0/LmpwZw"
224
+ }
225
+ },
226
+ {
227
+ "type": "video_result",
228
+ "url": "https://www.youtube.com/watch?v=wRKgzC-MhIc",
229
+ "title": "[] and {} vs list() and dict(), which is better?",
230
+ "description": "Enjoy the videos and music you love, upload original content, and share it all with friends, family, and the world on YouTube.",
231
+ "video": {},
232
+ "meta_url": {
233
+ "scheme": "https",
234
+ "netloc": "youtube.com",
235
+ "hostname": "www.youtube.com",
236
+ "favicon": "https://imgs.search.brave.com/Ux4Hee4evZhvjuTKwtapBycOGjGDci2Gvn2pbSzvbC0/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvOTkyZTZiMWU3/YzU3Nzc5YjExYzUy/N2VhZTIxOWNlYjM5/ZGVjN2MyZDY4Nzdh/ZDYzMTYxNmI5N2Rk/Y2Q3N2FkNy93d3cu/eW91dHViZS5jb20v",
237
+ "path": "› watch"
238
+ },
239
+ "thumbnail": {
240
+ "src": "https://imgs.search.brave.com/Hw9ep2Pio13X1VZjRw_h9R2VH_XvZFOuGlQJVnVkeq0/rs:fit:200:200:1/g:ce/aHR0cHM6Ly9pLnl0/aW1nLmNvbS92aS93/UktnekMtTWhJYy9o/cWRlZmF1bHQuanBn"
241
+ }
242
+ },
243
+ {
244
+ "type": "video_result",
245
+ "url": "https://www.youtube.com/watch?v=LWdsF79H1Pg",
246
+ "title": "print() vs. return in Python Functions - YouTube",
247
+ "description": "In this video, you will learn the differences between the return statement and the print function when they are used inside Python functions. We will see an ...",
248
+ "age": "June 11, 2022",
249
+ "page_age": "2022-06-11T21:33:26",
250
+ "video": {},
251
+ "meta_url": {
252
+ "scheme": "https",
253
+ "netloc": "youtube.com",
254
+ "hostname": "www.youtube.com",
255
+ "favicon": "https://imgs.search.brave.com/Ux4Hee4evZhvjuTKwtapBycOGjGDci2Gvn2pbSzvbC0/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvOTkyZTZiMWU3/YzU3Nzc5YjExYzUy/N2VhZTIxOWNlYjM5/ZGVjN2MyZDY4Nzdh/ZDYzMTYxNmI5N2Rk/Y2Q3N2FkNy93d3cu/eW91dHViZS5jb20v",
256
+ "path": "› watch"
257
+ },
258
+ "thumbnail": {
259
+ "src": "https://imgs.search.brave.com/ebglnr5_jwHHpvon3WU-5hzt0eHdTZSVGg3Ts6R38xY/rs:fit:200:200:1/g:ce/aHR0cHM6Ly9pLnl0/aW1nLmNvbS92aS9M/V2RzRjc5SDFQZy9t/YXhyZXNkZWZhdWx0/LmpwZw"
260
+ }
261
+ },
262
+ {
263
+ "type": "video_result",
264
+ "url": "https://www.youtube.com/watch?v=AovxLr8jUH4",
265
+ "title": "Python Tutorial for Beginners 5 - Python print() and input() Function ...",
266
+ "description": "In this Video I am going to show How to use print() Function and input() Function in Python. In python The print() function is used to print the specified ...",
267
+ "age": "August 28, 2018",
268
+ "page_age": "2018-08-28T20:11:09",
269
+ "video": {},
270
+ "meta_url": {
271
+ "scheme": "https",
272
+ "netloc": "youtube.com",
273
+ "hostname": "www.youtube.com",
274
+ "favicon": "https://imgs.search.brave.com/Ux4Hee4evZhvjuTKwtapBycOGjGDci2Gvn2pbSzvbC0/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvOTkyZTZiMWU3/YzU3Nzc5YjExYzUy/N2VhZTIxOWNlYjM5/ZGVjN2MyZDY4Nzdh/ZDYzMTYxNmI5N2Rk/Y2Q3N2FkNy93d3cu/eW91dHViZS5jb20v",
275
+ "path": "› watch"
276
+ },
277
+ "thumbnail": {
278
+ "src": "https://imgs.search.brave.com/nCoLEcWkKtiecprWbS6nufwGCaSbPH7o0-sMeIkFmjI/rs:fit:200:200:1/g:ce/aHR0cHM6Ly9pLnl0/aW1nLmNvbS92aS9B/b3Z4THI4alVINC9o/cWRlZmF1bHQuanBn"
279
+ }
280
+ }
281
+ ],
282
+ "mutated_by_goggles": false
283
+ },
284
+ "web": {
285
+ "type": "search",
286
+ "results": [
287
+ {
288
+ "title": "Welcome to Python.org",
289
+ "url": "https://www.python.org",
290
+ "is_source_local": false,
291
+ "is_source_both": false,
292
+ "description": "The official home of the <strong>Python</strong> Programming Language",
293
+ "page_age": "2023-09-09T15:55:05",
294
+ "profile": {
295
+ "name": "Python",
296
+ "url": "https://www.python.org",
297
+ "long_name": "python.org",
298
+ "img": "https://imgs.search.brave.com/vBaRH-v6oPS4csO4cdvuKhZ7-xDVvydin3oe3zXYxAI/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvNTJjMzZjNDBj/MmIzODgwMGUyOTRj/Y2E5MjM3YjRkYTZj/YWI1Yzk1NTlmYTgw/ZDBjNzM0MGMxZjQz/YWFjNTczYy93d3cu/cHl0aG9uLm9yZy8"
299
+ },
300
+ "language": "en",
301
+ "family_friendly": true,
302
+ "type": "search_result",
303
+ "subtype": "generic",
304
+ "meta_url": {
305
+ "scheme": "https",
306
+ "netloc": "python.org",
307
+ "hostname": "www.python.org",
308
+ "favicon": "https://imgs.search.brave.com/vBaRH-v6oPS4csO4cdvuKhZ7-xDVvydin3oe3zXYxAI/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvNTJjMzZjNDBj/MmIzODgwMGUyOTRj/Y2E5MjM3YjRkYTZj/YWI1Yzk1NTlmYTgw/ZDBjNzM0MGMxZjQz/YWFjNTczYy93d3cu/cHl0aG9uLm9yZy8",
309
+ "path": ""
310
+ },
311
+ "thumbnail": {
312
+ "src": "https://imgs.search.brave.com/GGfNfe5rxJ8QWEoxXniSLc0-POLU3qPyTIpuqPdbmXk/rs:fit:200:200:1/g:ce/aHR0cHM6Ly93d3cu/cHl0aG9uLm9yZy9z/dGF0aWMvb3Blbmdy/YXBoLWljb24tMjAw/eDIwMC5wbmc",
313
+ "original": "https://www.python.org/static/opengraph-icon-200x200.png",
314
+ "logo": false
315
+ },
316
+ "age": "September 9, 2023",
317
+ "cluster_type": "generic",
318
+ "cluster": [
319
+ {
320
+ "title": "Downloads",
321
+ "url": "https://www.python.org/downloads/",
322
+ "is_source_local": false,
323
+ "is_source_both": false,
324
+ "description": "The official home of the <strong>Python</strong> Programming Language",
325
+ "family_friendly": true
326
+ },
327
+ {
328
+ "title": "Macos",
329
+ "url": "https://www.python.org/downloads/macos/",
330
+ "is_source_local": false,
331
+ "is_source_both": false,
332
+ "description": "The official home of the <strong>Python</strong> Programming Language",
333
+ "family_friendly": true
334
+ },
335
+ {
336
+ "title": "Windows",
337
+ "url": "https://www.python.org/downloads/windows/",
338
+ "is_source_local": false,
339
+ "is_source_both": false,
340
+ "description": "The official home of the <strong>Python</strong> Programming Language",
341
+ "family_friendly": true
342
+ },
343
+ {
344
+ "title": "Getting Started",
345
+ "url": "https://www.python.org/about/gettingstarted/",
346
+ "is_source_local": false,
347
+ "is_source_both": false,
348
+ "description": "The official home of the <strong>Python</strong> Programming Language",
349
+ "family_friendly": true
350
+ }
351
+ ],
352
+ "extra_snippets": [
353
+ "Calculations are simple with Python, and expression syntax is straightforward: the operators +, -, * and / work as expected; parentheses () can be used for grouping. More about simple math functions in Python 3.",
354
+ "The core of extensible programming is defining functions. Python allows mandatory and optional arguments, keyword arguments, and even arbitrary argument lists. More about defining functions in Python 3",
355
+ "Lists (known as arrays in other languages) are one of the compound data types that Python understands. Lists can be indexed, sliced and manipulated with other built-in functions. More about lists in Python 3",
356
+ "# Python 3: Simple output (with Unicode) >>> print(\"Hello, I'm Python!\") Hello, I'm Python! # Input, assignment >>> name = input('What is your name?\\n') >>> print('Hi, %s.' % name) What is your name? Python Hi, Python."
357
+ ]
358
+ },
359
+ {
360
+ "title": "Python (programming language) - Wikipedia",
361
+ "url": "https://en.wikipedia.org/wiki/Python_(programming_language)",
362
+ "is_source_local": false,
363
+ "is_source_both": false,
364
+ "description": "<strong>Python</strong> is a high-level, general-purpose programming language. Its design philosophy emphasizes code readability with the use of significant indentation. <strong>Python</strong> is dynamically typed and garbage-collected. It supports multiple programming paradigms, including structured (particularly procedural), ...",
365
+ "page_age": "2024-05-01T12:54:03",
366
+ "profile": {
367
+ "name": "Wikipedia",
368
+ "url": "https://en.wikipedia.org/wiki/Python_(programming_language)",
369
+ "long_name": "en.wikipedia.org",
370
+ "img": "https://imgs.search.brave.com/0kxnVOiqv-faZvOJc7zpym4Zin1CTs1f1svfNZSzmfU/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvNjQwNGZhZWY0/ZTQ1YWUzYzQ3MDUw/MmMzMGY3NTQ0ZjNj/NDUwMDk5ZTI3MWRk/NWYyNTM4N2UwOTE0/NTI3ZDQzNy9lbi53/aWtpcGVkaWEub3Jn/Lw"
371
+ },
372
+ "language": "en",
373
+ "family_friendly": true,
374
+ "type": "search_result",
375
+ "subtype": "generic",
376
+ "meta_url": {
377
+ "scheme": "https",
378
+ "netloc": "en.wikipedia.org",
379
+ "hostname": "en.wikipedia.org",
380
+ "favicon": "https://imgs.search.brave.com/0kxnVOiqv-faZvOJc7zpym4Zin1CTs1f1svfNZSzmfU/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvNjQwNGZhZWY0/ZTQ1YWUzYzQ3MDUw/MmMzMGY3NTQ0ZjNj/NDUwMDk5ZTI3MWRk/NWYyNTM4N2UwOTE0/NTI3ZDQzNy9lbi53/aWtpcGVkaWEub3Jn/Lw",
381
+ "path": "› wiki › Python_(programming_language)"
382
+ },
383
+ "age": "4 days ago",
384
+ "extra_snippets": [
385
+ "Python is dynamically typed and garbage-collected. It supports multiple programming paradigms, including structured (particularly procedural), object-oriented and functional programming. It is often described as a \"batteries included\" language due to its comprehensive standard library.",
386
+ "Guido van Rossum began working on Python in the late 1980s as a successor to the ABC programming language and first released it in 1991 as Python 0.9.0. Python 2.0 was released in 2000. Python 3.0, released in 2008, was a major revision not completely backward-compatible with earlier versions. Python 2.7.18, released in 2020, was the last release of Python 2.",
387
+ "Python was invented in the late 1980s by Guido van Rossum at Centrum Wiskunde & Informatica (CWI) in the Netherlands as a successor to the ABC programming language, which was inspired by SETL, capable of exception handling and interfacing with the Amoeba operating system.",
388
+ "Python consistently ranks as one of the most popular programming languages, and has gained widespread use in the machine learning community."
389
+ ]
390
+ },
391
+ {
392
+ "title": "Python Tutorial",
393
+ "url": "https://www.w3schools.com/python/",
394
+ "is_source_local": false,
395
+ "is_source_both": false,
396
+ "description": "W3Schools offers free online tutorials, references and exercises in all the major languages of the web. Covering popular subjects like HTML, CSS, JavaScript, <strong>Python</strong>, SQL, Java, and many, many more.",
397
+ "page_age": "2017-12-07T00:00:00",
398
+ "profile": {
399
+ "name": "W3Schools",
400
+ "url": "https://www.w3schools.com/python/",
401
+ "long_name": "w3schools.com",
402
+ "img": "https://imgs.search.brave.com/JwO5r7z3HTBkU29vgNH_4rrSWLf2M4-8FMWNvbxrKX8/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvYjVlMGVkZDVj/ZGMyZWRmMzAwODRi/ZDAwZGE4NWI3NmU4/MjRhNjEzOGFhZWY3/ZGViMjY1OWY2ZDYw/YTZiOGUyZS93d3cu/dzNzY2hvb2xzLmNv/bS8"
403
+ },
404
+ "language": "en",
405
+ "family_friendly": true,
406
+ "type": "search_result",
407
+ "subtype": "generic",
408
+ "meta_url": {
409
+ "scheme": "https",
410
+ "netloc": "w3schools.com",
411
+ "hostname": "www.w3schools.com",
412
+ "favicon": "https://imgs.search.brave.com/JwO5r7z3HTBkU29vgNH_4rrSWLf2M4-8FMWNvbxrKX8/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvYjVlMGVkZDVj/ZGMyZWRmMzAwODRi/ZDAwZGE4NWI3NmU4/MjRhNjEzOGFhZWY3/ZGViMjY1OWY2ZDYw/YTZiOGUyZS93d3cu/dzNzY2hvb2xzLmNv/bS8",
413
+ "path": "› python"
414
+ },
415
+ "thumbnail": {
416
+ "src": "https://imgs.search.brave.com/EMfp8dodbJehmj0yCJh8317RHuaumsddnHI4bujvFcg/rs:fit:200:200:1/g:ce/aHR0cHM6Ly93d3cu/dzNzY2hvb2xzLmNv/bS9pbWFnZXMvdzNz/Y2hvb2xzX2xvZ29f/NDM2XzIucG5n",
417
+ "original": "https://www.w3schools.com/images/w3schools_logo_436_2.png",
418
+ "logo": true
419
+ },
420
+ "age": "December 7, 2017",
421
+ "extra_snippets": [
422
+ "Well organized and easy to understand Web building tutorials with lots of examples of how to use HTML, CSS, JavaScript, SQL, Python, PHP, Bootstrap, Java, XML and more.",
423
+ "HTML CSS JAVASCRIPT SQL PYTHON JAVA PHP HOW TO W3.CSS C C++ C# BOOTSTRAP REACT MYSQL JQUERY EXCEL XML DJANGO NUMPY PANDAS NODEJS R TYPESCRIPT ANGULAR GIT POSTGRESQL MONGODB ASP AI GO KOTLIN SASS VUE DSA GEN AI SCIPY AWS CYBERSECURITY DATA SCIENCE",
424
+ "Python Variables Variable Names Assign Multiple Values Output Variables Global Variables Variable Exercises Python Data Types Python Numbers Python Casting Python Strings",
425
+ "Python Strings Slicing Strings Modify Strings Concatenate Strings Format Strings Escape Characters String Methods String Exercises Python Booleans Python Operators Python Lists"
426
+ ]
427
+ },
428
+ {
429
+ "title": "Online Python - IDE, Editor, Compiler, Interpreter",
430
+ "url": "https://www.online-python.com/",
431
+ "is_source_local": false,
432
+ "is_source_both": false,
433
+ "description": "Build and Run your <strong>Python</strong> code instantly. Online-<strong>Python</strong> is a quick and easy tool that helps you to build, compile, test your <strong>python</strong> programs.",
434
+ "profile": {
435
+ "name": "Online-python",
436
+ "url": "https://www.online-python.com/",
437
+ "long_name": "online-python.com",
438
+ "img": "https://imgs.search.brave.com/kfaEvapwHxSsRObO52-I-otYFPHpG1h7UXJyUqDM2Ec/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvZGYxODdjNWQ0/NjZjZTNiMjk5NDY1/MWI5MTgyYjU3Y2Q3/MTI3NGM5MjUzY2Fi/OGQ3MTQ4MmIxMTQx/ZTcxNWFhMC93d3cu/b25saW5lLXB5dGhv/bi5jb20v"
439
+ },
440
+ "language": "en",
441
+ "family_friendly": true,
442
+ "type": "search_result",
443
+ "subtype": "generic",
444
+ "meta_url": {
445
+ "scheme": "https",
446
+ "netloc": "online-python.com",
447
+ "hostname": "www.online-python.com",
448
+ "favicon": "https://imgs.search.brave.com/kfaEvapwHxSsRObO52-I-otYFPHpG1h7UXJyUqDM2Ec/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvZGYxODdjNWQ0/NjZjZTNiMjk5NDY1/MWI5MTgyYjU3Y2Q3/MTI3NGM5MjUzY2Fi/OGQ3MTQ4MmIxMTQx/ZTcxNWFhMC93d3cu/b25saW5lLXB5dGhv/bi5jb20v",
449
+ "path": ""
450
+ },
451
+ "extra_snippets": [
452
+ "Build, run, and share Python code online for free with the help of online-integrated python's development environment (IDE). It is one of the most efficient, dependable, and potent online compilers for the Python programming language. It is not necessary for you to bother about establishing a Python environment in your local.",
453
+ "It is one of the most efficient, dependable, and potent online compilers for the Python programming language. It is not necessary for you to bother about establishing a Python environment in your local. Now You can immediately execute the Python code in the web browser of your choice.",
454
+ "It is not necessary for you to bother about establishing a Python environment in your local. Now You can immediately execute the Python code in the web browser of your choice. Using this Python editor is simple and quick to get up and running with. Simply type in the programme, and then press the RUN button!",
455
+ "Now You can immediately execute the Python code in the web browser of your choice. Using this Python editor is simple and quick to get up and running with. Simply type in the programme, and then press the RUN button! The code can be saved online by choosing the SHARE option, which also gives you the ability to access your code from any location providing you have internet access."
456
+ ]
457
+ },
458
+ {
459
+ "title": "Python · GitHub",
460
+ "url": "https://github.com/python",
461
+ "is_source_local": false,
462
+ "is_source_both": false,
463
+ "description": "Repositories related to the <strong>Python</strong> Programming language - <strong>Python</strong>",
464
+ "page_age": "2023-03-06T00:00:00",
465
+ "profile": {
466
+ "name": "GitHub",
467
+ "url": "https://github.com/python",
468
+ "long_name": "github.com",
469
+ "img": "https://imgs.search.brave.com/v8685zI4XInM0zxlNI2s7oE_2Sb-EL7lAy81WXbkQD8/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvYWQyNWM1NjA5/ZjZmZjNlYzI2MDNk/N2VkNmJhYjE2MzZl/MDY5ZTMxMDUzZmY1/NmU3NWIzNWVmMjk0/NTBjMjJjZi9naXRo/dWIuY29tLw"
470
+ },
471
+ "language": "en",
472
+ "family_friendly": true,
473
+ "type": "search_result",
474
+ "subtype": "generic",
475
+ "meta_url": {
476
+ "scheme": "https",
477
+ "netloc": "github.com",
478
+ "hostname": "github.com",
479
+ "favicon": "https://imgs.search.brave.com/v8685zI4XInM0zxlNI2s7oE_2Sb-EL7lAy81WXbkQD8/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvYWQyNWM1NjA5/ZjZmZjNlYzI2MDNk/N2VkNmJhYjE2MzZl/MDY5ZTMxMDUzZmY1/NmU3NWIzNWVmMjk0/NTBjMjJjZi9naXRo/dWIuY29tLw",
480
+ "path": "› python"
481
+ },
482
+ "thumbnail": {
483
+ "src": "https://imgs.search.brave.com/POoaRfu_7gfp-D_O3qMNJrwDqJNbiDu1HuBpNJ_MpVQ/rs:fit:200:200:1/g:ce/aHR0cHM6Ly9hdmF0/YXJzLmdpdGh1YnVz/ZXJjb250ZW50LmNv/bS91LzE1MjU5ODE_/cz0yMDAmYW1wO3Y9/NA",
484
+ "original": "https://avatars.githubusercontent.com/u/1525981?s=200&amp;v=4",
485
+ "logo": false
486
+ },
487
+ "age": "March 6, 2023",
488
+ "extra_snippets": ["Configuration for Python planets (e.g. http://planetpython.org)"]
489
+ },
490
+ {
491
+ "title": "Online Python Compiler (Interpreter)",
492
+ "url": "https://www.programiz.com/python-programming/online-compiler/",
493
+ "is_source_local": false,
494
+ "is_source_both": false,
495
+ "description": "Write and run <strong>Python</strong> code using our online compiler (interpreter). You can use <strong>Python</strong> Shell like IDLE, and take inputs from the user in our <strong>Python</strong> compiler.",
496
+ "page_age": "2020-06-02T00:00:00",
497
+ "profile": {
498
+ "name": "Programiz",
499
+ "url": "https://www.programiz.com/python-programming/online-compiler/",
500
+ "long_name": "programiz.com",
501
+ "img": "https://imgs.search.brave.com/ozj4JFayZ3Fs5c9eTp7M5g12azQ_Hblgu4dpTuHRz6U/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvMGJlN2U1YjVi/Y2M3ZDU5OGMwMWNi/M2Q3YjhjOTM1ZTFk/Y2NkZjE4NGQwOGIx/MTQ4NjI2YmNhODVj/MzFkMmJhYy93d3cu/cHJvZ3JhbWl6LmNv/bS8"
502
+ },
503
+ "language": "en",
504
+ "family_friendly": true,
505
+ "type": "search_result",
506
+ "subtype": "generic",
507
+ "meta_url": {
508
+ "scheme": "https",
509
+ "netloc": "programiz.com",
510
+ "hostname": "www.programiz.com",
511
+ "favicon": "https://imgs.search.brave.com/ozj4JFayZ3Fs5c9eTp7M5g12azQ_Hblgu4dpTuHRz6U/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvMGJlN2U1YjVi/Y2M3ZDU5OGMwMWNi/M2Q3YjhjOTM1ZTFk/Y2NkZjE4NGQwOGIx/MTQ4NjI2YmNhODVj/MzFkMmJhYy93d3cu/cHJvZ3JhbWl6LmNv/bS8",
512
+ "path": "› python-programming › online-compiler"
513
+ },
514
+ "age": "June 2, 2020",
515
+ "extra_snippets": [
516
+ "Python Online Compiler Online R Compiler SQL Online Editor Online HTML/CSS Editor Online Java Compiler C Online Compiler C++ Online Compiler C# Online Compiler JavaScript Online Compiler Online GoLang Compiler Online PHP Compiler Online Swift Compiler Online Rust Compiler",
517
+ "# Online Python compiler (interpreter) to run Python online. # Write Python 3 code in this online editor and run it. print(\"Try programiz.pro\")"
518
+ ]
519
+ },
520
+ {
521
+ "title": "Python Developer",
522
+ "url": "https://twitter.com/Python_Dv/status/1786763460992544791",
523
+ "is_source_local": false,
524
+ "is_source_both": false,
525
+ "description": "<strong>Python</strong> Developer",
526
+ "page_age": "2024-05-04T14:30:03",
527
+ "profile": {
528
+ "name": "X",
529
+ "url": "https://twitter.com/Python_Dv/status/1786763460992544791",
530
+ "long_name": "twitter.com",
531
+ "img": "https://imgs.search.brave.com/Zq483bGX0GnSgym-1P7iyOyEDX3PkDZSNT8m56F862A/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvN2MxOTUxNzhj/OTY1ZTQ3N2I0MjJk/MTY5NGM0MTRlYWVi/MjU1YWE2NDUwYmQ2/YTA2MDFhMDlkZDEx/NTAzZGNiNi90d2l0/dGVyLmNvbS8"
532
+ },
533
+ "language": "en",
534
+ "family_friendly": true,
535
+ "type": "search_result",
536
+ "subtype": "generic",
537
+ "meta_url": {
538
+ "scheme": "https",
539
+ "netloc": "twitter.com",
540
+ "hostname": "twitter.com",
541
+ "favicon": "https://imgs.search.brave.com/Zq483bGX0GnSgym-1P7iyOyEDX3PkDZSNT8m56F862A/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvN2MxOTUxNzhj/OTY1ZTQ3N2I0MjJk/MTY5NGM0MTRlYWVi/MjU1YWE2NDUwYmQ2/YTA2MDFhMDlkZDEx/NTAzZGNiNi90d2l0/dGVyLmNvbS8",
542
+ "path": "› Python_Dv › status › 1786763460992544791"
543
+ },
544
+ "age": "20 hours ago"
545
+ },
546
+ {
547
+ "title": "input table name? - python script - KNIME Extensions - KNIME Community Forum",
548
+ "url": "https://forum.knime.com/t/input-table-name-python-script/78978",
549
+ "is_source_local": false,
550
+ "is_source_both": false,
551
+ "description": "Hi, when running a <strong>python</strong> script node, I get the error seen on the screenshot Same happens with this code too: The script input is output from the csv reader node. How can I get the right name for that table? Best wishes, Dario",
552
+ "page_age": "2024-05-04T09:20:44",
553
+ "profile": {
554
+ "name": "Knime",
555
+ "url": "https://forum.knime.com/t/input-table-name-python-script/78978",
556
+ "long_name": "forum.knime.com",
557
+ "img": "https://imgs.search.brave.com/WQoOhAD5i6uEhJ-qXvlWMJwbGA52f2Ycc_ns36EK698/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvOTAxNzMxNjFl/MzJjNzU5NzRkOTMz/Mjg4NDU2OWUxM2Rj/YzVkOGM3MzIwNzI2/YTY1NzYxNzA1MDE5/NzQzOWU3NC9mb3J1/bS5rbmltZS5jb20v"
558
+ },
559
+ "language": "en",
560
+ "family_friendly": true,
561
+ "type": "search_result",
562
+ "subtype": "article",
563
+ "meta_url": {
564
+ "scheme": "https",
565
+ "netloc": "forum.knime.com",
566
+ "hostname": "forum.knime.com",
567
+ "favicon": "https://imgs.search.brave.com/WQoOhAD5i6uEhJ-qXvlWMJwbGA52f2Ycc_ns36EK698/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvOTAxNzMxNjFl/MzJjNzU5NzRkOTMz/Mjg4NDU2OWUxM2Rj/YzVkOGM3MzIwNzI2/YTY1NzYxNzA1MDE5/NzQzOWU3NC9mb3J1/bS5rbmltZS5jb20v",
568
+ "path": " › knime extensions"
569
+ },
570
+ "thumbnail": {
571
+ "src": "https://imgs.search.brave.com/DtEl38dcvuM1kGfhN0T5HfOrsMJcztWNyriLvtDJmKI/rs:fit:200:200:1/g:ce/aHR0cHM6Ly9mb3J1/bS1jZG4ua25pbWUu/Y29tL3VwbG9hZHMv/ZGVmYXVsdC9vcmln/aW5hbC8zWC9lLzYv/ZTY0M2M2NzFlNzAz/MDg2MjkwMWY2YzJh/OWFjOWI5ZmEwM2M3/ZjMwZi5wbmc",
572
+ "original": "https://forum-cdn.knime.com/uploads/default/original/3X/e/6/e643c671e7030862901f6c2a9ac9b9fa03c7f30f.png",
573
+ "logo": false
574
+ },
575
+ "age": "1 day ago",
576
+ "extra_snippets": [
577
+ "Hi, when running a python script node, I get the error seen on the screenshot Same happens with this code too: The script input is output from the csv reader node. How can I get the right name for that table? …"
578
+ ]
579
+ },
580
+ {
581
+ "title": "What does the Double Star operator mean in Python? - GeeksforGeeks",
582
+ "url": "https://www.geeksforgeeks.org/what-does-the-double-star-operator-mean-in-python/",
583
+ "is_source_local": false,
584
+ "is_source_both": false,
585
+ "description": "A Computer Science portal for geeks. It contains well written, well thought and well explained computer science and programming articles, quizzes and practice/competitive programming/company interview Questions.",
586
+ "page_age": "2023-03-14T17:15:04",
587
+ "profile": {
588
+ "name": "GeeksforGeeks",
589
+ "url": "https://www.geeksforgeeks.org/what-does-the-double-star-operator-mean-in-python/",
590
+ "long_name": "geeksforgeeks.org",
591
+ "img": "https://imgs.search.brave.com/fhzcfv5xltx6-YBvJI9RZgS7xZo0dPNaASsrB8YOsCs/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvYjBhOGQ3MmNi/ZWE5N2EwMmZjYzA1/ZTI0ZTFhMGUyMTE0/MGM0ZTBmMWZlM2Y2/Yzk2ODMxZTRhYTBi/NDdjYTE0OS93d3cu/Z2Vla3Nmb3JnZWVr/cy5vcmcv"
592
+ },
593
+ "language": "en",
594
+ "family_friendly": true,
595
+ "type": "search_result",
596
+ "subtype": "article",
597
+ "meta_url": {
598
+ "scheme": "https",
599
+ "netloc": "geeksforgeeks.org",
600
+ "hostname": "www.geeksforgeeks.org",
601
+ "favicon": "https://imgs.search.brave.com/fhzcfv5xltx6-YBvJI9RZgS7xZo0dPNaASsrB8YOsCs/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvYjBhOGQ3MmNi/ZWE5N2EwMmZjYzA1/ZTI0ZTFhMGUyMTE0/MGM0ZTBmMWZlM2Y2/Yzk2ODMxZTRhYTBi/NDdjYTE0OS93d3cu/Z2Vla3Nmb3JnZWVr/cy5vcmcv",
602
+ "path": "› what-does-the-double-star-operator-mean-in-python"
603
+ },
604
+ "thumbnail": {
605
+ "src": "https://imgs.search.brave.com/GcR-j_dLbyHkbHEI3ffLMi6xpXGhF_2Z8POIoqtokhM/rs:fit:200:200:1/g:ce/aHR0cHM6Ly9tZWRp/YS5nZWVrc2Zvcmdl/ZWtzLm9yZy93cC1j/b250ZW50L3VwbG9h/ZHMvZ2ZnXzIwMFgy/MDAtMTAweDEwMC5w/bmc",
606
+ "original": "https://media.geeksforgeeks.org/wp-content/uploads/gfg_200X200-100x100.png",
607
+ "logo": false
608
+ },
609
+ "age": "March 14, 2023",
610
+ "extra_snippets": [
611
+ "Difference between / vs. // operator in Python",
612
+ "Double Star or (**) is one of the Arithmetic Operator (Like +, -, *, **, /, //, %) in Python Language. It is also known as Power Operator.",
613
+ "The time complexity of the given Python program is O(n), where n is the number of key-value pairs in the input dictionary.",
614
+ "Inplace Operators in Python | Set 2 (ixor(), iand(), ipow(),…)"
615
+ ]
616
+ },
617
+ {
618
+ "title": "r/Python",
619
+ "url": "https://www.reddit.com/r/Python/",
620
+ "is_source_local": false,
621
+ "is_source_both": false,
622
+ "description": "The official <strong>Python</strong> community for Reddit! Stay up to date with the latest news, packages, and meta information relating to the <strong>Python</strong> programming language. --- If you have questions or are new to <strong>Python</strong> use r/LearnPython",
623
+ "page_age": "2022-12-30T16:25:02",
624
+ "profile": {
625
+ "name": "Reddit",
626
+ "url": "https://www.reddit.com/r/Python/",
627
+ "long_name": "reddit.com",
628
+ "img": "https://imgs.search.brave.com/mAZYEK9Wi13WLDUge7XZ8YuDTwm6DP6gBjvz1GdYZVY/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvN2ZiNTU0M2Nj/MTFhZjRiYWViZDlk/MjJiMjBjMzFjMDRk/Y2IzYWI0MGI0MjVk/OGY5NzQzOGQ5NzQ5/NWJhMWI0NC93d3cu/cmVkZGl0LmNvbS8"
629
+ },
630
+ "language": "en",
631
+ "family_friendly": true,
632
+ "type": "search_result",
633
+ "subtype": "generic",
634
+ "meta_url": {
635
+ "scheme": "https",
636
+ "netloc": "reddit.com",
637
+ "hostname": "www.reddit.com",
638
+ "favicon": "https://imgs.search.brave.com/mAZYEK9Wi13WLDUge7XZ8YuDTwm6DP6gBjvz1GdYZVY/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvN2ZiNTU0M2Nj/MTFhZjRiYWViZDlk/MjJiMjBjMzFjMDRk/Y2IzYWI0MGI0MjVk/OGY5NzQzOGQ5NzQ5/NWJhMWI0NC93d3cu/cmVkZGl0LmNvbS8",
639
+ "path": "› r › Python"
640
+ },
641
+ "thumbnail": {
642
+ "src": "https://imgs.search.brave.com/zWd10t3zg34ciHiAB-K5WWK3h_H4LedeDot9BVX7Ydo/rs:fit:200:200:1/g:ce/aHR0cHM6Ly9zdHls/ZXMucmVkZGl0bWVk/aWEuY29tL3Q1XzJx/aDB5L3N0eWxlcy9j/b21tdW5pdHlJY29u/X2NpZmVobDR4dDdu/YzEucG5n",
643
+ "original": "https://styles.redditmedia.com/t5_2qh0y/styles/communityIcon_cifehl4xt7nc1.png",
644
+ "logo": false
645
+ },
646
+ "age": "December 30, 2022",
647
+ "extra_snippets": [
648
+ "r/Python: The official Python community for Reddit! Stay up to date with the latest news, packages, and meta information relating to the Python…",
649
+ "By default, Python allows you to import and use anything, anywhere. Over time, this results in modules that were intended to be separate getting tightly coupled together, and domain boundaries breaking down. We experienced this first-hand at a unicorn startup, where the eng team paused development for over a year in an attempt to split up packages into independent services.",
650
+ "Hello r/Python! It's time to share what you've been working on! Whether it's a work-in-progress, a completed masterpiece, or just a rough idea, let us know what you're up to!",
651
+ "Whether it's your job, your hobby, or your passion project, all Python-related work is welcome here."
652
+ ]
653
+ },
654
+ {
655
+ "title": "GitHub - python/cpython: The Python programming language",
656
+ "url": "https://github.com/python/cpython",
657
+ "is_source_local": false,
658
+ "is_source_both": false,
659
+ "description": "The <strong>Python</strong> programming language. Contribute to <strong>python</strong>/cpython development by creating an account on GitHub.",
660
+ "page_age": "2022-10-29T00:00:00",
661
+ "profile": {
662
+ "name": "GitHub",
663
+ "url": "https://github.com/python/cpython",
664
+ "long_name": "github.com",
665
+ "img": "https://imgs.search.brave.com/v8685zI4XInM0zxlNI2s7oE_2Sb-EL7lAy81WXbkQD8/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvYWQyNWM1NjA5/ZjZmZjNlYzI2MDNk/N2VkNmJhYjE2MzZl/MDY5ZTMxMDUzZmY1/NmU3NWIzNWVmMjk0/NTBjMjJjZi9naXRo/dWIuY29tLw"
666
+ },
667
+ "language": "en",
668
+ "family_friendly": true,
669
+ "type": "search_result",
670
+ "subtype": "software",
671
+ "meta_url": {
672
+ "scheme": "https",
673
+ "netloc": "github.com",
674
+ "hostname": "github.com",
675
+ "favicon": "https://imgs.search.brave.com/v8685zI4XInM0zxlNI2s7oE_2Sb-EL7lAy81WXbkQD8/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvYWQyNWM1NjA5/ZjZmZjNlYzI2MDNk/N2VkNmJhYjE2MzZl/MDY5ZTMxMDUzZmY1/NmU3NWIzNWVmMjk0/NTBjMjJjZi9naXRo/dWIuY29tLw",
676
+ "path": "› python › cpython"
677
+ },
678
+ "thumbnail": {
679
+ "src": "https://imgs.search.brave.com/BJbWFRUqgP-tKIyGK9ByXjuYjHO2mtYigUOEFNz_gXk/rs:fit:200:200:1/g:ce/aHR0cHM6Ly9vcGVu/Z3JhcGguZ2l0aHVi/YXNzZXRzLmNvbS82/MTY5YmJkNTQ0YzAy/NDg0MGU4NDdjYTU1/YTU3ZGZmMDA2ZDAw/YWQ1NDIzOTFmYTQ3/YmJjODg3OWM0NWYw/MTZhL3B5dGhvbi9j/cHl0aG9u",
680
+ "original": "https://opengraph.githubassets.com/6169bbd544c024840e847ca55a57dff006d00ad542391fa47bbc8879c45f016a/python/cpython",
681
+ "logo": false
682
+ },
683
+ "age": "October 29, 2022",
684
+ "extra_snippets": [
685
+ "You can pass many options to the configure script; run ./configure --help to find out more. On macOS case-insensitive file systems and on Cygwin, the executable is called python.exe; elsewhere it's just python.",
686
+ "Building a complete Python installation requires the use of various additional third-party libraries, depending on your build platform and configure options. Not all standard library modules are buildable or useable on all platforms. Refer to the Install dependencies section of the Developer Guide for current detailed information on dependencies for various Linux distributions and macOS.",
687
+ "To get an optimized build of Python, configure --enable-optimizations before you run make. This sets the default make targets up to enable Profile Guided Optimization (PGO) and may be used to auto-enable Link Time Optimization (LTO) on some platforms. For more details, see the sections below.",
688
+ "Copyright © 2001-2024 Python Software Foundation. All rights reserved."
689
+ ]
690
+ },
691
+ {
692
+ "title": "5. Data Structures — Python 3.12.3 documentation",
693
+ "url": "https://docs.python.org/3/tutorial/datastructures.html",
694
+ "is_source_local": false,
695
+ "is_source_both": false,
696
+ "description": "This chapter describes some things you’ve learned about already in more detail, and adds some new things as well. More on Lists: The list data type has some more methods. Here are all of the method...",
697
+ "page_age": "2023-07-04T00:00:00",
698
+ "profile": {
699
+ "name": "Python documentation",
700
+ "url": "https://docs.python.org/3/tutorial/datastructures.html",
701
+ "long_name": "docs.python.org",
702
+ "img": "https://imgs.search.brave.com/F5Ym7eSElhGdGUFKLRxDj9Z_tc180ldpeMvQ2Q6ARbA/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvMTUzOTFjOGVi/YTcyOTVmODA3ODIy/YjE2NzFjY2ViMjhl/NzRlY2JhYTc5YjNm/ZjhmODAyZWI2OGUw/ZjU4NDVlNy9kb2Nz/LnB5dGhvbi5vcmcv"
703
+ },
704
+ "language": "en",
705
+ "family_friendly": true,
706
+ "type": "search_result",
707
+ "subtype": "generic",
708
+ "meta_url": {
709
+ "scheme": "https",
710
+ "netloc": "docs.python.org",
711
+ "hostname": "docs.python.org",
712
+ "favicon": "https://imgs.search.brave.com/F5Ym7eSElhGdGUFKLRxDj9Z_tc180ldpeMvQ2Q6ARbA/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvMTUzOTFjOGVi/YTcyOTVmODA3ODIy/YjE2NzFjY2ViMjhl/NzRlY2JhYTc5YjNm/ZjhmODAyZWI2OGUw/ZjU4NDVlNy9kb2Nz/LnB5dGhvbi5vcmcv",
713
+ "path": "› 3 › tutorial › datastructures.html"
714
+ },
715
+ "thumbnail": {
716
+ "src": "https://imgs.search.brave.com/Y7GrMRF8WorDIMLuOl97XC8ltYpoOCqNwWF2pQIIKls/rs:fit:200:200:1/g:ce/aHR0cHM6Ly9kb2Nz/LnB5dGhvbi5vcmcv/My9fc3RhdGljL29n/LWltYWdlLnBuZw",
717
+ "original": "https://docs.python.org/3/_static/og-image.png",
718
+ "logo": false
719
+ },
720
+ "age": "July 4, 2023",
721
+ "extra_snippets": [
722
+ "You might have noticed that methods like insert, remove or sort that only modify the list have no return value printed – they return the default None. [1] This is a design principle for all mutable data structures in Python.",
723
+ "We saw that lists and strings have many common properties, such as indexing and slicing operations. They are two examples of sequence data types (see Sequence Types — list, tuple, range). Since Python is an evolving language, other sequence data types may be added. There is also another standard sequence data type: the tuple.",
724
+ "Python also includes a data type for sets. A set is an unordered collection with no duplicate elements. Basic uses include membership testing and eliminating duplicate entries. Set objects also support mathematical operations like union, intersection, difference, and symmetric difference.",
725
+ "Another useful data type built into Python is the dictionary (see Mapping Types — dict). Dictionaries are sometimes found in other languages as “associative memories” or “associative arrays”. Unlike sequences, which are indexed by a range of numbers, dictionaries are indexed by keys, which can be any immutable type; strings and numbers can always be keys."
726
+ ]
727
+ },
728
+ {
729
+ "title": "Something wrong with python packages / AUR Issues, Discussion & PKGBUILD Requests / Arch Linux Forums",
730
+ "url": "https://bbs.archlinux.org/viewtopic.php?id=295466",
731
+ "is_source_local": false,
732
+ "is_source_both": false,
733
+ "description": "Big <strong>Python</strong> updates require <strong>Python</strong> packages to be rebuild. For some reason they didn&#x27;t think a bump that made it necessary to rebuild half the official repo was a news post.",
734
+ "page_age": "2024-05-04T08:30:02",
735
+ "profile": {
736
+ "name": "Archlinux",
737
+ "url": "https://bbs.archlinux.org/viewtopic.php?id=295466",
738
+ "long_name": "bbs.archlinux.org",
739
+ "img": "https://imgs.search.brave.com/3au9oqkzSri_aLEec3jo-0bFgLuICkydrWfjFcC8lkI/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvNWNkODM1MWJl/ZmJhMzkzNzYzMDkz/NmEyMWMxNjI5MjNk/NGJmZjFhNTBlZDNl/Mzk5MzJjOGZkYjZl/MjNmY2IzNS9iYnMu/YXJjaGxpbnV4Lm9y/Zy8"
740
+ },
741
+ "language": "en",
742
+ "family_friendly": true,
743
+ "type": "search_result",
744
+ "subtype": "generic",
745
+ "meta_url": {
746
+ "scheme": "https",
747
+ "netloc": "bbs.archlinux.org",
748
+ "hostname": "bbs.archlinux.org",
749
+ "favicon": "https://imgs.search.brave.com/3au9oqkzSri_aLEec3jo-0bFgLuICkydrWfjFcC8lkI/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvNWNkODM1MWJl/ZmJhMzkzNzYzMDkz/NmEyMWMxNjI5MjNk/NGJmZjFhNTBlZDNl/Mzk5MzJjOGZkYjZl/MjNmY2IzNS9iYnMu/YXJjaGxpbnV4Lm9y/Zy8",
750
+ "path": "› viewtopic.php"
751
+ },
752
+ "age": "1 day ago",
753
+ "extra_snippets": [
754
+ "Traceback (most recent call last): File \"/usr/lib/python3.12/importlib/metadata/__init__.py\", line 397, in from_name return next(cls.discover(name=name)) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ StopIteration During handling of the above exception, another exception occurred: Traceback (most recent call last): File \"/usr/bin/informant\", line 33, in <module> sys.exit(load_entry_point('informant==0.5.0', 'console_scripts', 'informant')()) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File \"/usr/bin/informant\", line 22, in importlib_load_entry_point for entry_point in distribution(dis"
755
+ ]
756
+ },
757
+ {
758
+ "title": "Introduction to Python",
759
+ "url": "https://www.w3schools.com/python/python_intro.asp",
760
+ "is_source_local": false,
761
+ "is_source_both": false,
762
+ "description": "W3Schools offers free online tutorials, references and exercises in all the major languages of the web. Covering popular subjects like HTML, CSS, JavaScript, <strong>Python</strong>, SQL, Java, and many, many more.",
763
+ "profile": {
764
+ "name": "W3Schools",
765
+ "url": "https://www.w3schools.com/python/python_intro.asp",
766
+ "long_name": "w3schools.com",
767
+ "img": "https://imgs.search.brave.com/JwO5r7z3HTBkU29vgNH_4rrSWLf2M4-8FMWNvbxrKX8/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvYjVlMGVkZDVj/ZGMyZWRmMzAwODRi/ZDAwZGE4NWI3NmU4/MjRhNjEzOGFhZWY3/ZGViMjY1OWY2ZDYw/YTZiOGUyZS93d3cu/dzNzY2hvb2xzLmNv/bS8"
768
+ },
769
+ "language": "en",
770
+ "family_friendly": true,
771
+ "type": "search_result",
772
+ "subtype": "generic",
773
+ "meta_url": {
774
+ "scheme": "https",
775
+ "netloc": "w3schools.com",
776
+ "hostname": "www.w3schools.com",
777
+ "favicon": "https://imgs.search.brave.com/JwO5r7z3HTBkU29vgNH_4rrSWLf2M4-8FMWNvbxrKX8/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvYjVlMGVkZDVj/ZGMyZWRmMzAwODRi/ZDAwZGE4NWI3NmU4/MjRhNjEzOGFhZWY3/ZGViMjY1OWY2ZDYw/YTZiOGUyZS93d3cu/dzNzY2hvb2xzLmNv/bS8",
778
+ "path": "› python › python_intro.asp"
779
+ },
780
+ "thumbnail": {
781
+ "src": "https://imgs.search.brave.com/EMfp8dodbJehmj0yCJh8317RHuaumsddnHI4bujvFcg/rs:fit:200:200:1/g:ce/aHR0cHM6Ly93d3cu/dzNzY2hvb2xzLmNv/bS9pbWFnZXMvdzNz/Y2hvb2xzX2xvZ29f/NDM2XzIucG5n",
782
+ "original": "https://www.w3schools.com/images/w3schools_logo_436_2.png",
783
+ "logo": true
784
+ },
785
+ "extra_snippets": [
786
+ "Well organized and easy to understand Web building tutorials with lots of examples of how to use HTML, CSS, JavaScript, SQL, Python, PHP, Bootstrap, Java, XML and more.",
787
+ "HTML CSS JAVASCRIPT SQL PYTHON JAVA PHP HOW TO W3.CSS C C++ C# BOOTSTRAP REACT MYSQL JQUERY EXCEL XML DJANGO NUMPY PANDAS NODEJS R TYPESCRIPT ANGULAR GIT POSTGRESQL MONGODB ASP AI GO KOTLIN SASS VUE DSA GEN AI SCIPY AWS CYBERSECURITY DATA SCIENCE",
788
+ "Python Variables Variable Names Assign Multiple Values Output Variables Global Variables Variable Exercises Python Data Types Python Numbers Python Casting Python Strings",
789
+ "Python Strings Slicing Strings Modify Strings Concatenate Strings Format Strings Escape Characters String Methods String Exercises Python Booleans Python Operators Python Lists"
790
+ ]
791
+ },
792
+ {
793
+ "title": "bug: AUR package wants to use python but does not find any preset version · Issue #1740 · asdf-vm/asdf",
794
+ "url": "https://github.com/asdf-vm/asdf/issues/1740",
795
+ "is_source_local": false,
796
+ "is_source_both": false,
797
+ "description": "Describe the Bug I am not sure why this is happening, I am trying to install tlpui from AUR and it fails, here are some logs to help: ==&gt; Making package: tlpui 2:1.6.5-1 (Mi 10 apr 2024 23:19:15 +0...",
798
+ "page_age": "2024-05-04T06:45:04",
799
+ "profile": {
800
+ "name": "GitHub",
801
+ "url": "https://github.com/asdf-vm/asdf/issues/1740",
802
+ "long_name": "github.com",
803
+ "img": "https://imgs.search.brave.com/v8685zI4XInM0zxlNI2s7oE_2Sb-EL7lAy81WXbkQD8/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvYWQyNWM1NjA5/ZjZmZjNlYzI2MDNk/N2VkNmJhYjE2MzZl/MDY5ZTMxMDUzZmY1/NmU3NWIzNWVmMjk0/NTBjMjJjZi9naXRo/dWIuY29tLw"
804
+ },
805
+ "language": "en",
806
+ "family_friendly": true,
807
+ "type": "search_result",
808
+ "subtype": "software",
809
+ "meta_url": {
810
+ "scheme": "https",
811
+ "netloc": "github.com",
812
+ "hostname": "github.com",
813
+ "favicon": "https://imgs.search.brave.com/v8685zI4XInM0zxlNI2s7oE_2Sb-EL7lAy81WXbkQD8/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvYWQyNWM1NjA5/ZjZmZjNlYzI2MDNk/N2VkNmJhYjE2MzZl/MDY5ZTMxMDUzZmY1/NmU3NWIzNWVmMjk0/NTBjMjJjZi9naXRo/dWIuY29tLw",
814
+ "path": "› asdf-vm › asdf › issues › 1740"
815
+ },
816
+ "thumbnail": {
817
+ "src": "https://imgs.search.brave.com/KrLW5s_2n4jyP8XLbc3ZPVBaLD963tQgWzG9EWPZlQs/rs:fit:200:200:1/g:ce/aHR0cHM6Ly9vcGVu/Z3JhcGguZ2l0aHVi/YXNzZXRzLmNvbS81/MTE0ZTdkOGIwODM2/YmQ2MTY3NzQ1ZGI4/MmZjMGE3OGUyMjcw/MGFlY2ZjMWZkODBl/MDYzZTNiN2ZjOWNj/NzYyL2FzZGYtdm0v/YXNkZi9pc3N1ZXMv/MTc0MA",
818
+ "original": "https://opengraph.githubassets.com/5114e7d8b0836bd6167745db82fc0a78e22700aecfc1fd80e063e3b7fc9cc762/asdf-vm/asdf/issues/1740",
819
+ "logo": false
820
+ },
821
+ "age": "1 day ago",
822
+ "extra_snippets": [
823
+ "==> Starting build()... No preset version installed for command python Please install a version by running one of the following: asdf install python 3.8 or add one of the following versions in your config file at /home/ferret/.tool-versions python 3.11.0 python 3.12.1 python 3.12.3 ==> ERROR: A failure occurred in build(). Aborting...",
824
+ "-> error making: tlpui-exit status 4 -> Failed to install the following packages. Manual intervention is required: tlpui - exit status 4 ferret@FX505DT in ~ $ cat /home/ferret/.tool-versions nodejs 21.6.0 python 3.12.3 ferret@FX505DT in ~ $ python -V Python 3.12.3 ferret@FX505DT in ~ $ which python /home/ferret/.asdf/shims/python",
825
+ "Describe the Bug I am not sure why this is happening, I am trying to install tlpui from AUR and it fails, here are some logs to help: ==> Making package: tlpui 2:1.6.5-1 (Mi 10 apr 2024 23:19:15 +0300) ==> Retrieving sources... -> Found ..."
826
+ ]
827
+ },
828
+ {
829
+ "title": "What are python.exe and python3.exe, and why do they appear to point to App Installer? | Windows 11 Forum",
830
+ "url": "https://www.elevenforum.com/t/what-are-python-exe-and-python3-exe-and-why-do-they-appear-to-point-to-app-installer.24886/",
831
+ "is_source_local": false,
832
+ "is_source_both": false,
833
+ "description": "I was looking at App execution aliases (Settings &gt; Apps &gt; Advanced app settings &gt; App execution aliases) on my new computer -- my first Windows 11 computer. Why are <strong>python</strong>.exe and python3.exe listed as App Installer? I assume that App Installer refers to installation of Microsoft Store / UWP...",
834
+ "page_age": "2024-05-03T17:30:04",
835
+ "profile": {
836
+ "name": "Windows 11 Forum",
837
+ "url": "https://www.elevenforum.com/t/what-are-python-exe-and-python3-exe-and-why-do-they-appear-to-point-to-app-installer.24886/",
838
+ "long_name": "elevenforum.com",
839
+ "img": "https://imgs.search.brave.com/XVRAYMEj6Im8i7jV5RxeTwpiRPtY9IWg4wRIuh-WhEw/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvZjk5MDZkMDIw/M2U1OWIwNjM5Y2U1/M2U2NzNiNzVkNTA5/NzA5OTI1ZTFmOTc4/MzU3OTlhYzU5OTVi/ZGNjNTY4MS93d3cu/ZWxldmVuZm9ydW0u/Y29tLw"
840
+ },
841
+ "language": "en",
842
+ "family_friendly": true,
843
+ "type": "search_result",
844
+ "subtype": "generic",
845
+ "meta_url": {
846
+ "scheme": "https",
847
+ "netloc": "elevenforum.com",
848
+ "hostname": "www.elevenforum.com",
849
+ "favicon": "https://imgs.search.brave.com/XVRAYMEj6Im8i7jV5RxeTwpiRPtY9IWg4wRIuh-WhEw/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvZjk5MDZkMDIw/M2U1OWIwNjM5Y2U1/M2U2NzNiNzVkNTA5/NzA5OTI1ZTFmOTc4/MzU3OTlhYzU5OTVi/ZGNjNTY4MS93d3cu/ZWxldmVuZm9ydW0u/Y29tLw",
850
+ "path": " › windows support forums › apps and software"
851
+ },
852
+ "thumbnail": {
853
+ "src": "https://imgs.search.brave.com/DVoFcE6d_-lx3BVGNS-RZK_lZzxQ8VhwZVf3AVqEJFA/rs:fit:200:200:1/g:ce/aHR0cHM6Ly93d3cu/ZWxldmVuZm9ydW0u/Y29tL2RhdGEvYXNz/ZXRzL2xvZ28vbWV0/YTEtMjAxLnBuZw",
854
+ "original": "https://www.elevenforum.com/data/assets/logo/meta1-201.png",
855
+ "logo": true
856
+ },
857
+ "age": "2 days ago",
858
+ "extra_snippets": [
859
+ "Why are python.exe and python3.exe listed as App Installer? I assume that App Installer refers to installation of Microsoft Store / UWP apps, but if that's the case, then why are they called python.exe and python3.exe? Or are python.exe and python3.exe simply serving as aliases / pointers pointing to App Installer, which is itself a Microsoft Store App?",
860
+ "Or are python.exe and python3.exe simply serving as aliases / pointers pointing to App Installer, which is itself a Microsoft Store App? I wish to soon install Python, along with an integrated development editor (IDE), on my machine, so that I can code in Python.",
861
+ "I wish to soon install Python, along with an integrated development editor (IDE), on my machine, so that I can code in Python. But is a Python interpreter already on my computer as suggested, if obliquely, by the presence of python.exe and python3.exe? I kind of doubt it."
862
+ ]
863
+ },
864
+ {
865
+ "title": "How to Watermark Your Images Using Python OpenCV in ...",
866
+ "url": "https://medium.com/@daily_data_prep/how-to-watermark-your-images-using-python-opencv-in-bulk-e472085389a1",
867
+ "is_source_local": false,
868
+ "is_source_both": false,
869
+ "description": "Medium is an open platform where readers find dynamic thinking, and where expert and undiscovered voices can share their writing on any topic.",
870
+ "page_age": "2024-05-03T14:05:06",
871
+ "profile": {
872
+ "name": "Medium",
873
+ "url": "https://medium.com/@daily_data_prep/how-to-watermark-your-images-using-python-opencv-in-bulk-e472085389a1",
874
+ "long_name": "medium.com",
875
+ "img": "https://imgs.search.brave.com/qvE2kIQCiAsnPv2C6P9xM5J2VVWdm55g-A-2Q_yIJ0g/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvOTZhYmQ1N2Q4/NDg4ZDcyODIyMDZi/MzFmOWNhNjE3Y2E4/Y2YzMThjNjljNDIx/ZjllZmNhYTcwODhl/YTcwNDEzYy9tZWRp/dW0uY29tLw"
876
+ },
877
+ "language": "en",
878
+ "family_friendly": true,
879
+ "type": "search_result",
880
+ "subtype": "generic",
881
+ "meta_url": {
882
+ "scheme": "https",
883
+ "netloc": "medium.com",
884
+ "hostname": "medium.com",
885
+ "favicon": "https://imgs.search.brave.com/qvE2kIQCiAsnPv2C6P9xM5J2VVWdm55g-A-2Q_yIJ0g/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvOTZhYmQ1N2Q4/NDg4ZDcyODIyMDZi/MzFmOWNhNjE3Y2E4/Y2YzMThjNjljNDIx/ZjllZmNhYTcwODhl/YTcwNDEzYy9tZWRp/dW0uY29tLw",
886
+ "path": "› @daily_data_prep › how-to-watermark-your-images-using-python-opencv-in-bulk-e472085389a1"
887
+ },
888
+ "age": "2 days ago"
889
+ },
890
+ {
891
+ "title": "Increment and Decrement Operators in Python?",
892
+ "url": "https://www.tutorialspoint.com/increment-and-decrement-operators-in-python",
893
+ "is_source_local": false,
894
+ "is_source_both": false,
895
+ "description": "Increment and Decrement Operators in <strong>Python</strong> - <strong>Python</strong> does not have unary increment/decrement operator (++/--). Instead to increment a value, usea += 1to decrement a value, use −a -= 1Example&gt;&gt;&gt; a = 0 &gt;&gt;&gt; &gt;&gt;&gt; #Increment &gt;&gt;&gt; a +=1 &gt;&gt;&gt; &gt;&gt;&gt; #Decrement &gt;&gt;&gt; a -= 1 &gt;&gt;&gt; &gt;&gt;&gt; #value of a &gt;&gt;&gt; a 0Python ...",
896
+ "page_age": "2023-08-23T00:00:00",
897
+ "profile": {
898
+ "name": "Tutorialspoint",
899
+ "url": "https://www.tutorialspoint.com/increment-and-decrement-operators-in-python",
900
+ "long_name": "tutorialspoint.com",
901
+ "img": "https://imgs.search.brave.com/Wt8BSkivPlFwcU5yBtf7YzuvTuRExyd_502cdABCS5c/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvYjcyYjAzYmVl/ODU4MzZiMjJiYTFh/MjJhZDNmNWE4YzA5/MDgyYTZhMDg3NTYw/M2NiY2NiZTUxN2I5/MjU1MWFmMS93d3cu/dHV0b3JpYWxzcG9p/bnQuY29tLw"
902
+ },
903
+ "language": "en",
904
+ "family_friendly": true,
905
+ "type": "search_result",
906
+ "subtype": "generic",
907
+ "meta_url": {
908
+ "scheme": "https",
909
+ "netloc": "tutorialspoint.com",
910
+ "hostname": "www.tutorialspoint.com",
911
+ "favicon": "https://imgs.search.brave.com/Wt8BSkivPlFwcU5yBtf7YzuvTuRExyd_502cdABCS5c/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvYjcyYjAzYmVl/ODU4MzZiMjJiYTFh/MjJhZDNmNWE4YzA5/MDgyYTZhMDg3NTYw/M2NiY2NiZTUxN2I5/MjU1MWFmMS93d3cu/dHV0b3JpYWxzcG9p/bnQuY29tLw",
912
+ "path": "› increment-and-decrement-operators-in-python"
913
+ },
914
+ "thumbnail": {
915
+ "src": "https://imgs.search.brave.com/ddG5vyZGLVudvecEbQJPeG8tGuaZ7g3Xz6Gyjdl5WA8/rs:fit:200:200:1/g:ce/aHR0cHM6Ly93d3cu/dHV0b3JpYWxzcG9p/bnQuY29tL2ltYWdl/cy90cF9sb2dvXzQz/Ni5wbmc",
916
+ "original": "https://www.tutorialspoint.com/images/tp_logo_436.png",
917
+ "logo": true
918
+ },
919
+ "age": "August 23, 2023",
920
+ "extra_snippets": [
921
+ "Increment and Decrement Operators in Python - Python does not have unary increment/decrement operator (++/--). Instead to increment a value, usea += 1to decrement a value, use −a -= 1Example>>> a = 0 >>> >>> #Increment >>> a +=1 >>> >>> #Decrement >>> a -= 1 >>> >>> #value of a >>> a 0Python does not provide multiple ways to do the same thing",
922
+ "So what above statement means in python is: create an object of type int having value 1 and give the name a to it. The object is an instance of int having value 1 and the name a refers to it. The assigned name a and the object to which it refers are distinct.",
923
+ "Python does not provide multiple ways to do the same thing .",
924
+ "However, be careful if you are coming from a language like C, Python doesn’t have \"variables\" in the sense that C does, instead python uses names and objects and in python integers (int’s) are immutable."
925
+ ]
926
+ },
927
+ {
928
+ "title": "Gumroad – How not to suck at Python / SideFX Houdini | CG Persia",
929
+ "url": "https://cgpersia.com/2024/05/gumroad-how-not-to-suck-at-python-sidefx-houdini-195370.html",
930
+ "is_source_local": false,
931
+ "is_source_both": false,
932
+ "description": "Info: This course is made for artists or TD (technical director) willing to learn <strong>Python</strong> to improve their workflows inside SideFX Houdini, get faster in production and develop all the tools you always wished you had.",
933
+ "page_age": "2024-05-03T08:35:03",
934
+ "profile": {
935
+ "name": "Cgpersia",
936
+ "url": "https://cgpersia.com/2024/05/gumroad-how-not-to-suck-at-python-sidefx-houdini-195370.html",
937
+ "long_name": "cgpersia.com",
938
+ "img": "https://imgs.search.brave.com/VjyaopAm-M9sWvM7n-KnGZ3T5swIOwwE80iF5QVqQPg/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvYmE0MzQ4NmI2/NjFhMTA1ZDBiN2Iw/ZWNiNDUxNjUwYjdh/MGE5ZjQ0ZjIxNzll/NmVkZDE2YzYyMDBh/NDNiMDgwMy9jZ3Bl/cnNpYS5jb20v"
939
+ },
940
+ "language": "en",
941
+ "family_friendly": true,
942
+ "type": "search_result",
943
+ "subtype": "generic",
944
+ "meta_url": {
945
+ "scheme": "https",
946
+ "netloc": "cgpersia.com",
947
+ "hostname": "cgpersia.com",
948
+ "favicon": "https://imgs.search.brave.com/VjyaopAm-M9sWvM7n-KnGZ3T5swIOwwE80iF5QVqQPg/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvYmE0MzQ4NmI2/NjFhMTA1ZDBiN2Iw/ZWNiNDUxNjUwYjdh/MGE5ZjQ0ZjIxNzll/NmVkZDE2YzYyMDBh/NDNiMDgwMy9jZ3Bl/cnNpYS5jb20v",
949
+ "path": "› 2024 › 05 › gumroad-how-not-to-suck-at-python-sidefx-houdini-195370.html"
950
+ },
951
+ "age": "2 days ago",
952
+ "extra_snippets": [
953
+ "Posted in: 2D, CG Releases, Downloads, Learning, Tutorials, Videos. Tagged: Gumroad, Python, Sidefx. Leave a Comment",
954
+ "01 – Python – Fundamentals Get the Fundamentals of python before starting the fun stuff ! 02 – Python Construction Part02 digging further into python concepts 03 – Houdini – Python Basics Applying some basic python in Houdini and starting to make tools !",
955
+ "02 – Python Construction Part02 digging further into python concepts 03 – Houdini – Python Basics Applying some basic python in Houdini and starting to make tools ! 04 – Houdini – Python Intermediate Applying some more advanced python in Houdini to make tools ! 05 – Houdini – Python Expert Using QtDesigner in combinaison with Houdini Python/Pyside to create advanced tools."
956
+ ]
957
+ },
958
+ {
959
+ "title": "How to install Python: The complete Python programmer’s guide",
960
+ "url": "https://www.pluralsight.com/resources/blog/software-development/python-installation-guide",
961
+ "is_source_local": false,
962
+ "is_source_both": false,
963
+ "description": "An easy guide on how set up your operating system so you can program in <strong>Python</strong>, and how to update or uninstall it. For Linux, Windows, and macOS.",
964
+ "page_age": "2024-05-02T07:30:02",
965
+ "profile": {
966
+ "name": "Pluralsight",
967
+ "url": "https://www.pluralsight.com/resources/blog/software-development/python-installation-guide",
968
+ "long_name": "pluralsight.com",
969
+ "img": "https://imgs.search.brave.com/zvwQNSVu9-jR2CRlNcsTzxjaXKPlXNuh-Jo9-0yA1OE/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvMTNkNWQyNjk3/M2Q0NzYyMmUyNDc3/ZjYwMWFlZDI5YTI4/ODhmYzc2MDkzMjAy/MjNkMWY1MDE3NTQw/MzI5NWVkZS93d3cu/cGx1cmFsc2lnaHQu/Y29tLw"
970
+ },
971
+ "language": "en",
972
+ "family_friendly": true,
973
+ "type": "search_result",
974
+ "subtype": "generic",
975
+ "meta_url": {
976
+ "scheme": "https",
977
+ "netloc": "pluralsight.com",
978
+ "hostname": "www.pluralsight.com",
979
+ "favicon": "https://imgs.search.brave.com/zvwQNSVu9-jR2CRlNcsTzxjaXKPlXNuh-Jo9-0yA1OE/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvMTNkNWQyNjk3/M2Q0NzYyMmUyNDc3/ZjYwMWFlZDI5YTI4/ODhmYzc2MDkzMjAy/MjNkMWY1MDE3NTQw/MzI5NWVkZS93d3cu/cGx1cmFsc2lnaHQu/Y29tLw",
980
+ "path": " › blog › blog"
981
+ },
982
+ "thumbnail": {
983
+ "src": "https://imgs.search.brave.com/xrv5PHH2Bzmq2rcIYzk__8h5RqCj6kS3I6SGCNw5dZM/rs:fit:200:200:1/g:ce/aHR0cHM6Ly93d3cu/cGx1cmFsc2lnaHQu/Y29tL2NvbnRlbnQv/ZGFtL3BzL2ltYWdl/cy9yZXNvdXJjZS1j/ZW50ZXIvYmxvZy9o/ZWFkZXItaGVyby1p/bWFnZXMvUHl0aG9u/LndlYnA",
984
+ "original": "https://www.pluralsight.com/content/dam/ps/images/resource-center/blog/header-hero-images/Python.webp",
985
+ "logo": false
986
+ },
987
+ "age": "3 days ago",
988
+ "extra_snippets": [
989
+ "Whether it’s your first time programming or you’re a seasoned programmer, you’ll have to install or update Python every now and then --- or if necessary, uninstall it. In this article, you'll learn how to do just that.",
990
+ "Some systems come with Python, so to start off, we’ll first check to see if it’s installed on your system before we proceed. To do that, we’ll need to open a terminal. Since you might be new to programming, let’s go over how to open a terminal for Linux, Windows, and macOS.",
991
+ "Before we dive into setting up your system so you can program in Python, let’s talk terminal basics and benefits.",
992
+ "However, let’s focus on why we need it for working with Python. We use a terminal, or command line, to:"
993
+ ]
994
+ }
995
+ ],
996
+ "family_friendly": true
997
+ }
998
+ }
backend/apps/rag/search/testdata/google_pse.json ADDED
@@ -0,0 +1,442 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "kind": "customsearch#search",
3
+ "url": {
4
+ "type": "application/json",
5
+ "template": "https://www.googleapis.com/customsearch/v1?q={searchTerms}&num={count?}&start={startIndex?}&lr={language?}&safe={safe?}&cx={cx?}&sort={sort?}&filter={filter?}&gl={gl?}&cr={cr?}&googlehost={googleHost?}&c2coff={disableCnTwTranslation?}&hq={hq?}&hl={hl?}&siteSearch={siteSearch?}&siteSearchFilter={siteSearchFilter?}&exactTerms={exactTerms?}&excludeTerms={excludeTerms?}&linkSite={linkSite?}&orTerms={orTerms?}&dateRestrict={dateRestrict?}&lowRange={lowRange?}&highRange={highRange?}&searchType={searchType}&fileType={fileType?}&rights={rights?}&imgSize={imgSize?}&imgType={imgType?}&imgColorType={imgColorType?}&imgDominantColor={imgDominantColor?}&alt=json"
6
+ },
7
+ "queries": {
8
+ "request": [
9
+ {
10
+ "title": "Google Custom Search - lectures",
11
+ "totalResults": "2450000000",
12
+ "searchTerms": "lectures",
13
+ "count": 10,
14
+ "startIndex": 1,
15
+ "inputEncoding": "utf8",
16
+ "outputEncoding": "utf8",
17
+ "safe": "off",
18
+ "cx": "0473ef98502d44e18"
19
+ }
20
+ ],
21
+ "nextPage": [
22
+ {
23
+ "title": "Google Custom Search - lectures",
24
+ "totalResults": "2450000000",
25
+ "searchTerms": "lectures",
26
+ "count": 10,
27
+ "startIndex": 11,
28
+ "inputEncoding": "utf8",
29
+ "outputEncoding": "utf8",
30
+ "safe": "off",
31
+ "cx": "0473ef98502d44e18"
32
+ }
33
+ ]
34
+ },
35
+ "context": {
36
+ "title": "LLM Search"
37
+ },
38
+ "searchInformation": {
39
+ "searchTime": 0.445959,
40
+ "formattedSearchTime": "0.45",
41
+ "totalResults": "2450000000",
42
+ "formattedTotalResults": "2,450,000,000"
43
+ },
44
+ "items": [
45
+ {
46
+ "kind": "customsearch#result",
47
+ "title": "The Feynman Lectures on Physics",
48
+ "htmlTitle": "The Feynman \u003cb\u003eLectures\u003c/b\u003e on Physics",
49
+ "link": "https://www.feynmanlectures.caltech.edu/",
50
+ "displayLink": "www.feynmanlectures.caltech.edu",
51
+ "snippet": "This edition has been designed for ease of reading on devices of any size or shape; text, figures and equations can all be zoomed without degradation.",
52
+ "htmlSnippet": "This edition has been designed for ease of reading on devices of any size or shape; text, figures and equations can all be zoomed without degradation.",
53
+ "cacheId": "CyXMWYWs9UEJ",
54
+ "formattedUrl": "https://www.feynmanlectures.caltech.edu/",
55
+ "htmlFormattedUrl": "https://www.feynman\u003cb\u003electures\u003c/b\u003e.caltech.edu/",
56
+ "pagemap": {
57
+ "metatags": [
58
+ {
59
+ "viewport": "width=device-width, initial-scale=1.0"
60
+ }
61
+ ]
62
+ }
63
+ },
64
+ {
65
+ "kind": "customsearch#result",
66
+ "title": "Video Lectures",
67
+ "htmlTitle": "Video \u003cb\u003eLectures\u003c/b\u003e",
68
+ "link": "https://www.reddit.com/r/lectures/",
69
+ "displayLink": "www.reddit.com",
70
+ "snippet": "r/lectures: This subreddit is all about video lectures, talks and interesting public speeches. The topics include mathematics, physics, computer…",
71
+ "htmlSnippet": "r/\u003cb\u003electures\u003c/b\u003e: This subreddit is all about video \u003cb\u003electures\u003c/b\u003e, talks and interesting public speeches. The topics include mathematics, physics, computer…",
72
+ "formattedUrl": "https://www.reddit.com/r/lectures/",
73
+ "htmlFormattedUrl": "https://www.reddit.com/r/\u003cb\u003electures\u003c/b\u003e/",
74
+ "pagemap": {
75
+ "cse_thumbnail": [
76
+ {
77
+ "src": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcTZtOjhfkgUKQbL3DZxe5F6OVsgeDNffleObjJ7n9RllKQTSsimax7VIaY&s",
78
+ "width": "192",
79
+ "height": "192"
80
+ }
81
+ ],
82
+ "metatags": [
83
+ {
84
+ "og:image": "https://www.redditstatic.com/shreddit/assets/favicon/192x192.png",
85
+ "theme-color": "#000000",
86
+ "og:image:width": "256",
87
+ "og:type": "website",
88
+ "twitter:card": "summary",
89
+ "twitter:title": "r/lectures",
90
+ "og:site_name": "Reddit",
91
+ "og:title": "r/lectures",
92
+ "og:image:height": "256",
93
+ "bingbot": "noarchive",
94
+ "msapplication-navbutton-color": "#000000",
95
+ "og:description": "This subreddit is all about video lectures, talks and interesting public speeches.\n\nThe topics include mathematics, physics, computer science, programming, engineering, biology, medicine, economics, politics, social sciences, and any other subjects!",
96
+ "twitter:image": "https://www.redditstatic.com/shreddit/assets/favicon/192x192.png",
97
+ "apple-mobile-web-app-status-bar-style": "black",
98
+ "twitter:site": "@reddit",
99
+ "viewport": "width=device-width, initial-scale=1, viewport-fit=cover",
100
+ "apple-mobile-web-app-capable": "yes",
101
+ "og:ttl": "600",
102
+ "og:url": "https://www.reddit.com/r/lectures/"
103
+ }
104
+ ],
105
+ "cse_image": [
106
+ {
107
+ "src": "https://www.redditstatic.com/shreddit/assets/favicon/192x192.png"
108
+ }
109
+ ]
110
+ }
111
+ },
112
+ {
113
+ "kind": "customsearch#result",
114
+ "title": "Lectures & Discussions | Flint Institute of Arts",
115
+ "htmlTitle": "\u003cb\u003eLectures\u003c/b\u003e &amp; Discussions | Flint Institute of Arts",
116
+ "link": "https://flintarts.org/events/lectures",
117
+ "displayLink": "flintarts.org",
118
+ "snippet": "It will trace the intricate relationship between jewelry, attire, and the expression of personal identity, social hierarchy, and spiritual belief systems that ...",
119
+ "htmlSnippet": "It will trace the intricate relationship between jewelry, attire, and the expression of personal identity, social hierarchy, and spiritual belief systems that&nbsp;...",
120
+ "cacheId": "jvpb9DxrfxoJ",
121
+ "formattedUrl": "https://flintarts.org/events/lectures",
122
+ "htmlFormattedUrl": "https://flintarts.org/events/\u003cb\u003electures\u003c/b\u003e",
123
+ "pagemap": {
124
+ "cse_thumbnail": [
125
+ {
126
+ "src": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcS23tMtAeNhJbOWdGxShYsmnyzFdzOC9Hb7lRykA9Pw72z1IlKTkjTdZw&s",
127
+ "width": "447",
128
+ "height": "113"
129
+ }
130
+ ],
131
+ "metatags": [
132
+ {
133
+ "og:image": "https://flintarts.org/uploads/images/page-headers/_headerImage/nightshot.jpg",
134
+ "og:type": "website",
135
+ "viewport": "width=device-width, initial-scale=1",
136
+ "og:title": "Lectures & Discussions | Flint Institute of Arts",
137
+ "og:description": "The Flint Institute of Arts is the second largest art museum in Michigan and one of the largest museum art schools in the nation."
138
+ }
139
+ ],
140
+ "cse_image": [
141
+ {
142
+ "src": "https://flintarts.org/uploads/images/page-headers/_headerImage/nightshot.jpg"
143
+ }
144
+ ]
145
+ }
146
+ },
147
+ {
148
+ "kind": "customsearch#result",
149
+ "title": "Mandel Lectures | Mandel Center for the Humanities ... - Waltham",
150
+ "htmlTitle": "Mandel \u003cb\u003eLectures\u003c/b\u003e | Mandel Center for the Humanities ... - Waltham",
151
+ "link": "https://www.brandeis.edu/mandel-center-humanities/mandel-lectures.html",
152
+ "displayLink": "www.brandeis.edu",
153
+ "snippet": "Past Lectures · Lecture 1: \"Invisible Music: The Sonic Idea of Black Revolution From Captivity to Reconstruction\" · Lecture 2: \"Solidarity in Sound: Grassroots ...",
154
+ "htmlSnippet": "Past \u003cb\u003eLectures\u003c/b\u003e &middot; \u003cb\u003eLecture\u003c/b\u003e 1: &quot;Invisible Music: The Sonic Idea of Black Revolution From Captivity to Reconstruction&quot; &middot; \u003cb\u003eLecture\u003c/b\u003e 2: &quot;Solidarity in Sound: Grassroots&nbsp;...",
155
+ "cacheId": "cQLOZr0kgEEJ",
156
+ "formattedUrl": "https://www.brandeis.edu/mandel-center-humanities/mandel-lectures.html",
157
+ "htmlFormattedUrl": "https://www.brandeis.edu/mandel-center-humanities/mandel-\u003cb\u003electures\u003c/b\u003e.html",
158
+ "pagemap": {
159
+ "cse_thumbnail": [
160
+ {
161
+ "src": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQWlU7bcJ5pIHk7RBCk2QKE-48ejF7hyPV0pr-20_cBt2BGdfKtiYXBuyw&s",
162
+ "width": "275",
163
+ "height": "183"
164
+ }
165
+ ],
166
+ "metatags": [
167
+ {
168
+ "og:image": "https://www.brandeis.edu/mandel-center-humanities/events/events-images/mlhzumba",
169
+ "twitter:card": "summary_large_image",
170
+ "viewport": "width=device-width,initial-scale=1,minimum-scale=1",
171
+ "og:title": "Mandel Lectures in the Humanities",
172
+ "og:url": "https://www.brandeis.edu/mandel-center-humanities/mandel-lectures.html",
173
+ "og:description": "Annual Lecture Series",
174
+ "twitter:image": "https://www.brandeis.edu/mandel-center-humanities/events/events-images/mlhzumba"
175
+ }
176
+ ],
177
+ "cse_image": [
178
+ {
179
+ "src": "https://www.brandeis.edu/mandel-center-humanities/events/events-images/mlhzumba"
180
+ }
181
+ ]
182
+ }
183
+ },
184
+ {
185
+ "kind": "customsearch#result",
186
+ "title": "Brian Douglas - YouTube",
187
+ "htmlTitle": "Brian Douglas - YouTube",
188
+ "link": "https://www.youtube.com/channel/UCq0imsn84ShAe9PBOFnoIrg",
189
+ "displayLink": "www.youtube.com",
190
+ "snippet": "Welcome to Control Systems Lectures! This collection of videos is intended to supplement a first year controls class, not replace it.",
191
+ "htmlSnippet": "Welcome to Control Systems \u003cb\u003eLectures\u003c/b\u003e! This collection of videos is intended to supplement a first year controls class, not replace it.",
192
+ "cacheId": "NEROyBHolL0J",
193
+ "formattedUrl": "https://www.youtube.com/channel/UCq0imsn84ShAe9PBOFnoIrg",
194
+ "htmlFormattedUrl": "https://www.youtube.com/channel/UCq0imsn84ShAe9PBOFnoIrg",
195
+ "pagemap": {
196
+ "hcard": [
197
+ {
198
+ "fn": "Brian Douglas",
199
+ "url": "https://www.youtube.com/channel/UCq0imsn84ShAe9PBOFnoIrg"
200
+ }
201
+ ],
202
+ "cse_thumbnail": [
203
+ {
204
+ "src": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcR7G0CeCBz_wVTZgjnhEr2QbiKP7f3uYzKitZYn74Mi32cDmVxvsegJoLI&s",
205
+ "width": "225",
206
+ "height": "225"
207
+ }
208
+ ],
209
+ "imageobject": [
210
+ {
211
+ "width": "900",
212
+ "url": "https://yt3.googleusercontent.com/ytc/AIdro_nLo68wetImbwGUYP3stve_iKmAEccjhqB-q4o79xdInN4=s900-c-k-c0x00ffffff-no-rj",
213
+ "height": "900"
214
+ }
215
+ ],
216
+ "person": [
217
+ {
218
+ "name": "Brian Douglas",
219
+ "url": "https://www.youtube.com/channel/UCq0imsn84ShAe9PBOFnoIrg"
220
+ }
221
+ ],
222
+ "metatags": [
223
+ {
224
+ "apple-itunes-app": "app-id=544007664, app-argument=https://m.youtube.com/channel/UCq0imsn84ShAe9PBOFnoIrg?referring_app=com.apple.mobilesafari-smartbanner, affiliate-data=ct=smart_app_banner_polymer&pt=9008",
225
+ "og:image": "https://yt3.googleusercontent.com/ytc/AIdro_nLo68wetImbwGUYP3stve_iKmAEccjhqB-q4o79xdInN4=s900-c-k-c0x00ffffff-no-rj",
226
+ "twitter:app:url:iphone": "vnd.youtube://www.youtube.com/channel/UCq0imsn84ShAe9PBOFnoIrg",
227
+ "twitter:app:id:googleplay": "com.google.android.youtube",
228
+ "theme-color": "rgb(255, 255, 255)",
229
+ "og:image:width": "900",
230
+ "twitter:card": "summary",
231
+ "og:site_name": "YouTube",
232
+ "twitter:url": "https://www.youtube.com/channel/UCq0imsn84ShAe9PBOFnoIrg",
233
+ "twitter:app:url:ipad": "vnd.youtube://www.youtube.com/channel/UCq0imsn84ShAe9PBOFnoIrg",
234
+ "al:android:package": "com.google.android.youtube",
235
+ "twitter:app:name:googleplay": "YouTube",
236
+ "al:ios:url": "vnd.youtube://www.youtube.com/channel/UCq0imsn84ShAe9PBOFnoIrg",
237
+ "twitter:app:id:iphone": "544007664",
238
+ "og:description": "Welcome to Control Systems Lectures! This collection of videos is intended to supplement a first year controls class, not replace it. My goal is to take specific concepts in controls and expand on them in order to provide an intuitive understanding which will ultimately make you a better controls engineer. \n\nI'm glad you made it to my channel and I hope you find it useful.\n\nShoot me a message at controlsystemlectures@gmail.com, leave a comment or question and I'll get back to you if I can. Don't forget to subscribe!\n \nTwitter: @BrianBDouglas for engineering tweets and announcement of new videos.\nWebpage: http://engineeringmedia.com\n\nHere is the hardware/software I use: http://www.youtube.com/watch?v=m-M5_mIyHe4\n\nHere's a list of my favorite references: http://bit.ly/2skvmWd\n\n--Brian",
239
+ "al:ios:app_store_id": "544007664",
240
+ "twitter:image": "https://yt3.googleusercontent.com/ytc/AIdro_nLo68wetImbwGUYP3stve_iKmAEccjhqB-q4o79xdInN4=s900-c-k-c0x00ffffff-no-rj",
241
+ "twitter:site": "@youtube",
242
+ "og:type": "profile",
243
+ "twitter:title": "Brian Douglas",
244
+ "al:ios:app_name": "YouTube",
245
+ "og:title": "Brian Douglas",
246
+ "og:image:height": "900",
247
+ "twitter:app:id:ipad": "544007664",
248
+ "al:web:url": "https://www.youtube.com/channel/UCq0imsn84ShAe9PBOFnoIrg?feature=applinks",
249
+ "al:android:url": "https://www.youtube.com/channel/UCq0imsn84ShAe9PBOFnoIrg?feature=applinks",
250
+ "fb:app_id": "87741124305",
251
+ "twitter:app:url:googleplay": "https://www.youtube.com/channel/UCq0imsn84ShAe9PBOFnoIrg",
252
+ "twitter:app:name:ipad": "YouTube",
253
+ "viewport": "width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no,",
254
+ "twitter:description": "Welcome to Control Systems Lectures! This collection of videos is intended to supplement a first year controls class, not replace it. My goal is to take specific concepts in controls and expand on them in order to provide an intuitive understanding which will ultimately make you a better controls engineer. \n\nI'm glad you made it to my channel and I hope you find it useful.\n\nShoot me a message at controlsystemlectures@gmail.com, leave a comment or question and I'll get back to you if I can. Don't forget to subscribe!\n \nTwitter: @BrianBDouglas for engineering tweets and announcement of new videos.\nWebpage: http://engineeringmedia.com\n\nHere is the hardware/software I use: http://www.youtube.com/watch?v=m-M5_mIyHe4\n\nHere's a list of my favorite references: http://bit.ly/2skvmWd\n\n--Brian",
255
+ "og:url": "https://www.youtube.com/channel/UCq0imsn84ShAe9PBOFnoIrg",
256
+ "al:android:app_name": "YouTube",
257
+ "twitter:app:name:iphone": "YouTube"
258
+ }
259
+ ],
260
+ "cse_image": [
261
+ {
262
+ "src": "https://yt3.googleusercontent.com/ytc/AIdro_nLo68wetImbwGUYP3stve_iKmAEccjhqB-q4o79xdInN4=s900-c-k-c0x00ffffff-no-rj"
263
+ }
264
+ ]
265
+ }
266
+ },
267
+ {
268
+ "kind": "customsearch#result",
269
+ "title": "Lecture - Wikipedia",
270
+ "htmlTitle": "\u003cb\u003eLecture\u003c/b\u003e - Wikipedia",
271
+ "link": "https://en.wikipedia.org/wiki/Lecture",
272
+ "displayLink": "en.wikipedia.org",
273
+ "snippet": "Lecture ... For the academic rank, see Lecturer. A lecture (from Latin: lēctūra 'reading') is an oral presentation intended to present information or teach people ...",
274
+ "htmlSnippet": "\u003cb\u003eLecture\u003c/b\u003e ... For the academic rank, see \u003cb\u003eLecturer\u003c/b\u003e. A \u003cb\u003electure\u003c/b\u003e (from Latin: lēctūra &#39;reading&#39;) is an oral presentation intended to present information or teach people&nbsp;...",
275
+ "cacheId": "d9Pjta02fmgJ",
276
+ "formattedUrl": "https://en.wikipedia.org/wiki/Lecture",
277
+ "htmlFormattedUrl": "https://en.wikipedia.org/wiki/Lecture",
278
+ "pagemap": {
279
+ "metatags": [
280
+ {
281
+ "referrer": "origin",
282
+ "og:image": "https://upload.wikimedia.org/wikipedia/commons/thumb/2/26/ADFA_Lecture_Theatres.jpg/1200px-ADFA_Lecture_Theatres.jpg",
283
+ "theme-color": "#eaecf0",
284
+ "og:image:width": "1200",
285
+ "og:type": "website",
286
+ "viewport": "width=device-width, initial-scale=1.0, user-scalable=yes, minimum-scale=0.25, maximum-scale=5.0",
287
+ "og:title": "Lecture - Wikipedia",
288
+ "og:image:height": "799",
289
+ "format-detection": "telephone=no"
290
+ }
291
+ ]
292
+ }
293
+ },
294
+ {
295
+ "kind": "customsearch#result",
296
+ "title": "Mount Wilson Observatory | Lectures",
297
+ "htmlTitle": "Mount Wilson Observatory | \u003cb\u003eLectures\u003c/b\u003e",
298
+ "link": "https://www.mtwilson.edu/lectures/",
299
+ "displayLink": "www.mtwilson.edu",
300
+ "snippet": "Talks & Telescopes: August 24, 2024 – Panel: The Triumph of Hubble ... Compelling talks followed by picnicking and convivial stargazing through both the big ...",
301
+ "htmlSnippet": "Talks &amp; Telescopes: August 24, 2024 – Panel: The Triumph of Hubble ... Compelling talks followed by picnicking and convivial stargazing through both the big&nbsp;...",
302
+ "cacheId": "wdXI0azqx5UJ",
303
+ "formattedUrl": "https://www.mtwilson.edu/lectures/",
304
+ "htmlFormattedUrl": "https://www.mtwilson.edu/\u003cb\u003electures\u003c/b\u003e/",
305
+ "pagemap": {
306
+ "metatags": [
307
+ {
308
+ "viewport": "width=device-width,initial-scale=1,user-scalable=no"
309
+ }
310
+ ],
311
+ "webpage": [
312
+ {
313
+ "image": "http://www.mtwilson.edu/wp-content/uploads/2016/09/Logo.jpg",
314
+ "url": "https://www.facebook.com/WilsonObs"
315
+ }
316
+ ]
317
+ }
318
+ },
319
+ {
320
+ "kind": "customsearch#result",
321
+ "title": "Lectures | NBER",
322
+ "htmlTitle": "\u003cb\u003eLectures\u003c/b\u003e | NBER",
323
+ "link": "https://www.nber.org/research/lectures",
324
+ "displayLink": "www.nber.org",
325
+ "snippet": "Results 1 - 50 of 354 ... Among featured events at the NBER Summer Institute are the Martin Feldstein Lecture, which examines a current issue involving economic ...",
326
+ "htmlSnippet": "Results 1 - 50 of 354 \u003cb\u003e...\u003c/b\u003e Among featured events at the NBER Summer Institute are the Martin Feldstein \u003cb\u003eLecture\u003c/b\u003e, which examines a current issue involving economic&nbsp;...",
327
+ "cacheId": "CvvP3U3nb44J",
328
+ "formattedUrl": "https://www.nber.org/research/lectures",
329
+ "htmlFormattedUrl": "https://www.nber.org/research/\u003cb\u003electures\u003c/b\u003e",
330
+ "pagemap": {
331
+ "cse_thumbnail": [
332
+ {
333
+ "src": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcTmeViEZyV1YmFEFLhcA6WdgAG3v3RV6tB93ncyxSJ5JPst_p2aWrL7D1k&s",
334
+ "width": "310",
335
+ "height": "163"
336
+ }
337
+ ],
338
+ "metatags": [
339
+ {
340
+ "og:image": "https://www.nber.org/sites/default/files/2022-06/NBER-FB-Share-Tile-1200.jpg",
341
+ "og:site_name": "NBER",
342
+ "handheldfriendly": "true",
343
+ "viewport": "width=device-width, initial-scale=1.0",
344
+ "og:title": "Lectures",
345
+ "mobileoptimized": "width",
346
+ "og:url": "https://www.nber.org/research/lectures"
347
+ }
348
+ ],
349
+ "cse_image": [
350
+ {
351
+ "src": "https://www.nber.org/sites/default/files/2022-06/NBER-FB-Share-Tile-1200.jpg"
352
+ }
353
+ ]
354
+ }
355
+ },
356
+ {
357
+ "kind": "customsearch#result",
358
+ "title": "STUDENTS CANNOT ACCESS RECORDED LECTURES ... - Solved",
359
+ "htmlTitle": "STUDENTS CANNOT ACCESS RECORDED LECTURES ... - Solved",
360
+ "link": "https://community.canvaslms.com/t5/Canvas-Question-Forum/STUDENTS-CANNOT-ACCESS-RECORDED-LECTURES/td-p/190358",
361
+ "displayLink": "community.canvaslms.com",
362
+ "snippet": "Mar 19, 2020 ... I believe the issue is that students were not invited. Are you trying to capture your screen? If not, there is an option to just record your web ...",
363
+ "htmlSnippet": "Mar 19, 2020 \u003cb\u003e...\u003c/b\u003e I believe the issue is that students were not invited. Are you trying to capture your screen? If not, there is an option to just record your web&nbsp;...",
364
+ "cacheId": "wqrynQXX61sJ",
365
+ "formattedUrl": "https://community.canvaslms.com/t5/Canvas...LECTURES/td-p/190358",
366
+ "htmlFormattedUrl": "https://community.canvaslms.com/t5/Canvas...\u003cb\u003eLECTURES\u003c/b\u003e/td-p/190358",
367
+ "pagemap": {
368
+ "cse_thumbnail": [
369
+ {
370
+ "src": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcRUqXau3N8LfKgSD7OJOvV7xzGarLKRU-ckWXy1ZQ1p4CLPsedvLKmLMhk&s",
371
+ "width": "310",
372
+ "height": "163"
373
+ }
374
+ ],
375
+ "metatags": [
376
+ {
377
+ "og:image": "https://community.canvaslms.com/html/@6A1FDD4D5FF35E4BBB4083A1022FA0DB/assets/CommunityPreview23.png",
378
+ "og:type": "article",
379
+ "article:section": "Canvas Question Forum",
380
+ "article:published_time": "2020-03-19T15:50:03.409Z",
381
+ "og:site_name": "Instructure Community",
382
+ "article:modified_time": "2020-03-19T13:55:53-07:00",
383
+ "viewport": "width=device-width, initial-scale=1.0, user-scalable=yes",
384
+ "og:title": "STUDENTS CANNOT ACCESS RECORDED LECTURES",
385
+ "og:url": "https://community.canvaslms.com/t5/Canvas-Question-Forum/STUDENTS-CANNOT-ACCESS-RECORDED-LECTURES/m-p/190358#M93667",
386
+ "og:description": "I can access and see my recorded lectures but my students can't. They have an error message when they try to open the recorded presentation or notes.",
387
+ "article:author": "https://community.canvaslms.com/t5/user/viewprofilepage/user-id/794287",
388
+ "twitter:image": "https://community.canvaslms.com/html/@6A1FDD4D5FF35E4BBB4083A1022FA0DB/assets/CommunityPreview23.png"
389
+ }
390
+ ],
391
+ "cse_image": [
392
+ {
393
+ "src": "https://community.canvaslms.com/html/@6A1FDD4D5FF35E4BBB4083A1022FA0DB/assets/CommunityPreview23.png"
394
+ }
395
+ ]
396
+ }
397
+ },
398
+ {
399
+ "kind": "customsearch#result",
400
+ "title": "Public Lecture Series - Sam Fox School of Design & Visual Arts",
401
+ "htmlTitle": "Public \u003cb\u003eLecture\u003c/b\u003e Series - Sam Fox School of Design &amp; Visual Arts",
402
+ "link": "https://samfoxschool.wustl.edu/calendar/series/2-public-lecture-series",
403
+ "displayLink": "samfoxschool.wustl.edu",
404
+ "snippet": "The Sam Fox School's Spring 2024 Public Lecture Series highlights design and art as catalysts for change. Renowned speakers will delve into themes like ...",
405
+ "htmlSnippet": "The Sam Fox School&#39;s Spring 2024 Public \u003cb\u003eLecture\u003c/b\u003e Series highlights design and art as catalysts for change. Renowned speakers will delve into themes like&nbsp;...",
406
+ "cacheId": "B-cgQG0j6tUJ",
407
+ "formattedUrl": "https://samfoxschool.wustl.edu/calendar/series/2-public-lecture-series",
408
+ "htmlFormattedUrl": "https://samfoxschool.wustl.edu/calendar/series/2-public-lecture-series",
409
+ "pagemap": {
410
+ "cse_thumbnail": [
411
+ {
412
+ "src": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQSmHaGianm-64m-qauYjkPK_Q0JKWe-7yom4m1ogFYTmpWArA7k6dmk0sR&s",
413
+ "width": "307",
414
+ "height": "164"
415
+ }
416
+ ],
417
+ "website": [
418
+ {
419
+ "name": "Public Lecture Series - Sam Fox School of Design & Visual Arts — Washington University in St. Louis"
420
+ }
421
+ ],
422
+ "metatags": [
423
+ {
424
+ "og:image": "https://dvsp0hlm0xrn3.cloudfront.net/assets/default_og_image-44e73dee4b9d1e2c6a6295901371270c8ec5899eaed48ee8167a9b12f1b0f8b3.jpg",
425
+ "og:type": "website",
426
+ "og:site_name": "Sam Fox School of Design & Visual Arts — Washington University in St. Louis",
427
+ "viewport": "width=device-width, initial-scale=1.0",
428
+ "og:title": "Public Lecture Series - Sam Fox School of Design & Visual Arts — Washington University in St. Louis",
429
+ "csrf-token": "jBQsfZGY3RH8NVs0-KVDBYB-2N2kib4UYZHYdrShfTdLkvzfSvGeOaMrRKTRdYBPRKzdcGIuP7zwm9etqX_uvg",
430
+ "csrf-param": "authenticity_token",
431
+ "og:description": "The Sam Fox School's Spring 2024 Public Lecture Series highlights design and art as catalysts for change. Renowned speakers will delve into themes like social equity, resilient cities, and the impact of emerging technologies on contemporary life. Speakers include artists, architects, designers, and critics of the highest caliber, widely recognized for their research-based practices and multidisciplinary approaches to their fields."
432
+ }
433
+ ],
434
+ "cse_image": [
435
+ {
436
+ "src": "https://dvsp0hlm0xrn3.cloudfront.net/assets/default_og_image-44e73dee4b9d1e2c6a6295901371270c8ec5899eaed48ee8167a9b12f1b0f8b3.jpg"
437
+ }
438
+ ]
439
+ }
440
+ }
441
+ ]
442
+ }
backend/apps/rag/search/testdata/searxng.json ADDED
@@ -0,0 +1,476 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "query": "python",
3
+ "number_of_results": 116000000,
4
+ "results": [
5
+ {
6
+ "url": "https://www.python.org/",
7
+ "title": "Welcome to Python.org",
8
+ "content": "Python is a versatile and powerful language that lets you work quickly and integrate systems more effectively. Learn how to get started, download the latest version, access documentation, find jobs, and join the Python community.",
9
+ "engine": "bing",
10
+ "parsed_url": ["https", "www.python.org", "/", "", "", ""],
11
+ "template": "default.html",
12
+ "engines": ["bing", "qwant", "duckduckgo"],
13
+ "positions": [1, 1, 1],
14
+ "score": 9.0,
15
+ "category": "general"
16
+ },
17
+ {
18
+ "url": "https://wiki.nerdvpn.de/wiki/Python_(programming_language)",
19
+ "title": "Python (programming language) - Wikipedia",
20
+ "content": "Python is a high-level, general-purpose programming language. Its design philosophy emphasizes code readability with the use of significant indentation. Python is dynamically typed and garbage-collected. It supports multiple programming paradigms, including structured (particularly procedural), object-oriented and functional programming.",
21
+ "engine": "bing",
22
+ "parsed_url": ["https", "wiki.nerdvpn.de", "/wiki/Python_(programming_language)", "", "", ""],
23
+ "template": "default.html",
24
+ "engines": ["bing", "qwant", "duckduckgo"],
25
+ "positions": [4, 3, 2],
26
+ "score": 3.25,
27
+ "category": "general"
28
+ },
29
+ {
30
+ "url": "https://docs.python.org/3/tutorial/index.html",
31
+ "title": "The Python Tutorial \u2014 Python 3.12.3 documentation",
32
+ "content": "3 days ago \u00b7 Python is an easy to learn, powerful programming language. It has efficient high-level data structures and a simple but effective approach to object-oriented programming. Python\u2019s elegant syntax and dynamic typing, together with its interpreted nature, make it an ideal language for scripting and rapid application development in many \u2026",
33
+ "engine": "bing",
34
+ "parsed_url": ["https", "docs.python.org", "/3/tutorial/index.html", "", "", ""],
35
+ "template": "default.html",
36
+ "engines": ["bing", "qwant", "duckduckgo"],
37
+ "positions": [5, 5, 3],
38
+ "score": 2.2,
39
+ "category": "general"
40
+ },
41
+ {
42
+ "url": "https://www.python.org/downloads/",
43
+ "title": "Download Python | Python.org",
44
+ "content": "Python is a popular programming language for various purposes. Find the latest version of Python for different operating systems, download release notes, and learn about the development process.",
45
+ "engine": "bing",
46
+ "parsed_url": ["https", "www.python.org", "/downloads/", "", "", ""],
47
+ "template": "default.html",
48
+ "engines": ["bing", "duckduckgo"],
49
+ "positions": [2, 2],
50
+ "score": 2.0,
51
+ "category": "general"
52
+ },
53
+ {
54
+ "url": "https://www.python.org/about/gettingstarted/",
55
+ "title": "Python For Beginners | Python.org",
56
+ "content": "Learn the basics of Python, a popular and easy-to-use programming language, from installing it to using it for various purposes. Find out how to access online documentation, tutorials, books, code samples, and more resources to help you get started with Python.",
57
+ "engine": "bing",
58
+ "parsed_url": ["https", "www.python.org", "/about/gettingstarted/", "", "", ""],
59
+ "template": "default.html",
60
+ "engines": ["bing", "qwant", "duckduckgo"],
61
+ "positions": [9, 4, 4],
62
+ "score": 1.8333333333333333,
63
+ "category": "general"
64
+ },
65
+ {
66
+ "url": "https://www.python.org/shell/",
67
+ "title": "Welcome to Python.org",
68
+ "content": "Python is a versatile and easy-to-use programming language that lets you work quickly. Learn more about Python, download the latest version, access documentation, find jobs, and join the community.",
69
+ "engine": "bing",
70
+ "parsed_url": ["https", "www.python.org", "/shell/", "", "", ""],
71
+ "template": "default.html",
72
+ "engines": ["bing", "qwant", "duckduckgo"],
73
+ "positions": [3, 10, 8],
74
+ "score": 1.675,
75
+ "category": "general"
76
+ },
77
+ {
78
+ "url": "https://realpython.com/",
79
+ "title": "Python Tutorials \u2013 Real Python",
80
+ "content": "Real Python offers comprehensive and up-to-date tutorials, books, and courses for Python developers of all skill levels. Whether you want to learn Python basics, web development, data science, machine learning, or more, you can find clear and practical guides and code examples here.",
81
+ "engine": "bing",
82
+ "parsed_url": ["https", "realpython.com", "/", "", "", ""],
83
+ "template": "default.html",
84
+ "engines": ["bing", "qwant", "duckduckgo"],
85
+ "positions": [6, 6, 5],
86
+ "score": 1.6,
87
+ "category": "general"
88
+ },
89
+ {
90
+ "url": "https://wiki.nerdvpn.de/wiki/Python",
91
+ "title": "Python",
92
+ "content": "Topics referred to by the same term",
93
+ "engine": "wikipedia",
94
+ "parsed_url": ["https", "wiki.nerdvpn.de", "/wiki/Python", "", "", ""],
95
+ "template": "default.html",
96
+ "engines": ["wikipedia"],
97
+ "positions": [1],
98
+ "score": 1.0,
99
+ "category": "general"
100
+ },
101
+ {
102
+ "title": "Online Python - IDE, Editor, Compiler, Interpreter",
103
+ "content": "Online Python IDE is a free online tool that lets you write, execute, and share Python code in the web browser. Learn about Python, its features, and its popularity as a general-purpose programming language for web development, data science, and more.",
104
+ "url": "https://www.online-python.com/",
105
+ "engine": "duckduckgo",
106
+ "parsed_url": ["https", "www.online-python.com", "/", "", "", ""],
107
+ "template": "default.html",
108
+ "engines": ["qwant", "duckduckgo"],
109
+ "positions": [8, 6],
110
+ "score": 0.5833333333333333,
111
+ "category": "general"
112
+ },
113
+ {
114
+ "url": "https://micropython.org/",
115
+ "title": "MicroPython - Python for microcontrollers",
116
+ "content": "MicroPython is a full Python compiler and runtime that runs on the bare-metal. You get an interactive prompt (the REPL) to execute commands immediately, along ...",
117
+ "img_src": null,
118
+ "engine": "google",
119
+ "parsed_url": ["https", "micropython.org", "/", "", "", ""],
120
+ "template": "default.html",
121
+ "engines": ["google"],
122
+ "positions": [1],
123
+ "score": 1.0,
124
+ "category": "general"
125
+ },
126
+ {
127
+ "url": "https://dictionary.cambridge.org/uk/dictionary/english/python",
128
+ "title": "PYTHON | \u0417\u043d\u0430\u0447\u0435\u043d\u043d\u044f \u0432 \u0430\u043d\u0433\u043b\u0456\u0439\u0441\u044c\u043a\u0456\u0439 \u043c\u043e\u0432\u0456 - Cambridge Dictionary",
129
+ "content": "Apr 17, 2024 \u2014 \u0412\u0438\u0437\u043d\u0430\u0447\u0435\u043d\u043d\u044f PYTHON: 1. a very large snake that kills animals for food by wrapping itself around them and crushing them\u2026. \u0414\u0456\u0437\u043d\u0430\u0439\u0442\u0435\u0441\u044f \u0431\u0456\u043b\u044c\u0448\u0435.",
130
+ "img_src": null,
131
+ "engine": "google",
132
+ "parsed_url": [
133
+ "https",
134
+ "dictionary.cambridge.org",
135
+ "/uk/dictionary/english/python",
136
+ "",
137
+ "",
138
+ ""
139
+ ],
140
+ "template": "default.html",
141
+ "engines": ["google"],
142
+ "positions": [2],
143
+ "score": 0.5,
144
+ "category": "general"
145
+ },
146
+ {
147
+ "url": "https://www.codetoday.co.uk/code",
148
+ "title": "Web-based Python Editor (with Turtle graphics)",
149
+ "content": "Quick way of starting to write Python code, including drawing with Turtle, provided by CodeToday using Trinket.io Ideal for young children to start ...",
150
+ "img_src": null,
151
+ "engine": "google",
152
+ "parsed_url": ["https", "www.codetoday.co.uk", "/code", "", "", ""],
153
+ "template": "default.html",
154
+ "engines": ["google"],
155
+ "positions": [3],
156
+ "score": 0.3333333333333333,
157
+ "category": "general"
158
+ },
159
+ {
160
+ "url": "https://snapcraft.io/docs/python-plugin",
161
+ "title": "The python plugin | Snapcraft documentation",
162
+ "content": "The python plugin can be used by either Python 2 or Python 3 based parts using a setup.py script for building the project, or using a package published to ...",
163
+ "img_src": null,
164
+ "engine": "google",
165
+ "parsed_url": ["https", "snapcraft.io", "/docs/python-plugin", "", "", ""],
166
+ "template": "default.html",
167
+ "engines": ["google"],
168
+ "positions": [4],
169
+ "score": 0.25,
170
+ "category": "general"
171
+ },
172
+ {
173
+ "url": "https://www.developer-tech.com/categories/developer-languages/developer-languages-python/",
174
+ "title": "Latest Python Developer News",
175
+ "content": "Python's status as the primary language for AI and machine learning projects, from its extensive data-handling capabilities to its flexibility and ...",
176
+ "img_src": null,
177
+ "engine": "google",
178
+ "parsed_url": [
179
+ "https",
180
+ "www.developer-tech.com",
181
+ "/categories/developer-languages/developer-languages-python/",
182
+ "",
183
+ "",
184
+ ""
185
+ ],
186
+ "template": "default.html",
187
+ "engines": ["google"],
188
+ "positions": [5],
189
+ "score": 0.2,
190
+ "category": "general"
191
+ },
192
+ {
193
+ "url": "https://subjectguides.york.ac.uk/coding/python",
194
+ "title": "Coding: a Practical Guide - Python - Subject Guides",
195
+ "content": "Python is a coding language used for a wide range of things, including working with data, building systems and software, and even creating games.",
196
+ "img_src": null,
197
+ "engine": "google",
198
+ "parsed_url": ["https", "subjectguides.york.ac.uk", "/coding/python", "", "", ""],
199
+ "template": "default.html",
200
+ "engines": ["google"],
201
+ "positions": [6],
202
+ "score": 0.16666666666666666,
203
+ "category": "general"
204
+ },
205
+ {
206
+ "url": "https://hub.salford.ac.uk/psytech/python/getting-started-python/",
207
+ "title": "Getting Started - Python - Salford PsyTech Home - The Hub",
208
+ "content": "Python in itself is a very friendly programming language, when we get to grips with writing code, once you grasp the logic, it will become very intuitive.",
209
+ "img_src": null,
210
+ "engine": "google",
211
+ "parsed_url": [
212
+ "https",
213
+ "hub.salford.ac.uk",
214
+ "/psytech/python/getting-started-python/",
215
+ "",
216
+ "",
217
+ ""
218
+ ],
219
+ "template": "default.html",
220
+ "engines": ["google"],
221
+ "positions": [7],
222
+ "score": 0.14285714285714285,
223
+ "category": "general"
224
+ },
225
+ {
226
+ "url": "https://snapcraft.io/docs/python-apps",
227
+ "title": "Python apps | Snapcraft documentation",
228
+ "content": "Snapcraft can be used to package and distribute Python applications in a way that enables convenient installation by users. The process of creating a snap ...",
229
+ "img_src": null,
230
+ "engine": "google",
231
+ "parsed_url": ["https", "snapcraft.io", "/docs/python-apps", "", "", ""],
232
+ "template": "default.html",
233
+ "engines": ["google"],
234
+ "positions": [8],
235
+ "score": 0.125,
236
+ "category": "general"
237
+ },
238
+ {
239
+ "url": "https://anvil.works/",
240
+ "title": "Anvil | Build Web Apps with Nothing but Python",
241
+ "content": "Anvil is a free Python-based drag-and-drop web app builder.\u200eSign Up \u00b7 \u200eSign in \u00b7 \u200ePricing \u00b7 \u200eForum",
242
+ "img_src": null,
243
+ "engine": "google",
244
+ "parsed_url": ["https", "anvil.works", "/", "", "", ""],
245
+ "template": "default.html",
246
+ "engines": ["google"],
247
+ "positions": [9],
248
+ "score": 0.1111111111111111,
249
+ "category": "general"
250
+ },
251
+ {
252
+ "url": "https://docs.python.org/",
253
+ "title": "Python 3.12.3 documentation",
254
+ "content": "3 days ago \u00b7 This is the official documentation for Python 3.12.3. Documentation sections: What's new in Python 3.12? Or all \"What's new\" documents since Python 2.0. Tutorial. Start here: a tour of Python's syntax and features. Library reference. Standard library and builtins. Language reference.",
255
+ "engine": "bing",
256
+ "parsed_url": ["https", "docs.python.org", "/", "", "", ""],
257
+ "template": "default.html",
258
+ "engines": ["bing", "duckduckgo"],
259
+ "positions": [7, 13],
260
+ "score": 0.43956043956043955,
261
+ "category": "general"
262
+ },
263
+ {
264
+ "title": "How to Use Python: Your First Steps - Real Python",
265
+ "content": "Learn the basics of Python syntax, installation, error handling, and more in this tutorial. You'll also code your first Python program and test your knowledge with a quiz.",
266
+ "url": "https://realpython.com/python-first-steps/",
267
+ "engine": "duckduckgo",
268
+ "parsed_url": ["https", "realpython.com", "/python-first-steps/", "", "", ""],
269
+ "template": "default.html",
270
+ "engines": ["qwant", "duckduckgo"],
271
+ "positions": [14, 7],
272
+ "score": 0.42857142857142855,
273
+ "category": "general"
274
+ },
275
+ {
276
+ "title": "The Python Tutorial \u2014 Python 3.11.8 documentation",
277
+ "content": "This tutorial introduces the reader informally to the basic concepts and features of the Python language and system. It helps to have a Python interpreter handy for hands-on experience, but all examples are self-contained, so the tutorial can be read off-line as well. For a description of standard objects and modules, see The Python Standard ...",
278
+ "url": "https://docs.python.org/3.11/tutorial/",
279
+ "engine": "duckduckgo",
280
+ "parsed_url": ["https", "docs.python.org", "/3.11/tutorial/", "", "", ""],
281
+ "template": "default.html",
282
+ "engines": ["duckduckgo"],
283
+ "positions": [7],
284
+ "score": 0.14285714285714285,
285
+ "category": "general"
286
+ },
287
+ {
288
+ "url": "https://realpython.com/python-introduction/",
289
+ "title": "Introduction to Python 3 \u2013 Real Python",
290
+ "content": "Python programming language, including a brief history of the development of Python and reasons why you might select Python as your language of choice.",
291
+ "engine": "bing",
292
+ "parsed_url": ["https", "realpython.com", "/python-introduction/", "", "", ""],
293
+ "template": "default.html",
294
+ "engines": ["bing"],
295
+ "positions": [8],
296
+ "score": 0.125,
297
+ "category": "general"
298
+ },
299
+ {
300
+ "title": "Our Documentation | Python.org",
301
+ "content": "Find online or download Python's documentation, tutorials, and guides for beginners and advanced users. Learn how to port from Python 2 to Python 3, contribute to Python, and access Python videos and books.",
302
+ "url": "https://www.python.org/doc/",
303
+ "engine": "duckduckgo",
304
+ "parsed_url": ["https", "www.python.org", "/doc/", "", "", ""],
305
+ "template": "default.html",
306
+ "engines": ["duckduckgo"],
307
+ "positions": [9],
308
+ "score": 0.1111111111111111,
309
+ "category": "general"
310
+ },
311
+ {
312
+ "title": "Welcome to Python.org",
313
+ "url": "http://www.get-python.org/shell/",
314
+ "content": "The mission of the Python Software Foundation is to promote, protect, and advance the Python programming language, and to support and facilitate the growth of a diverse and international community of Python programmers. Learn more. Become a Member Donate to the PSF.",
315
+ "engine": "qwant",
316
+ "parsed_url": ["http", "www.get-python.org", "/shell/", "", "", ""],
317
+ "template": "default.html",
318
+ "engines": ["qwant"],
319
+ "positions": [9],
320
+ "score": 0.1111111111111111,
321
+ "category": "general"
322
+ },
323
+ {
324
+ "title": "About Python\u2122 | Python.org",
325
+ "content": "Python is a powerful, fast, and versatile programming language that runs on various platforms and is easy to learn. Learn how to get started, explore the applications, and join the community of Python programmers and users.",
326
+ "url": "https://www.python.org/about/",
327
+ "engine": "duckduckgo",
328
+ "parsed_url": ["https", "www.python.org", "/about/", "", "", ""],
329
+ "template": "default.html",
330
+ "engines": ["duckduckgo"],
331
+ "positions": [11],
332
+ "score": 0.09090909090909091,
333
+ "category": "general"
334
+ },
335
+ {
336
+ "title": "Online Python Compiler (Interpreter) - Programiz",
337
+ "content": "Write and run Python code using this online tool. You can use Python Shell like IDLE, and take inputs from the user in our Python compiler.",
338
+ "url": "https://www.programiz.com/python-programming/online-compiler/",
339
+ "engine": "duckduckgo",
340
+ "parsed_url": [
341
+ "https",
342
+ "www.programiz.com",
343
+ "/python-programming/online-compiler/",
344
+ "",
345
+ "",
346
+ ""
347
+ ],
348
+ "template": "default.html",
349
+ "engines": ["duckduckgo"],
350
+ "positions": [12],
351
+ "score": 0.08333333333333333,
352
+ "category": "general"
353
+ },
354
+ {
355
+ "title": "Welcome to Python.org",
356
+ "content": "Python is a versatile and powerful language that lets you work quickly and integrate systems more effectively. Download the latest version, read the documentation, find jobs, events, success stories, and more on Python.org.",
357
+ "url": "https://www.python.org/?downloads",
358
+ "engine": "duckduckgo",
359
+ "parsed_url": ["https", "www.python.org", "/", "", "downloads", ""],
360
+ "template": "default.html",
361
+ "engines": ["duckduckgo"],
362
+ "positions": [15],
363
+ "score": 0.06666666666666667,
364
+ "category": "general"
365
+ },
366
+ {
367
+ "url": "https://www.matillion.com/blog/the-importance-of-python-and-its-growing-influence-on-data-productivty-a-matillion-perspective",
368
+ "title": "The Importance of Python and its Growing Influence on ...",
369
+ "content": "Jan 30, 2024 \u2014 The synergy of low-code functionality with Python's versatility empowers data professionals to orchestrate complex transformations seamlessly.",
370
+ "img_src": null,
371
+ "engine": "google",
372
+ "parsed_url": [
373
+ "https",
374
+ "www.matillion.com",
375
+ "/blog/the-importance-of-python-and-its-growing-influence-on-data-productivty-a-matillion-perspective",
376
+ "",
377
+ "",
378
+ ""
379
+ ],
380
+ "template": "default.html",
381
+ "engines": ["google"],
382
+ "positions": [10],
383
+ "score": 0.1,
384
+ "category": "general"
385
+ },
386
+ {
387
+ "title": "BeginnersGuide - Python Wiki",
388
+ "content": "This is the program that reads Python programs and carries out their instructions; you need it before you can do any Python programming. Mac and Linux distributions may include an outdated version of Python (Python 2), but you should install an updated one (Python 3). See BeginnersGuide/Download for instructions to download the correct version ...",
389
+ "url": "https://wiki.python.org/moin/BeginnersGuide",
390
+ "engine": "duckduckgo",
391
+ "parsed_url": ["https", "wiki.python.org", "/moin/BeginnersGuide", "", "", ""],
392
+ "template": "default.html",
393
+ "engines": ["duckduckgo"],
394
+ "positions": [16],
395
+ "score": 0.0625,
396
+ "category": "general"
397
+ },
398
+ {
399
+ "title": "Learn Python - Free Interactive Python Tutorial",
400
+ "content": "Learn Python from scratch or improve your skills with this website that offers tutorials, exercises, tests and certification. Explore topics such as basics, data science, advanced features and more with DataCamp.",
401
+ "url": "https://www.learnpython.org/",
402
+ "engine": "duckduckgo",
403
+ "parsed_url": ["https", "www.learnpython.org", "/", "", "", ""],
404
+ "template": "default.html",
405
+ "engines": ["duckduckgo"],
406
+ "positions": [17],
407
+ "score": 0.058823529411764705,
408
+ "category": "general"
409
+ }
410
+ ],
411
+ "answers": [],
412
+ "corrections": [],
413
+ "infoboxes": [
414
+ {
415
+ "infobox": "Python",
416
+ "id": "https://en.wikipedia.org/wiki/Python_(programming_language)",
417
+ "content": "general-purpose programming language",
418
+ "img_src": "https://upload.wikimedia.org/wikipedia/commons/thumb/6/6f/.PY_file_recreation.png/500px-.PY_file_recreation.png",
419
+ "urls": [
420
+ {
421
+ "title": "Official website",
422
+ "url": "https://www.python.org/",
423
+ "official": true
424
+ },
425
+ {
426
+ "title": "Wikipedia (en)",
427
+ "url": "https://en.wikipedia.org/wiki/Python_(programming_language)"
428
+ },
429
+ {
430
+ "title": "Wikidata",
431
+ "url": "http://www.wikidata.org/entity/Q28865"
432
+ }
433
+ ],
434
+ "attributes": [
435
+ {
436
+ "label": "Inception",
437
+ "value": "Wednesday, February 20, 1991",
438
+ "entity": "P571"
439
+ },
440
+ {
441
+ "label": "Developer",
442
+ "value": "Python Software Foundation, Guido van Rossum",
443
+ "entity": "P178"
444
+ },
445
+ {
446
+ "label": "Copyright license",
447
+ "value": "Python Software Foundation License",
448
+ "entity": "P275"
449
+ },
450
+ {
451
+ "label": "Programmed in",
452
+ "value": "C, Python",
453
+ "entity": "P277"
454
+ },
455
+ {
456
+ "label": "Software version identifier",
457
+ "value": "3.12.3, 3.13.0a6",
458
+ "entity": "P348"
459
+ }
460
+ ],
461
+ "engine": "wikidata",
462
+ "engines": ["wikidata"]
463
+ }
464
+ ],
465
+ "suggestions": [
466
+ "python turtle",
467
+ "micro python tutorial",
468
+ "python docs",
469
+ "python compiler",
470
+ "snapcraft python",
471
+ "micropython vs python",
472
+ "python online",
473
+ "python download"
474
+ ],
475
+ "unresponsive_engines": []
476
+ }
backend/apps/rag/search/testdata/serper.json ADDED
@@ -0,0 +1,190 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "searchParameters": {
3
+ "q": "apple inc",
4
+ "gl": "us",
5
+ "hl": "en",
6
+ "autocorrect": true,
7
+ "page": 1,
8
+ "type": "search"
9
+ },
10
+ "knowledgeGraph": {
11
+ "title": "Apple",
12
+ "type": "Technology company",
13
+ "website": "http://www.apple.com/",
14
+ "imageUrl": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQwGQRv5TjjkycpctY66mOg_e2-npacrmjAb6_jAWhzlzkFE3OTjxyzbA&s=0",
15
+ "description": "Apple Inc. is an American multinational technology company specializing in consumer electronics, software and online services headquartered in Cupertino, California, United States.",
16
+ "descriptionSource": "Wikipedia",
17
+ "descriptionLink": "https://en.wikipedia.org/wiki/Apple_Inc.",
18
+ "attributes": {
19
+ "Headquarters": "Cupertino, CA",
20
+ "CEO": "Tim Cook (Aug 24, 2011–)",
21
+ "Founded": "April 1, 1976, Los Altos, CA",
22
+ "Sales": "1 (800) 692-7753",
23
+ "Products": "iPhone, Apple Watch, iPad, and more",
24
+ "Founders": "Steve Jobs, Steve Wozniak, and Ronald Wayne",
25
+ "Subsidiaries": "Apple Store, Beats Electronics, Beddit, and more"
26
+ }
27
+ },
28
+ "organic": [
29
+ {
30
+ "title": "Apple",
31
+ "link": "https://www.apple.com/",
32
+ "snippet": "Discover the innovative world of Apple and shop everything iPhone, iPad, Apple Watch, Mac, and Apple TV, plus explore accessories, entertainment, ...",
33
+ "sitelinks": [
34
+ {
35
+ "title": "Support",
36
+ "link": "https://support.apple.com/"
37
+ },
38
+ {
39
+ "title": "iPhone",
40
+ "link": "https://www.apple.com/iphone/"
41
+ },
42
+ {
43
+ "title": "Apple makes business better.",
44
+ "link": "https://www.apple.com/business/"
45
+ },
46
+ {
47
+ "title": "Mac",
48
+ "link": "https://www.apple.com/mac/"
49
+ }
50
+ ],
51
+ "position": 1
52
+ },
53
+ {
54
+ "title": "Apple Inc. - Wikipedia",
55
+ "link": "https://en.wikipedia.org/wiki/Apple_Inc.",
56
+ "snippet": "Apple Inc. is an American multinational technology company specializing in consumer electronics, software and online services headquartered in Cupertino, ...",
57
+ "attributes": {
58
+ "Products": "AirPods; Apple Watch; iPad; iPhone; Mac",
59
+ "Founders": "Steve Jobs; Steve Wozniak; Ronald Wayne",
60
+ "Founded": "April 1, 1976; 46 years ago in Los Altos, California, U.S",
61
+ "Industry": "Consumer electronics; Software services; Online services"
62
+ },
63
+ "sitelinks": [
64
+ {
65
+ "title": "History",
66
+ "link": "https://en.wikipedia.org/wiki/History_of_Apple_Inc."
67
+ },
68
+ {
69
+ "title": "Timeline of Apple Inc. products",
70
+ "link": "https://en.wikipedia.org/wiki/Timeline_of_Apple_Inc._products"
71
+ },
72
+ {
73
+ "title": "List of software by Apple Inc.",
74
+ "link": "https://en.wikipedia.org/wiki/List_of_software_by_Apple_Inc."
75
+ },
76
+ {
77
+ "title": "Apple Store",
78
+ "link": "https://en.wikipedia.org/wiki/Apple_Store"
79
+ }
80
+ ],
81
+ "position": 2
82
+ },
83
+ {
84
+ "title": "Apple Inc. | History, Products, Headquarters, & Facts | Britannica",
85
+ "link": "https://www.britannica.com/topic/Apple-Inc",
86
+ "snippet": "Apple Inc., formerly Apple Computer, Inc., American manufacturer of personal computers, smartphones, tablet computers, computer peripherals, ...",
87
+ "date": "Aug 31, 2022",
88
+ "attributes": {
89
+ "Related People": "Steve Jobs Steve Wozniak Jony Ive Tim Cook Angela Ahrendts",
90
+ "Date": "1976 - present",
91
+ "Areas Of Involvement": "peripheral device"
92
+ },
93
+ "position": 3
94
+ },
95
+ {
96
+ "title": "AAPL: Apple Inc Stock Price Quote - NASDAQ GS - Bloomberg.com",
97
+ "link": "https://www.bloomberg.com/quote/AAPL:US",
98
+ "snippet": "Stock analysis for Apple Inc (AAPL:NASDAQ GS) including stock price, stock chart, company news, key statistics, fundamentals and company profile.",
99
+ "position": 4
100
+ },
101
+ {
102
+ "title": "Apple Inc. (AAPL) Company Profile & Facts - Yahoo Finance",
103
+ "link": "https://finance.yahoo.com/quote/AAPL/profile/",
104
+ "snippet": "Apple Inc. designs, manufactures, and markets smartphones, personal computers, tablets, wearables, and accessories worldwide. It also sells various related ...",
105
+ "position": 5
106
+ },
107
+ {
108
+ "title": "AAPL | Apple Inc. Stock Price & News - WSJ",
109
+ "link": "https://www.wsj.com/market-data/quotes/AAPL",
110
+ "snippet": "Apple, Inc. engages in the design, manufacture, and sale of smartphones, personal computers, tablets, wearables and accessories, and other varieties of ...",
111
+ "position": 6
112
+ },
113
+ {
114
+ "title": "Apple Inc Company Profile - Apple Inc Overview - GlobalData",
115
+ "link": "https://www.globaldata.com/company-profile/apple-inc/",
116
+ "snippet": "Apple Inc (Apple) designs, manufactures, and markets smartphones, tablets, personal computers (PCs), portable and wearable devices. The company also offers ...",
117
+ "position": 7
118
+ },
119
+ {
120
+ "title": "Apple Inc (AAPL) Stock Price & News - Google Finance",
121
+ "link": "https://www.google.com/finance/quote/AAPL:NASDAQ?hl=en",
122
+ "snippet": "Get the latest Apple Inc (AAPL) real-time quote, historical performance, charts, and other financial information to help you make more informed trading and ...",
123
+ "position": 8
124
+ }
125
+ ],
126
+ "peopleAlsoAsk": [
127
+ {
128
+ "question": "What does Apple Inc mean?",
129
+ "snippet": "Apple Inc., formerly Apple Computer, Inc., American manufacturer of personal\ncomputers, smartphones, tablet computers, computer peripherals, and computer\nsoftware. It was the first successful personal computer company and the\npopularizer of the graphical user interface.\nAug 31, 2022",
130
+ "title": "Apple Inc. | History, Products, Headquarters, & Facts | Britannica",
131
+ "link": "https://www.britannica.com/topic/Apple-Inc"
132
+ },
133
+ {
134
+ "question": "Is Apple and Apple Inc same?",
135
+ "snippet": "Apple was founded as Apple Computer Company on April 1, 1976, by Steve Jobs,\nSteve Wozniak and Ronald Wayne to develop and sell Wozniak's Apple I personal\ncomputer. It was incorporated by Jobs and Wozniak as Apple Computer, Inc.",
136
+ "title": "Apple Inc. - Wikipedia",
137
+ "link": "https://en.wikipedia.org/wiki/Apple_Inc."
138
+ },
139
+ {
140
+ "question": "Who owns Apple Inc?",
141
+ "snippet": "Apple Inc. is owned by two main institutional investors (Vanguard Group and\nBlackRock, Inc). While its major individual shareholders comprise people like\nArt Levinson, Tim Cook, Bruce Sewell, Al Gore, Johny Sroujli, and others.",
142
+ "title": "Who Owns Apple In 2022? - FourWeekMBA",
143
+ "link": "https://fourweekmba.com/who-owns-apple/"
144
+ },
145
+ {
146
+ "question": "What products does Apple Inc offer?",
147
+ "snippet": "APPLE FOOTER\nStore.\nMac.\niPad.\niPhone.\nWatch.\nAirPods.\nTV & Home.\nAirTag.",
148
+ "title": "More items...",
149
+ "link": "https://www.apple.com/business/"
150
+ }
151
+ ],
152
+ "relatedSearches": [
153
+ {
154
+ "query": "Who invented the iPhone"
155
+ },
156
+ {
157
+ "query": "Apple Inc competitors"
158
+ },
159
+ {
160
+ "query": "Apple iPad"
161
+ },
162
+ {
163
+ "query": "iPhones"
164
+ },
165
+ {
166
+ "query": "Apple Inc us"
167
+ },
168
+ {
169
+ "query": "Apple company history"
170
+ },
171
+ {
172
+ "query": "Apple Store"
173
+ },
174
+ {
175
+ "query": "Apple customer service"
176
+ },
177
+ {
178
+ "query": "Apple Watch"
179
+ },
180
+ {
181
+ "query": "Apple Inc Industry"
182
+ },
183
+ {
184
+ "query": "Apple Inc registered address"
185
+ },
186
+ {
187
+ "query": "Apple Inc Bloomberg"
188
+ }
189
+ ]
190
+ }