github-actions[bot] commited on
Commit
e43bb93
·
0 Parent(s):

GitHub deploy: 58d3aecbb8652f589a0aa4d6392afa3beb96913e

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 +20 -0
  2. .env.example +13 -0
  3. .eslintignore +13 -0
  4. .eslintrc.cjs +31 -0
  5. .gitattributes +3 -0
  6. .github/workflows/deploy-to-hf-spaces.yml +63 -0
  7. .gitignore +309 -0
  8. .npmrc +1 -0
  9. .prettierignore +316 -0
  10. .prettierrc +9 -0
  11. CHANGELOG.md +0 -0
  12. CODE_OF_CONDUCT.md +99 -0
  13. Caddyfile.localhost +64 -0
  14. Dockerfile +176 -0
  15. INSTALLATION.md +35 -0
  16. LICENSE +27 -0
  17. Makefile +33 -0
  18. README.md +236 -0
  19. TROUBLESHOOTING.md +36 -0
  20. backend/.dockerignore +14 -0
  21. backend/.gitignore +12 -0
  22. backend/dev.sh +2 -0
  23. backend/open_webui/__init__.py +96 -0
  24. backend/open_webui/alembic.ini +114 -0
  25. backend/open_webui/config.py +2395 -0
  26. backend/open_webui/constants.py +119 -0
  27. backend/open_webui/env.py +421 -0
  28. backend/open_webui/functions.py +316 -0
  29. backend/open_webui/internal/db.py +116 -0
  30. backend/open_webui/internal/migrations/001_initial_schema.py +254 -0
  31. backend/open_webui/internal/migrations/002_add_local_sharing.py +48 -0
  32. backend/open_webui/internal/migrations/003_add_auth_api_key.py +48 -0
  33. backend/open_webui/internal/migrations/004_add_archived.py +46 -0
  34. backend/open_webui/internal/migrations/005_add_updated_at.py +130 -0
  35. backend/open_webui/internal/migrations/006_migrate_timestamps_and_charfields.py +130 -0
  36. backend/open_webui/internal/migrations/007_add_user_last_active_at.py +79 -0
  37. backend/open_webui/internal/migrations/008_add_memory.py +53 -0
  38. backend/open_webui/internal/migrations/009_add_models.py +61 -0
  39. backend/open_webui/internal/migrations/010_migrate_modelfiles_to_models.py +130 -0
  40. backend/open_webui/internal/migrations/011_add_user_settings.py +48 -0
  41. backend/open_webui/internal/migrations/012_add_tools.py +61 -0
  42. backend/open_webui/internal/migrations/013_add_user_info.py +48 -0
  43. backend/open_webui/internal/migrations/014_add_files.py +55 -0
  44. backend/open_webui/internal/migrations/015_add_functions.py +61 -0
  45. backend/open_webui/internal/migrations/016_add_valves_and_is_active.py +50 -0
  46. backend/open_webui/internal/migrations/017_add_user_oauth_sub.py +45 -0
  47. backend/open_webui/internal/migrations/018_add_function_is_global.py +49 -0
  48. backend/open_webui/internal/wrappers.py +66 -0
  49. backend/open_webui/main.py +1350 -0
  50. backend/open_webui/migrations/README +4 -0
.dockerignore ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .github
2
+ .DS_Store
3
+ docs
4
+ kubernetes
5
+ node_modules
6
+ /.svelte-kit
7
+ /package
8
+ .env
9
+ .env.*
10
+ vite.config.js.timestamp-*
11
+ vite.config.ts.timestamp-*
12
+ __pycache__
13
+ .idea
14
+ venv
15
+ _old
16
+ uploads
17
+ .ipynb_checkpoints
18
+ **/*.db
19
+ _test
20
+ backend/data/*
.env.example ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Ollama URL for the backend to connect
2
+ # The path '/ollama' will be redirected to the specified backend URL
3
+ OLLAMA_BASE_URL='http://localhost:11434'
4
+
5
+ OPENAI_API_BASE_URL=''
6
+ OPENAI_API_KEY=''
7
+
8
+ # AUTOMATIC1111_BASE_URL="http://localhost:7860"
9
+
10
+ # DO NOT TRACK
11
+ SCARF_NO_ANALYTICS=true
12
+ DO_NOT_TRACK=true
13
+ ANONYMIZED_TELEMETRY=false
.eslintignore ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .DS_Store
2
+ node_modules
3
+ /build
4
+ /.svelte-kit
5
+ /package
6
+ .env
7
+ .env.*
8
+ !.env.example
9
+
10
+ # Ignore files for PNPM, NPM and YARN
11
+ pnpm-lock.yaml
12
+ package-lock.json
13
+ yarn.lock
.eslintrc.cjs ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ module.exports = {
2
+ root: true,
3
+ extends: [
4
+ 'eslint:recommended',
5
+ 'plugin:@typescript-eslint/recommended',
6
+ 'plugin:svelte/recommended',
7
+ 'plugin:cypress/recommended',
8
+ 'prettier'
9
+ ],
10
+ parser: '@typescript-eslint/parser',
11
+ plugins: ['@typescript-eslint'],
12
+ parserOptions: {
13
+ sourceType: 'module',
14
+ ecmaVersion: 2020,
15
+ extraFileExtensions: ['.svelte']
16
+ },
17
+ env: {
18
+ browser: true,
19
+ es2017: true,
20
+ node: true
21
+ },
22
+ overrides: [
23
+ {
24
+ files: ['*.svelte'],
25
+ parser: 'svelte-eslint-parser',
26
+ parserOptions: {
27
+ parser: '@typescript-eslint/parser'
28
+ }
29
+ }
30
+ ]
31
+ };
.gitattributes ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ *.sh text eol=lf
2
+ *.ttf filter=lfs diff=lfs merge=lfs -text
3
+ *.jpg filter=lfs diff=lfs merge=lfs -text
.github/workflows/deploy-to-hf-spaces.yml ADDED
@@ -0,0 +1,63 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ with:
32
+ lfs: true
33
+
34
+ - name: Remove git history
35
+ run: rm -rf .git
36
+
37
+ - name: Prepend YAML front matter to README.md
38
+ run: |
39
+ echo "---" > temp_readme.md
40
+ echo "title: Open WebUI" >> temp_readme.md
41
+ echo "emoji: 🐳" >> temp_readme.md
42
+ echo "colorFrom: purple" >> temp_readme.md
43
+ echo "colorTo: gray" >> temp_readme.md
44
+ echo "sdk: docker" >> temp_readme.md
45
+ echo "app_port: 8080" >> temp_readme.md
46
+ echo "---" >> temp_readme.md
47
+ cat README.md >> temp_readme.md
48
+ mv temp_readme.md README.md
49
+
50
+ - name: Configure git
51
+ run: |
52
+ git config --global user.email "71052323+github-actions[bot]@users.noreply.github.com"
53
+ git config --global user.name "github-actions[bot]"
54
+ - name: Set up Git and push to Space
55
+ run: |
56
+ git init --initial-branch=main
57
+ git lfs install
58
+ git lfs track "*.ttf"
59
+ git lfs track "*.jpg"
60
+ rm demo.gif
61
+ git add .
62
+ git commit -m "GitHub deploy: ${{ github.sha }}"
63
+ git push --force https://lenaya:${HF_TOKEN}@huggingface.co/spaces/lenaya/open-webui main
.gitignore ADDED
@@ -0,0 +1,309 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .DS_Store
2
+ node_modules
3
+ /build
4
+ /.svelte-kit
5
+ /package
6
+ .env
7
+ .env.*
8
+ !.env.example
9
+ vite.config.js.timestamp-*
10
+ vite.config.ts.timestamp-*
11
+ # Byte-compiled / optimized / DLL files
12
+ __pycache__/
13
+ *.py[cod]
14
+ *$py.class
15
+
16
+ # C extensions
17
+ *.so
18
+
19
+ # Pyodide distribution
20
+ static/pyodide/*
21
+ !static/pyodide/pyodide-lock.json
22
+
23
+ # Distribution / packaging
24
+ .Python
25
+ build/
26
+ develop-eggs/
27
+ dist/
28
+ downloads/
29
+ eggs/
30
+ .eggs/
31
+ lib64/
32
+ parts/
33
+ sdist/
34
+ var/
35
+ wheels/
36
+ share/python-wheels/
37
+ *.egg-info/
38
+ .installed.cfg
39
+ *.egg
40
+ MANIFEST
41
+
42
+ # PyInstaller
43
+ # Usually these files are written by a python script from a template
44
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
45
+ *.manifest
46
+ *.spec
47
+
48
+ # Installer logs
49
+ pip-log.txt
50
+ pip-delete-this-directory.txt
51
+
52
+ # Unit test / coverage reports
53
+ htmlcov/
54
+ .tox/
55
+ .nox/
56
+ .coverage
57
+ .coverage.*
58
+ .cache
59
+ nosetests.xml
60
+ coverage.xml
61
+ *.cover
62
+ *.py,cover
63
+ .hypothesis/
64
+ .pytest_cache/
65
+ cover/
66
+
67
+ # Translations
68
+ *.mo
69
+ *.pot
70
+
71
+ # Django stuff:
72
+ *.log
73
+ local_settings.py
74
+ db.sqlite3
75
+ db.sqlite3-journal
76
+
77
+ # Flask stuff:
78
+ instance/
79
+ .webassets-cache
80
+
81
+ # Scrapy stuff:
82
+ .scrapy
83
+
84
+ # Sphinx documentation
85
+ docs/_build/
86
+
87
+ # PyBuilder
88
+ .pybuilder/
89
+ target/
90
+
91
+ # Jupyter Notebook
92
+ .ipynb_checkpoints
93
+
94
+ # IPython
95
+ profile_default/
96
+ ipython_config.py
97
+
98
+ # pyenv
99
+ # For a library or package, you might want to ignore these files since the code is
100
+ # intended to run in multiple environments; otherwise, check them in:
101
+ # .python-version
102
+
103
+ # pipenv
104
+ # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
105
+ # However, in case of collaboration, if having platform-specific dependencies or dependencies
106
+ # having no cross-platform support, pipenv may install dependencies that don't work, or not
107
+ # install all needed dependencies.
108
+ #Pipfile.lock
109
+
110
+ # poetry
111
+ # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
112
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
113
+ # commonly ignored for libraries.
114
+ # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
115
+ #poetry.lock
116
+
117
+ # pdm
118
+ # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
119
+ #pdm.lock
120
+ # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
121
+ # in version control.
122
+ # https://pdm.fming.dev/#use-with-ide
123
+ .pdm.toml
124
+
125
+ # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
126
+ __pypackages__/
127
+
128
+ # Celery stuff
129
+ celerybeat-schedule
130
+ celerybeat.pid
131
+
132
+ # SageMath parsed files
133
+ *.sage.py
134
+
135
+ # Environments
136
+ .env
137
+ .venv
138
+ env/
139
+ venv/
140
+ ENV/
141
+ env.bak/
142
+ venv.bak/
143
+
144
+ # Spyder project settings
145
+ .spyderproject
146
+ .spyproject
147
+
148
+ # Rope project settings
149
+ .ropeproject
150
+
151
+ # mkdocs documentation
152
+ /site
153
+
154
+ # mypy
155
+ .mypy_cache/
156
+ .dmypy.json
157
+ dmypy.json
158
+
159
+ # Pyre type checker
160
+ .pyre/
161
+
162
+ # pytype static type analyzer
163
+ .pytype/
164
+
165
+ # Cython debug symbols
166
+ cython_debug/
167
+
168
+ # PyCharm
169
+ # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
170
+ # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
171
+ # and can be added to the global gitignore or merged into this file. For a more nuclear
172
+ # option (not recommended) you can uncomment the following to ignore the entire idea folder.
173
+ .idea/
174
+
175
+ # Logs
176
+ logs
177
+ *.log
178
+ npm-debug.log*
179
+ yarn-debug.log*
180
+ yarn-error.log*
181
+ lerna-debug.log*
182
+ .pnpm-debug.log*
183
+
184
+ # Diagnostic reports (https://nodejs.org/api/report.html)
185
+ report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
186
+
187
+ # Runtime data
188
+ pids
189
+ *.pid
190
+ *.seed
191
+ *.pid.lock
192
+
193
+ # Directory for instrumented libs generated by jscoverage/JSCover
194
+ lib-cov
195
+
196
+ # Coverage directory used by tools like istanbul
197
+ coverage
198
+ *.lcov
199
+
200
+ # nyc test coverage
201
+ .nyc_output
202
+
203
+ # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
204
+ .grunt
205
+
206
+ # Bower dependency directory (https://bower.io/)
207
+ bower_components
208
+
209
+ # node-waf configuration
210
+ .lock-wscript
211
+
212
+ # Compiled binary addons (https://nodejs.org/api/addons.html)
213
+ build/Release
214
+
215
+ # Dependency directories
216
+ node_modules/
217
+ jspm_packages/
218
+
219
+ # Snowpack dependency directory (https://snowpack.dev/)
220
+ web_modules/
221
+
222
+ # TypeScript cache
223
+ *.tsbuildinfo
224
+
225
+ # Optional npm cache directory
226
+ .npm
227
+
228
+ # Optional eslint cache
229
+ .eslintcache
230
+
231
+ # Optional stylelint cache
232
+ .stylelintcache
233
+
234
+ # Microbundle cache
235
+ .rpt2_cache/
236
+ .rts2_cache_cjs/
237
+ .rts2_cache_es/
238
+ .rts2_cache_umd/
239
+
240
+ # Optional REPL history
241
+ .node_repl_history
242
+
243
+ # Output of 'npm pack'
244
+ *.tgz
245
+
246
+ # Yarn Integrity file
247
+ .yarn-integrity
248
+
249
+ # dotenv environment variable files
250
+ .env
251
+ .env.development.local
252
+ .env.test.local
253
+ .env.production.local
254
+ .env.local
255
+
256
+ # parcel-bundler cache (https://parceljs.org/)
257
+ .cache
258
+ .parcel-cache
259
+
260
+ # Next.js build output
261
+ .next
262
+ out
263
+
264
+ # Nuxt.js build / generate output
265
+ .nuxt
266
+ dist
267
+
268
+ # Gatsby files
269
+ .cache/
270
+ # Comment in the public line in if your project uses Gatsby and not Next.js
271
+ # https://nextjs.org/blog/next-9-1#public-directory-support
272
+ # public
273
+
274
+ # vuepress build output
275
+ .vuepress/dist
276
+
277
+ # vuepress v2.x temp and cache directory
278
+ .temp
279
+ .cache
280
+
281
+ # Docusaurus cache and generated files
282
+ .docusaurus
283
+
284
+ # Serverless directories
285
+ .serverless/
286
+
287
+ # FuseBox cache
288
+ .fusebox/
289
+
290
+ # DynamoDB Local files
291
+ .dynamodb/
292
+
293
+ # TernJS port file
294
+ .tern-port
295
+
296
+ # Stores VSCode versions used for testing VSCode extensions
297
+ .vscode-test
298
+
299
+ # yarn v2
300
+ .yarn/cache
301
+ .yarn/unplugged
302
+ .yarn/build-state.yml
303
+ .yarn/install-state.gz
304
+ .pnp.*
305
+
306
+ # cypress artifacts
307
+ cypress/videos
308
+ cypress/screenshots
309
+ .vscode/settings.json
.npmrc ADDED
@@ -0,0 +1 @@
 
 
1
+ engine-strict=true
.prettierignore ADDED
@@ -0,0 +1,316 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Ignore files for PNPM, NPM and YARN
2
+ pnpm-lock.yaml
3
+ package-lock.json
4
+ yarn.lock
5
+
6
+ kubernetes/
7
+
8
+ # Copy of .gitignore
9
+ .DS_Store
10
+ node_modules
11
+ /build
12
+ /.svelte-kit
13
+ /package
14
+ .env
15
+ .env.*
16
+ !.env.example
17
+ vite.config.js.timestamp-*
18
+ vite.config.ts.timestamp-*
19
+ # Byte-compiled / optimized / DLL files
20
+ __pycache__/
21
+ *.py[cod]
22
+ *$py.class
23
+
24
+ # C extensions
25
+ *.so
26
+
27
+ # Distribution / packaging
28
+ .Python
29
+ build/
30
+ develop-eggs/
31
+ dist/
32
+ downloads/
33
+ eggs/
34
+ .eggs/
35
+ lib64/
36
+ parts/
37
+ sdist/
38
+ var/
39
+ wheels/
40
+ share/python-wheels/
41
+ *.egg-info/
42
+ .installed.cfg
43
+ *.egg
44
+ MANIFEST
45
+
46
+ # PyInstaller
47
+ # Usually these files are written by a python script from a template
48
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
49
+ *.manifest
50
+ *.spec
51
+
52
+ # Installer logs
53
+ pip-log.txt
54
+ pip-delete-this-directory.txt
55
+
56
+ # Unit test / coverage reports
57
+ htmlcov/
58
+ .tox/
59
+ .nox/
60
+ .coverage
61
+ .coverage.*
62
+ .cache
63
+ nosetests.xml
64
+ coverage.xml
65
+ *.cover
66
+ *.py,cover
67
+ .hypothesis/
68
+ .pytest_cache/
69
+ cover/
70
+
71
+ # Translations
72
+ *.mo
73
+ *.pot
74
+
75
+ # Django stuff:
76
+ *.log
77
+ local_settings.py
78
+ db.sqlite3
79
+ db.sqlite3-journal
80
+
81
+ # Flask stuff:
82
+ instance/
83
+ .webassets-cache
84
+
85
+ # Scrapy stuff:
86
+ .scrapy
87
+
88
+ # Sphinx documentation
89
+ docs/_build/
90
+
91
+ # PyBuilder
92
+ .pybuilder/
93
+ target/
94
+
95
+ # Jupyter Notebook
96
+ .ipynb_checkpoints
97
+
98
+ # IPython
99
+ profile_default/
100
+ ipython_config.py
101
+
102
+ # pyenv
103
+ # For a library or package, you might want to ignore these files since the code is
104
+ # intended to run in multiple environments; otherwise, check them in:
105
+ # .python-version
106
+
107
+ # pipenv
108
+ # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
109
+ # However, in case of collaboration, if having platform-specific dependencies or dependencies
110
+ # having no cross-platform support, pipenv may install dependencies that don't work, or not
111
+ # install all needed dependencies.
112
+ #Pipfile.lock
113
+
114
+ # poetry
115
+ # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
116
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
117
+ # commonly ignored for libraries.
118
+ # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
119
+ #poetry.lock
120
+
121
+ # pdm
122
+ # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
123
+ #pdm.lock
124
+ # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
125
+ # in version control.
126
+ # https://pdm.fming.dev/#use-with-ide
127
+ .pdm.toml
128
+
129
+ # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
130
+ __pypackages__/
131
+
132
+ # Celery stuff
133
+ celerybeat-schedule
134
+ celerybeat.pid
135
+
136
+ # SageMath parsed files
137
+ *.sage.py
138
+
139
+ # Environments
140
+ .env
141
+ .venv
142
+ env/
143
+ venv/
144
+ ENV/
145
+ env.bak/
146
+ venv.bak/
147
+
148
+ # Spyder project settings
149
+ .spyderproject
150
+ .spyproject
151
+
152
+ # Rope project settings
153
+ .ropeproject
154
+
155
+ # mkdocs documentation
156
+ /site
157
+
158
+ # mypy
159
+ .mypy_cache/
160
+ .dmypy.json
161
+ dmypy.json
162
+
163
+ # Pyre type checker
164
+ .pyre/
165
+
166
+ # pytype static type analyzer
167
+ .pytype/
168
+
169
+ # Cython debug symbols
170
+ cython_debug/
171
+
172
+ # PyCharm
173
+ # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
174
+ # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
175
+ # and can be added to the global gitignore or merged into this file. For a more nuclear
176
+ # option (not recommended) you can uncomment the following to ignore the entire idea folder.
177
+ .idea/
178
+
179
+ # Logs
180
+ logs
181
+ *.log
182
+ npm-debug.log*
183
+ yarn-debug.log*
184
+ yarn-error.log*
185
+ lerna-debug.log*
186
+ .pnpm-debug.log*
187
+
188
+ # Diagnostic reports (https://nodejs.org/api/report.html)
189
+ report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
190
+
191
+ # Runtime data
192
+ pids
193
+ *.pid
194
+ *.seed
195
+ *.pid.lock
196
+
197
+ # Directory for instrumented libs generated by jscoverage/JSCover
198
+ lib-cov
199
+
200
+ # Coverage directory used by tools like istanbul
201
+ coverage
202
+ *.lcov
203
+
204
+ # nyc test coverage
205
+ .nyc_output
206
+
207
+ # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
208
+ .grunt
209
+
210
+ # Bower dependency directory (https://bower.io/)
211
+ bower_components
212
+
213
+ # node-waf configuration
214
+ .lock-wscript
215
+
216
+ # Compiled binary addons (https://nodejs.org/api/addons.html)
217
+ build/Release
218
+
219
+ # Dependency directories
220
+ node_modules/
221
+ jspm_packages/
222
+
223
+ # Snowpack dependency directory (https://snowpack.dev/)
224
+ web_modules/
225
+
226
+ # TypeScript cache
227
+ *.tsbuildinfo
228
+
229
+ # Optional npm cache directory
230
+ .npm
231
+
232
+ # Optional eslint cache
233
+ .eslintcache
234
+
235
+ # Optional stylelint cache
236
+ .stylelintcache
237
+
238
+ # Microbundle cache
239
+ .rpt2_cache/
240
+ .rts2_cache_cjs/
241
+ .rts2_cache_es/
242
+ .rts2_cache_umd/
243
+
244
+ # Optional REPL history
245
+ .node_repl_history
246
+
247
+ # Output of 'npm pack'
248
+ *.tgz
249
+
250
+ # Yarn Integrity file
251
+ .yarn-integrity
252
+
253
+ # dotenv environment variable files
254
+ .env
255
+ .env.development.local
256
+ .env.test.local
257
+ .env.production.local
258
+ .env.local
259
+
260
+ # parcel-bundler cache (https://parceljs.org/)
261
+ .cache
262
+ .parcel-cache
263
+
264
+ # Next.js build output
265
+ .next
266
+ out
267
+
268
+ # Nuxt.js build / generate output
269
+ .nuxt
270
+ dist
271
+
272
+ # Gatsby files
273
+ .cache/
274
+ # Comment in the public line in if your project uses Gatsby and not Next.js
275
+ # https://nextjs.org/blog/next-9-1#public-directory-support
276
+ # public
277
+
278
+ # vuepress build output
279
+ .vuepress/dist
280
+
281
+ # vuepress v2.x temp and cache directory
282
+ .temp
283
+ .cache
284
+
285
+ # Docusaurus cache and generated files
286
+ .docusaurus
287
+
288
+ # Serverless directories
289
+ .serverless/
290
+
291
+ # FuseBox cache
292
+ .fusebox/
293
+
294
+ # DynamoDB Local files
295
+ .dynamodb/
296
+
297
+ # TernJS port file
298
+ .tern-port
299
+
300
+ # Stores VSCode versions used for testing VSCode extensions
301
+ .vscode-test
302
+
303
+ # yarn v2
304
+ .yarn/cache
305
+ .yarn/unplugged
306
+ .yarn/build-state.yml
307
+ .yarn/install-state.gz
308
+ .pnp.*
309
+
310
+ # cypress artifacts
311
+ cypress/videos
312
+ cypress/screenshots
313
+
314
+
315
+
316
+ /static/*
.prettierrc ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "useTabs": true,
3
+ "singleQuote": true,
4
+ "trailingComma": "none",
5
+ "printWidth": 100,
6
+ "plugins": ["prettier-plugin-svelte"],
7
+ "pluginSearchDirs": ["."],
8
+ "overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }]
9
+ }
CHANGELOG.md ADDED
The diff for this file is too large to render. See raw diff
 
CODE_OF_CONDUCT.md ADDED
@@ -0,0 +1,99 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Contributor Covenant Code of Conduct
2
+
3
+ ## Our Pledge
4
+
5
+ As members, contributors, and leaders of this community, we pledge to make participation in our open-source project a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socioeconomic status, nationality, personal appearance, race, religion, or sexual identity and orientation.
6
+
7
+ We are committed to creating and maintaining an open, respectful, and professional environment where positive contributions and meaningful discussions can flourish. By participating in this project, you agree to uphold these values and align your behavior to the standards outlined in this Code of Conduct.
8
+
9
+ ## Why These Standards Are Important
10
+
11
+ Open-source projects rely on a community of volunteers dedicating their time, expertise, and effort toward a shared goal. These projects are inherently collaborative but also fragile, as the success of the project depends on the goodwill, energy, and productivity of those involved.
12
+
13
+ Maintaining a positive and respectful environment is essential to safeguarding the integrity of this project and protecting contributors' efforts. Behavior that disrupts this atmosphere—whether through hostility, entitlement, or unprofessional conduct—can severely harm the morale and productivity of the community. **Strict enforcement of these standards ensures a safe and supportive space for meaningful collaboration.**
14
+
15
+ This is a community where **respect and professionalism are mandatory.** Violations of these standards will result in **zero tolerance** and immediate enforcement to prevent disruption and ensure the well-being of all participants.
16
+
17
+ ## Our Standards
18
+
19
+ Examples of behavior that contribute to a positive and professional community include:
20
+
21
+ - **Respecting others.** Be considerate, listen actively, and engage with empathy toward others' viewpoints and experiences.
22
+ - **Constructive feedback.** Provide actionable, thoughtful, and respectful feedback that helps improve the project and encourages collaboration. Avoid unproductive negativity or hypercriticism.
23
+ - **Recognizing volunteer contributions.** Appreciate that contributors dedicate their free time and resources selflessly. Approach them with gratitude and patience.
24
+ - **Focusing on shared goals.** Collaborate in ways that prioritize the health, success, and sustainability of the community over individual agendas.
25
+
26
+ Examples of unacceptable behavior include:
27
+
28
+ - The use of discriminatory, demeaning, or sexualized language or behavior.
29
+ - Personal attacks, derogatory comments, trolling, or inflammatory political or ideological arguments.
30
+ - Harassment, intimidation, or any behavior intended to create a hostile, uncomfortable, or unsafe environment.
31
+ - Publishing others' private information (e.g., physical or email addresses) without explicit permission.
32
+ - **Entitlement, demand, or aggression toward contributors.** Volunteers are under no obligation to provide immediate or personalized support. Rude or dismissive behavior will not be tolerated.
33
+ - **Unproductive or destructive behavior.** This includes venting frustration as hostility ("tantrums"), hypercriticism, attention-seeking negativity, or anything that distracts from the project's goals.
34
+ - **Spamming and promotional exploitation.** Sharing irrelevant product promotions or self-promotion in the community is not allowed unless it directly contributes value to the discussion.
35
+
36
+ ### Feedback and Community Engagement
37
+
38
+ - **Constructive feedback is encouraged, but hostile or entitled behavior will result in immediate action.** If you disagree with elements of the project, we encourage you to offer meaningful improvements or fork the project if necessary. Healthy discussions and technical disagreements are welcome only when handled with professionalism.
39
+ - **Respect contributors' time and efforts.** No one is entitled to personalized or on-demand assistance. This is a community built on collaboration and shared effort; demanding or demeaning behavior undermines that trust and will not be allowed.
40
+
41
+ ### Zero Tolerance: No Warnings, Immediate Action
42
+
43
+ This community operates under a **zero-tolerance policy.** Any behavior deemed unacceptable under this Code of Conduct will result in **immediate enforcement, without prior warning.**
44
+
45
+ We employ this approach to ensure that unproductive or disruptive behavior does not escalate further or cause unnecessary harm to other contributors. The standards are clear, and violations of any kind—whether mild or severe—will be addressed decisively to protect the community.
46
+
47
+ ## Enforcement Responsibilities
48
+
49
+ Community leaders are responsible for upholding and enforcing these standards. They are empowered to take **immediate and appropriate action** to address any behaviors they deem unacceptable under this Code of Conduct. These actions are taken with the goal of protecting the community and preserving its safe, positive, and productive environment.
50
+
51
+ ## Scope
52
+
53
+ This Code of Conduct applies to all community spaces, including forums, repositories, social media accounts, and in-person events. It also applies when an individual represents the community in public settings, such as conferences or official communications.
54
+
55
+ Additionally, any behavior outside of these defined spaces that negatively impacts the community or its members may fall within the scope of this Code of Conduct.
56
+
57
+ ## Reporting Violations
58
+
59
+ Instances of unacceptable behavior can be reported to the leadership team at **[email protected]**. Reports will be handled promptly, confidentially, and with consideration for the safety and well-being of the reporter.
60
+
61
+ All community leaders are required to uphold confidentiality and impartiality when addressing reports of violations.
62
+
63
+ ## Enforcement Guidelines
64
+
65
+ ### Ban
66
+
67
+ **Community Impact**: Community leaders will issue a ban to any participant whose behavior is deemed unacceptable according to this Code of Conduct. Bans are enforced immediately and without prior notice.
68
+
69
+ A ban may be temporary or permanent, depending on the severity of the violation. This includes—but is not limited to—behavior such as:
70
+
71
+ - Harassment or abusive behavior toward contributors.
72
+ - Persistent negativity or hostility that disrupts the collaborative environment.
73
+ - Disrespectful, demanding, or aggressive interactions with others.
74
+ - Attempts to cause harm or sabotage the community.
75
+
76
+ **Consequence**: A banned individual is immediately removed from access to all community spaces, communication channels, and events. Community leaders reserve the right to enforce either a time-limited suspension or a permanent ban based on the specific circumstances of the violation.
77
+
78
+ This approach ensures that disruptive behaviors are addressed swiftly and decisively in order to maintain the integrity and productivity of the community.
79
+
80
+ ## Why Zero Tolerance Is Necessary
81
+
82
+ Open-source projects thrive on collaboration, goodwill, and mutual respect. Toxic behaviors—such as entitlement, hostility, or persistent negativity—threaten not just individual contributors but the health of the project as a whole. Allowing such behaviors to persist robs contributors of their time, energy, and enthusiasm for the work they do.
83
+
84
+ By enforcing a zero-tolerance policy, we ensure that the community remains a safe, welcoming space for all participants. These measures are not about harshness—they are about protecting contributors and fostering a productive environment where innovation can thrive.
85
+
86
+ Our expectations are clear, and our enforcement reflects our commitment to this project's long-term success.
87
+
88
+ ## Attribution
89
+
90
+ This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.0, available at
91
+ https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
92
+
93
+ Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/diversity).
94
+
95
+ [homepage]: https://www.contributor-covenant.org
96
+
97
+ For answers to common questions about this code of conduct, see the FAQ at
98
+ https://www.contributor-covenant.org/faq. Translations are available at
99
+ https://www.contributor-covenant.org/translations.
Caddyfile.localhost ADDED
@@ -0,0 +1,64 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Run with
2
+ # caddy run --envfile ./example.env --config ./Caddyfile.localhost
3
+ #
4
+ # This is configured for
5
+ # - Automatic HTTPS (even for localhost)
6
+ # - Reverse Proxying to Ollama API Base URL (http://localhost:11434/api)
7
+ # - CORS
8
+ # - HTTP Basic Auth API Tokens (uncomment basicauth section)
9
+
10
+
11
+ # CORS Preflight (OPTIONS) + Request (GET, POST, PATCH, PUT, DELETE)
12
+ (cors-api) {
13
+ @match-cors-api-preflight method OPTIONS
14
+ handle @match-cors-api-preflight {
15
+ header {
16
+ Access-Control-Allow-Origin "{http.request.header.origin}"
17
+ Access-Control-Allow-Methods "GET, POST, PUT, PATCH, DELETE, OPTIONS"
18
+ Access-Control-Allow-Headers "Origin, Accept, Authorization, Content-Type, X-Requested-With"
19
+ Access-Control-Allow-Credentials "true"
20
+ Access-Control-Max-Age "3600"
21
+ defer
22
+ }
23
+ respond "" 204
24
+ }
25
+
26
+ @match-cors-api-request {
27
+ not {
28
+ header Origin "{http.request.scheme}://{http.request.host}"
29
+ }
30
+ header Origin "{http.request.header.origin}"
31
+ }
32
+ handle @match-cors-api-request {
33
+ header {
34
+ Access-Control-Allow-Origin "{http.request.header.origin}"
35
+ Access-Control-Allow-Methods "GET, POST, PUT, PATCH, DELETE, OPTIONS"
36
+ Access-Control-Allow-Headers "Origin, Accept, Authorization, Content-Type, X-Requested-With"
37
+ Access-Control-Allow-Credentials "true"
38
+ Access-Control-Max-Age "3600"
39
+ defer
40
+ }
41
+ }
42
+ }
43
+
44
+ # replace localhost with example.com or whatever
45
+ localhost {
46
+ ## HTTP Basic Auth
47
+ ## (uncomment to enable)
48
+ # basicauth {
49
+ # # see .example.env for how to generate tokens
50
+ # {env.OLLAMA_API_ID} {env.OLLAMA_API_TOKEN_DIGEST}
51
+ # }
52
+
53
+ handle /api/* {
54
+ # Comment to disable CORS
55
+ import cors-api
56
+
57
+ reverse_proxy localhost:11434
58
+ }
59
+
60
+ # Same-Origin Static Web Server
61
+ file_server {
62
+ root ./build/
63
+ }
64
+ }
Dockerfile ADDED
@@ -0,0 +1,176 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # syntax=docker/dockerfile:1
2
+ # Initialize device type args
3
+ # use build args in the docker build command with --build-arg="BUILDARG=true"
4
+ ARG USE_CUDA=false
5
+ ARG USE_OLLAMA=false
6
+ # Tested with cu117 for CUDA 11 and cu121 for CUDA 12 (default)
7
+ ARG USE_CUDA_VER=cu121
8
+ # any sentence transformer model; models to use can be found at https://huggingface.co/models?library=sentence-transformers
9
+ # Leaderboard: https://huggingface.co/spaces/mteb/leaderboard
10
+ # for better performance and multilangauge support use "intfloat/multilingual-e5-large" (~2.5GB) or "intfloat/multilingual-e5-base" (~1.5GB)
11
+ # IMPORTANT: If you change the embedding model (sentence-transformers/all-MiniLM-L6-v2) and vice versa, you aren't able to use RAG Chat with your previous documents loaded in the WebUI! You need to re-embed them.
12
+ ARG USE_EMBEDDING_MODEL=sentence-transformers/all-MiniLM-L6-v2
13
+ ARG USE_RERANKING_MODEL=""
14
+
15
+ # Tiktoken encoding name; models to use can be found at https://huggingface.co/models?library=tiktoken
16
+ ARG USE_TIKTOKEN_ENCODING_NAME="cl100k_base"
17
+
18
+ ARG BUILD_HASH=dev-build
19
+ # Override at your own risk - non-root configurations are untested
20
+ ARG UID=0
21
+ ARG GID=0
22
+
23
+ ######## WebUI frontend ########
24
+ FROM --platform=$BUILDPLATFORM node:22-alpine3.20 AS build
25
+ ARG BUILD_HASH
26
+
27
+ WORKDIR /app
28
+
29
+ COPY package.json package-lock.json ./
30
+ RUN npm ci
31
+
32
+ COPY . .
33
+ ENV APP_BUILD_HASH=${BUILD_HASH}
34
+ RUN npm run build
35
+
36
+ ######## WebUI backend ########
37
+ FROM python:3.11-slim-bookworm AS base
38
+
39
+ # Use args
40
+ ARG USE_CUDA
41
+ ARG USE_OLLAMA
42
+ ARG USE_CUDA_VER
43
+ ARG USE_EMBEDDING_MODEL
44
+ ARG USE_RERANKING_MODEL
45
+ ARG UID
46
+ ARG GID
47
+
48
+ ## Basis ##
49
+ ENV ENV=prod \
50
+ PORT=8080 \
51
+ # pass build args to the build
52
+ USE_OLLAMA_DOCKER=${USE_OLLAMA} \
53
+ USE_CUDA_DOCKER=${USE_CUDA} \
54
+ USE_CUDA_DOCKER_VER=${USE_CUDA_VER} \
55
+ USE_EMBEDDING_MODEL_DOCKER=${USE_EMBEDDING_MODEL} \
56
+ USE_RERANKING_MODEL_DOCKER=${USE_RERANKING_MODEL}
57
+
58
+ ## Basis URL Config ##
59
+ ENV OLLAMA_BASE_URL="/ollama" \
60
+ OPENAI_API_BASE_URL=""
61
+
62
+ ## API Key and Security Config ##
63
+ ENV OPENAI_API_KEY="" \
64
+ WEBUI_SECRET_KEY="" \
65
+ SCARF_NO_ANALYTICS=true \
66
+ DO_NOT_TRACK=true \
67
+ ANONYMIZED_TELEMETRY=false
68
+
69
+ #### Other models #########################################################
70
+ ## whisper TTS model settings ##
71
+ ENV WHISPER_MODEL="base" \
72
+ WHISPER_MODEL_DIR="/app/backend/data/cache/whisper/models"
73
+
74
+ ## RAG Embedding model settings ##
75
+ ENV RAG_EMBEDDING_MODEL="$USE_EMBEDDING_MODEL_DOCKER" \
76
+ RAG_RERANKING_MODEL="$USE_RERANKING_MODEL_DOCKER" \
77
+ SENTENCE_TRANSFORMERS_HOME="/app/backend/data/cache/embedding/models"
78
+
79
+ ## Tiktoken model settings ##
80
+ ENV TIKTOKEN_ENCODING_NAME="cl100k_base" \
81
+ TIKTOKEN_CACHE_DIR="/app/backend/data/cache/tiktoken"
82
+
83
+ ## Hugging Face download cache ##
84
+ ENV HF_HOME="/app/backend/data/cache/embedding/models"
85
+
86
+ ## Torch Extensions ##
87
+ # ENV TORCH_EXTENSIONS_DIR="/.cache/torch_extensions"
88
+
89
+ #### Other models ##########################################################
90
+
91
+ WORKDIR /app/backend
92
+
93
+ ENV HOME=/root
94
+ # Create user and group if not root
95
+ RUN if [ $UID -ne 0 ]; then \
96
+ if [ $GID -ne 0 ]; then \
97
+ addgroup --gid $GID app; \
98
+ fi; \
99
+ adduser --uid $UID --gid $GID --home $HOME --disabled-password --no-create-home app; \
100
+ fi
101
+
102
+ RUN mkdir -p $HOME/.cache/chroma
103
+ RUN echo -n 00000000-0000-0000-0000-000000000000 > $HOME/.cache/chroma/telemetry_user_id
104
+
105
+ # Make sure the user has access to the app and root directory
106
+ RUN chown -R $UID:$GID /app $HOME
107
+
108
+ RUN if [ "$USE_OLLAMA" = "true" ]; then \
109
+ apt-get update && \
110
+ # Install pandoc and netcat
111
+ apt-get install -y --no-install-recommends git build-essential pandoc netcat-openbsd curl && \
112
+ apt-get install -y --no-install-recommends gcc python3-dev && \
113
+ # for RAG OCR
114
+ apt-get install -y --no-install-recommends ffmpeg libsm6 libxext6 && \
115
+ # install helper tools
116
+ apt-get install -y --no-install-recommends curl jq && \
117
+ # install ollama
118
+ curl -fsSL https://ollama.com/install.sh | sh && \
119
+ # cleanup
120
+ rm -rf /var/lib/apt/lists/*; \
121
+ else \
122
+ apt-get update && \
123
+ # Install pandoc, netcat and gcc
124
+ apt-get install -y --no-install-recommends git build-essential pandoc gcc netcat-openbsd curl jq && \
125
+ apt-get install -y --no-install-recommends gcc python3-dev && \
126
+ # for RAG OCR
127
+ apt-get install -y --no-install-recommends ffmpeg libsm6 libxext6 && \
128
+ # cleanup
129
+ rm -rf /var/lib/apt/lists/*; \
130
+ fi
131
+
132
+ # install python dependencies
133
+ COPY --chown=$UID:$GID ./backend/requirements.txt ./requirements.txt
134
+
135
+ RUN pip3 install uv && \
136
+ if [ "$USE_CUDA" = "true" ]; then \
137
+ # If you use CUDA the whisper and embedding model will be downloaded on first use
138
+ pip3 install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/$USE_CUDA_DOCKER_VER --no-cache-dir && \
139
+ uv pip install --system -r requirements.txt --no-cache-dir && \
140
+ python -c "import os; from sentence_transformers import SentenceTransformer; SentenceTransformer(os.environ['RAG_EMBEDDING_MODEL'], device='cpu')" && \
141
+ python -c "import os; from faster_whisper import WhisperModel; WhisperModel(os.environ['WHISPER_MODEL'], device='cpu', compute_type='int8', download_root=os.environ['WHISPER_MODEL_DIR'])"; \
142
+ python -c "import os; import tiktoken; tiktoken.get_encoding(os.environ['TIKTOKEN_ENCODING_NAME'])"; \
143
+ else \
144
+ pip3 install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cpu --no-cache-dir && \
145
+ uv pip install --system -r requirements.txt --no-cache-dir && \
146
+ python -c "import os; from sentence_transformers import SentenceTransformer; SentenceTransformer(os.environ['RAG_EMBEDDING_MODEL'], device='cpu')" && \
147
+ python -c "import os; from faster_whisper import WhisperModel; WhisperModel(os.environ['WHISPER_MODEL'], device='cpu', compute_type='int8', download_root=os.environ['WHISPER_MODEL_DIR'])"; \
148
+ python -c "import os; import tiktoken; tiktoken.get_encoding(os.environ['TIKTOKEN_ENCODING_NAME'])"; \
149
+ fi; \
150
+ chown -R $UID:$GID /app/backend/data/
151
+
152
+
153
+
154
+ # copy embedding weight from build
155
+ # RUN mkdir -p /root/.cache/chroma/onnx_models/all-MiniLM-L6-v2
156
+ # COPY --from=build /app/onnx /root/.cache/chroma/onnx_models/all-MiniLM-L6-v2/onnx
157
+
158
+ # copy built frontend files
159
+ COPY --chown=$UID:$GID --from=build /app/build /app/build
160
+ COPY --chown=$UID:$GID --from=build /app/CHANGELOG.md /app/CHANGELOG.md
161
+ COPY --chown=$UID:$GID --from=build /app/package.json /app/package.json
162
+
163
+ # copy backend files
164
+ COPY --chown=$UID:$GID ./backend .
165
+
166
+ EXPOSE 8080
167
+
168
+ HEALTHCHECK CMD curl --silent --fail http://localhost:${PORT:-8080}/health | jq -ne 'input.status == true' || exit 1
169
+
170
+ USER $UID:$GID
171
+
172
+ ARG BUILD_HASH
173
+ ENV WEBUI_BUILD_VERSION=${BUILD_HASH}
174
+ ENV DOCKER=true
175
+
176
+ CMD [ "bash", "start.sh"]
INSTALLATION.md ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ### Installing Both Ollama and Open WebUI Using Kustomize
2
+
3
+ For cpu-only pod
4
+
5
+ ```bash
6
+ kubectl apply -f ./kubernetes/manifest/base
7
+ ```
8
+
9
+ For gpu-enabled pod
10
+
11
+ ```bash
12
+ kubectl apply -k ./kubernetes/manifest
13
+ ```
14
+
15
+ ### Installing Both Ollama and Open WebUI Using Helm
16
+
17
+ Package Helm file first
18
+
19
+ ```bash
20
+ helm package ./kubernetes/helm/
21
+ ```
22
+
23
+ For cpu-only pod
24
+
25
+ ```bash
26
+ helm install ollama-webui ./ollama-webui-*.tgz
27
+ ```
28
+
29
+ For gpu-enabled pod
30
+
31
+ ```bash
32
+ helm install ollama-webui ./ollama-webui-*.tgz --set ollama.resources.limits.nvidia.com/gpu="1"
33
+ ```
34
+
35
+ Check the `kubernetes/helm/values.yaml` file to know which parameters are available for customization
LICENSE ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Copyright (c) 2023-2025 Timothy Jaeryang Baek
2
+ All rights reserved.
3
+
4
+ Redistribution and use in source and binary forms, with or without
5
+ modification, are permitted provided that the following conditions are met:
6
+
7
+ 1. Redistributions of source code must retain the above copyright notice, this
8
+ list of conditions and the following disclaimer.
9
+
10
+ 2. Redistributions in binary form must reproduce the above copyright notice,
11
+ this list of conditions and the following disclaimer in the documentation
12
+ and/or other materials provided with the distribution.
13
+
14
+ 3. Neither the name of the copyright holder nor the names of its
15
+ contributors may be used to endorse or promote products derived from
16
+ this software without specific prior written permission.
17
+
18
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
19
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
20
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
21
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
22
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
23
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
24
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
25
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
26
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
27
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
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,236 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: Open WebUI
3
+ emoji: 🐳
4
+ colorFrom: purple
5
+ colorTo: gray
6
+ sdk: docker
7
+ app_port: 8080
8
+ ---
9
+ # Open WebUI 👋
10
+
11
+ ![GitHub stars](https://img.shields.io/github/stars/open-webui/open-webui?style=social)
12
+ ![GitHub forks](https://img.shields.io/github/forks/open-webui/open-webui?style=social)
13
+ ![GitHub watchers](https://img.shields.io/github/watchers/open-webui/open-webui?style=social)
14
+ ![GitHub repo size](https://img.shields.io/github/repo-size/open-webui/open-webui)
15
+ ![GitHub language count](https://img.shields.io/github/languages/count/open-webui/open-webui)
16
+ ![GitHub top language](https://img.shields.io/github/languages/top/open-webui/open-webui)
17
+ ![GitHub last commit](https://img.shields.io/github/last-commit/open-webui/open-webui?color=red)
18
+ ![Hits](https://hits.seeyoufarm.com/api/count/incr/badge.svg?url=https%3A%2F%2Fgithub.com%2Follama-webui%2Follama-wbui&count_bg=%2379C83D&title_bg=%23555555&icon=&icon_color=%23E7E7E7&title=hits&edge_flat=false)
19
+ [![Discord](https://img.shields.io/badge/Discord-Open_WebUI-blue?logo=discord&logoColor=white)](https://discord.gg/5rJgQTnV4s)
20
+ [![](https://img.shields.io/static/v1?label=Sponsor&message=%E2%9D%A4&logo=GitHub&color=%23fe8e86)](https://github.com/sponsors/tjbck)
21
+
22
+ **Open WebUI is an [extensible](https://docs.openwebui.com/features/plugin/), feature-rich, and user-friendly self-hosted AI platform designed to operate entirely offline.** It supports various LLM runners like **Ollama** and **OpenAI-compatible APIs**, with **built-in inference engine** for RAG, making it a **powerful AI deployment solution**.
23
+
24
+ ![Open WebUI Demo](./demo.gif)
25
+
26
+ > [!TIP]
27
+ > **Looking for an [Enterprise Plan](https://docs.openwebui.com/enterprise)?** – **[Speak with Our Sales Team Today!](mailto:[email protected])**
28
+ >
29
+ > Get **enhanced capabilities**, including **custom theming and branding**, **Service Level Agreement (SLA) support**, **Long-Term Support (LTS) versions**, and **more!**
30
+
31
+ For more information, be sure to check out our [Open WebUI Documentation](https://docs.openwebui.com/).
32
+
33
+ ## Key Features of Open WebUI ⭐
34
+
35
+ - 🚀 **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.
36
+
37
+ - 🤝 **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**.
38
+
39
+ - 🛡️ **Granular Permissions and User Groups**: By allowing administrators to create detailed user roles and permissions, we ensure a secure user environment. This granularity not only enhances security but also allows for customized user experiences, fostering a sense of ownership and responsibility amongst users.
40
+
41
+ - 📱 **Responsive Design**: Enjoy a seamless experience across Desktop PC, Laptop, and Mobile devices.
42
+
43
+ - 📱 **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.
44
+
45
+ - ✒️🔢 **Full Markdown and LaTeX Support**: Elevate your LLM experience with comprehensive Markdown and LaTeX capabilities for enriched interaction.
46
+
47
+ - 🎤📹 **Hands-Free Voice/Video Call**: Experience seamless communication with integrated hands-free voice and video call features, allowing for a more dynamic and interactive chat environment.
48
+
49
+ - 🛠️ **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.
50
+
51
+ - 🐍 **Native Python Function Calling Tool**: Enhance your LLMs with built-in code editor support in the tools workspace. Bring Your Own Function (BYOF) by simply adding your pure Python functions, enabling seamless integration with LLMs.
52
+
53
+ - 📚 **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.
54
+
55
+ - 🔍 **Web Search for RAG**: Perform web searches using providers like `SearXNG`, `Google PSE`, `Brave Search`, `serpstack`, `serper`, `Serply`, `DuckDuckGo`, `TavilySearch`, `SearchApi` and `Bing` and inject the results directly into your chat experience.
56
+
57
+ - 🌐 **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.
58
+
59
+ - 🎨 **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.
60
+
61
+ - ⚙️ **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.
62
+
63
+ - 🔐 **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.
64
+
65
+ - 🌐🌍 **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!
66
+
67
+ - 🧩 **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.
68
+
69
+ - 🌟 **Continuous Updates**: We are committed to improving Open WebUI with regular updates, fixes, and new features.
70
+
71
+ Want to learn more about Open WebUI's features? Check out our [Open WebUI documentation](https://docs.openwebui.com/features) for a comprehensive overview!
72
+
73
+ ## 🔗 Also Check Out Open WebUI Community!
74
+
75
+ 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! 🚀
76
+
77
+ ## How to Install 🚀
78
+
79
+ ### Installation via Python pip 🐍
80
+
81
+ Open WebUI can be installed using pip, the Python package installer. Before proceeding, ensure you're using **Python 3.11** to avoid compatibility issues.
82
+
83
+ 1. **Install Open WebUI**:
84
+ Open your terminal and run the following command to install Open WebUI:
85
+
86
+ ```bash
87
+ pip install open-webui
88
+ ```
89
+
90
+ 2. **Running Open WebUI**:
91
+ After installation, you can start Open WebUI by executing:
92
+
93
+ ```bash
94
+ open-webui serve
95
+ ```
96
+
97
+ This will start the Open WebUI server, which you can access at [http://localhost:8080](http://localhost:8080)
98
+
99
+ ### Quick Start with Docker 🐳
100
+
101
+ > [!NOTE]
102
+ > 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.
103
+
104
+ > [!WARNING]
105
+ > 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.
106
+
107
+ > [!TIP]
108
+ > 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.
109
+
110
+ ### Installation with Default Configuration
111
+
112
+ - **If Ollama is on your computer**, use this command:
113
+
114
+ ```bash
115
+ 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
116
+ ```
117
+
118
+ - **If Ollama is on a Different Server**, use this command:
119
+
120
+ To connect to Ollama on another server, change the `OLLAMA_BASE_URL` to the server's URL:
121
+
122
+ ```bash
123
+ 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
124
+ ```
125
+
126
+ - **To run Open WebUI with Nvidia GPU support**, use this command:
127
+
128
+ ```bash
129
+ 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
130
+ ```
131
+
132
+ ### Installation for OpenAI API Usage Only
133
+
134
+ - **If you're only using OpenAI API**, use this command:
135
+
136
+ ```bash
137
+ 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
138
+ ```
139
+
140
+ ### Installing Open WebUI with Bundled Ollama Support
141
+
142
+ 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:
143
+
144
+ - **With GPU Support**:
145
+ Utilize GPU resources by running the following command:
146
+
147
+ ```bash
148
+ 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
149
+ ```
150
+
151
+ - **For CPU Only**:
152
+ If you're not using a GPU, use this command instead:
153
+
154
+ ```bash
155
+ 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
156
+ ```
157
+
158
+ 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.
159
+
160
+ After installation, you can access Open WebUI at [http://localhost:3000](http://localhost:3000). Enjoy! 😄
161
+
162
+ ### Other Installation Methods
163
+
164
+ 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.
165
+
166
+ ### Troubleshooting
167
+
168
+ 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).
169
+
170
+ #### Open WebUI: Server Connection Error
171
+
172
+ 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`.
173
+
174
+ **Example Docker Command**:
175
+
176
+ ```bash
177
+ 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
178
+ ```
179
+
180
+ ### Keeping Your Docker Installation Up-to-Date
181
+
182
+ In case you want to update your local Docker installation to the latest version, you can do it with [Watchtower](https://containrrr.dev/watchtower/):
183
+
184
+ ```bash
185
+ docker run --rm --volume /var/run/docker.sock:/var/run/docker.sock containrrr/watchtower --run-once open-webui
186
+ ```
187
+
188
+ In the last part of the command, replace `open-webui` with your container name if it is different.
189
+
190
+ Check our Updating Guide available in our [Open WebUI Documentation](https://docs.openwebui.com/getting-started/updating).
191
+
192
+ ### Using the Dev Branch 🌙
193
+
194
+ > [!WARNING]
195
+ > The `:dev` branch contains the latest unstable features and changes. Use it at your own risk as it may have bugs or incomplete features.
196
+
197
+ If you want to try out the latest bleeding-edge features and are okay with occasional instability, you can use the `:dev` tag like this:
198
+
199
+ ```bash
200
+ docker run -d -p 3000:8080 -v open-webui:/app/backend/data --name open-webui --add-host=host.docker.internal:host-gateway --restart always ghcr.io/open-webui/open-webui:dev
201
+ ```
202
+
203
+ ### Offline Mode
204
+
205
+ If you are running Open WebUI in an offline environment, you can set the `HF_HUB_OFFLINE` environment variable to `1` to prevent attempts to download models from the internet.
206
+
207
+ ```bash
208
+ export HF_HUB_OFFLINE=1
209
+ ```
210
+
211
+ ## What's Next? 🌟
212
+
213
+ Discover upcoming features on our roadmap in the [Open WebUI Documentation](https://docs.openwebui.com/roadmap/).
214
+
215
+ ## License 📜
216
+
217
+ This project is licensed under the [BSD-3-Clause License](LICENSE) - see the [LICENSE](LICENSE) file for details. 📄
218
+
219
+ ## Support 💬
220
+
221
+ If you have any questions, suggestions, or need assistance, please open an issue or join our
222
+ [Open WebUI Discord community](https://discord.gg/5rJgQTnV4s) to connect with us! 🤝
223
+
224
+ ## Star History
225
+
226
+ <a href="https://star-history.com/#open-webui/open-webui&Date">
227
+ <picture>
228
+ <source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=open-webui/open-webui&type=Date&theme=dark" />
229
+ <source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=open-webui/open-webui&type=Date" />
230
+ <img alt="Star History Chart" src="https://api.star-history.com/svg?repos=open-webui/open-webui&type=Date" />
231
+ </picture>
232
+ </a>
233
+
234
+ ---
235
+
236
+ Created by [Timothy Jaeryang Baek](https://github.com/tjbck) - Let's make Open WebUI even more amazing together! 💪
TROUBLESHOOTING.md ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Open WebUI Troubleshooting Guide
2
+
3
+ ## Understanding the Open WebUI Architecture
4
+
5
+ The Open WebUI system is designed to streamline interactions between the client (your browser) and the Ollama API. At the heart of this design is a backend reverse proxy, enhancing security and resolving CORS issues.
6
+
7
+ - **How it Works**: The Open WebUI is designed to interact with the Ollama API through a specific route. When a request is made from the WebUI to Ollama, it is not directly sent to the Ollama API. Initially, the request is sent to the Open WebUI backend via `/ollama` route. From there, the backend is responsible for forwarding the request to the Ollama API. This forwarding is accomplished by using the route specified in the `OLLAMA_BASE_URL` environment variable. Therefore, a request made to `/ollama` in the WebUI is effectively the same as making a request to `OLLAMA_BASE_URL` in the backend. For instance, a request to `/ollama/api/tags` in the WebUI is equivalent to `OLLAMA_BASE_URL/api/tags` in the backend.
8
+
9
+ - **Security Benefits**: This design prevents direct exposure of the Ollama API to the frontend, safeguarding against potential CORS (Cross-Origin Resource Sharing) issues and unauthorized access. Requiring authentication to access the Ollama API further enhances this security layer.
10
+
11
+ ## Open WebUI: Server Connection Error
12
+
13
+ If you're experiencing connection issues, it’s often due to the WebUI docker container not being able to reach the Ollama server at 127.0.0.1:11434 (host.docker.internal:11434) inside the container . Use the `--network=host` flag in your docker command to resolve this. Note that the port changes from 3000 to 8080, resulting in the link: `http://localhost:8080`.
14
+
15
+ **Example Docker Command**:
16
+
17
+ ```bash
18
+ docker run -d --network=host -v open-webui:/app/backend/data -e OLLAMA_BASE_URL=http://127.0.0.1:11434 --name open-webui --restart always ghcr.io/open-webui/open-webui:main
19
+ ```
20
+
21
+ ### Error on Slow Responses for Ollama
22
+
23
+ Open WebUI has a default timeout of 5 minutes for Ollama to finish generating the response. If needed, this can be adjusted via the environment variable AIOHTTP_CLIENT_TIMEOUT, which sets the timeout in seconds.
24
+
25
+ ### General Connection Errors
26
+
27
+ **Ensure Ollama Version is Up-to-Date**: Always start by checking that you have the latest version of Ollama. Visit [Ollama's official site](https://ollama.com/) for the latest updates.
28
+
29
+ **Troubleshooting Steps**:
30
+
31
+ 1. **Verify Ollama URL Format**:
32
+ - When running the Web UI container, ensure the `OLLAMA_BASE_URL` is correctly set. (e.g., `http://192.168.1.1:11434` for different host setups).
33
+ - In the Open WebUI, navigate to "Settings" > "General".
34
+ - Confirm that the Ollama Server URL is correctly set to `[OLLAMA URL]` (e.g., `http://localhost:11434`).
35
+
36
+ By following these enhanced troubleshooting steps, connection issues should be effectively resolved. For further assistance or queries, feel free to reach out to us on our community Discord.
backend/.dockerignore ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ __pycache__
2
+ .env
3
+ _old
4
+ uploads
5
+ .ipynb_checkpoints
6
+ *.db
7
+ _test
8
+ !/data
9
+ /data/*
10
+ !/data/litellm
11
+ /data/litellm/*
12
+ !data/litellm/config.yaml
13
+
14
+ !data/config.json
backend/.gitignore ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ __pycache__
2
+ .env
3
+ _old
4
+ uploads
5
+ .ipynb_checkpoints
6
+ *.db
7
+ _test
8
+ Pipfile
9
+ !/data
10
+ /data/*
11
+ /open_webui/data/*
12
+ .webui_secret_key
backend/dev.sh ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ PORT="${PORT:-8080}"
2
+ uvicorn open_webui.main:app --port $PORT --host 0.0.0.0 --forwarded-allow-ips '*' --reload
backend/open_webui/__init__.py ADDED
@@ -0,0 +1,96 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import base64
2
+ import os
3
+ import random
4
+ from pathlib import Path
5
+
6
+ import typer
7
+ import uvicorn
8
+ from typing import Optional
9
+ from typing_extensions import Annotated
10
+
11
+ app = typer.Typer()
12
+
13
+ KEY_FILE = Path.cwd() / ".webui_secret_key"
14
+
15
+
16
+ def version_callback(value: bool):
17
+ if value:
18
+ from open_webui.env import VERSION
19
+
20
+ typer.echo(f"Open WebUI version: {VERSION}")
21
+ raise typer.Exit()
22
+
23
+
24
+ @app.command()
25
+ def main(
26
+ version: Annotated[
27
+ Optional[bool], typer.Option("--version", callback=version_callback)
28
+ ] = None,
29
+ ):
30
+ pass
31
+
32
+
33
+ @app.command()
34
+ def serve(
35
+ host: str = "0.0.0.0",
36
+ port: int = 8080,
37
+ ):
38
+ os.environ["FROM_INIT_PY"] = "true"
39
+ if os.getenv("WEBUI_SECRET_KEY") is None:
40
+ typer.echo(
41
+ "Loading WEBUI_SECRET_KEY from file, not provided as an environment variable."
42
+ )
43
+ if not KEY_FILE.exists():
44
+ typer.echo(f"Generating a new secret key and saving it to {KEY_FILE}")
45
+ KEY_FILE.write_bytes(base64.b64encode(random.randbytes(12)))
46
+ typer.echo(f"Loading WEBUI_SECRET_KEY from {KEY_FILE}")
47
+ os.environ["WEBUI_SECRET_KEY"] = KEY_FILE.read_text()
48
+
49
+ if os.getenv("USE_CUDA_DOCKER", "false") == "true":
50
+ typer.echo(
51
+ "CUDA is enabled, appending LD_LIBRARY_PATH to include torch/cudnn & cublas libraries."
52
+ )
53
+ LD_LIBRARY_PATH = os.getenv("LD_LIBRARY_PATH", "").split(":")
54
+ os.environ["LD_LIBRARY_PATH"] = ":".join(
55
+ LD_LIBRARY_PATH
56
+ + [
57
+ "/usr/local/lib/python3.11/site-packages/torch/lib",
58
+ "/usr/local/lib/python3.11/site-packages/nvidia/cudnn/lib",
59
+ ]
60
+ )
61
+ try:
62
+ import torch
63
+
64
+ assert torch.cuda.is_available(), "CUDA not available"
65
+ typer.echo("CUDA seems to be working")
66
+ except Exception as e:
67
+ typer.echo(
68
+ "Error when testing CUDA but USE_CUDA_DOCKER is true. "
69
+ "Resetting USE_CUDA_DOCKER to false and removing "
70
+ f"LD_LIBRARY_PATH modifications: {e}"
71
+ )
72
+ os.environ["USE_CUDA_DOCKER"] = "false"
73
+ os.environ["LD_LIBRARY_PATH"] = ":".join(LD_LIBRARY_PATH)
74
+
75
+ import open_webui.main # we need set environment variables before importing main
76
+
77
+ uvicorn.run(open_webui.main.app, host=host, port=port, forwarded_allow_ips="*")
78
+
79
+
80
+ @app.command()
81
+ def dev(
82
+ host: str = "0.0.0.0",
83
+ port: int = 8080,
84
+ reload: bool = True,
85
+ ):
86
+ uvicorn.run(
87
+ "open_webui.main:app",
88
+ host=host,
89
+ port=port,
90
+ reload=reload,
91
+ forwarded_allow_ips="*",
92
+ )
93
+
94
+
95
+ if __name__ == "__main__":
96
+ app()
backend/open_webui/alembic.ini ADDED
@@ -0,0 +1,114 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # A generic, single database configuration.
2
+
3
+ [alembic]
4
+ # path to migration scripts
5
+ script_location = migrations
6
+
7
+ # template used to generate migration file names; The default value is %%(rev)s_%%(slug)s
8
+ # Uncomment the line below if you want the files to be prepended with date and time
9
+ # file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s
10
+
11
+ # sys.path path, will be prepended to sys.path if present.
12
+ # defaults to the current working directory.
13
+ prepend_sys_path = .
14
+
15
+ # timezone to use when rendering the date within the migration file
16
+ # as well as the filename.
17
+ # If specified, requires the python>=3.9 or backports.zoneinfo library.
18
+ # Any required deps can installed by adding `alembic[tz]` to the pip requirements
19
+ # string value is passed to ZoneInfo()
20
+ # leave blank for localtime
21
+ # timezone =
22
+
23
+ # max length of characters to apply to the
24
+ # "slug" field
25
+ # truncate_slug_length = 40
26
+
27
+ # set to 'true' to run the environment during
28
+ # the 'revision' command, regardless of autogenerate
29
+ # revision_environment = false
30
+
31
+ # set to 'true' to allow .pyc and .pyo files without
32
+ # a source .py file to be detected as revisions in the
33
+ # versions/ directory
34
+ # sourceless = false
35
+
36
+ # version location specification; This defaults
37
+ # to migrations/versions. When using multiple version
38
+ # directories, initial revisions must be specified with --version-path.
39
+ # The path separator used here should be the separator specified by "version_path_separator" below.
40
+ # version_locations = %(here)s/bar:%(here)s/bat:migrations/versions
41
+
42
+ # version path separator; As mentioned above, this is the character used to split
43
+ # version_locations. The default within new alembic.ini files is "os", which uses os.pathsep.
44
+ # If this key is omitted entirely, it falls back to the legacy behavior of splitting on spaces and/or commas.
45
+ # Valid values for version_path_separator are:
46
+ #
47
+ # version_path_separator = :
48
+ # version_path_separator = ;
49
+ # version_path_separator = space
50
+ version_path_separator = os # Use os.pathsep. Default configuration used for new projects.
51
+
52
+ # set to 'true' to search source files recursively
53
+ # in each "version_locations" directory
54
+ # new in Alembic version 1.10
55
+ # recursive_version_locations = false
56
+
57
+ # the output encoding used when revision files
58
+ # are written from script.py.mako
59
+ # output_encoding = utf-8
60
+
61
+ # sqlalchemy.url = REPLACE_WITH_DATABASE_URL
62
+
63
+
64
+ [post_write_hooks]
65
+ # post_write_hooks defines scripts or Python functions that are run
66
+ # on newly generated revision scripts. See the documentation for further
67
+ # detail and examples
68
+
69
+ # format using "black" - use the console_scripts runner, against the "black" entrypoint
70
+ # hooks = black
71
+ # black.type = console_scripts
72
+ # black.entrypoint = black
73
+ # black.options = -l 79 REVISION_SCRIPT_FILENAME
74
+
75
+ # lint with attempts to fix using "ruff" - use the exec runner, execute a binary
76
+ # hooks = ruff
77
+ # ruff.type = exec
78
+ # ruff.executable = %(here)s/.venv/bin/ruff
79
+ # ruff.options = --fix REVISION_SCRIPT_FILENAME
80
+
81
+ # Logging configuration
82
+ [loggers]
83
+ keys = root,sqlalchemy,alembic
84
+
85
+ [handlers]
86
+ keys = console
87
+
88
+ [formatters]
89
+ keys = generic
90
+
91
+ [logger_root]
92
+ level = WARN
93
+ handlers = console
94
+ qualname =
95
+
96
+ [logger_sqlalchemy]
97
+ level = WARN
98
+ handlers =
99
+ qualname = sqlalchemy.engine
100
+
101
+ [logger_alembic]
102
+ level = INFO
103
+ handlers =
104
+ qualname = alembic
105
+
106
+ [handler_console]
107
+ class = StreamHandler
108
+ args = (sys.stderr,)
109
+ level = NOTSET
110
+ formatter = generic
111
+
112
+ [formatter_generic]
113
+ format = %(levelname)-5.5s [%(name)s] %(message)s
114
+ datefmt = %H:%M:%S
backend/open_webui/config.py ADDED
@@ -0,0 +1,2395 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ import logging
3
+ import os
4
+ import shutil
5
+ import base64
6
+
7
+ from datetime import datetime
8
+ from pathlib import Path
9
+ from typing import Generic, Optional, TypeVar
10
+ from urllib.parse import urlparse
11
+
12
+ import chromadb
13
+ import requests
14
+ from pydantic import BaseModel
15
+ from sqlalchemy import JSON, Column, DateTime, Integer, func
16
+
17
+ from open_webui.env import (
18
+ DATA_DIR,
19
+ DATABASE_URL,
20
+ ENV,
21
+ FRONTEND_BUILD_DIR,
22
+ OFFLINE_MODE,
23
+ OPEN_WEBUI_DIR,
24
+ WEBUI_AUTH,
25
+ WEBUI_FAVICON_URL,
26
+ WEBUI_NAME,
27
+ log,
28
+ )
29
+ from open_webui.internal.db import Base, get_db
30
+
31
+
32
+ class EndpointFilter(logging.Filter):
33
+ def filter(self, record: logging.LogRecord) -> bool:
34
+ return record.getMessage().find("/health") == -1
35
+
36
+
37
+ # Filter out /endpoint
38
+ logging.getLogger("uvicorn.access").addFilter(EndpointFilter())
39
+
40
+ ####################################
41
+ # Config helpers
42
+ ####################################
43
+
44
+
45
+ # Function to run the alembic migrations
46
+ def run_migrations():
47
+ print("Running migrations")
48
+ try:
49
+ from alembic import command
50
+ from alembic.config import Config
51
+
52
+ alembic_cfg = Config(OPEN_WEBUI_DIR / "alembic.ini")
53
+
54
+ # Set the script location dynamically
55
+ migrations_path = OPEN_WEBUI_DIR / "migrations"
56
+ alembic_cfg.set_main_option("script_location", str(migrations_path))
57
+
58
+ command.upgrade(alembic_cfg, "head")
59
+ except Exception as e:
60
+ print(f"Error: {e}")
61
+
62
+
63
+ run_migrations()
64
+
65
+
66
+ class Config(Base):
67
+ __tablename__ = "config"
68
+
69
+ id = Column(Integer, primary_key=True)
70
+ data = Column(JSON, nullable=False)
71
+ version = Column(Integer, nullable=False, default=0)
72
+ created_at = Column(DateTime, nullable=False, server_default=func.now())
73
+ updated_at = Column(DateTime, nullable=True, onupdate=func.now())
74
+
75
+
76
+ def load_json_config():
77
+ with open(f"{DATA_DIR}/config.json", "r") as file:
78
+ return json.load(file)
79
+
80
+
81
+ def save_to_db(data):
82
+ with get_db() as db:
83
+ existing_config = db.query(Config).first()
84
+ if not existing_config:
85
+ new_config = Config(data=data, version=0)
86
+ db.add(new_config)
87
+ else:
88
+ existing_config.data = data
89
+ existing_config.updated_at = datetime.now()
90
+ db.add(existing_config)
91
+ db.commit()
92
+
93
+
94
+ def reset_config():
95
+ with get_db() as db:
96
+ db.query(Config).delete()
97
+ db.commit()
98
+
99
+
100
+ # When initializing, check if config.json exists and migrate it to the database
101
+ if os.path.exists(f"{DATA_DIR}/config.json"):
102
+ data = load_json_config()
103
+ save_to_db(data)
104
+ os.rename(f"{DATA_DIR}/config.json", f"{DATA_DIR}/old_config.json")
105
+
106
+ DEFAULT_CONFIG = {
107
+ "version": 0,
108
+ "ui": {
109
+ "default_locale": "",
110
+ "prompt_suggestions": [
111
+ {
112
+ "title": [
113
+ "Help me study",
114
+ "vocabulary for a college entrance exam",
115
+ ],
116
+ "content": "Help me study vocabulary: write a sentence for me to fill in the blank, and I'll try to pick the correct option.",
117
+ },
118
+ {
119
+ "title": [
120
+ "Give me ideas",
121
+ "for what to do with my kids' art",
122
+ ],
123
+ "content": "What are 5 creative things I could do with my kids' art? I don't want to throw them away, but it's also so much clutter.",
124
+ },
125
+ {
126
+ "title": ["Tell me a fun fact", "about the Roman Empire"],
127
+ "content": "Tell me a random fun fact about the Roman Empire",
128
+ },
129
+ {
130
+ "title": [
131
+ "Show me a code snippet",
132
+ "of a website's sticky header",
133
+ ],
134
+ "content": "Show me a code snippet of a website's sticky header in CSS and JavaScript.",
135
+ },
136
+ {
137
+ "title": [
138
+ "Explain options trading",
139
+ "if I'm familiar with buying and selling stocks",
140
+ ],
141
+ "content": "Explain options trading in simple terms if I'm familiar with buying and selling stocks.",
142
+ },
143
+ {
144
+ "title": ["Overcome procrastination", "give me tips"],
145
+ "content": "Could you start by asking me about instances when I procrastinate the most and then give me some suggestions to overcome it?",
146
+ },
147
+ {
148
+ "title": [
149
+ "Grammar check",
150
+ "rewrite it for better readability ",
151
+ ],
152
+ "content": 'Check the following sentence for grammar and clarity: "[sentence]". Rewrite it for better readability while maintaining its original meaning.',
153
+ },
154
+ ],
155
+ },
156
+ }
157
+
158
+
159
+ def get_config():
160
+ with get_db() as db:
161
+ config_entry = db.query(Config).order_by(Config.id.desc()).first()
162
+ return config_entry.data if config_entry else DEFAULT_CONFIG
163
+
164
+
165
+ CONFIG_DATA = get_config()
166
+
167
+
168
+ def get_config_value(config_path: str):
169
+ path_parts = config_path.split(".")
170
+ cur_config = CONFIG_DATA
171
+ for key in path_parts:
172
+ if key in cur_config:
173
+ cur_config = cur_config[key]
174
+ else:
175
+ return None
176
+ return cur_config
177
+
178
+
179
+ PERSISTENT_CONFIG_REGISTRY = []
180
+
181
+
182
+ def save_config(config):
183
+ global CONFIG_DATA
184
+ global PERSISTENT_CONFIG_REGISTRY
185
+ try:
186
+ save_to_db(config)
187
+ CONFIG_DATA = config
188
+
189
+ # Trigger updates on all registered PersistentConfig entries
190
+ for config_item in PERSISTENT_CONFIG_REGISTRY:
191
+ config_item.update()
192
+ except Exception as e:
193
+ log.exception(e)
194
+ return False
195
+ return True
196
+
197
+
198
+ T = TypeVar("T")
199
+
200
+
201
+ class PersistentConfig(Generic[T]):
202
+ def __init__(self, env_name: str, config_path: str, env_value: T):
203
+ self.env_name = env_name
204
+ self.config_path = config_path
205
+ self.env_value = env_value
206
+ self.config_value = get_config_value(config_path)
207
+ if self.config_value is not None:
208
+ log.info(f"'{env_name}' loaded from the latest database entry")
209
+ self.value = self.config_value
210
+ else:
211
+ self.value = env_value
212
+
213
+ PERSISTENT_CONFIG_REGISTRY.append(self)
214
+
215
+ def __str__(self):
216
+ return str(self.value)
217
+
218
+ @property
219
+ def __dict__(self):
220
+ raise TypeError(
221
+ "PersistentConfig object cannot be converted to dict, use config_get or .value instead."
222
+ )
223
+
224
+ def __getattribute__(self, item):
225
+ if item == "__dict__":
226
+ raise TypeError(
227
+ "PersistentConfig object cannot be converted to dict, use config_get or .value instead."
228
+ )
229
+ return super().__getattribute__(item)
230
+
231
+ def update(self):
232
+ new_value = get_config_value(self.config_path)
233
+ if new_value is not None:
234
+ self.value = new_value
235
+ log.info(f"Updated {self.env_name} to new value {self.value}")
236
+
237
+ def save(self):
238
+ log.info(f"Saving '{self.env_name}' to the database")
239
+ path_parts = self.config_path.split(".")
240
+ sub_config = CONFIG_DATA
241
+ for key in path_parts[:-1]:
242
+ if key not in sub_config:
243
+ sub_config[key] = {}
244
+ sub_config = sub_config[key]
245
+ sub_config[path_parts[-1]] = self.value
246
+ save_to_db(CONFIG_DATA)
247
+ self.config_value = self.value
248
+
249
+
250
+ class AppConfig:
251
+ _state: dict[str, PersistentConfig]
252
+
253
+ def __init__(self):
254
+ super().__setattr__("_state", {})
255
+
256
+ def __setattr__(self, key, value):
257
+ if isinstance(value, PersistentConfig):
258
+ self._state[key] = value
259
+ else:
260
+ self._state[key].value = value
261
+ self._state[key].save()
262
+
263
+ def __getattr__(self, key):
264
+ return self._state[key].value
265
+
266
+
267
+ ####################################
268
+ # WEBUI_AUTH (Required for security)
269
+ ####################################
270
+
271
+ ENABLE_API_KEY = PersistentConfig(
272
+ "ENABLE_API_KEY",
273
+ "auth.api_key.enable",
274
+ os.environ.get("ENABLE_API_KEY", "True").lower() == "true",
275
+ )
276
+
277
+ ENABLE_API_KEY_ENDPOINT_RESTRICTIONS = PersistentConfig(
278
+ "ENABLE_API_KEY_ENDPOINT_RESTRICTIONS",
279
+ "auth.api_key.endpoint_restrictions",
280
+ os.environ.get("ENABLE_API_KEY_ENDPOINT_RESTRICTIONS", "False").lower() == "true",
281
+ )
282
+
283
+ API_KEY_ALLOWED_ENDPOINTS = PersistentConfig(
284
+ "API_KEY_ALLOWED_ENDPOINTS",
285
+ "auth.api_key.allowed_endpoints",
286
+ os.environ.get("API_KEY_ALLOWED_ENDPOINTS", ""),
287
+ )
288
+
289
+
290
+ JWT_EXPIRES_IN = PersistentConfig(
291
+ "JWT_EXPIRES_IN", "auth.jwt_expiry", os.environ.get("JWT_EXPIRES_IN", "-1")
292
+ )
293
+
294
+ ####################################
295
+ # OAuth config
296
+ ####################################
297
+
298
+ ENABLE_OAUTH_SIGNUP = PersistentConfig(
299
+ "ENABLE_OAUTH_SIGNUP",
300
+ "oauth.enable_signup",
301
+ os.environ.get("ENABLE_OAUTH_SIGNUP", "False").lower() == "true",
302
+ )
303
+
304
+ OAUTH_MERGE_ACCOUNTS_BY_EMAIL = PersistentConfig(
305
+ "OAUTH_MERGE_ACCOUNTS_BY_EMAIL",
306
+ "oauth.merge_accounts_by_email",
307
+ os.environ.get("OAUTH_MERGE_ACCOUNTS_BY_EMAIL", "False").lower() == "true",
308
+ )
309
+
310
+ OAUTH_PROVIDERS = {}
311
+
312
+ GOOGLE_CLIENT_ID = PersistentConfig(
313
+ "GOOGLE_CLIENT_ID",
314
+ "oauth.google.client_id",
315
+ os.environ.get("GOOGLE_CLIENT_ID", ""),
316
+ )
317
+
318
+ GOOGLE_CLIENT_SECRET = PersistentConfig(
319
+ "GOOGLE_CLIENT_SECRET",
320
+ "oauth.google.client_secret",
321
+ os.environ.get("GOOGLE_CLIENT_SECRET", ""),
322
+ )
323
+
324
+
325
+ GOOGLE_OAUTH_SCOPE = PersistentConfig(
326
+ "GOOGLE_OAUTH_SCOPE",
327
+ "oauth.google.scope",
328
+ os.environ.get("GOOGLE_OAUTH_SCOPE", "openid email profile"),
329
+ )
330
+
331
+ GOOGLE_REDIRECT_URI = PersistentConfig(
332
+ "GOOGLE_REDIRECT_URI",
333
+ "oauth.google.redirect_uri",
334
+ os.environ.get("GOOGLE_REDIRECT_URI", ""),
335
+ )
336
+
337
+ MICROSOFT_CLIENT_ID = PersistentConfig(
338
+ "MICROSOFT_CLIENT_ID",
339
+ "oauth.microsoft.client_id",
340
+ os.environ.get("MICROSOFT_CLIENT_ID", ""),
341
+ )
342
+
343
+ MICROSOFT_CLIENT_SECRET = PersistentConfig(
344
+ "MICROSOFT_CLIENT_SECRET",
345
+ "oauth.microsoft.client_secret",
346
+ os.environ.get("MICROSOFT_CLIENT_SECRET", ""),
347
+ )
348
+
349
+ MICROSOFT_CLIENT_TENANT_ID = PersistentConfig(
350
+ "MICROSOFT_CLIENT_TENANT_ID",
351
+ "oauth.microsoft.tenant_id",
352
+ os.environ.get("MICROSOFT_CLIENT_TENANT_ID", ""),
353
+ )
354
+
355
+ MICROSOFT_OAUTH_SCOPE = PersistentConfig(
356
+ "MICROSOFT_OAUTH_SCOPE",
357
+ "oauth.microsoft.scope",
358
+ os.environ.get("MICROSOFT_OAUTH_SCOPE", "openid email profile"),
359
+ )
360
+
361
+ MICROSOFT_REDIRECT_URI = PersistentConfig(
362
+ "MICROSOFT_REDIRECT_URI",
363
+ "oauth.microsoft.redirect_uri",
364
+ os.environ.get("MICROSOFT_REDIRECT_URI", ""),
365
+ )
366
+
367
+ GITHUB_CLIENT_ID = PersistentConfig(
368
+ "GITHUB_CLIENT_ID",
369
+ "oauth.github.client_id",
370
+ os.environ.get("GITHUB_CLIENT_ID", ""),
371
+ )
372
+
373
+ GITHUB_CLIENT_SECRET = PersistentConfig(
374
+ "GITHUB_CLIENT_SECRET",
375
+ "oauth.github.client_secret",
376
+ os.environ.get("GITHUB_CLIENT_SECRET", ""),
377
+ )
378
+
379
+ GITHUB_CLIENT_SCOPE = PersistentConfig(
380
+ "GITHUB_CLIENT_SCOPE",
381
+ "oauth.github.scope",
382
+ os.environ.get("GITHUB_CLIENT_SCOPE", "user:email"),
383
+ )
384
+
385
+ GITHUB_CLIENT_REDIRECT_URI = PersistentConfig(
386
+ "GITHUB_CLIENT_REDIRECT_URI",
387
+ "oauth.github.redirect_uri",
388
+ os.environ.get("GITHUB_CLIENT_REDIRECT_URI", ""),
389
+ )
390
+
391
+ OAUTH_CLIENT_ID = PersistentConfig(
392
+ "OAUTH_CLIENT_ID",
393
+ "oauth.oidc.client_id",
394
+ os.environ.get("OAUTH_CLIENT_ID", ""),
395
+ )
396
+
397
+ OAUTH_CLIENT_SECRET = PersistentConfig(
398
+ "OAUTH_CLIENT_SECRET",
399
+ "oauth.oidc.client_secret",
400
+ os.environ.get("OAUTH_CLIENT_SECRET", ""),
401
+ )
402
+
403
+ OPENID_PROVIDER_URL = PersistentConfig(
404
+ "OPENID_PROVIDER_URL",
405
+ "oauth.oidc.provider_url",
406
+ os.environ.get("OPENID_PROVIDER_URL", ""),
407
+ )
408
+
409
+ OPENID_REDIRECT_URI = PersistentConfig(
410
+ "OPENID_REDIRECT_URI",
411
+ "oauth.oidc.redirect_uri",
412
+ os.environ.get("OPENID_REDIRECT_URI", ""),
413
+ )
414
+
415
+ OAUTH_SCOPES = PersistentConfig(
416
+ "OAUTH_SCOPES",
417
+ "oauth.oidc.scopes",
418
+ os.environ.get("OAUTH_SCOPES", "openid email profile"),
419
+ )
420
+
421
+ OAUTH_PROVIDER_NAME = PersistentConfig(
422
+ "OAUTH_PROVIDER_NAME",
423
+ "oauth.oidc.provider_name",
424
+ os.environ.get("OAUTH_PROVIDER_NAME", "SSO"),
425
+ )
426
+
427
+ OAUTH_USERNAME_CLAIM = PersistentConfig(
428
+ "OAUTH_USERNAME_CLAIM",
429
+ "oauth.oidc.username_claim",
430
+ os.environ.get("OAUTH_USERNAME_CLAIM", "name"),
431
+ )
432
+
433
+ OAUTH_PICTURE_CLAIM = PersistentConfig(
434
+ "OAUTH_PICTURE_CLAIM",
435
+ "oauth.oidc.avatar_claim",
436
+ os.environ.get("OAUTH_PICTURE_CLAIM", "picture"),
437
+ )
438
+
439
+ OAUTH_EMAIL_CLAIM = PersistentConfig(
440
+ "OAUTH_EMAIL_CLAIM",
441
+ "oauth.oidc.email_claim",
442
+ os.environ.get("OAUTH_EMAIL_CLAIM", "email"),
443
+ )
444
+
445
+ OAUTH_GROUPS_CLAIM = PersistentConfig(
446
+ "OAUTH_GROUPS_CLAIM",
447
+ "oauth.oidc.group_claim",
448
+ os.environ.get("OAUTH_GROUP_CLAIM", "groups"),
449
+ )
450
+
451
+ ENABLE_OAUTH_ROLE_MANAGEMENT = PersistentConfig(
452
+ "ENABLE_OAUTH_ROLE_MANAGEMENT",
453
+ "oauth.enable_role_mapping",
454
+ os.environ.get("ENABLE_OAUTH_ROLE_MANAGEMENT", "False").lower() == "true",
455
+ )
456
+
457
+ ENABLE_OAUTH_GROUP_MANAGEMENT = PersistentConfig(
458
+ "ENABLE_OAUTH_GROUP_MANAGEMENT",
459
+ "oauth.enable_group_mapping",
460
+ os.environ.get("ENABLE_OAUTH_GROUP_MANAGEMENT", "False").lower() == "true",
461
+ )
462
+
463
+ OAUTH_ROLES_CLAIM = PersistentConfig(
464
+ "OAUTH_ROLES_CLAIM",
465
+ "oauth.roles_claim",
466
+ os.environ.get("OAUTH_ROLES_CLAIM", "roles"),
467
+ )
468
+
469
+ OAUTH_ALLOWED_ROLES = PersistentConfig(
470
+ "OAUTH_ALLOWED_ROLES",
471
+ "oauth.allowed_roles",
472
+ [
473
+ role.strip()
474
+ for role in os.environ.get("OAUTH_ALLOWED_ROLES", "user,admin").split(",")
475
+ ],
476
+ )
477
+
478
+ OAUTH_ADMIN_ROLES = PersistentConfig(
479
+ "OAUTH_ADMIN_ROLES",
480
+ "oauth.admin_roles",
481
+ [role.strip() for role in os.environ.get("OAUTH_ADMIN_ROLES", "admin").split(",")],
482
+ )
483
+
484
+ OAUTH_ALLOWED_DOMAINS = PersistentConfig(
485
+ "OAUTH_ALLOWED_DOMAINS",
486
+ "oauth.allowed_domains",
487
+ [
488
+ domain.strip()
489
+ for domain in os.environ.get("OAUTH_ALLOWED_DOMAINS", "*").split(",")
490
+ ],
491
+ )
492
+
493
+
494
+ def load_oauth_providers():
495
+ OAUTH_PROVIDERS.clear()
496
+ if GOOGLE_CLIENT_ID.value and GOOGLE_CLIENT_SECRET.value:
497
+
498
+ def google_oauth_register(client):
499
+ client.register(
500
+ name="google",
501
+ client_id=GOOGLE_CLIENT_ID.value,
502
+ client_secret=GOOGLE_CLIENT_SECRET.value,
503
+ server_metadata_url="https://accounts.google.com/.well-known/openid-configuration",
504
+ client_kwargs={"scope": GOOGLE_OAUTH_SCOPE.value},
505
+ redirect_uri=GOOGLE_REDIRECT_URI.value,
506
+ )
507
+
508
+ OAUTH_PROVIDERS["google"] = {
509
+ "redirect_uri": GOOGLE_REDIRECT_URI.value,
510
+ "register": google_oauth_register,
511
+ }
512
+
513
+ if (
514
+ MICROSOFT_CLIENT_ID.value
515
+ and MICROSOFT_CLIENT_SECRET.value
516
+ and MICROSOFT_CLIENT_TENANT_ID.value
517
+ ):
518
+
519
+ def microsoft_oauth_register(client):
520
+ client.register(
521
+ name="microsoft",
522
+ client_id=MICROSOFT_CLIENT_ID.value,
523
+ client_secret=MICROSOFT_CLIENT_SECRET.value,
524
+ server_metadata_url=f"https://login.microsoftonline.com/{MICROSOFT_CLIENT_TENANT_ID.value}/v2.0/.well-known/openid-configuration",
525
+ client_kwargs={
526
+ "scope": MICROSOFT_OAUTH_SCOPE.value,
527
+ },
528
+ redirect_uri=MICROSOFT_REDIRECT_URI.value,
529
+ )
530
+
531
+ OAUTH_PROVIDERS["microsoft"] = {
532
+ "redirect_uri": MICROSOFT_REDIRECT_URI.value,
533
+ "picture_url": "https://graph.microsoft.com/v1.0/me/photo/$value",
534
+ "register": microsoft_oauth_register,
535
+ }
536
+
537
+ if GITHUB_CLIENT_ID.value and GITHUB_CLIENT_SECRET.value:
538
+
539
+ def github_oauth_register(client):
540
+ client.register(
541
+ name="github",
542
+ client_id=GITHUB_CLIENT_ID.value,
543
+ client_secret=GITHUB_CLIENT_SECRET.value,
544
+ access_token_url="https://github.com/login/oauth/access_token",
545
+ authorize_url="https://github.com/login/oauth/authorize",
546
+ api_base_url="https://api.github.com",
547
+ userinfo_endpoint="https://api.github.com/user",
548
+ client_kwargs={"scope": GITHUB_CLIENT_SCOPE.value},
549
+ redirect_uri=GITHUB_CLIENT_REDIRECT_URI.value,
550
+ )
551
+
552
+ OAUTH_PROVIDERS["github"] = {
553
+ "redirect_uri": GITHUB_CLIENT_REDIRECT_URI.value,
554
+ "register": github_oauth_register,
555
+ "sub_claim": "id",
556
+ }
557
+
558
+ if (
559
+ OAUTH_CLIENT_ID.value
560
+ and OAUTH_CLIENT_SECRET.value
561
+ and OPENID_PROVIDER_URL.value
562
+ ):
563
+
564
+ def oidc_oauth_register(client):
565
+ client.register(
566
+ name="oidc",
567
+ client_id=OAUTH_CLIENT_ID.value,
568
+ client_secret=OAUTH_CLIENT_SECRET.value,
569
+ server_metadata_url=OPENID_PROVIDER_URL.value,
570
+ client_kwargs={
571
+ "scope": OAUTH_SCOPES.value,
572
+ },
573
+ redirect_uri=OPENID_REDIRECT_URI.value,
574
+ )
575
+
576
+ OAUTH_PROVIDERS["oidc"] = {
577
+ "name": OAUTH_PROVIDER_NAME.value,
578
+ "redirect_uri": OPENID_REDIRECT_URI.value,
579
+ "register": oidc_oauth_register,
580
+ }
581
+
582
+
583
+ load_oauth_providers()
584
+
585
+ ####################################
586
+ # Static DIR
587
+ ####################################
588
+
589
+ STATIC_DIR = Path(os.getenv("STATIC_DIR", OPEN_WEBUI_DIR / "static")).resolve()
590
+
591
+ frontend_favicon = FRONTEND_BUILD_DIR / "static" / "favicon.png"
592
+
593
+ if frontend_favicon.exists():
594
+ try:
595
+ shutil.copyfile(frontend_favicon, STATIC_DIR / "favicon.png")
596
+ except Exception as e:
597
+ logging.error(f"An error occurred: {e}")
598
+
599
+ frontend_splash = FRONTEND_BUILD_DIR / "static" / "splash.png"
600
+
601
+ if frontend_splash.exists():
602
+ try:
603
+ shutil.copyfile(frontend_splash, STATIC_DIR / "splash.png")
604
+ except Exception as e:
605
+ logging.error(f"An error occurred: {e}")
606
+
607
+ frontend_loader = FRONTEND_BUILD_DIR / "static" / "loader.js"
608
+
609
+ if frontend_loader.exists():
610
+ try:
611
+ shutil.copyfile(frontend_loader, STATIC_DIR / "loader.js")
612
+ except Exception as e:
613
+ logging.error(f"An error occurred: {e}")
614
+
615
+
616
+ ####################################
617
+ # CUSTOM_NAME (Legacy)
618
+ ####################################
619
+
620
+ CUSTOM_NAME = os.environ.get("CUSTOM_NAME", "")
621
+
622
+ if CUSTOM_NAME:
623
+ try:
624
+ r = requests.get(f"https://api.openwebui.com/api/v1/custom/{CUSTOM_NAME}")
625
+ data = r.json()
626
+ if r.ok:
627
+ if "logo" in data:
628
+ WEBUI_FAVICON_URL = url = (
629
+ f"https://api.openwebui.com{data['logo']}"
630
+ if data["logo"][0] == "/"
631
+ else data["logo"]
632
+ )
633
+
634
+ r = requests.get(url, stream=True)
635
+ if r.status_code == 200:
636
+ with open(f"{STATIC_DIR}/favicon.png", "wb") as f:
637
+ r.raw.decode_content = True
638
+ shutil.copyfileobj(r.raw, f)
639
+
640
+ if "splash" in data:
641
+ url = (
642
+ f"https://api.openwebui.com{data['splash']}"
643
+ if data["splash"][0] == "/"
644
+ else data["splash"]
645
+ )
646
+
647
+ r = requests.get(url, stream=True)
648
+ if r.status_code == 200:
649
+ with open(f"{STATIC_DIR}/splash.png", "wb") as f:
650
+ r.raw.decode_content = True
651
+ shutil.copyfileobj(r.raw, f)
652
+
653
+ WEBUI_NAME = data["name"]
654
+ except Exception as e:
655
+ log.exception(e)
656
+ pass
657
+
658
+
659
+ ####################################
660
+ # LICENSE_KEY
661
+ ####################################
662
+
663
+ LICENSE_KEY = PersistentConfig(
664
+ "LICENSE_KEY",
665
+ "license.key",
666
+ os.environ.get("LICENSE_KEY", ""),
667
+ )
668
+
669
+ ####################################
670
+ # STORAGE PROVIDER
671
+ ####################################
672
+
673
+ STORAGE_PROVIDER = os.environ.get("STORAGE_PROVIDER", "local") # defaults to local, s3
674
+
675
+ S3_ACCESS_KEY_ID = os.environ.get("S3_ACCESS_KEY_ID", None)
676
+ S3_SECRET_ACCESS_KEY = os.environ.get("S3_SECRET_ACCESS_KEY", None)
677
+ S3_REGION_NAME = os.environ.get("S3_REGION_NAME", None)
678
+ S3_BUCKET_NAME = os.environ.get("S3_BUCKET_NAME", None)
679
+ S3_KEY_PREFIX = os.environ.get("S3_KEY_PREFIX", None)
680
+ S3_ENDPOINT_URL = os.environ.get("S3_ENDPOINT_URL", None)
681
+
682
+ GCS_BUCKET_NAME = os.environ.get("GCS_BUCKET_NAME", None)
683
+ GOOGLE_APPLICATION_CREDENTIALS_JSON = os.environ.get(
684
+ "GOOGLE_APPLICATION_CREDENTIALS_JSON", None
685
+ )
686
+
687
+ AZURE_STORAGE_ENDPOINT = os.environ.get("AZURE_STORAGE_ENDPOINT", None)
688
+ AZURE_STORAGE_CONTAINER_NAME = os.environ.get("AZURE_STORAGE_CONTAINER_NAME", None)
689
+ AZURE_STORAGE_KEY = os.environ.get("AZURE_STORAGE_KEY", None)
690
+
691
+ ####################################
692
+ # File Upload DIR
693
+ ####################################
694
+
695
+ UPLOAD_DIR = f"{DATA_DIR}/uploads"
696
+ Path(UPLOAD_DIR).mkdir(parents=True, exist_ok=True)
697
+
698
+
699
+ ####################################
700
+ # Cache DIR
701
+ ####################################
702
+
703
+ CACHE_DIR = f"{DATA_DIR}/cache"
704
+ Path(CACHE_DIR).mkdir(parents=True, exist_ok=True)
705
+
706
+
707
+ ####################################
708
+ # DIRECT CONNECTIONS
709
+ ####################################
710
+
711
+ ENABLE_DIRECT_CONNECTIONS = PersistentConfig(
712
+ "ENABLE_DIRECT_CONNECTIONS",
713
+ "direct.enable",
714
+ os.environ.get("ENABLE_DIRECT_CONNECTIONS", "True").lower() == "true",
715
+ )
716
+
717
+ ####################################
718
+ # OLLAMA_BASE_URL
719
+ ####################################
720
+
721
+ ENABLE_OLLAMA_API = PersistentConfig(
722
+ "ENABLE_OLLAMA_API",
723
+ "ollama.enable",
724
+ os.environ.get("ENABLE_OLLAMA_API", "True").lower() == "true",
725
+ )
726
+
727
+ OLLAMA_API_BASE_URL = os.environ.get(
728
+ "OLLAMA_API_BASE_URL", "http://localhost:11434/api"
729
+ )
730
+
731
+ OLLAMA_BASE_URL = os.environ.get("OLLAMA_BASE_URL", "")
732
+ if OLLAMA_BASE_URL:
733
+ # Remove trailing slash
734
+ OLLAMA_BASE_URL = (
735
+ OLLAMA_BASE_URL[:-1] if OLLAMA_BASE_URL.endswith("/") else OLLAMA_BASE_URL
736
+ )
737
+
738
+
739
+ K8S_FLAG = os.environ.get("K8S_FLAG", "")
740
+ USE_OLLAMA_DOCKER = os.environ.get("USE_OLLAMA_DOCKER", "false")
741
+
742
+ if OLLAMA_BASE_URL == "" and OLLAMA_API_BASE_URL != "":
743
+ OLLAMA_BASE_URL = (
744
+ OLLAMA_API_BASE_URL[:-4]
745
+ if OLLAMA_API_BASE_URL.endswith("/api")
746
+ else OLLAMA_API_BASE_URL
747
+ )
748
+
749
+ if ENV == "prod":
750
+ if OLLAMA_BASE_URL == "/ollama" and not K8S_FLAG:
751
+ if USE_OLLAMA_DOCKER.lower() == "true":
752
+ # if you use all-in-one docker container (Open WebUI + Ollama)
753
+ # with the docker build arg USE_OLLAMA=true (--build-arg="USE_OLLAMA=true") this only works with http://localhost:11434
754
+ OLLAMA_BASE_URL = "http://localhost:11434"
755
+ else:
756
+ OLLAMA_BASE_URL = "http://host.docker.internal:11434"
757
+ elif K8S_FLAG:
758
+ OLLAMA_BASE_URL = "http://ollama-service.open-webui.svc.cluster.local:11434"
759
+
760
+
761
+ OLLAMA_BASE_URLS = os.environ.get("OLLAMA_BASE_URLS", "")
762
+ OLLAMA_BASE_URLS = OLLAMA_BASE_URLS if OLLAMA_BASE_URLS != "" else OLLAMA_BASE_URL
763
+
764
+ OLLAMA_BASE_URLS = [url.strip() for url in OLLAMA_BASE_URLS.split(";")]
765
+ OLLAMA_BASE_URLS = PersistentConfig(
766
+ "OLLAMA_BASE_URLS", "ollama.base_urls", OLLAMA_BASE_URLS
767
+ )
768
+
769
+ OLLAMA_API_CONFIGS = PersistentConfig(
770
+ "OLLAMA_API_CONFIGS",
771
+ "ollama.api_configs",
772
+ {},
773
+ )
774
+
775
+ ####################################
776
+ # OPENAI_API
777
+ ####################################
778
+
779
+
780
+ ENABLE_OPENAI_API = PersistentConfig(
781
+ "ENABLE_OPENAI_API",
782
+ "openai.enable",
783
+ os.environ.get("ENABLE_OPENAI_API", "True").lower() == "true",
784
+ )
785
+
786
+
787
+ OPENAI_API_KEY = os.environ.get("OPENAI_API_KEY", "")
788
+ OPENAI_API_BASE_URL = os.environ.get("OPENAI_API_BASE_URL", "")
789
+
790
+ GEMINI_API_KEY = os.environ.get("GEMINI_API_KEY", "")
791
+ GEMINI_API_BASE_URL = os.environ.get("GEMINI_API_BASE_URL", "")
792
+
793
+
794
+ if OPENAI_API_BASE_URL == "":
795
+ OPENAI_API_BASE_URL = "https://api.openai.com/v1"
796
+
797
+ OPENAI_API_KEYS = os.environ.get("OPENAI_API_KEYS", "")
798
+ OPENAI_API_KEYS = OPENAI_API_KEYS if OPENAI_API_KEYS != "" else OPENAI_API_KEY
799
+
800
+ OPENAI_API_KEYS = [url.strip() for url in OPENAI_API_KEYS.split(";")]
801
+ OPENAI_API_KEYS = PersistentConfig(
802
+ "OPENAI_API_KEYS", "openai.api_keys", OPENAI_API_KEYS
803
+ )
804
+
805
+ OPENAI_API_BASE_URLS = os.environ.get("OPENAI_API_BASE_URLS", "")
806
+ OPENAI_API_BASE_URLS = (
807
+ OPENAI_API_BASE_URLS if OPENAI_API_BASE_URLS != "" else OPENAI_API_BASE_URL
808
+ )
809
+
810
+ OPENAI_API_BASE_URLS = [
811
+ url.strip() if url != "" else "https://api.openai.com/v1"
812
+ for url in OPENAI_API_BASE_URLS.split(";")
813
+ ]
814
+ OPENAI_API_BASE_URLS = PersistentConfig(
815
+ "OPENAI_API_BASE_URLS", "openai.api_base_urls", OPENAI_API_BASE_URLS
816
+ )
817
+
818
+ OPENAI_API_CONFIGS = PersistentConfig(
819
+ "OPENAI_API_CONFIGS",
820
+ "openai.api_configs",
821
+ {},
822
+ )
823
+
824
+ # Get the actual OpenAI API key based on the base URL
825
+ OPENAI_API_KEY = ""
826
+ try:
827
+ OPENAI_API_KEY = OPENAI_API_KEYS.value[
828
+ OPENAI_API_BASE_URLS.value.index("https://api.openai.com/v1")
829
+ ]
830
+ except Exception:
831
+ pass
832
+ OPENAI_API_BASE_URL = "https://api.openai.com/v1"
833
+
834
+ ####################################
835
+ # WEBUI
836
+ ####################################
837
+
838
+
839
+ WEBUI_URL = PersistentConfig(
840
+ "WEBUI_URL", "webui.url", os.environ.get("WEBUI_URL", "http://localhost:3000")
841
+ )
842
+
843
+
844
+ ENABLE_SIGNUP = PersistentConfig(
845
+ "ENABLE_SIGNUP",
846
+ "ui.enable_signup",
847
+ (
848
+ False
849
+ if not WEBUI_AUTH
850
+ else os.environ.get("ENABLE_SIGNUP", "True").lower() == "true"
851
+ ),
852
+ )
853
+
854
+ ENABLE_LOGIN_FORM = PersistentConfig(
855
+ "ENABLE_LOGIN_FORM",
856
+ "ui.ENABLE_LOGIN_FORM",
857
+ os.environ.get("ENABLE_LOGIN_FORM", "True").lower() == "true",
858
+ )
859
+
860
+
861
+ DEFAULT_LOCALE = PersistentConfig(
862
+ "DEFAULT_LOCALE",
863
+ "ui.default_locale",
864
+ os.environ.get("DEFAULT_LOCALE", ""),
865
+ )
866
+
867
+ DEFAULT_MODELS = PersistentConfig(
868
+ "DEFAULT_MODELS", "ui.default_models", os.environ.get("DEFAULT_MODELS", None)
869
+ )
870
+
871
+ DEFAULT_PROMPT_SUGGESTIONS = PersistentConfig(
872
+ "DEFAULT_PROMPT_SUGGESTIONS",
873
+ "ui.prompt_suggestions",
874
+ [
875
+ {
876
+ "title": ["Help me study", "vocabulary for a college entrance exam"],
877
+ "content": "Help me study vocabulary: write a sentence for me to fill in the blank, and I'll try to pick the correct option.",
878
+ },
879
+ {
880
+ "title": ["Give me ideas", "for what to do with my kids' art"],
881
+ "content": "What are 5 creative things I could do with my kids' art? I don't want to throw them away, but it's also so much clutter.",
882
+ },
883
+ {
884
+ "title": ["Tell me a fun fact", "about the Roman Empire"],
885
+ "content": "Tell me a random fun fact about the Roman Empire",
886
+ },
887
+ {
888
+ "title": ["Show me a code snippet", "of a website's sticky header"],
889
+ "content": "Show me a code snippet of a website's sticky header in CSS and JavaScript.",
890
+ },
891
+ {
892
+ "title": [
893
+ "Explain options trading",
894
+ "if I'm familiar with buying and selling stocks",
895
+ ],
896
+ "content": "Explain options trading in simple terms if I'm familiar with buying and selling stocks.",
897
+ },
898
+ {
899
+ "title": ["Overcome procrastination", "give me tips"],
900
+ "content": "Could you start by asking me about instances when I procrastinate the most and then give me some suggestions to overcome it?",
901
+ },
902
+ ],
903
+ )
904
+
905
+ MODEL_ORDER_LIST = PersistentConfig(
906
+ "MODEL_ORDER_LIST",
907
+ "ui.model_order_list",
908
+ [],
909
+ )
910
+
911
+ DEFAULT_USER_ROLE = PersistentConfig(
912
+ "DEFAULT_USER_ROLE",
913
+ "ui.default_user_role",
914
+ os.getenv("DEFAULT_USER_ROLE", "pending"),
915
+ )
916
+
917
+ USER_PERMISSIONS_WORKSPACE_MODELS_ACCESS = (
918
+ os.environ.get("USER_PERMISSIONS_WORKSPACE_MODELS_ACCESS", "False").lower()
919
+ == "true"
920
+ )
921
+
922
+ USER_PERMISSIONS_WORKSPACE_KNOWLEDGE_ACCESS = (
923
+ os.environ.get("USER_PERMISSIONS_WORKSPACE_KNOWLEDGE_ACCESS", "False").lower()
924
+ == "true"
925
+ )
926
+
927
+ USER_PERMISSIONS_WORKSPACE_PROMPTS_ACCESS = (
928
+ os.environ.get("USER_PERMISSIONS_WORKSPACE_PROMPTS_ACCESS", "False").lower()
929
+ == "true"
930
+ )
931
+
932
+ USER_PERMISSIONS_WORKSPACE_TOOLS_ACCESS = (
933
+ os.environ.get("USER_PERMISSIONS_WORKSPACE_TOOLS_ACCESS", "False").lower() == "true"
934
+ )
935
+
936
+ USER_PERMISSIONS_CHAT_CONTROLS = (
937
+ os.environ.get("USER_PERMISSIONS_CHAT_CONTROLS", "True").lower() == "true"
938
+ )
939
+
940
+ USER_PERMISSIONS_CHAT_FILE_UPLOAD = (
941
+ os.environ.get("USER_PERMISSIONS_CHAT_FILE_UPLOAD", "True").lower() == "true"
942
+ )
943
+
944
+ USER_PERMISSIONS_CHAT_DELETE = (
945
+ os.environ.get("USER_PERMISSIONS_CHAT_DELETE", "True").lower() == "true"
946
+ )
947
+
948
+ USER_PERMISSIONS_CHAT_EDIT = (
949
+ os.environ.get("USER_PERMISSIONS_CHAT_EDIT", "True").lower() == "true"
950
+ )
951
+
952
+ USER_PERMISSIONS_CHAT_TEMPORARY = (
953
+ os.environ.get("USER_PERMISSIONS_CHAT_TEMPORARY", "True").lower() == "true"
954
+ )
955
+
956
+ USER_PERMISSIONS_FEATURES_WEB_SEARCH = (
957
+ os.environ.get("USER_PERMISSIONS_FEATURES_WEB_SEARCH", "True").lower() == "true"
958
+ )
959
+
960
+ USER_PERMISSIONS_FEATURES_IMAGE_GENERATION = (
961
+ os.environ.get("USER_PERMISSIONS_FEATURES_IMAGE_GENERATION", "True").lower()
962
+ == "true"
963
+ )
964
+
965
+ USER_PERMISSIONS_FEATURES_CODE_INTERPRETER = (
966
+ os.environ.get("USER_PERMISSIONS_FEATURES_CODE_INTERPRETER", "True").lower()
967
+ == "true"
968
+ )
969
+
970
+
971
+ DEFAULT_USER_PERMISSIONS = {
972
+ "workspace": {
973
+ "models": USER_PERMISSIONS_WORKSPACE_MODELS_ACCESS,
974
+ "knowledge": USER_PERMISSIONS_WORKSPACE_KNOWLEDGE_ACCESS,
975
+ "prompts": USER_PERMISSIONS_WORKSPACE_PROMPTS_ACCESS,
976
+ "tools": USER_PERMISSIONS_WORKSPACE_TOOLS_ACCESS,
977
+ },
978
+ "chat": {
979
+ "controls": USER_PERMISSIONS_CHAT_CONTROLS,
980
+ "file_upload": USER_PERMISSIONS_CHAT_FILE_UPLOAD,
981
+ "delete": USER_PERMISSIONS_CHAT_DELETE,
982
+ "edit": USER_PERMISSIONS_CHAT_EDIT,
983
+ "temporary": USER_PERMISSIONS_CHAT_TEMPORARY,
984
+ },
985
+ "features": {
986
+ "web_search": USER_PERMISSIONS_FEATURES_WEB_SEARCH,
987
+ "image_generation": USER_PERMISSIONS_FEATURES_IMAGE_GENERATION,
988
+ "code_interpreter": USER_PERMISSIONS_FEATURES_CODE_INTERPRETER,
989
+ },
990
+ }
991
+
992
+ USER_PERMISSIONS = PersistentConfig(
993
+ "USER_PERMISSIONS",
994
+ "user.permissions",
995
+ DEFAULT_USER_PERMISSIONS,
996
+ )
997
+
998
+ ENABLE_CHANNELS = PersistentConfig(
999
+ "ENABLE_CHANNELS",
1000
+ "channels.enable",
1001
+ os.environ.get("ENABLE_CHANNELS", "False").lower() == "true",
1002
+ )
1003
+
1004
+
1005
+ ENABLE_EVALUATION_ARENA_MODELS = PersistentConfig(
1006
+ "ENABLE_EVALUATION_ARENA_MODELS",
1007
+ "evaluation.arena.enable",
1008
+ os.environ.get("ENABLE_EVALUATION_ARENA_MODELS", "True").lower() == "true",
1009
+ )
1010
+ EVALUATION_ARENA_MODELS = PersistentConfig(
1011
+ "EVALUATION_ARENA_MODELS",
1012
+ "evaluation.arena.models",
1013
+ [],
1014
+ )
1015
+
1016
+ DEFAULT_ARENA_MODEL = {
1017
+ "id": "arena-model",
1018
+ "name": "Arena Model",
1019
+ "meta": {
1020
+ "profile_image_url": "/favicon.png",
1021
+ "description": "Submit your questions to anonymous AI chatbots and vote on the best response.",
1022
+ "model_ids": None,
1023
+ },
1024
+ }
1025
+
1026
+ WEBHOOK_URL = PersistentConfig(
1027
+ "WEBHOOK_URL", "webhook_url", os.environ.get("WEBHOOK_URL", "")
1028
+ )
1029
+
1030
+ ENABLE_ADMIN_EXPORT = os.environ.get("ENABLE_ADMIN_EXPORT", "True").lower() == "true"
1031
+
1032
+ ENABLE_ADMIN_CHAT_ACCESS = (
1033
+ os.environ.get("ENABLE_ADMIN_CHAT_ACCESS", "True").lower() == "true"
1034
+ )
1035
+
1036
+ ENABLE_COMMUNITY_SHARING = PersistentConfig(
1037
+ "ENABLE_COMMUNITY_SHARING",
1038
+ "ui.enable_community_sharing",
1039
+ os.environ.get("ENABLE_COMMUNITY_SHARING", "True").lower() == "true",
1040
+ )
1041
+
1042
+ ENABLE_MESSAGE_RATING = PersistentConfig(
1043
+ "ENABLE_MESSAGE_RATING",
1044
+ "ui.enable_message_rating",
1045
+ os.environ.get("ENABLE_MESSAGE_RATING", "True").lower() == "true",
1046
+ )
1047
+
1048
+
1049
+ def validate_cors_origins(origins):
1050
+ for origin in origins:
1051
+ if origin != "*":
1052
+ validate_cors_origin(origin)
1053
+
1054
+
1055
+ def validate_cors_origin(origin):
1056
+ parsed_url = urlparse(origin)
1057
+
1058
+ # Check if the scheme is either http or https
1059
+ if parsed_url.scheme not in ["http", "https"]:
1060
+ raise ValueError(
1061
+ f"Invalid scheme in CORS_ALLOW_ORIGIN: '{origin}'. Only 'http' and 'https' are allowed."
1062
+ )
1063
+
1064
+ # Ensure that the netloc (domain + port) is present, indicating it's a valid URL
1065
+ if not parsed_url.netloc:
1066
+ raise ValueError(f"Invalid URL structure in CORS_ALLOW_ORIGIN: '{origin}'.")
1067
+
1068
+
1069
+ # For production, you should only need one host as
1070
+ # fastapi serves the svelte-kit built frontend and backend from the same host and port.
1071
+ # To test CORS_ALLOW_ORIGIN locally, you can set something like
1072
+ # CORS_ALLOW_ORIGIN=http://localhost:5173;http://localhost:8080
1073
+ # in your .env file depending on your frontend port, 5173 in this case.
1074
+ CORS_ALLOW_ORIGIN = os.environ.get("CORS_ALLOW_ORIGIN", "*").split(";")
1075
+
1076
+ if "*" in CORS_ALLOW_ORIGIN:
1077
+ log.warning(
1078
+ "\n\nWARNING: CORS_ALLOW_ORIGIN IS SET TO '*' - NOT RECOMMENDED FOR PRODUCTION DEPLOYMENTS.\n"
1079
+ )
1080
+
1081
+ validate_cors_origins(CORS_ALLOW_ORIGIN)
1082
+
1083
+
1084
+ class BannerModel(BaseModel):
1085
+ id: str
1086
+ type: str
1087
+ title: Optional[str] = None
1088
+ content: str
1089
+ dismissible: bool
1090
+ timestamp: int
1091
+
1092
+
1093
+ try:
1094
+ banners = json.loads(os.environ.get("WEBUI_BANNERS", "[]"))
1095
+ banners = [BannerModel(**banner) for banner in banners]
1096
+ except Exception as e:
1097
+ print(f"Error loading WEBUI_BANNERS: {e}")
1098
+ banners = []
1099
+
1100
+ WEBUI_BANNERS = PersistentConfig("WEBUI_BANNERS", "ui.banners", banners)
1101
+
1102
+
1103
+ SHOW_ADMIN_DETAILS = PersistentConfig(
1104
+ "SHOW_ADMIN_DETAILS",
1105
+ "auth.admin.show",
1106
+ os.environ.get("SHOW_ADMIN_DETAILS", "true").lower() == "true",
1107
+ )
1108
+
1109
+ ADMIN_EMAIL = PersistentConfig(
1110
+ "ADMIN_EMAIL",
1111
+ "auth.admin.email",
1112
+ os.environ.get("ADMIN_EMAIL", None),
1113
+ )
1114
+
1115
+
1116
+ ####################################
1117
+ # TASKS
1118
+ ####################################
1119
+
1120
+
1121
+ TASK_MODEL = PersistentConfig(
1122
+ "TASK_MODEL",
1123
+ "task.model.default",
1124
+ os.environ.get("TASK_MODEL", ""),
1125
+ )
1126
+
1127
+ TASK_MODEL_EXTERNAL = PersistentConfig(
1128
+ "TASK_MODEL_EXTERNAL",
1129
+ "task.model.external",
1130
+ os.environ.get("TASK_MODEL_EXTERNAL", ""),
1131
+ )
1132
+
1133
+ TITLE_GENERATION_PROMPT_TEMPLATE = PersistentConfig(
1134
+ "TITLE_GENERATION_PROMPT_TEMPLATE",
1135
+ "task.title.prompt_template",
1136
+ os.environ.get("TITLE_GENERATION_PROMPT_TEMPLATE", ""),
1137
+ )
1138
+
1139
+ DEFAULT_TITLE_GENERATION_PROMPT_TEMPLATE = """### Task:
1140
+ Generate a concise, 3-5 word title with an emoji summarizing the chat history.
1141
+ ### Guidelines:
1142
+ - The title should clearly represent the main theme or subject of the conversation.
1143
+ - Use emojis that enhance understanding of the topic, but avoid quotation marks or special formatting.
1144
+ - Write the title in the chat's primary language; default to English if multilingual.
1145
+ - Prioritize accuracy over excessive creativity; keep it clear and simple.
1146
+ ### Output:
1147
+ JSON format: { "title": "your concise title here" }
1148
+ ### Examples:
1149
+ - { "title": "📉 Stock Market Trends" },
1150
+ - { "title": "🍪 Perfect Chocolate Chip Recipe" },
1151
+ - { "title": "Evolution of Music Streaming" },
1152
+ - { "title": "Remote Work Productivity Tips" },
1153
+ - { "title": "Artificial Intelligence in Healthcare" },
1154
+ - { "title": "🎮 Video Game Development Insights" }
1155
+ ### Chat History:
1156
+ <chat_history>
1157
+ {{MESSAGES:END:2}}
1158
+ </chat_history>"""
1159
+
1160
+ TAGS_GENERATION_PROMPT_TEMPLATE = PersistentConfig(
1161
+ "TAGS_GENERATION_PROMPT_TEMPLATE",
1162
+ "task.tags.prompt_template",
1163
+ os.environ.get("TAGS_GENERATION_PROMPT_TEMPLATE", ""),
1164
+ )
1165
+
1166
+ DEFAULT_TAGS_GENERATION_PROMPT_TEMPLATE = """### Task:
1167
+ Generate 1-3 broad tags categorizing the main themes of the chat history, along with 1-3 more specific subtopic tags.
1168
+
1169
+ ### Guidelines:
1170
+ - Start with high-level domains (e.g. Science, Technology, Philosophy, Arts, Politics, Business, Health, Sports, Entertainment, Education)
1171
+ - Consider including relevant subfields/subdomains if they are strongly represented throughout the conversation
1172
+ - If content is too short (less than 3 messages) or too diverse, use only ["General"]
1173
+ - Use the chat's primary language; default to English if multilingual
1174
+ - Prioritize accuracy over specificity
1175
+
1176
+ ### Output:
1177
+ JSON format: { "tags": ["tag1", "tag2", "tag3"] }
1178
+
1179
+ ### Chat History:
1180
+ <chat_history>
1181
+ {{MESSAGES:END:6}}
1182
+ </chat_history>"""
1183
+
1184
+ IMAGE_PROMPT_GENERATION_PROMPT_TEMPLATE = PersistentConfig(
1185
+ "IMAGE_PROMPT_GENERATION_PROMPT_TEMPLATE",
1186
+ "task.image.prompt_template",
1187
+ os.environ.get("IMAGE_PROMPT_GENERATION_PROMPT_TEMPLATE", ""),
1188
+ )
1189
+
1190
+ DEFAULT_IMAGE_PROMPT_GENERATION_PROMPT_TEMPLATE = """### Task:
1191
+ Generate a detailed prompt for am image generation task based on the given language and context. Describe the image as if you were explaining it to someone who cannot see it. Include relevant details, colors, shapes, and any other important elements.
1192
+
1193
+ ### Guidelines:
1194
+ - Be descriptive and detailed, focusing on the most important aspects of the image.
1195
+ - Avoid making assumptions or adding information not present in the image.
1196
+ - Use the chat's primary language; default to English if multilingual.
1197
+ - If the image is too complex, focus on the most prominent elements.
1198
+
1199
+ ### Output:
1200
+ Strictly return in JSON format:
1201
+ {
1202
+ "prompt": "Your detailed description here."
1203
+ }
1204
+
1205
+ ### Chat History:
1206
+ <chat_history>
1207
+ {{MESSAGES:END:6}}
1208
+ </chat_history>"""
1209
+
1210
+ ENABLE_TAGS_GENERATION = PersistentConfig(
1211
+ "ENABLE_TAGS_GENERATION",
1212
+ "task.tags.enable",
1213
+ os.environ.get("ENABLE_TAGS_GENERATION", "True").lower() == "true",
1214
+ )
1215
+
1216
+ ENABLE_TITLE_GENERATION = PersistentConfig(
1217
+ "ENABLE_TITLE_GENERATION",
1218
+ "task.title.enable",
1219
+ os.environ.get("ENABLE_TITLE_GENERATION", "True").lower() == "true",
1220
+ )
1221
+
1222
+
1223
+ ENABLE_SEARCH_QUERY_GENERATION = PersistentConfig(
1224
+ "ENABLE_SEARCH_QUERY_GENERATION",
1225
+ "task.query.search.enable",
1226
+ os.environ.get("ENABLE_SEARCH_QUERY_GENERATION", "True").lower() == "true",
1227
+ )
1228
+
1229
+ ENABLE_RETRIEVAL_QUERY_GENERATION = PersistentConfig(
1230
+ "ENABLE_RETRIEVAL_QUERY_GENERATION",
1231
+ "task.query.retrieval.enable",
1232
+ os.environ.get("ENABLE_RETRIEVAL_QUERY_GENERATION", "True").lower() == "true",
1233
+ )
1234
+
1235
+
1236
+ QUERY_GENERATION_PROMPT_TEMPLATE = PersistentConfig(
1237
+ "QUERY_GENERATION_PROMPT_TEMPLATE",
1238
+ "task.query.prompt_template",
1239
+ os.environ.get("QUERY_GENERATION_PROMPT_TEMPLATE", ""),
1240
+ )
1241
+
1242
+ DEFAULT_QUERY_GENERATION_PROMPT_TEMPLATE = """### Task:
1243
+ Analyze the chat history to determine the necessity of generating search queries, in the given language. By default, **prioritize generating 1-3 broad and relevant search queries** unless it is absolutely certain that no additional information is required. The aim is to retrieve comprehensive, updated, and valuable information even with minimal uncertainty. If no search is unequivocally needed, return an empty list.
1244
+
1245
+ ### Guidelines:
1246
+ - Respond **EXCLUSIVELY** with a JSON object. Any form of extra commentary, explanation, or additional text is strictly prohibited.
1247
+ - When generating search queries, respond in the format: { "queries": ["query1", "query2"] }, ensuring each query is distinct, concise, and relevant to the topic.
1248
+ - If and only if it is entirely certain that no useful results can be retrieved by a search, return: { "queries": [] }.
1249
+ - Err on the side of suggesting search queries if there is **any chance** they might provide useful or updated information.
1250
+ - Be concise and focused on composing high-quality search queries, avoiding unnecessary elaboration, commentary, or assumptions.
1251
+ - Today's date is: {{CURRENT_DATE}}.
1252
+ - Always prioritize providing actionable and broad queries that maximize informational coverage.
1253
+
1254
+ ### Output:
1255
+ Strictly return in JSON format:
1256
+ {
1257
+ "queries": ["query1", "query2"]
1258
+ }
1259
+
1260
+ ### Chat History:
1261
+ <chat_history>
1262
+ {{MESSAGES:END:6}}
1263
+ </chat_history>
1264
+ """
1265
+
1266
+ ENABLE_AUTOCOMPLETE_GENERATION = PersistentConfig(
1267
+ "ENABLE_AUTOCOMPLETE_GENERATION",
1268
+ "task.autocomplete.enable",
1269
+ os.environ.get("ENABLE_AUTOCOMPLETE_GENERATION", "True").lower() == "true",
1270
+ )
1271
+
1272
+ AUTOCOMPLETE_GENERATION_INPUT_MAX_LENGTH = PersistentConfig(
1273
+ "AUTOCOMPLETE_GENERATION_INPUT_MAX_LENGTH",
1274
+ "task.autocomplete.input_max_length",
1275
+ int(os.environ.get("AUTOCOMPLETE_GENERATION_INPUT_MAX_LENGTH", "-1")),
1276
+ )
1277
+
1278
+ AUTOCOMPLETE_GENERATION_PROMPT_TEMPLATE = PersistentConfig(
1279
+ "AUTOCOMPLETE_GENERATION_PROMPT_TEMPLATE",
1280
+ "task.autocomplete.prompt_template",
1281
+ os.environ.get("AUTOCOMPLETE_GENERATION_PROMPT_TEMPLATE", ""),
1282
+ )
1283
+
1284
+
1285
+ DEFAULT_AUTOCOMPLETE_GENERATION_PROMPT_TEMPLATE = """### Task:
1286
+ You are an autocompletion system. Continue the text in `<text>` based on the **completion type** in `<type>` and the given language.
1287
+
1288
+ ### **Instructions**:
1289
+ 1. Analyze `<text>` for context and meaning.
1290
+ 2. Use `<type>` to guide your output:
1291
+ - **General**: Provide a natural, concise continuation.
1292
+ - **Search Query**: Complete as if generating a realistic search query.
1293
+ 3. Start as if you are directly continuing `<text>`. Do **not** repeat, paraphrase, or respond as a model. Simply complete the text.
1294
+ 4. Ensure the continuation:
1295
+ - Flows naturally from `<text>`.
1296
+ - Avoids repetition, overexplaining, or unrelated ideas.
1297
+ 5. If unsure, return: `{ "text": "" }`.
1298
+
1299
+ ### **Output Rules**:
1300
+ - Respond only in JSON format: `{ "text": "<your_completion>" }`.
1301
+
1302
+ ### **Examples**:
1303
+ #### Example 1:
1304
+ Input:
1305
+ <type>General</type>
1306
+ <text>The sun was setting over the horizon, painting the sky</text>
1307
+ Output:
1308
+ { "text": "with vibrant shades of orange and pink." }
1309
+
1310
+ #### Example 2:
1311
+ Input:
1312
+ <type>Search Query</type>
1313
+ <text>Top-rated restaurants in</text>
1314
+ Output:
1315
+ { "text": "New York City for Italian cuisine." }
1316
+
1317
+ ---
1318
+ ### Context:
1319
+ <chat_history>
1320
+ {{MESSAGES:END:6}}
1321
+ </chat_history>
1322
+ <type>{{TYPE}}</type>
1323
+ <text>{{PROMPT}}</text>
1324
+ #### Output:
1325
+ """
1326
+
1327
+ TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE = PersistentConfig(
1328
+ "TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE",
1329
+ "task.tools.prompt_template",
1330
+ os.environ.get("TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE", ""),
1331
+ )
1332
+
1333
+
1334
+ DEFAULT_TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE = """Available Tools: {{TOOLS}}
1335
+
1336
+ Your task is to choose and return the correct tool(s) from the list of available tools based on the query. Follow these guidelines:
1337
+
1338
+ - Return only the JSON object, without any additional text or explanation.
1339
+
1340
+ - If no tools match the query, return an empty array:
1341
+ {
1342
+ "tool_calls": []
1343
+ }
1344
+
1345
+ - If one or more tools match the query, construct a JSON response containing a "tool_calls" array with objects that include:
1346
+ - "name": The tool's name.
1347
+ - "parameters": A dictionary of required parameters and their corresponding values.
1348
+
1349
+ The format for the JSON response is strictly:
1350
+ {
1351
+ "tool_calls": [
1352
+ {"name": "toolName1", "parameters": {"key1": "value1"}},
1353
+ {"name": "toolName2", "parameters": {"key2": "value2"}}
1354
+ ]
1355
+ }"""
1356
+
1357
+
1358
+ DEFAULT_EMOJI_GENERATION_PROMPT_TEMPLATE = """Your task is to reflect the speaker's likely facial expression through a fitting emoji. Interpret emotions from the message and reflect their facial expression using fitting, diverse emojis (e.g., 😊, 😢, 😡, 😱).
1359
+
1360
+ Message: ```{{prompt}}```"""
1361
+
1362
+ DEFAULT_MOA_GENERATION_PROMPT_TEMPLATE = """You have been provided with a set of responses from various models to the latest user query: "{{prompt}}"
1363
+
1364
+ Your task is to synthesize these responses into a single, high-quality response. It is crucial to critically evaluate the information provided in these responses, recognizing that some of it may be biased or incorrect. Your response should not simply replicate the given answers but should offer a refined, accurate, and comprehensive reply to the instruction. Ensure your response is well-structured, coherent, and adheres to the highest standards of accuracy and reliability.
1365
+
1366
+ Responses from models: {{responses}}"""
1367
+
1368
+
1369
+ ####################################
1370
+ # Code Interpreter
1371
+ ####################################
1372
+
1373
+
1374
+ CODE_EXECUTION_ENGINE = PersistentConfig(
1375
+ "CODE_EXECUTION_ENGINE",
1376
+ "code_execution.engine",
1377
+ os.environ.get("CODE_EXECUTION_ENGINE", "pyodide"),
1378
+ )
1379
+
1380
+ CODE_EXECUTION_JUPYTER_URL = PersistentConfig(
1381
+ "CODE_EXECUTION_JUPYTER_URL",
1382
+ "code_execution.jupyter.url",
1383
+ os.environ.get("CODE_EXECUTION_JUPYTER_URL", ""),
1384
+ )
1385
+
1386
+ CODE_EXECUTION_JUPYTER_AUTH = PersistentConfig(
1387
+ "CODE_EXECUTION_JUPYTER_AUTH",
1388
+ "code_execution.jupyter.auth",
1389
+ os.environ.get("CODE_EXECUTION_JUPYTER_AUTH", ""),
1390
+ )
1391
+
1392
+ CODE_EXECUTION_JUPYTER_AUTH_TOKEN = PersistentConfig(
1393
+ "CODE_EXECUTION_JUPYTER_AUTH_TOKEN",
1394
+ "code_execution.jupyter.auth_token",
1395
+ os.environ.get("CODE_EXECUTION_JUPYTER_AUTH_TOKEN", ""),
1396
+ )
1397
+
1398
+
1399
+ CODE_EXECUTION_JUPYTER_AUTH_PASSWORD = PersistentConfig(
1400
+ "CODE_EXECUTION_JUPYTER_AUTH_PASSWORD",
1401
+ "code_execution.jupyter.auth_password",
1402
+ os.environ.get("CODE_EXECUTION_JUPYTER_AUTH_PASSWORD", ""),
1403
+ )
1404
+
1405
+ CODE_EXECUTION_JUPYTER_TIMEOUT = PersistentConfig(
1406
+ "CODE_EXECUTION_JUPYTER_TIMEOUT",
1407
+ "code_execution.jupyter.timeout",
1408
+ int(os.environ.get("CODE_EXECUTION_JUPYTER_TIMEOUT", "60")),
1409
+ )
1410
+
1411
+ ENABLE_CODE_INTERPRETER = PersistentConfig(
1412
+ "ENABLE_CODE_INTERPRETER",
1413
+ "code_interpreter.enable",
1414
+ os.environ.get("ENABLE_CODE_INTERPRETER", "True").lower() == "true",
1415
+ )
1416
+
1417
+ CODE_INTERPRETER_ENGINE = PersistentConfig(
1418
+ "CODE_INTERPRETER_ENGINE",
1419
+ "code_interpreter.engine",
1420
+ os.environ.get("CODE_INTERPRETER_ENGINE", "pyodide"),
1421
+ )
1422
+
1423
+ CODE_INTERPRETER_PROMPT_TEMPLATE = PersistentConfig(
1424
+ "CODE_INTERPRETER_PROMPT_TEMPLATE",
1425
+ "code_interpreter.prompt_template",
1426
+ os.environ.get("CODE_INTERPRETER_PROMPT_TEMPLATE", ""),
1427
+ )
1428
+
1429
+ CODE_INTERPRETER_JUPYTER_URL = PersistentConfig(
1430
+ "CODE_INTERPRETER_JUPYTER_URL",
1431
+ "code_interpreter.jupyter.url",
1432
+ os.environ.get(
1433
+ "CODE_INTERPRETER_JUPYTER_URL", os.environ.get("CODE_EXECUTION_JUPYTER_URL", "")
1434
+ ),
1435
+ )
1436
+
1437
+ CODE_INTERPRETER_JUPYTER_AUTH = PersistentConfig(
1438
+ "CODE_INTERPRETER_JUPYTER_AUTH",
1439
+ "code_interpreter.jupyter.auth",
1440
+ os.environ.get(
1441
+ "CODE_INTERPRETER_JUPYTER_AUTH",
1442
+ os.environ.get("CODE_EXECUTION_JUPYTER_AUTH", ""),
1443
+ ),
1444
+ )
1445
+
1446
+ CODE_INTERPRETER_JUPYTER_AUTH_TOKEN = PersistentConfig(
1447
+ "CODE_INTERPRETER_JUPYTER_AUTH_TOKEN",
1448
+ "code_interpreter.jupyter.auth_token",
1449
+ os.environ.get(
1450
+ "CODE_INTERPRETER_JUPYTER_AUTH_TOKEN",
1451
+ os.environ.get("CODE_EXECUTION_JUPYTER_AUTH_TOKEN", ""),
1452
+ ),
1453
+ )
1454
+
1455
+
1456
+ CODE_INTERPRETER_JUPYTER_AUTH_PASSWORD = PersistentConfig(
1457
+ "CODE_INTERPRETER_JUPYTER_AUTH_PASSWORD",
1458
+ "code_interpreter.jupyter.auth_password",
1459
+ os.environ.get(
1460
+ "CODE_INTERPRETER_JUPYTER_AUTH_PASSWORD",
1461
+ os.environ.get("CODE_EXECUTION_JUPYTER_AUTH_PASSWORD", ""),
1462
+ ),
1463
+ )
1464
+
1465
+ CODE_INTERPRETER_JUPYTER_TIMEOUT = PersistentConfig(
1466
+ "CODE_INTERPRETER_JUPYTER_TIMEOUT",
1467
+ "code_interpreter.jupyter.timeout",
1468
+ int(
1469
+ os.environ.get(
1470
+ "CODE_INTERPRETER_JUPYTER_TIMEOUT",
1471
+ os.environ.get("CODE_EXECUTION_JUPYTER_TIMEOUT", "60"),
1472
+ )
1473
+ ),
1474
+ )
1475
+
1476
+
1477
+ DEFAULT_CODE_INTERPRETER_PROMPT = """
1478
+ #### Tools Available
1479
+
1480
+ 1. **Code Interpreter**: `<code_interpreter type="code" lang="python"></code_interpreter>`
1481
+ - You have access to a Python shell that runs directly in the user's browser, enabling fast execution of code for analysis, calculations, or problem-solving. Use it in this response.
1482
+ - The Python code you write can incorporate a wide array of libraries, handle data manipulation or visualization, perform API calls for web-related tasks, or tackle virtually any computational challenge. Use this flexibility to **think outside the box, craft elegant solutions, and harness Python's full potential**.
1483
+ - To use it, **you must enclose your code within `<code_interpreter type="code" lang="python">` XML tags** and stop right away. If you don't, the code won't execute. Do NOT use triple backticks.
1484
+ - When coding, **always aim to print meaningful outputs** (e.g., results, tables, summaries, or visuals) to better interpret and verify the findings. Avoid relying on implicit outputs; prioritize explicit and clear print statements so the results are effectively communicated to the user.
1485
+ - After obtaining the printed output, **always provide a concise analysis, interpretation, or next steps to help the user understand the findings or refine the outcome further.**
1486
+ - If the results are unclear, unexpected, or require validation, refine the code and execute it again as needed. Always aim to deliver meaningful insights from the results, iterating if necessary.
1487
+ - **If a link to an image, audio, or any file is provided in markdown format in the output, ALWAYS regurgitate word for word, explicitly display it as part of the response to ensure the user can access it easily, do NOT change the link.**
1488
+ - All responses should be communicated in the chat's primary language, ensuring seamless understanding. If the chat is multilingual, default to English for clarity.
1489
+
1490
+ Ensure that the tools are effectively utilized to achieve the highest-quality analysis for the user."""
1491
+
1492
+
1493
+ ####################################
1494
+ # Vector Database
1495
+ ####################################
1496
+
1497
+ VECTOR_DB = os.environ.get("VECTOR_DB", "chroma")
1498
+
1499
+ # Chroma
1500
+ CHROMA_DATA_PATH = f"{DATA_DIR}/vector_db"
1501
+ CHROMA_TENANT = os.environ.get("CHROMA_TENANT", chromadb.DEFAULT_TENANT)
1502
+ CHROMA_DATABASE = os.environ.get("CHROMA_DATABASE", chromadb.DEFAULT_DATABASE)
1503
+ CHROMA_HTTP_HOST = os.environ.get("CHROMA_HTTP_HOST", "")
1504
+ CHROMA_HTTP_PORT = int(os.environ.get("CHROMA_HTTP_PORT", "8000"))
1505
+ CHROMA_CLIENT_AUTH_PROVIDER = os.environ.get("CHROMA_CLIENT_AUTH_PROVIDER", "")
1506
+ CHROMA_CLIENT_AUTH_CREDENTIALS = os.environ.get("CHROMA_CLIENT_AUTH_CREDENTIALS", "")
1507
+ # Comma-separated list of header=value pairs
1508
+ CHROMA_HTTP_HEADERS = os.environ.get("CHROMA_HTTP_HEADERS", "")
1509
+ if CHROMA_HTTP_HEADERS:
1510
+ CHROMA_HTTP_HEADERS = dict(
1511
+ [pair.split("=") for pair in CHROMA_HTTP_HEADERS.split(",")]
1512
+ )
1513
+ else:
1514
+ CHROMA_HTTP_HEADERS = None
1515
+ CHROMA_HTTP_SSL = os.environ.get("CHROMA_HTTP_SSL", "false").lower() == "true"
1516
+ # this uses the model defined in the Dockerfile ENV variable. If you dont use docker or docker based deployments such as k8s, the default embedding model will be used (sentence-transformers/all-MiniLM-L6-v2)
1517
+
1518
+ # Milvus
1519
+
1520
+ MILVUS_URI = os.environ.get("MILVUS_URI", f"{DATA_DIR}/vector_db/milvus.db")
1521
+ MILVUS_DB = os.environ.get("MILVUS_DB", "default")
1522
+ MILVUS_TOKEN = os.environ.get("MILVUS_TOKEN", None)
1523
+
1524
+ # Qdrant
1525
+ QDRANT_URI = os.environ.get("QDRANT_URI", None)
1526
+ QDRANT_API_KEY = os.environ.get("QDRANT_API_KEY", None)
1527
+
1528
+ # OpenSearch
1529
+ OPENSEARCH_URI = os.environ.get("OPENSEARCH_URI", "https://localhost:9200")
1530
+ OPENSEARCH_SSL = os.environ.get("OPENSEARCH_SSL", True)
1531
+ OPENSEARCH_CERT_VERIFY = os.environ.get("OPENSEARCH_CERT_VERIFY", False)
1532
+ OPENSEARCH_USERNAME = os.environ.get("OPENSEARCH_USERNAME", None)
1533
+ OPENSEARCH_PASSWORD = os.environ.get("OPENSEARCH_PASSWORD", None)
1534
+
1535
+ # Pgvector
1536
+ PGVECTOR_DB_URL = os.environ.get("PGVECTOR_DB_URL", DATABASE_URL)
1537
+ if VECTOR_DB == "pgvector" and not PGVECTOR_DB_URL.startswith("postgres"):
1538
+ raise ValueError(
1539
+ "Pgvector requires setting PGVECTOR_DB_URL or using Postgres with vector extension as the primary database."
1540
+ )
1541
+ PGVECTOR_INITIALIZE_MAX_VECTOR_LENGTH = int(
1542
+ os.environ.get("PGVECTOR_INITIALIZE_MAX_VECTOR_LENGTH", "1536")
1543
+ )
1544
+
1545
+ ####################################
1546
+ # Information Retrieval (RAG)
1547
+ ####################################
1548
+
1549
+
1550
+ # If configured, Google Drive will be available as an upload option.
1551
+ ENABLE_GOOGLE_DRIVE_INTEGRATION = PersistentConfig(
1552
+ "ENABLE_GOOGLE_DRIVE_INTEGRATION",
1553
+ "google_drive.enable",
1554
+ os.getenv("ENABLE_GOOGLE_DRIVE_INTEGRATION", "False").lower() == "true",
1555
+ )
1556
+
1557
+ GOOGLE_DRIVE_CLIENT_ID = PersistentConfig(
1558
+ "GOOGLE_DRIVE_CLIENT_ID",
1559
+ "google_drive.client_id",
1560
+ os.environ.get("GOOGLE_DRIVE_CLIENT_ID", ""),
1561
+ )
1562
+
1563
+ GOOGLE_DRIVE_API_KEY = PersistentConfig(
1564
+ "GOOGLE_DRIVE_API_KEY",
1565
+ "google_drive.api_key",
1566
+ os.environ.get("GOOGLE_DRIVE_API_KEY", ""),
1567
+ )
1568
+
1569
+ # RAG Content Extraction
1570
+ CONTENT_EXTRACTION_ENGINE = PersistentConfig(
1571
+ "CONTENT_EXTRACTION_ENGINE",
1572
+ "rag.CONTENT_EXTRACTION_ENGINE",
1573
+ os.environ.get("CONTENT_EXTRACTION_ENGINE", "").lower(),
1574
+ )
1575
+
1576
+ TIKA_SERVER_URL = PersistentConfig(
1577
+ "TIKA_SERVER_URL",
1578
+ "rag.tika_server_url",
1579
+ os.getenv("TIKA_SERVER_URL", "http://tika:9998"), # Default for sidecar deployment
1580
+ )
1581
+
1582
+ RAG_TOP_K = PersistentConfig(
1583
+ "RAG_TOP_K", "rag.top_k", int(os.environ.get("RAG_TOP_K", "3"))
1584
+ )
1585
+ RAG_RELEVANCE_THRESHOLD = PersistentConfig(
1586
+ "RAG_RELEVANCE_THRESHOLD",
1587
+ "rag.relevance_threshold",
1588
+ float(os.environ.get("RAG_RELEVANCE_THRESHOLD", "0.0")),
1589
+ )
1590
+
1591
+ ENABLE_RAG_HYBRID_SEARCH = PersistentConfig(
1592
+ "ENABLE_RAG_HYBRID_SEARCH",
1593
+ "rag.enable_hybrid_search",
1594
+ os.environ.get("ENABLE_RAG_HYBRID_SEARCH", "").lower() == "true",
1595
+ )
1596
+
1597
+ RAG_FULL_CONTEXT = PersistentConfig(
1598
+ "RAG_FULL_CONTEXT",
1599
+ "rag.full_context",
1600
+ os.getenv("RAG_FULL_CONTEXT", "False").lower() == "true",
1601
+ )
1602
+
1603
+ RAG_FILE_MAX_COUNT = PersistentConfig(
1604
+ "RAG_FILE_MAX_COUNT",
1605
+ "rag.file.max_count",
1606
+ (
1607
+ int(os.environ.get("RAG_FILE_MAX_COUNT"))
1608
+ if os.environ.get("RAG_FILE_MAX_COUNT")
1609
+ else None
1610
+ ),
1611
+ )
1612
+
1613
+ RAG_FILE_MAX_SIZE = PersistentConfig(
1614
+ "RAG_FILE_MAX_SIZE",
1615
+ "rag.file.max_size",
1616
+ (
1617
+ int(os.environ.get("RAG_FILE_MAX_SIZE"))
1618
+ if os.environ.get("RAG_FILE_MAX_SIZE")
1619
+ else None
1620
+ ),
1621
+ )
1622
+
1623
+ ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION = PersistentConfig(
1624
+ "ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION",
1625
+ "rag.enable_web_loader_ssl_verification",
1626
+ os.environ.get("ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION", "True").lower() == "true",
1627
+ )
1628
+
1629
+ RAG_EMBEDDING_ENGINE = PersistentConfig(
1630
+ "RAG_EMBEDDING_ENGINE",
1631
+ "rag.embedding_engine",
1632
+ os.environ.get("RAG_EMBEDDING_ENGINE", ""),
1633
+ )
1634
+
1635
+ PDF_EXTRACT_IMAGES = PersistentConfig(
1636
+ "PDF_EXTRACT_IMAGES",
1637
+ "rag.pdf_extract_images",
1638
+ os.environ.get("PDF_EXTRACT_IMAGES", "False").lower() == "true",
1639
+ )
1640
+
1641
+ RAG_EMBEDDING_MODEL = PersistentConfig(
1642
+ "RAG_EMBEDDING_MODEL",
1643
+ "rag.embedding_model",
1644
+ os.environ.get("RAG_EMBEDDING_MODEL", "sentence-transformers/all-MiniLM-L6-v2"),
1645
+ )
1646
+ log.info(f"Embedding model set: {RAG_EMBEDDING_MODEL.value}")
1647
+
1648
+ RAG_EMBEDDING_MODEL_AUTO_UPDATE = (
1649
+ not OFFLINE_MODE
1650
+ and os.environ.get("RAG_EMBEDDING_MODEL_AUTO_UPDATE", "True").lower() == "true"
1651
+ )
1652
+
1653
+ RAG_EMBEDDING_MODEL_TRUST_REMOTE_CODE = (
1654
+ os.environ.get("RAG_EMBEDDING_MODEL_TRUST_REMOTE_CODE", "True").lower() == "true"
1655
+ )
1656
+
1657
+ RAG_EMBEDDING_BATCH_SIZE = PersistentConfig(
1658
+ "RAG_EMBEDDING_BATCH_SIZE",
1659
+ "rag.embedding_batch_size",
1660
+ int(
1661
+ os.environ.get("RAG_EMBEDDING_BATCH_SIZE")
1662
+ or os.environ.get("RAG_EMBEDDING_OPENAI_BATCH_SIZE", "1")
1663
+ ),
1664
+ )
1665
+
1666
+ RAG_RERANKING_MODEL = PersistentConfig(
1667
+ "RAG_RERANKING_MODEL",
1668
+ "rag.reranking_model",
1669
+ os.environ.get("RAG_RERANKING_MODEL", ""),
1670
+ )
1671
+ if RAG_RERANKING_MODEL.value != "":
1672
+ log.info(f"Reranking model set: {RAG_RERANKING_MODEL.value}")
1673
+
1674
+ RAG_RERANKING_MODEL_AUTO_UPDATE = (
1675
+ not OFFLINE_MODE
1676
+ and os.environ.get("RAG_RERANKING_MODEL_AUTO_UPDATE", "True").lower() == "true"
1677
+ )
1678
+
1679
+ RAG_RERANKING_MODEL_TRUST_REMOTE_CODE = (
1680
+ os.environ.get("RAG_RERANKING_MODEL_TRUST_REMOTE_CODE", "True").lower() == "true"
1681
+ )
1682
+
1683
+
1684
+ RAG_TEXT_SPLITTER = PersistentConfig(
1685
+ "RAG_TEXT_SPLITTER",
1686
+ "rag.text_splitter",
1687
+ os.environ.get("RAG_TEXT_SPLITTER", ""),
1688
+ )
1689
+
1690
+
1691
+ TIKTOKEN_CACHE_DIR = os.environ.get("TIKTOKEN_CACHE_DIR", f"{CACHE_DIR}/tiktoken")
1692
+ TIKTOKEN_ENCODING_NAME = PersistentConfig(
1693
+ "TIKTOKEN_ENCODING_NAME",
1694
+ "rag.tiktoken_encoding_name",
1695
+ os.environ.get("TIKTOKEN_ENCODING_NAME", "cl100k_base"),
1696
+ )
1697
+
1698
+
1699
+ CHUNK_SIZE = PersistentConfig(
1700
+ "CHUNK_SIZE", "rag.chunk_size", int(os.environ.get("CHUNK_SIZE", "1000"))
1701
+ )
1702
+ CHUNK_OVERLAP = PersistentConfig(
1703
+ "CHUNK_OVERLAP",
1704
+ "rag.chunk_overlap",
1705
+ int(os.environ.get("CHUNK_OVERLAP", "100")),
1706
+ )
1707
+
1708
+ DEFAULT_RAG_TEMPLATE = """### Task:
1709
+ Respond to the user query using the provided context, incorporating inline citations in the format [source_id] **only when the <source_id> tag is explicitly provided** in the context.
1710
+
1711
+ ### Guidelines:
1712
+ - If you don't know the answer, clearly state that.
1713
+ - If uncertain, ask the user for clarification.
1714
+ - Respond in the same language as the user's query.
1715
+ - If the context is unreadable or of poor quality, inform the user and provide the best possible answer.
1716
+ - If the answer isn't present in the context but you possess the knowledge, explain this to the user and provide the answer using your own understanding.
1717
+ - **Only include inline citations using [source_id] (e.g., [1], [2]) when a `<source_id>` tag is explicitly provided in the context.**
1718
+ - Do not cite if the <source_id> tag is not provided in the context.
1719
+ - Do not use XML tags in your response.
1720
+ - Ensure citations are concise and directly related to the information provided.
1721
+
1722
+ ### Example of Citation:
1723
+ If the user asks about a specific topic and the information is found in "whitepaper.pdf" with a provided <source_id>, the response should include the citation like so:
1724
+ * "According to the study, the proposed method increases efficiency by 20% [whitepaper.pdf]."
1725
+ If no <source_id> is present, the response should omit the citation.
1726
+
1727
+ ### Output:
1728
+ Provide a clear and direct response to the user's query, including inline citations in the format [source_id] only when the <source_id> tag is present in the context.
1729
+
1730
+ <context>
1731
+ {{CONTEXT}}
1732
+ </context>
1733
+
1734
+ <user_query>
1735
+ {{QUERY}}
1736
+ </user_query>
1737
+ """
1738
+
1739
+ RAG_TEMPLATE = PersistentConfig(
1740
+ "RAG_TEMPLATE",
1741
+ "rag.template",
1742
+ os.environ.get("RAG_TEMPLATE", DEFAULT_RAG_TEMPLATE),
1743
+ )
1744
+
1745
+ RAG_OPENAI_API_BASE_URL = PersistentConfig(
1746
+ "RAG_OPENAI_API_BASE_URL",
1747
+ "rag.openai_api_base_url",
1748
+ os.getenv("RAG_OPENAI_API_BASE_URL", OPENAI_API_BASE_URL),
1749
+ )
1750
+ RAG_OPENAI_API_KEY = PersistentConfig(
1751
+ "RAG_OPENAI_API_KEY",
1752
+ "rag.openai_api_key",
1753
+ os.getenv("RAG_OPENAI_API_KEY", OPENAI_API_KEY),
1754
+ )
1755
+
1756
+ RAG_OLLAMA_BASE_URL = PersistentConfig(
1757
+ "RAG_OLLAMA_BASE_URL",
1758
+ "rag.ollama.url",
1759
+ os.getenv("RAG_OLLAMA_BASE_URL", OLLAMA_BASE_URL),
1760
+ )
1761
+
1762
+ RAG_OLLAMA_API_KEY = PersistentConfig(
1763
+ "RAG_OLLAMA_API_KEY",
1764
+ "rag.ollama.key",
1765
+ os.getenv("RAG_OLLAMA_API_KEY", ""),
1766
+ )
1767
+
1768
+
1769
+ ENABLE_RAG_LOCAL_WEB_FETCH = (
1770
+ os.getenv("ENABLE_RAG_LOCAL_WEB_FETCH", "False").lower() == "true"
1771
+ )
1772
+
1773
+ YOUTUBE_LOADER_LANGUAGE = PersistentConfig(
1774
+ "YOUTUBE_LOADER_LANGUAGE",
1775
+ "rag.youtube_loader_language",
1776
+ os.getenv("YOUTUBE_LOADER_LANGUAGE", "en").split(","),
1777
+ )
1778
+
1779
+ YOUTUBE_LOADER_PROXY_URL = PersistentConfig(
1780
+ "YOUTUBE_LOADER_PROXY_URL",
1781
+ "rag.youtube_loader_proxy_url",
1782
+ os.getenv("YOUTUBE_LOADER_PROXY_URL", ""),
1783
+ )
1784
+
1785
+
1786
+ ENABLE_RAG_WEB_SEARCH = PersistentConfig(
1787
+ "ENABLE_RAG_WEB_SEARCH",
1788
+ "rag.web.search.enable",
1789
+ os.getenv("ENABLE_RAG_WEB_SEARCH", "False").lower() == "true",
1790
+ )
1791
+
1792
+ RAG_WEB_SEARCH_ENGINE = PersistentConfig(
1793
+ "RAG_WEB_SEARCH_ENGINE",
1794
+ "rag.web.search.engine",
1795
+ os.getenv("RAG_WEB_SEARCH_ENGINE", ""),
1796
+ )
1797
+
1798
+ RAG_WEB_SEARCH_FULL_CONTEXT = PersistentConfig(
1799
+ "RAG_WEB_SEARCH_FULL_CONTEXT",
1800
+ "rag.web.search.full_context",
1801
+ os.getenv("RAG_WEB_SEARCH_FULL_CONTEXT", "False").lower() == "true",
1802
+ )
1803
+
1804
+ # You can provide a list of your own websites to filter after performing a web search.
1805
+ # This ensures the highest level of safety and reliability of the information sources.
1806
+ RAG_WEB_SEARCH_DOMAIN_FILTER_LIST = PersistentConfig(
1807
+ "RAG_WEB_SEARCH_DOMAIN_FILTER_LIST",
1808
+ "rag.web.search.domain.filter_list",
1809
+ [
1810
+ # "wikipedia.com",
1811
+ # "wikimedia.org",
1812
+ # "wikidata.org",
1813
+ ],
1814
+ )
1815
+
1816
+
1817
+ SEARXNG_QUERY_URL = PersistentConfig(
1818
+ "SEARXNG_QUERY_URL",
1819
+ "rag.web.search.searxng_query_url",
1820
+ os.getenv("SEARXNG_QUERY_URL", ""),
1821
+ )
1822
+
1823
+ GOOGLE_PSE_API_KEY = PersistentConfig(
1824
+ "GOOGLE_PSE_API_KEY",
1825
+ "rag.web.search.google_pse_api_key",
1826
+ os.getenv("GOOGLE_PSE_API_KEY", ""),
1827
+ )
1828
+
1829
+ GOOGLE_PSE_ENGINE_ID = PersistentConfig(
1830
+ "GOOGLE_PSE_ENGINE_ID",
1831
+ "rag.web.search.google_pse_engine_id",
1832
+ os.getenv("GOOGLE_PSE_ENGINE_ID", ""),
1833
+ )
1834
+
1835
+ BRAVE_SEARCH_API_KEY = PersistentConfig(
1836
+ "BRAVE_SEARCH_API_KEY",
1837
+ "rag.web.search.brave_search_api_key",
1838
+ os.getenv("BRAVE_SEARCH_API_KEY", ""),
1839
+ )
1840
+
1841
+ KAGI_SEARCH_API_KEY = PersistentConfig(
1842
+ "KAGI_SEARCH_API_KEY",
1843
+ "rag.web.search.kagi_search_api_key",
1844
+ os.getenv("KAGI_SEARCH_API_KEY", ""),
1845
+ )
1846
+
1847
+ MOJEEK_SEARCH_API_KEY = PersistentConfig(
1848
+ "MOJEEK_SEARCH_API_KEY",
1849
+ "rag.web.search.mojeek_search_api_key",
1850
+ os.getenv("MOJEEK_SEARCH_API_KEY", ""),
1851
+ )
1852
+
1853
+ BOCHA_SEARCH_API_KEY = PersistentConfig(
1854
+ "BOCHA_SEARCH_API_KEY",
1855
+ "rag.web.search.bocha_search_api_key",
1856
+ os.getenv("BOCHA_SEARCH_API_KEY", ""),
1857
+ )
1858
+
1859
+ SERPSTACK_API_KEY = PersistentConfig(
1860
+ "SERPSTACK_API_KEY",
1861
+ "rag.web.search.serpstack_api_key",
1862
+ os.getenv("SERPSTACK_API_KEY", ""),
1863
+ )
1864
+
1865
+ SERPSTACK_HTTPS = PersistentConfig(
1866
+ "SERPSTACK_HTTPS",
1867
+ "rag.web.search.serpstack_https",
1868
+ os.getenv("SERPSTACK_HTTPS", "True").lower() == "true",
1869
+ )
1870
+
1871
+ SERPER_API_KEY = PersistentConfig(
1872
+ "SERPER_API_KEY",
1873
+ "rag.web.search.serper_api_key",
1874
+ os.getenv("SERPER_API_KEY", ""),
1875
+ )
1876
+
1877
+ SERPLY_API_KEY = PersistentConfig(
1878
+ "SERPLY_API_KEY",
1879
+ "rag.web.search.serply_api_key",
1880
+ os.getenv("SERPLY_API_KEY", ""),
1881
+ )
1882
+
1883
+ TAVILY_API_KEY = PersistentConfig(
1884
+ "TAVILY_API_KEY",
1885
+ "rag.web.search.tavily_api_key",
1886
+ os.getenv("TAVILY_API_KEY", ""),
1887
+ )
1888
+
1889
+ JINA_API_KEY = PersistentConfig(
1890
+ "JINA_API_KEY",
1891
+ "rag.web.search.jina_api_key",
1892
+ os.getenv("JINA_API_KEY", ""),
1893
+ )
1894
+
1895
+ SEARCHAPI_API_KEY = PersistentConfig(
1896
+ "SEARCHAPI_API_KEY",
1897
+ "rag.web.search.searchapi_api_key",
1898
+ os.getenv("SEARCHAPI_API_KEY", ""),
1899
+ )
1900
+
1901
+ SEARCHAPI_ENGINE = PersistentConfig(
1902
+ "SEARCHAPI_ENGINE",
1903
+ "rag.web.search.searchapi_engine",
1904
+ os.getenv("SEARCHAPI_ENGINE", ""),
1905
+ )
1906
+
1907
+ SERPAPI_API_KEY = PersistentConfig(
1908
+ "SERPAPI_API_KEY",
1909
+ "rag.web.search.serpapi_api_key",
1910
+ os.getenv("SERPAPI_API_KEY", ""),
1911
+ )
1912
+
1913
+ SERPAPI_ENGINE = PersistentConfig(
1914
+ "SERPAPI_ENGINE",
1915
+ "rag.web.search.serpapi_engine",
1916
+ os.getenv("SERPAPI_ENGINE", ""),
1917
+ )
1918
+
1919
+ BING_SEARCH_V7_ENDPOINT = PersistentConfig(
1920
+ "BING_SEARCH_V7_ENDPOINT",
1921
+ "rag.web.search.bing_search_v7_endpoint",
1922
+ os.environ.get(
1923
+ "BING_SEARCH_V7_ENDPOINT", "https://api.bing.microsoft.com/v7.0/search"
1924
+ ),
1925
+ )
1926
+
1927
+ BING_SEARCH_V7_SUBSCRIPTION_KEY = PersistentConfig(
1928
+ "BING_SEARCH_V7_SUBSCRIPTION_KEY",
1929
+ "rag.web.search.bing_search_v7_subscription_key",
1930
+ os.environ.get("BING_SEARCH_V7_SUBSCRIPTION_KEY", ""),
1931
+ )
1932
+
1933
+ EXA_API_KEY = PersistentConfig(
1934
+ "EXA_API_KEY",
1935
+ "rag.web.search.exa_api_key",
1936
+ os.getenv("EXA_API_KEY", ""),
1937
+ )
1938
+
1939
+ RAG_WEB_SEARCH_RESULT_COUNT = PersistentConfig(
1940
+ "RAG_WEB_SEARCH_RESULT_COUNT",
1941
+ "rag.web.search.result_count",
1942
+ int(os.getenv("RAG_WEB_SEARCH_RESULT_COUNT", "3")),
1943
+ )
1944
+
1945
+ RAG_WEB_SEARCH_CONCURRENT_REQUESTS = PersistentConfig(
1946
+ "RAG_WEB_SEARCH_CONCURRENT_REQUESTS",
1947
+ "rag.web.search.concurrent_requests",
1948
+ int(os.getenv("RAG_WEB_SEARCH_CONCURRENT_REQUESTS", "10")),
1949
+ )
1950
+
1951
+ RAG_WEB_LOADER_ENGINE = PersistentConfig(
1952
+ "RAG_WEB_LOADER_ENGINE",
1953
+ "rag.web.loader.engine",
1954
+ os.environ.get("RAG_WEB_LOADER_ENGINE", "safe_web"),
1955
+ )
1956
+
1957
+ RAG_WEB_SEARCH_TRUST_ENV = PersistentConfig(
1958
+ "RAG_WEB_SEARCH_TRUST_ENV",
1959
+ "rag.web.search.trust_env",
1960
+ os.getenv("RAG_WEB_SEARCH_TRUST_ENV", "False").lower() == "true",
1961
+ )
1962
+
1963
+ PLAYWRIGHT_WS_URI = PersistentConfig(
1964
+ "PLAYWRIGHT_WS_URI",
1965
+ "rag.web.loader.engine.playwright.ws.uri",
1966
+ os.environ.get("PLAYWRIGHT_WS_URI", None),
1967
+ )
1968
+
1969
+ FIRECRAWL_API_KEY = PersistentConfig(
1970
+ "FIRECRAWL_API_KEY",
1971
+ "firecrawl.api_key",
1972
+ os.environ.get("FIRECRAWL_API_KEY", ""),
1973
+ )
1974
+
1975
+ FIRECRAWL_API_BASE_URL = PersistentConfig(
1976
+ "FIRECRAWL_API_BASE_URL",
1977
+ "firecrawl.api_url",
1978
+ os.environ.get("FIRECRAWL_API_BASE_URL", "https://api.firecrawl.dev"),
1979
+ )
1980
+
1981
+ ####################################
1982
+ # Images
1983
+ ####################################
1984
+
1985
+ IMAGE_GENERATION_ENGINE = PersistentConfig(
1986
+ "IMAGE_GENERATION_ENGINE",
1987
+ "image_generation.engine",
1988
+ os.getenv("IMAGE_GENERATION_ENGINE", "openai"),
1989
+ )
1990
+
1991
+ ENABLE_IMAGE_GENERATION = PersistentConfig(
1992
+ "ENABLE_IMAGE_GENERATION",
1993
+ "image_generation.enable",
1994
+ os.environ.get("ENABLE_IMAGE_GENERATION", "").lower() == "true",
1995
+ )
1996
+
1997
+ ENABLE_IMAGE_PROMPT_GENERATION = PersistentConfig(
1998
+ "ENABLE_IMAGE_PROMPT_GENERATION",
1999
+ "image_generation.prompt.enable",
2000
+ os.environ.get("ENABLE_IMAGE_PROMPT_GENERATION", "true").lower() == "true",
2001
+ )
2002
+
2003
+ AUTOMATIC1111_BASE_URL = PersistentConfig(
2004
+ "AUTOMATIC1111_BASE_URL",
2005
+ "image_generation.automatic1111.base_url",
2006
+ os.getenv("AUTOMATIC1111_BASE_URL", ""),
2007
+ )
2008
+ AUTOMATIC1111_API_AUTH = PersistentConfig(
2009
+ "AUTOMATIC1111_API_AUTH",
2010
+ "image_generation.automatic1111.api_auth",
2011
+ os.getenv("AUTOMATIC1111_API_AUTH", ""),
2012
+ )
2013
+
2014
+ AUTOMATIC1111_CFG_SCALE = PersistentConfig(
2015
+ "AUTOMATIC1111_CFG_SCALE",
2016
+ "image_generation.automatic1111.cfg_scale",
2017
+ (
2018
+ float(os.environ.get("AUTOMATIC1111_CFG_SCALE"))
2019
+ if os.environ.get("AUTOMATIC1111_CFG_SCALE")
2020
+ else None
2021
+ ),
2022
+ )
2023
+
2024
+
2025
+ AUTOMATIC1111_SAMPLER = PersistentConfig(
2026
+ "AUTOMATIC1111_SAMPLER",
2027
+ "image_generation.automatic1111.sampler",
2028
+ (
2029
+ os.environ.get("AUTOMATIC1111_SAMPLER")
2030
+ if os.environ.get("AUTOMATIC1111_SAMPLER")
2031
+ else None
2032
+ ),
2033
+ )
2034
+
2035
+ AUTOMATIC1111_SCHEDULER = PersistentConfig(
2036
+ "AUTOMATIC1111_SCHEDULER",
2037
+ "image_generation.automatic1111.scheduler",
2038
+ (
2039
+ os.environ.get("AUTOMATIC1111_SCHEDULER")
2040
+ if os.environ.get("AUTOMATIC1111_SCHEDULER")
2041
+ else None
2042
+ ),
2043
+ )
2044
+
2045
+ COMFYUI_BASE_URL = PersistentConfig(
2046
+ "COMFYUI_BASE_URL",
2047
+ "image_generation.comfyui.base_url",
2048
+ os.getenv("COMFYUI_BASE_URL", ""),
2049
+ )
2050
+
2051
+ COMFYUI_API_KEY = PersistentConfig(
2052
+ "COMFYUI_API_KEY",
2053
+ "image_generation.comfyui.api_key",
2054
+ os.getenv("COMFYUI_API_KEY", ""),
2055
+ )
2056
+
2057
+ COMFYUI_DEFAULT_WORKFLOW = """
2058
+ {
2059
+ "3": {
2060
+ "inputs": {
2061
+ "seed": 0,
2062
+ "steps": 20,
2063
+ "cfg": 8,
2064
+ "sampler_name": "euler",
2065
+ "scheduler": "normal",
2066
+ "denoise": 1,
2067
+ "model": [
2068
+ "4",
2069
+ 0
2070
+ ],
2071
+ "positive": [
2072
+ "6",
2073
+ 0
2074
+ ],
2075
+ "negative": [
2076
+ "7",
2077
+ 0
2078
+ ],
2079
+ "latent_image": [
2080
+ "5",
2081
+ 0
2082
+ ]
2083
+ },
2084
+ "class_type": "KSampler",
2085
+ "_meta": {
2086
+ "title": "KSampler"
2087
+ }
2088
+ },
2089
+ "4": {
2090
+ "inputs": {
2091
+ "ckpt_name": "model.safetensors"
2092
+ },
2093
+ "class_type": "CheckpointLoaderSimple",
2094
+ "_meta": {
2095
+ "title": "Load Checkpoint"
2096
+ }
2097
+ },
2098
+ "5": {
2099
+ "inputs": {
2100
+ "width": 512,
2101
+ "height": 512,
2102
+ "batch_size": 1
2103
+ },
2104
+ "class_type": "EmptyLatentImage",
2105
+ "_meta": {
2106
+ "title": "Empty Latent Image"
2107
+ }
2108
+ },
2109
+ "6": {
2110
+ "inputs": {
2111
+ "text": "Prompt",
2112
+ "clip": [
2113
+ "4",
2114
+ 1
2115
+ ]
2116
+ },
2117
+ "class_type": "CLIPTextEncode",
2118
+ "_meta": {
2119
+ "title": "CLIP Text Encode (Prompt)"
2120
+ }
2121
+ },
2122
+ "7": {
2123
+ "inputs": {
2124
+ "text": "",
2125
+ "clip": [
2126
+ "4",
2127
+ 1
2128
+ ]
2129
+ },
2130
+ "class_type": "CLIPTextEncode",
2131
+ "_meta": {
2132
+ "title": "CLIP Text Encode (Prompt)"
2133
+ }
2134
+ },
2135
+ "8": {
2136
+ "inputs": {
2137
+ "samples": [
2138
+ "3",
2139
+ 0
2140
+ ],
2141
+ "vae": [
2142
+ "4",
2143
+ 2
2144
+ ]
2145
+ },
2146
+ "class_type": "VAEDecode",
2147
+ "_meta": {
2148
+ "title": "VAE Decode"
2149
+ }
2150
+ },
2151
+ "9": {
2152
+ "inputs": {
2153
+ "filename_prefix": "ComfyUI",
2154
+ "images": [
2155
+ "8",
2156
+ 0
2157
+ ]
2158
+ },
2159
+ "class_type": "SaveImage",
2160
+ "_meta": {
2161
+ "title": "Save Image"
2162
+ }
2163
+ }
2164
+ }
2165
+ """
2166
+
2167
+
2168
+ COMFYUI_WORKFLOW = PersistentConfig(
2169
+ "COMFYUI_WORKFLOW",
2170
+ "image_generation.comfyui.workflow",
2171
+ os.getenv("COMFYUI_WORKFLOW", COMFYUI_DEFAULT_WORKFLOW),
2172
+ )
2173
+
2174
+ COMFYUI_WORKFLOW_NODES = PersistentConfig(
2175
+ "COMFYUI_WORKFLOW",
2176
+ "image_generation.comfyui.nodes",
2177
+ [],
2178
+ )
2179
+
2180
+ IMAGES_OPENAI_API_BASE_URL = PersistentConfig(
2181
+ "IMAGES_OPENAI_API_BASE_URL",
2182
+ "image_generation.openai.api_base_url",
2183
+ os.getenv("IMAGES_OPENAI_API_BASE_URL", OPENAI_API_BASE_URL),
2184
+ )
2185
+ IMAGES_OPENAI_API_KEY = PersistentConfig(
2186
+ "IMAGES_OPENAI_API_KEY",
2187
+ "image_generation.openai.api_key",
2188
+ os.getenv("IMAGES_OPENAI_API_KEY", OPENAI_API_KEY),
2189
+ )
2190
+
2191
+ IMAGES_GEMINI_API_BASE_URL = PersistentConfig(
2192
+ "IMAGES_GEMINI_API_BASE_URL",
2193
+ "image_generation.gemini.api_base_url",
2194
+ os.getenv("IMAGES_GEMINI_API_BASE_URL", GEMINI_API_BASE_URL),
2195
+ )
2196
+ IMAGES_GEMINI_API_KEY = PersistentConfig(
2197
+ "IMAGES_GEMINI_API_KEY",
2198
+ "image_generation.gemini.api_key",
2199
+ os.getenv("IMAGES_GEMINI_API_KEY", GEMINI_API_KEY),
2200
+ )
2201
+
2202
+ IMAGE_SIZE = PersistentConfig(
2203
+ "IMAGE_SIZE", "image_generation.size", os.getenv("IMAGE_SIZE", "512x512")
2204
+ )
2205
+
2206
+ IMAGE_STEPS = PersistentConfig(
2207
+ "IMAGE_STEPS", "image_generation.steps", int(os.getenv("IMAGE_STEPS", 50))
2208
+ )
2209
+
2210
+ IMAGE_GENERATION_MODEL = PersistentConfig(
2211
+ "IMAGE_GENERATION_MODEL",
2212
+ "image_generation.model",
2213
+ os.getenv("IMAGE_GENERATION_MODEL", ""),
2214
+ )
2215
+
2216
+ ####################################
2217
+ # Audio
2218
+ ####################################
2219
+
2220
+ # Transcription
2221
+ WHISPER_MODEL = PersistentConfig(
2222
+ "WHISPER_MODEL",
2223
+ "audio.stt.whisper_model",
2224
+ os.getenv("WHISPER_MODEL", "base"),
2225
+ )
2226
+
2227
+ WHISPER_MODEL_DIR = os.getenv("WHISPER_MODEL_DIR", f"{CACHE_DIR}/whisper/models")
2228
+ WHISPER_MODEL_AUTO_UPDATE = (
2229
+ not OFFLINE_MODE
2230
+ and os.environ.get("WHISPER_MODEL_AUTO_UPDATE", "").lower() == "true"
2231
+ )
2232
+
2233
+ # Add Deepgram configuration
2234
+ DEEPGRAM_API_KEY = PersistentConfig(
2235
+ "DEEPGRAM_API_KEY",
2236
+ "audio.stt.deepgram.api_key",
2237
+ os.getenv("DEEPGRAM_API_KEY", ""),
2238
+ )
2239
+
2240
+ AUDIO_STT_OPENAI_API_BASE_URL = PersistentConfig(
2241
+ "AUDIO_STT_OPENAI_API_BASE_URL",
2242
+ "audio.stt.openai.api_base_url",
2243
+ os.getenv("AUDIO_STT_OPENAI_API_BASE_URL", OPENAI_API_BASE_URL),
2244
+ )
2245
+
2246
+ AUDIO_STT_OPENAI_API_KEY = PersistentConfig(
2247
+ "AUDIO_STT_OPENAI_API_KEY",
2248
+ "audio.stt.openai.api_key",
2249
+ os.getenv("AUDIO_STT_OPENAI_API_KEY", OPENAI_API_KEY),
2250
+ )
2251
+
2252
+ AUDIO_STT_ENGINE = PersistentConfig(
2253
+ "AUDIO_STT_ENGINE",
2254
+ "audio.stt.engine",
2255
+ os.getenv("AUDIO_STT_ENGINE", ""),
2256
+ )
2257
+
2258
+ AUDIO_STT_MODEL = PersistentConfig(
2259
+ "AUDIO_STT_MODEL",
2260
+ "audio.stt.model",
2261
+ os.getenv("AUDIO_STT_MODEL", ""),
2262
+ )
2263
+
2264
+ AUDIO_TTS_OPENAI_API_BASE_URL = PersistentConfig(
2265
+ "AUDIO_TTS_OPENAI_API_BASE_URL",
2266
+ "audio.tts.openai.api_base_url",
2267
+ os.getenv("AUDIO_TTS_OPENAI_API_BASE_URL", OPENAI_API_BASE_URL),
2268
+ )
2269
+ AUDIO_TTS_OPENAI_API_KEY = PersistentConfig(
2270
+ "AUDIO_TTS_OPENAI_API_KEY",
2271
+ "audio.tts.openai.api_key",
2272
+ os.getenv("AUDIO_TTS_OPENAI_API_KEY", OPENAI_API_KEY),
2273
+ )
2274
+
2275
+ AUDIO_TTS_API_KEY = PersistentConfig(
2276
+ "AUDIO_TTS_API_KEY",
2277
+ "audio.tts.api_key",
2278
+ os.getenv("AUDIO_TTS_API_KEY", ""),
2279
+ )
2280
+
2281
+ AUDIO_TTS_ENGINE = PersistentConfig(
2282
+ "AUDIO_TTS_ENGINE",
2283
+ "audio.tts.engine",
2284
+ os.getenv("AUDIO_TTS_ENGINE", ""),
2285
+ )
2286
+
2287
+
2288
+ AUDIO_TTS_MODEL = PersistentConfig(
2289
+ "AUDIO_TTS_MODEL",
2290
+ "audio.tts.model",
2291
+ os.getenv("AUDIO_TTS_MODEL", "tts-1"), # OpenAI default model
2292
+ )
2293
+
2294
+ AUDIO_TTS_VOICE = PersistentConfig(
2295
+ "AUDIO_TTS_VOICE",
2296
+ "audio.tts.voice",
2297
+ os.getenv("AUDIO_TTS_VOICE", "alloy"), # OpenAI default voice
2298
+ )
2299
+
2300
+ AUDIO_TTS_SPLIT_ON = PersistentConfig(
2301
+ "AUDIO_TTS_SPLIT_ON",
2302
+ "audio.tts.split_on",
2303
+ os.getenv("AUDIO_TTS_SPLIT_ON", "punctuation"),
2304
+ )
2305
+
2306
+ AUDIO_TTS_AZURE_SPEECH_REGION = PersistentConfig(
2307
+ "AUDIO_TTS_AZURE_SPEECH_REGION",
2308
+ "audio.tts.azure.speech_region",
2309
+ os.getenv("AUDIO_TTS_AZURE_SPEECH_REGION", "eastus"),
2310
+ )
2311
+
2312
+ AUDIO_TTS_AZURE_SPEECH_OUTPUT_FORMAT = PersistentConfig(
2313
+ "AUDIO_TTS_AZURE_SPEECH_OUTPUT_FORMAT",
2314
+ "audio.tts.azure.speech_output_format",
2315
+ os.getenv(
2316
+ "AUDIO_TTS_AZURE_SPEECH_OUTPUT_FORMAT", "audio-24khz-160kbitrate-mono-mp3"
2317
+ ),
2318
+ )
2319
+
2320
+
2321
+ ####################################
2322
+ # LDAP
2323
+ ####################################
2324
+
2325
+ ENABLE_LDAP = PersistentConfig(
2326
+ "ENABLE_LDAP",
2327
+ "ldap.enable",
2328
+ os.environ.get("ENABLE_LDAP", "false").lower() == "true",
2329
+ )
2330
+
2331
+ LDAP_SERVER_LABEL = PersistentConfig(
2332
+ "LDAP_SERVER_LABEL",
2333
+ "ldap.server.label",
2334
+ os.environ.get("LDAP_SERVER_LABEL", "LDAP Server"),
2335
+ )
2336
+
2337
+ LDAP_SERVER_HOST = PersistentConfig(
2338
+ "LDAP_SERVER_HOST",
2339
+ "ldap.server.host",
2340
+ os.environ.get("LDAP_SERVER_HOST", "localhost"),
2341
+ )
2342
+
2343
+ LDAP_SERVER_PORT = PersistentConfig(
2344
+ "LDAP_SERVER_PORT",
2345
+ "ldap.server.port",
2346
+ int(os.environ.get("LDAP_SERVER_PORT", "389")),
2347
+ )
2348
+
2349
+ LDAP_ATTRIBUTE_FOR_MAIL = PersistentConfig(
2350
+ "LDAP_ATTRIBUTE_FOR_MAIL",
2351
+ "ldap.server.attribute_for_mail",
2352
+ os.environ.get("LDAP_ATTRIBUTE_FOR_MAIL", "mail"),
2353
+ )
2354
+
2355
+ LDAP_ATTRIBUTE_FOR_USERNAME = PersistentConfig(
2356
+ "LDAP_ATTRIBUTE_FOR_USERNAME",
2357
+ "ldap.server.attribute_for_username",
2358
+ os.environ.get("LDAP_ATTRIBUTE_FOR_USERNAME", "uid"),
2359
+ )
2360
+
2361
+ LDAP_APP_DN = PersistentConfig(
2362
+ "LDAP_APP_DN", "ldap.server.app_dn", os.environ.get("LDAP_APP_DN", "")
2363
+ )
2364
+
2365
+ LDAP_APP_PASSWORD = PersistentConfig(
2366
+ "LDAP_APP_PASSWORD",
2367
+ "ldap.server.app_password",
2368
+ os.environ.get("LDAP_APP_PASSWORD", ""),
2369
+ )
2370
+
2371
+ LDAP_SEARCH_BASE = PersistentConfig(
2372
+ "LDAP_SEARCH_BASE", "ldap.server.users_dn", os.environ.get("LDAP_SEARCH_BASE", "")
2373
+ )
2374
+
2375
+ LDAP_SEARCH_FILTERS = PersistentConfig(
2376
+ "LDAP_SEARCH_FILTER",
2377
+ "ldap.server.search_filter",
2378
+ os.environ.get("LDAP_SEARCH_FILTER", ""),
2379
+ )
2380
+
2381
+ LDAP_USE_TLS = PersistentConfig(
2382
+ "LDAP_USE_TLS",
2383
+ "ldap.server.use_tls",
2384
+ os.environ.get("LDAP_USE_TLS", "True").lower() == "true",
2385
+ )
2386
+
2387
+ LDAP_CA_CERT_FILE = PersistentConfig(
2388
+ "LDAP_CA_CERT_FILE",
2389
+ "ldap.server.ca_cert_file",
2390
+ os.environ.get("LDAP_CA_CERT_FILE", ""),
2391
+ )
2392
+
2393
+ LDAP_CIPHERS = PersistentConfig(
2394
+ "LDAP_CIPHERS", "ldap.server.ciphers", os.environ.get("LDAP_CIPHERS", "ALL")
2395
+ )
backend/open_webui/constants.py ADDED
@@ -0,0 +1,119 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from enum import Enum
2
+
3
+
4
+ class MESSAGES(str, Enum):
5
+ DEFAULT = lambda msg="": f"{msg if msg else ''}"
6
+ MODEL_ADDED = lambda model="": f"The model '{model}' has been added successfully."
7
+ MODEL_DELETED = (
8
+ lambda model="": f"The model '{model}' has been deleted successfully."
9
+ )
10
+
11
+
12
+ class WEBHOOK_MESSAGES(str, Enum):
13
+ DEFAULT = lambda msg="": f"{msg if msg else ''}"
14
+ USER_SIGNUP = lambda username="": (
15
+ f"New user signed up: {username}" if username else "New user signed up"
16
+ )
17
+
18
+
19
+ class ERROR_MESSAGES(str, Enum):
20
+ def __str__(self) -> str:
21
+ return super().__str__()
22
+
23
+ DEFAULT = (
24
+ lambda err="": f'{"Something went wrong :/" if err == "" else "[ERROR: " + str(err) + "]"}'
25
+ )
26
+ ENV_VAR_NOT_FOUND = "Required environment variable not found. Terminating now."
27
+ CREATE_USER_ERROR = "Oops! Something went wrong while creating your account. Please try again later. If the issue persists, contact support for assistance."
28
+ DELETE_USER_ERROR = "Oops! Something went wrong. We encountered an issue while trying to delete the user. Please give it another shot."
29
+ EMAIL_MISMATCH = "Uh-oh! This email does not match the email your provider is registered with. Please check your email and try again."
30
+ EMAIL_TAKEN = "Uh-oh! This email is already registered. Sign in with your existing account or choose another email to start anew."
31
+ USERNAME_TAKEN = (
32
+ "Uh-oh! This username is already registered. Please choose another username."
33
+ )
34
+ COMMAND_TAKEN = "Uh-oh! This command is already registered. Please choose another command string."
35
+ FILE_EXISTS = "Uh-oh! This file is already registered. Please choose another file."
36
+
37
+ ID_TAKEN = "Uh-oh! This id is already registered. Please choose another id string."
38
+ MODEL_ID_TAKEN = "Uh-oh! This model id is already registered. Please choose another model id string."
39
+ NAME_TAG_TAKEN = "Uh-oh! This name tag is already registered. Please choose another name tag string."
40
+
41
+ INVALID_TOKEN = (
42
+ "Your session has expired or the token is invalid. Please sign in again."
43
+ )
44
+ INVALID_CRED = "The email or password provided is incorrect. Please check for typos and try logging in again."
45
+ INVALID_EMAIL_FORMAT = "The email format you entered is invalid. Please double-check and make sure you're using a valid email address (e.g., [email protected])."
46
+ INVALID_PASSWORD = (
47
+ "The password provided is incorrect. Please check for typos and try again."
48
+ )
49
+ INVALID_TRUSTED_HEADER = "Your provider has not provided a trusted header. Please contact your administrator for assistance."
50
+
51
+ EXISTING_USERS = "You can't turn off authentication because there are existing users. If you want to disable WEBUI_AUTH, make sure your web interface doesn't have any existing users and is a fresh installation."
52
+
53
+ UNAUTHORIZED = "401 Unauthorized"
54
+ ACCESS_PROHIBITED = "You do not have permission to access this resource. Please contact your administrator for assistance."
55
+ ACTION_PROHIBITED = (
56
+ "The requested action has been restricted as a security measure."
57
+ )
58
+
59
+ FILE_NOT_SENT = "FILE_NOT_SENT"
60
+ FILE_NOT_SUPPORTED = "Oops! It seems like the file format you're trying to upload is not supported. Please upload a file with a supported format and try again."
61
+
62
+ NOT_FOUND = "We could not find what you're looking for :/"
63
+ USER_NOT_FOUND = "We could not find what you're looking for :/"
64
+ API_KEY_NOT_FOUND = "Oops! It looks like there's a hiccup. The API key is missing. Please make sure to provide a valid API key to access this feature."
65
+ API_KEY_NOT_ALLOWED = "Use of API key is not enabled in the environment."
66
+
67
+ MALICIOUS = "Unusual activities detected, please try again in a few minutes."
68
+
69
+ PANDOC_NOT_INSTALLED = "Pandoc is not installed on the server. Please contact your administrator for assistance."
70
+ INCORRECT_FORMAT = (
71
+ lambda err="": f"Invalid format. Please use the correct format{err}"
72
+ )
73
+ RATE_LIMIT_EXCEEDED = "API rate limit exceeded"
74
+
75
+ MODEL_NOT_FOUND = lambda name="": f"Model '{name}' was not found"
76
+ OPENAI_NOT_FOUND = lambda name="": "OpenAI API was not found"
77
+ OLLAMA_NOT_FOUND = "WebUI could not connect to Ollama"
78
+ CREATE_API_KEY_ERROR = "Oops! Something went wrong while creating your API key. Please try again later. If the issue persists, contact support for assistance."
79
+ API_KEY_CREATION_NOT_ALLOWED = "API key creation is not allowed in the environment."
80
+
81
+ EMPTY_CONTENT = "The content provided is empty. Please ensure that there is text or data present before proceeding."
82
+
83
+ DB_NOT_SQLITE = "This feature is only available when running with SQLite databases."
84
+
85
+ INVALID_URL = (
86
+ "Oops! The URL you provided is invalid. Please double-check and try again."
87
+ )
88
+
89
+ WEB_SEARCH_ERROR = (
90
+ lambda err="": f"{err if err else 'Oops! Something went wrong while searching the web.'}"
91
+ )
92
+
93
+ OLLAMA_API_DISABLED = (
94
+ "The Ollama API is disabled. Please enable it to use this feature."
95
+ )
96
+
97
+ FILE_TOO_LARGE = (
98
+ lambda size="": f"Oops! The file you're trying to upload is too large. Please upload a file that is less than {size}."
99
+ )
100
+
101
+ DUPLICATE_CONTENT = (
102
+ "Duplicate content detected. Please provide unique content to proceed."
103
+ )
104
+ FILE_NOT_PROCESSED = "Extracted content is not available for this file. Please ensure that the file is processed before proceeding."
105
+
106
+
107
+ class TASKS(str, Enum):
108
+ def __str__(self) -> str:
109
+ return super().__str__()
110
+
111
+ DEFAULT = lambda task="": f"{task if task else 'generation'}"
112
+ TITLE_GENERATION = "title_generation"
113
+ TAGS_GENERATION = "tags_generation"
114
+ EMOJI_GENERATION = "emoji_generation"
115
+ QUERY_GENERATION = "query_generation"
116
+ IMAGE_PROMPT_GENERATION = "image_prompt_generation"
117
+ AUTOCOMPLETE_GENERATION = "autocomplete_generation"
118
+ FUNCTION_CALLING = "function_calling"
119
+ MOA_RESPONSE_GENERATION = "moa_response_generation"
backend/open_webui/env.py ADDED
@@ -0,0 +1,421 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import importlib.metadata
2
+ import json
3
+ import logging
4
+ import os
5
+ import pkgutil
6
+ import sys
7
+ import shutil
8
+ from pathlib import Path
9
+
10
+ import markdown
11
+ from bs4 import BeautifulSoup
12
+ from open_webui.constants import ERROR_MESSAGES
13
+
14
+ ####################################
15
+ # Load .env file
16
+ ####################################
17
+
18
+ OPEN_WEBUI_DIR = Path(__file__).parent # the path containing this file
19
+ print(OPEN_WEBUI_DIR)
20
+
21
+ BACKEND_DIR = OPEN_WEBUI_DIR.parent # the path containing this file
22
+ BASE_DIR = BACKEND_DIR.parent # the path containing the backend/
23
+
24
+ print(BACKEND_DIR)
25
+ print(BASE_DIR)
26
+
27
+ try:
28
+ from dotenv import find_dotenv, load_dotenv
29
+
30
+ load_dotenv(find_dotenv(str(BASE_DIR / ".env")))
31
+ except ImportError:
32
+ print("dotenv not installed, skipping...")
33
+
34
+ DOCKER = os.environ.get("DOCKER", "False").lower() == "true"
35
+
36
+ # device type embedding models - "cpu" (default), "cuda" (nvidia gpu required) or "mps" (apple silicon) - choosing this right can lead to better performance
37
+ USE_CUDA = os.environ.get("USE_CUDA_DOCKER", "false")
38
+
39
+ if USE_CUDA.lower() == "true":
40
+ try:
41
+ import torch
42
+
43
+ assert torch.cuda.is_available(), "CUDA not available"
44
+ DEVICE_TYPE = "cuda"
45
+ except Exception as e:
46
+ cuda_error = (
47
+ "Error when testing CUDA but USE_CUDA_DOCKER is true. "
48
+ f"Resetting USE_CUDA_DOCKER to false: {e}"
49
+ )
50
+ os.environ["USE_CUDA_DOCKER"] = "false"
51
+ USE_CUDA = "false"
52
+ DEVICE_TYPE = "cpu"
53
+ else:
54
+ DEVICE_TYPE = "cpu"
55
+
56
+ try:
57
+ import torch
58
+
59
+ if torch.backends.mps.is_available() and torch.backends.mps.is_built():
60
+ DEVICE_TYPE = "mps"
61
+ except Exception:
62
+ pass
63
+
64
+ ####################################
65
+ # LOGGING
66
+ ####################################
67
+
68
+ log_levels = ["CRITICAL", "ERROR", "WARNING", "INFO", "DEBUG"]
69
+
70
+ GLOBAL_LOG_LEVEL = os.environ.get("GLOBAL_LOG_LEVEL", "").upper()
71
+ if GLOBAL_LOG_LEVEL in log_levels:
72
+ logging.basicConfig(stream=sys.stdout, level=GLOBAL_LOG_LEVEL, force=True)
73
+ else:
74
+ GLOBAL_LOG_LEVEL = "INFO"
75
+
76
+ log = logging.getLogger(__name__)
77
+ log.info(f"GLOBAL_LOG_LEVEL: {GLOBAL_LOG_LEVEL}")
78
+
79
+ if "cuda_error" in locals():
80
+ log.exception(cuda_error)
81
+
82
+ log_sources = [
83
+ "AUDIO",
84
+ "COMFYUI",
85
+ "CONFIG",
86
+ "DB",
87
+ "IMAGES",
88
+ "MAIN",
89
+ "MODELS",
90
+ "OLLAMA",
91
+ "OPENAI",
92
+ "RAG",
93
+ "WEBHOOK",
94
+ "SOCKET",
95
+ "OAUTH",
96
+ ]
97
+
98
+ SRC_LOG_LEVELS = {}
99
+
100
+ for source in log_sources:
101
+ log_env_var = source + "_LOG_LEVEL"
102
+ SRC_LOG_LEVELS[source] = os.environ.get(log_env_var, "").upper()
103
+ if SRC_LOG_LEVELS[source] not in log_levels:
104
+ SRC_LOG_LEVELS[source] = GLOBAL_LOG_LEVEL
105
+ log.info(f"{log_env_var}: {SRC_LOG_LEVELS[source]}")
106
+
107
+ log.setLevel(SRC_LOG_LEVELS["CONFIG"])
108
+
109
+
110
+ WEBUI_NAME = os.environ.get("WEBUI_NAME", "Open WebUI")
111
+ if WEBUI_NAME != "Open WebUI":
112
+ WEBUI_NAME += " (Open WebUI)"
113
+
114
+ WEBUI_FAVICON_URL = "https://openwebui.com/favicon.png"
115
+
116
+ TRUSTED_SIGNATURE_KEY = os.environ.get("TRUSTED_SIGNATURE_KEY", "")
117
+
118
+ ####################################
119
+ # ENV (dev,test,prod)
120
+ ####################################
121
+
122
+ ENV = os.environ.get("ENV", "dev")
123
+
124
+ FROM_INIT_PY = os.environ.get("FROM_INIT_PY", "False").lower() == "true"
125
+
126
+ if FROM_INIT_PY:
127
+ PACKAGE_DATA = {"version": importlib.metadata.version("open-webui")}
128
+ else:
129
+ try:
130
+ PACKAGE_DATA = json.loads((BASE_DIR / "package.json").read_text())
131
+ except Exception:
132
+ PACKAGE_DATA = {"version": "0.0.0"}
133
+
134
+
135
+ VERSION = PACKAGE_DATA["version"]
136
+
137
+
138
+ # Function to parse each section
139
+ def parse_section(section):
140
+ items = []
141
+ for li in section.find_all("li"):
142
+ # Extract raw HTML string
143
+ raw_html = str(li)
144
+
145
+ # Extract text without HTML tags
146
+ text = li.get_text(separator=" ", strip=True)
147
+
148
+ # Split into title and content
149
+ parts = text.split(": ", 1)
150
+ title = parts[0].strip() if len(parts) > 1 else ""
151
+ content = parts[1].strip() if len(parts) > 1 else text
152
+
153
+ items.append({"title": title, "content": content, "raw": raw_html})
154
+ return items
155
+
156
+
157
+ try:
158
+ changelog_path = BASE_DIR / "CHANGELOG.md"
159
+ with open(str(changelog_path.absolute()), "r", encoding="utf8") as file:
160
+ changelog_content = file.read()
161
+
162
+ except Exception:
163
+ changelog_content = (pkgutil.get_data("open_webui", "CHANGELOG.md") or b"").decode()
164
+
165
+
166
+ # Convert markdown content to HTML
167
+ html_content = markdown.markdown(changelog_content)
168
+
169
+ # Parse the HTML content
170
+ soup = BeautifulSoup(html_content, "html.parser")
171
+
172
+ # Initialize JSON structure
173
+ changelog_json = {}
174
+
175
+ # Iterate over each version
176
+ for version in soup.find_all("h2"):
177
+ version_number = version.get_text().strip().split(" - ")[0][1:-1] # Remove brackets
178
+ date = version.get_text().strip().split(" - ")[1]
179
+
180
+ version_data = {"date": date}
181
+
182
+ # Find the next sibling that is a h3 tag (section title)
183
+ current = version.find_next_sibling()
184
+
185
+ while current and current.name != "h2":
186
+ if current.name == "h3":
187
+ section_title = current.get_text().lower() # e.g., "added", "fixed"
188
+ section_items = parse_section(current.find_next_sibling("ul"))
189
+ version_data[section_title] = section_items
190
+
191
+ # Move to the next element
192
+ current = current.find_next_sibling()
193
+
194
+ changelog_json[version_number] = version_data
195
+
196
+
197
+ CHANGELOG = changelog_json
198
+
199
+ ####################################
200
+ # SAFE_MODE
201
+ ####################################
202
+
203
+ SAFE_MODE = os.environ.get("SAFE_MODE", "false").lower() == "true"
204
+
205
+ ####################################
206
+ # ENABLE_FORWARD_USER_INFO_HEADERS
207
+ ####################################
208
+
209
+ ENABLE_FORWARD_USER_INFO_HEADERS = (
210
+ os.environ.get("ENABLE_FORWARD_USER_INFO_HEADERS", "False").lower() == "true"
211
+ )
212
+
213
+
214
+ ####################################
215
+ # WEBUI_BUILD_HASH
216
+ ####################################
217
+
218
+ WEBUI_BUILD_HASH = os.environ.get("WEBUI_BUILD_HASH", "dev-build")
219
+
220
+ ####################################
221
+ # DATA/FRONTEND BUILD DIR
222
+ ####################################
223
+
224
+ DATA_DIR = Path(os.getenv("DATA_DIR", BACKEND_DIR / "data")).resolve()
225
+
226
+ if FROM_INIT_PY:
227
+ NEW_DATA_DIR = Path(os.getenv("DATA_DIR", OPEN_WEBUI_DIR / "data")).resolve()
228
+ NEW_DATA_DIR.mkdir(parents=True, exist_ok=True)
229
+
230
+ # Check if the data directory exists in the package directory
231
+ if DATA_DIR.exists() and DATA_DIR != NEW_DATA_DIR:
232
+ log.info(f"Moving {DATA_DIR} to {NEW_DATA_DIR}")
233
+ for item in DATA_DIR.iterdir():
234
+ dest = NEW_DATA_DIR / item.name
235
+ if item.is_dir():
236
+ shutil.copytree(item, dest, dirs_exist_ok=True)
237
+ else:
238
+ shutil.copy2(item, dest)
239
+
240
+ # Zip the data directory
241
+ shutil.make_archive(DATA_DIR.parent / "open_webui_data", "zip", DATA_DIR)
242
+
243
+ # Remove the old data directory
244
+ shutil.rmtree(DATA_DIR)
245
+
246
+ DATA_DIR = Path(os.getenv("DATA_DIR", OPEN_WEBUI_DIR / "data"))
247
+
248
+
249
+ STATIC_DIR = Path(os.getenv("STATIC_DIR", OPEN_WEBUI_DIR / "static"))
250
+
251
+ FONTS_DIR = Path(os.getenv("FONTS_DIR", OPEN_WEBUI_DIR / "static" / "fonts"))
252
+
253
+ FRONTEND_BUILD_DIR = Path(os.getenv("FRONTEND_BUILD_DIR", BASE_DIR / "build")).resolve()
254
+
255
+ if FROM_INIT_PY:
256
+ FRONTEND_BUILD_DIR = Path(
257
+ os.getenv("FRONTEND_BUILD_DIR", OPEN_WEBUI_DIR / "frontend")
258
+ ).resolve()
259
+
260
+
261
+ ####################################
262
+ # Database
263
+ ####################################
264
+
265
+ # Check if the file exists
266
+ if os.path.exists(f"{DATA_DIR}/ollama.db"):
267
+ # Rename the file
268
+ os.rename(f"{DATA_DIR}/ollama.db", f"{DATA_DIR}/webui.db")
269
+ log.info("Database migrated from Ollama-WebUI successfully.")
270
+ else:
271
+ pass
272
+
273
+ DATABASE_URL = os.environ.get("DATABASE_URL", f"sqlite:///{DATA_DIR}/webui.db")
274
+
275
+ # Replace the postgres:// with postgresql://
276
+ if "postgres://" in DATABASE_URL:
277
+ DATABASE_URL = DATABASE_URL.replace("postgres://", "postgresql://")
278
+
279
+ DATABASE_SCHEMA = os.environ.get("DATABASE_SCHEMA", None)
280
+
281
+ DATABASE_POOL_SIZE = os.environ.get("DATABASE_POOL_SIZE", 0)
282
+
283
+ if DATABASE_POOL_SIZE == "":
284
+ DATABASE_POOL_SIZE = 0
285
+ else:
286
+ try:
287
+ DATABASE_POOL_SIZE = int(DATABASE_POOL_SIZE)
288
+ except Exception:
289
+ DATABASE_POOL_SIZE = 0
290
+
291
+ DATABASE_POOL_MAX_OVERFLOW = os.environ.get("DATABASE_POOL_MAX_OVERFLOW", 0)
292
+
293
+ if DATABASE_POOL_MAX_OVERFLOW == "":
294
+ DATABASE_POOL_MAX_OVERFLOW = 0
295
+ else:
296
+ try:
297
+ DATABASE_POOL_MAX_OVERFLOW = int(DATABASE_POOL_MAX_OVERFLOW)
298
+ except Exception:
299
+ DATABASE_POOL_MAX_OVERFLOW = 0
300
+
301
+ DATABASE_POOL_TIMEOUT = os.environ.get("DATABASE_POOL_TIMEOUT", 30)
302
+
303
+ if DATABASE_POOL_TIMEOUT == "":
304
+ DATABASE_POOL_TIMEOUT = 30
305
+ else:
306
+ try:
307
+ DATABASE_POOL_TIMEOUT = int(DATABASE_POOL_TIMEOUT)
308
+ except Exception:
309
+ DATABASE_POOL_TIMEOUT = 30
310
+
311
+ DATABASE_POOL_RECYCLE = os.environ.get("DATABASE_POOL_RECYCLE", 3600)
312
+
313
+ if DATABASE_POOL_RECYCLE == "":
314
+ DATABASE_POOL_RECYCLE = 3600
315
+ else:
316
+ try:
317
+ DATABASE_POOL_RECYCLE = int(DATABASE_POOL_RECYCLE)
318
+ except Exception:
319
+ DATABASE_POOL_RECYCLE = 3600
320
+
321
+ RESET_CONFIG_ON_START = (
322
+ os.environ.get("RESET_CONFIG_ON_START", "False").lower() == "true"
323
+ )
324
+
325
+
326
+ ENABLE_REALTIME_CHAT_SAVE = (
327
+ os.environ.get("ENABLE_REALTIME_CHAT_SAVE", "False").lower() == "true"
328
+ )
329
+
330
+ ####################################
331
+ # REDIS
332
+ ####################################
333
+
334
+ REDIS_URL = os.environ.get("REDIS_URL", "redis://localhost:6379/0")
335
+
336
+ ####################################
337
+ # WEBUI_AUTH (Required for security)
338
+ ####################################
339
+
340
+ WEBUI_AUTH = os.environ.get("WEBUI_AUTH", "True").lower() == "true"
341
+ WEBUI_AUTH_TRUSTED_EMAIL_HEADER = os.environ.get(
342
+ "WEBUI_AUTH_TRUSTED_EMAIL_HEADER", None
343
+ )
344
+ WEBUI_AUTH_TRUSTED_NAME_HEADER = os.environ.get("WEBUI_AUTH_TRUSTED_NAME_HEADER", None)
345
+
346
+ BYPASS_MODEL_ACCESS_CONTROL = (
347
+ os.environ.get("BYPASS_MODEL_ACCESS_CONTROL", "False").lower() == "true"
348
+ )
349
+
350
+ ####################################
351
+ # WEBUI_SECRET_KEY
352
+ ####################################
353
+
354
+ WEBUI_SECRET_KEY = os.environ.get(
355
+ "WEBUI_SECRET_KEY",
356
+ os.environ.get(
357
+ "WEBUI_JWT_SECRET_KEY", "t0p-s3cr3t"
358
+ ), # DEPRECATED: remove at next major version
359
+ )
360
+
361
+ WEBUI_SESSION_COOKIE_SAME_SITE = os.environ.get("WEBUI_SESSION_COOKIE_SAME_SITE", "lax")
362
+
363
+ WEBUI_SESSION_COOKIE_SECURE = (
364
+ os.environ.get("WEBUI_SESSION_COOKIE_SECURE", "false").lower() == "true"
365
+ )
366
+
367
+ WEBUI_AUTH_COOKIE_SAME_SITE = os.environ.get(
368
+ "WEBUI_AUTH_COOKIE_SAME_SITE", WEBUI_SESSION_COOKIE_SAME_SITE
369
+ )
370
+
371
+ WEBUI_AUTH_COOKIE_SECURE = (
372
+ os.environ.get(
373
+ "WEBUI_AUTH_COOKIE_SECURE",
374
+ os.environ.get("WEBUI_SESSION_COOKIE_SECURE", "false"),
375
+ ).lower()
376
+ == "true"
377
+ )
378
+
379
+ if WEBUI_AUTH and WEBUI_SECRET_KEY == "":
380
+ raise ValueError(ERROR_MESSAGES.ENV_VAR_NOT_FOUND)
381
+
382
+ ENABLE_WEBSOCKET_SUPPORT = (
383
+ os.environ.get("ENABLE_WEBSOCKET_SUPPORT", "True").lower() == "true"
384
+ )
385
+
386
+ WEBSOCKET_MANAGER = os.environ.get("WEBSOCKET_MANAGER", "")
387
+
388
+ WEBSOCKET_REDIS_URL = os.environ.get("WEBSOCKET_REDIS_URL", REDIS_URL)
389
+
390
+ AIOHTTP_CLIENT_TIMEOUT = os.environ.get("AIOHTTP_CLIENT_TIMEOUT", "")
391
+
392
+ if AIOHTTP_CLIENT_TIMEOUT == "":
393
+ AIOHTTP_CLIENT_TIMEOUT = None
394
+ else:
395
+ try:
396
+ AIOHTTP_CLIENT_TIMEOUT = int(AIOHTTP_CLIENT_TIMEOUT)
397
+ except Exception:
398
+ AIOHTTP_CLIENT_TIMEOUT = 300
399
+
400
+ AIOHTTP_CLIENT_TIMEOUT_OPENAI_MODEL_LIST = os.environ.get(
401
+ "AIOHTTP_CLIENT_TIMEOUT_OPENAI_MODEL_LIST", ""
402
+ )
403
+
404
+ if AIOHTTP_CLIENT_TIMEOUT_OPENAI_MODEL_LIST == "":
405
+ AIOHTTP_CLIENT_TIMEOUT_OPENAI_MODEL_LIST = None
406
+ else:
407
+ try:
408
+ AIOHTTP_CLIENT_TIMEOUT_OPENAI_MODEL_LIST = int(
409
+ AIOHTTP_CLIENT_TIMEOUT_OPENAI_MODEL_LIST
410
+ )
411
+ except Exception:
412
+ AIOHTTP_CLIENT_TIMEOUT_OPENAI_MODEL_LIST = 5
413
+
414
+ ####################################
415
+ # OFFLINE_MODE
416
+ ####################################
417
+
418
+ OFFLINE_MODE = os.environ.get("OFFLINE_MODE", "false").lower() == "true"
419
+
420
+ if OFFLINE_MODE:
421
+ os.environ["HF_HUB_OFFLINE"] = "1"
backend/open_webui/functions.py ADDED
@@ -0,0 +1,316 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import logging
2
+ import sys
3
+ import inspect
4
+ import json
5
+
6
+ from pydantic import BaseModel
7
+ from typing import AsyncGenerator, Generator, Iterator
8
+ from fastapi import (
9
+ Depends,
10
+ FastAPI,
11
+ File,
12
+ Form,
13
+ HTTPException,
14
+ Request,
15
+ UploadFile,
16
+ status,
17
+ )
18
+ from starlette.responses import Response, StreamingResponse
19
+
20
+
21
+ from open_webui.socket.main import (
22
+ get_event_call,
23
+ get_event_emitter,
24
+ )
25
+
26
+
27
+ from open_webui.models.functions import Functions
28
+ from open_webui.models.models import Models
29
+
30
+ from open_webui.utils.plugin import load_function_module_by_id
31
+ from open_webui.utils.tools import get_tools
32
+ from open_webui.utils.access_control import has_access
33
+
34
+ from open_webui.env import SRC_LOG_LEVELS, GLOBAL_LOG_LEVEL
35
+
36
+ from open_webui.utils.misc import (
37
+ add_or_update_system_message,
38
+ get_last_user_message,
39
+ prepend_to_first_user_message_content,
40
+ openai_chat_chunk_message_template,
41
+ openai_chat_completion_message_template,
42
+ )
43
+ from open_webui.utils.payload import (
44
+ apply_model_params_to_body_openai,
45
+ apply_model_system_prompt_to_body,
46
+ )
47
+
48
+
49
+ logging.basicConfig(stream=sys.stdout, level=GLOBAL_LOG_LEVEL)
50
+ log = logging.getLogger(__name__)
51
+ log.setLevel(SRC_LOG_LEVELS["MAIN"])
52
+
53
+
54
+ def get_function_module_by_id(request: Request, pipe_id: str):
55
+ # Check if function is already loaded
56
+ if pipe_id not in request.app.state.FUNCTIONS:
57
+ function_module, _, _ = load_function_module_by_id(pipe_id)
58
+ request.app.state.FUNCTIONS[pipe_id] = function_module
59
+ else:
60
+ function_module = request.app.state.FUNCTIONS[pipe_id]
61
+
62
+ if hasattr(function_module, "valves") and hasattr(function_module, "Valves"):
63
+ valves = Functions.get_function_valves_by_id(pipe_id)
64
+ function_module.valves = function_module.Valves(**(valves if valves else {}))
65
+ return function_module
66
+
67
+
68
+ async def get_function_models(request):
69
+ pipes = Functions.get_functions_by_type("pipe", active_only=True)
70
+ pipe_models = []
71
+
72
+ for pipe in pipes:
73
+ function_module = get_function_module_by_id(request, pipe.id)
74
+
75
+ # Check if function is a manifold
76
+ if hasattr(function_module, "pipes"):
77
+ sub_pipes = []
78
+
79
+ # Check if pipes is a function or a list
80
+
81
+ try:
82
+ if callable(function_module.pipes):
83
+ sub_pipes = function_module.pipes()
84
+ else:
85
+ sub_pipes = function_module.pipes
86
+ except Exception as e:
87
+ log.exception(e)
88
+ sub_pipes = []
89
+
90
+ log.debug(
91
+ f"get_function_models: function '{pipe.id}' is a manifold of {sub_pipes}"
92
+ )
93
+
94
+ for p in sub_pipes:
95
+ sub_pipe_id = f'{pipe.id}.{p["id"]}'
96
+ sub_pipe_name = p["name"]
97
+
98
+ if hasattr(function_module, "name"):
99
+ sub_pipe_name = f"{function_module.name}{sub_pipe_name}"
100
+
101
+ pipe_flag = {"type": pipe.type}
102
+
103
+ pipe_models.append(
104
+ {
105
+ "id": sub_pipe_id,
106
+ "name": sub_pipe_name,
107
+ "object": "model",
108
+ "created": pipe.created_at,
109
+ "owned_by": "openai",
110
+ "pipe": pipe_flag,
111
+ }
112
+ )
113
+ else:
114
+ pipe_flag = {"type": "pipe"}
115
+
116
+ log.debug(
117
+ f"get_function_models: function '{pipe.id}' is a single pipe {{ 'id': {pipe.id}, 'name': {pipe.name} }}"
118
+ )
119
+
120
+ pipe_models.append(
121
+ {
122
+ "id": pipe.id,
123
+ "name": pipe.name,
124
+ "object": "model",
125
+ "created": pipe.created_at,
126
+ "owned_by": "openai",
127
+ "pipe": pipe_flag,
128
+ }
129
+ )
130
+
131
+ return pipe_models
132
+
133
+
134
+ async def generate_function_chat_completion(
135
+ request, form_data, user, models: dict = {}
136
+ ):
137
+ async def execute_pipe(pipe, params):
138
+ if inspect.iscoroutinefunction(pipe):
139
+ return await pipe(**params)
140
+ else:
141
+ return pipe(**params)
142
+
143
+ async def get_message_content(res: str | Generator | AsyncGenerator) -> str:
144
+ if isinstance(res, str):
145
+ return res
146
+ if isinstance(res, Generator):
147
+ return "".join(map(str, res))
148
+ if isinstance(res, AsyncGenerator):
149
+ return "".join([str(stream) async for stream in res])
150
+
151
+ def process_line(form_data: dict, line):
152
+ if isinstance(line, BaseModel):
153
+ line = line.model_dump_json()
154
+ line = f"data: {line}"
155
+ if isinstance(line, dict):
156
+ line = f"data: {json.dumps(line)}"
157
+
158
+ try:
159
+ line = line.decode("utf-8")
160
+ except Exception:
161
+ pass
162
+
163
+ if line.startswith("data:"):
164
+ return f"{line}\n\n"
165
+ else:
166
+ line = openai_chat_chunk_message_template(form_data["model"], line)
167
+ return f"data: {json.dumps(line)}\n\n"
168
+
169
+ def get_pipe_id(form_data: dict) -> str:
170
+ pipe_id = form_data["model"]
171
+ if "." in pipe_id:
172
+ pipe_id, _ = pipe_id.split(".", 1)
173
+ return pipe_id
174
+
175
+ def get_function_params(function_module, form_data, user, extra_params=None):
176
+ if extra_params is None:
177
+ extra_params = {}
178
+
179
+ pipe_id = get_pipe_id(form_data)
180
+
181
+ # Get the signature of the function
182
+ sig = inspect.signature(function_module.pipe)
183
+ params = {"body": form_data} | {
184
+ k: v for k, v in extra_params.items() if k in sig.parameters
185
+ }
186
+
187
+ if "__user__" in params and hasattr(function_module, "UserValves"):
188
+ user_valves = Functions.get_user_valves_by_id_and_user_id(pipe_id, user.id)
189
+ try:
190
+ params["__user__"]["valves"] = function_module.UserValves(**user_valves)
191
+ except Exception as e:
192
+ log.exception(e)
193
+ params["__user__"]["valves"] = function_module.UserValves()
194
+
195
+ return params
196
+
197
+ model_id = form_data.get("model")
198
+ model_info = Models.get_model_by_id(model_id)
199
+
200
+ metadata = form_data.pop("metadata", {})
201
+
202
+ files = metadata.get("files", [])
203
+ tool_ids = metadata.get("tool_ids", [])
204
+ # Check if tool_ids is None
205
+ if tool_ids is None:
206
+ tool_ids = []
207
+
208
+ __event_emitter__ = None
209
+ __event_call__ = None
210
+ __task__ = None
211
+ __task_body__ = None
212
+
213
+ if metadata:
214
+ if all(k in metadata for k in ("session_id", "chat_id", "message_id")):
215
+ __event_emitter__ = get_event_emitter(metadata)
216
+ __event_call__ = get_event_call(metadata)
217
+ __task__ = metadata.get("task", None)
218
+ __task_body__ = metadata.get("task_body", None)
219
+
220
+ extra_params = {
221
+ "__event_emitter__": __event_emitter__,
222
+ "__event_call__": __event_call__,
223
+ "__task__": __task__,
224
+ "__task_body__": __task_body__,
225
+ "__files__": files,
226
+ "__user__": {
227
+ "id": user.id,
228
+ "email": user.email,
229
+ "name": user.name,
230
+ "role": user.role,
231
+ },
232
+ "__metadata__": metadata,
233
+ "__request__": request,
234
+ }
235
+ extra_params["__tools__"] = get_tools(
236
+ request,
237
+ tool_ids,
238
+ user,
239
+ {
240
+ **extra_params,
241
+ "__model__": models.get(form_data["model"], None),
242
+ "__messages__": form_data["messages"],
243
+ "__files__": files,
244
+ },
245
+ )
246
+
247
+ if model_info:
248
+ if model_info.base_model_id:
249
+ form_data["model"] = model_info.base_model_id
250
+
251
+ params = model_info.params.model_dump()
252
+ form_data = apply_model_params_to_body_openai(params, form_data)
253
+ form_data = apply_model_system_prompt_to_body(params, form_data, metadata, user)
254
+
255
+ pipe_id = get_pipe_id(form_data)
256
+ function_module = get_function_module_by_id(request, pipe_id)
257
+
258
+ pipe = function_module.pipe
259
+ params = get_function_params(function_module, form_data, user, extra_params)
260
+
261
+ if form_data.get("stream", False):
262
+
263
+ async def stream_content():
264
+ try:
265
+ res = await execute_pipe(pipe, params)
266
+
267
+ # Directly return if the response is a StreamingResponse
268
+ if isinstance(res, StreamingResponse):
269
+ async for data in res.body_iterator:
270
+ yield data
271
+ return
272
+ if isinstance(res, dict):
273
+ yield f"data: {json.dumps(res)}\n\n"
274
+ return
275
+
276
+ except Exception as e:
277
+ log.error(f"Error: {e}")
278
+ yield f"data: {json.dumps({'error': {'detail':str(e)}})}\n\n"
279
+ return
280
+
281
+ if isinstance(res, str):
282
+ message = openai_chat_chunk_message_template(form_data["model"], res)
283
+ yield f"data: {json.dumps(message)}\n\n"
284
+
285
+ if isinstance(res, Iterator):
286
+ for line in res:
287
+ yield process_line(form_data, line)
288
+
289
+ if isinstance(res, AsyncGenerator):
290
+ async for line in res:
291
+ yield process_line(form_data, line)
292
+
293
+ if isinstance(res, str) or isinstance(res, Generator):
294
+ finish_message = openai_chat_chunk_message_template(
295
+ form_data["model"], ""
296
+ )
297
+ finish_message["choices"][0]["finish_reason"] = "stop"
298
+ yield f"data: {json.dumps(finish_message)}\n\n"
299
+ yield "data: [DONE]"
300
+
301
+ return StreamingResponse(stream_content(), media_type="text/event-stream")
302
+ else:
303
+ try:
304
+ res = await execute_pipe(pipe, params)
305
+
306
+ except Exception as e:
307
+ log.error(f"Error: {e}")
308
+ return {"error": {"detail": str(e)}}
309
+
310
+ if isinstance(res, StreamingResponse) or isinstance(res, dict):
311
+ return res
312
+ if isinstance(res, BaseModel):
313
+ return res.model_dump()
314
+
315
+ message = await get_message_content(res)
316
+ return openai_chat_completion_message_template(form_data["model"], message)
backend/open_webui/internal/db.py ADDED
@@ -0,0 +1,116 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ import logging
3
+ from contextlib import contextmanager
4
+ from typing import Any, Optional
5
+
6
+ from open_webui.internal.wrappers import register_connection
7
+ from open_webui.env import (
8
+ OPEN_WEBUI_DIR,
9
+ DATABASE_URL,
10
+ DATABASE_SCHEMA,
11
+ SRC_LOG_LEVELS,
12
+ DATABASE_POOL_MAX_OVERFLOW,
13
+ DATABASE_POOL_RECYCLE,
14
+ DATABASE_POOL_SIZE,
15
+ DATABASE_POOL_TIMEOUT,
16
+ )
17
+ from peewee_migrate import Router
18
+ from sqlalchemy import Dialect, create_engine, MetaData, types
19
+ from sqlalchemy.ext.declarative import declarative_base
20
+ from sqlalchemy.orm import scoped_session, sessionmaker
21
+ from sqlalchemy.pool import QueuePool, NullPool
22
+ from sqlalchemy.sql.type_api import _T
23
+ from typing_extensions import Self
24
+
25
+ log = logging.getLogger(__name__)
26
+ log.setLevel(SRC_LOG_LEVELS["DB"])
27
+
28
+
29
+ class JSONField(types.TypeDecorator):
30
+ impl = types.Text
31
+ cache_ok = True
32
+
33
+ def process_bind_param(self, value: Optional[_T], dialect: Dialect) -> Any:
34
+ return json.dumps(value)
35
+
36
+ def process_result_value(self, value: Optional[_T], dialect: Dialect) -> Any:
37
+ if value is not None:
38
+ return json.loads(value)
39
+
40
+ def copy(self, **kw: Any) -> Self:
41
+ return JSONField(self.impl.length)
42
+
43
+ def db_value(self, value):
44
+ return json.dumps(value)
45
+
46
+ def python_value(self, value):
47
+ if value is not None:
48
+ return json.loads(value)
49
+
50
+
51
+ # Workaround to handle the peewee migration
52
+ # This is required to ensure the peewee migration is handled before the alembic migration
53
+ def handle_peewee_migration(DATABASE_URL):
54
+ # db = None
55
+ try:
56
+ # Replace the postgresql:// with postgres:// to handle the peewee migration
57
+ db = register_connection(DATABASE_URL.replace("postgresql://", "postgres://"))
58
+ migrate_dir = OPEN_WEBUI_DIR / "internal" / "migrations"
59
+ router = Router(db, logger=log, migrate_dir=migrate_dir)
60
+ router.run()
61
+ db.close()
62
+
63
+ except Exception as e:
64
+ log.error(f"Failed to initialize the database connection: {e}")
65
+ raise
66
+ finally:
67
+ # Properly closing the database connection
68
+ if db and not db.is_closed():
69
+ db.close()
70
+
71
+ # Assert if db connection has been closed
72
+ assert db.is_closed(), "Database connection is still open."
73
+
74
+
75
+ handle_peewee_migration(DATABASE_URL)
76
+
77
+
78
+ SQLALCHEMY_DATABASE_URL = DATABASE_URL
79
+ if "sqlite" in SQLALCHEMY_DATABASE_URL:
80
+ engine = create_engine(
81
+ SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False}
82
+ )
83
+ else:
84
+ if DATABASE_POOL_SIZE > 0:
85
+ engine = create_engine(
86
+ SQLALCHEMY_DATABASE_URL,
87
+ pool_size=DATABASE_POOL_SIZE,
88
+ max_overflow=DATABASE_POOL_MAX_OVERFLOW,
89
+ pool_timeout=DATABASE_POOL_TIMEOUT,
90
+ pool_recycle=DATABASE_POOL_RECYCLE,
91
+ pool_pre_ping=True,
92
+ poolclass=QueuePool,
93
+ )
94
+ else:
95
+ engine = create_engine(
96
+ SQLALCHEMY_DATABASE_URL, pool_pre_ping=True, poolclass=NullPool
97
+ )
98
+
99
+
100
+ SessionLocal = sessionmaker(
101
+ autocommit=False, autoflush=False, bind=engine, expire_on_commit=False
102
+ )
103
+ metadata_obj = MetaData(schema=DATABASE_SCHEMA)
104
+ Base = declarative_base(metadata=metadata_obj)
105
+ Session = scoped_session(SessionLocal)
106
+
107
+
108
+ def get_session():
109
+ db = SessionLocal()
110
+ try:
111
+ yield db
112
+ finally:
113
+ db.close()
114
+
115
+
116
+ get_db = contextmanager(get_session)
backend/open_webui/internal/migrations/001_initial_schema.py ADDED
@@ -0,0 +1,254 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Peewee migrations -- 001_initial_schema.py.
2
+
3
+ Some examples (model - class or model name)::
4
+
5
+ > Model = migrator.orm['table_name'] # Return model in current state by name
6
+ > Model = migrator.ModelClass # Return model in current state by name
7
+
8
+ > migrator.sql(sql) # Run custom SQL
9
+ > migrator.run(func, *args, **kwargs) # Run python function with the given args
10
+ > migrator.create_model(Model) # Create a model (could be used as decorator)
11
+ > migrator.remove_model(model, cascade=True) # Remove a model
12
+ > migrator.add_fields(model, **fields) # Add fields to a model
13
+ > migrator.change_fields(model, **fields) # Change fields
14
+ > migrator.remove_fields(model, *field_names, cascade=True)
15
+ > migrator.rename_field(model, old_field_name, new_field_name)
16
+ > migrator.rename_table(model, new_table_name)
17
+ > migrator.add_index(model, *col_names, unique=False)
18
+ > migrator.add_not_null(model, *field_names)
19
+ > migrator.add_default(model, field_name, default)
20
+ > migrator.add_constraint(model, name, sql)
21
+ > migrator.drop_index(model, *col_names)
22
+ > migrator.drop_not_null(model, *field_names)
23
+ > migrator.drop_constraints(model, *constraints)
24
+
25
+ """
26
+
27
+ from contextlib import suppress
28
+
29
+ import peewee as pw
30
+ from peewee_migrate import Migrator
31
+
32
+
33
+ with suppress(ImportError):
34
+ import playhouse.postgres_ext as pw_pext
35
+
36
+
37
+ def migrate(migrator: Migrator, database: pw.Database, *, fake=False):
38
+ """Write your migrations here."""
39
+
40
+ # We perform different migrations for SQLite and other databases
41
+ # This is because SQLite is very loose with enforcing its schema, and trying to migrate other databases like SQLite
42
+ # will require per-database SQL queries.
43
+ # Instead, we assume that because external DB support was added at a later date, it is safe to assume a newer base
44
+ # schema instead of trying to migrate from an older schema.
45
+ if isinstance(database, pw.SqliteDatabase):
46
+ migrate_sqlite(migrator, database, fake=fake)
47
+ else:
48
+ migrate_external(migrator, database, fake=fake)
49
+
50
+
51
+ def migrate_sqlite(migrator: Migrator, database: pw.Database, *, fake=False):
52
+ @migrator.create_model
53
+ class Auth(pw.Model):
54
+ id = pw.CharField(max_length=255, unique=True)
55
+ email = pw.CharField(max_length=255)
56
+ password = pw.CharField(max_length=255)
57
+ active = pw.BooleanField()
58
+
59
+ class Meta:
60
+ table_name = "auth"
61
+
62
+ @migrator.create_model
63
+ class Chat(pw.Model):
64
+ id = pw.CharField(max_length=255, unique=True)
65
+ user_id = pw.CharField(max_length=255)
66
+ title = pw.CharField()
67
+ chat = pw.TextField()
68
+ timestamp = pw.BigIntegerField()
69
+
70
+ class Meta:
71
+ table_name = "chat"
72
+
73
+ @migrator.create_model
74
+ class ChatIdTag(pw.Model):
75
+ id = pw.CharField(max_length=255, unique=True)
76
+ tag_name = pw.CharField(max_length=255)
77
+ chat_id = pw.CharField(max_length=255)
78
+ user_id = pw.CharField(max_length=255)
79
+ timestamp = pw.BigIntegerField()
80
+
81
+ class Meta:
82
+ table_name = "chatidtag"
83
+
84
+ @migrator.create_model
85
+ class Document(pw.Model):
86
+ id = pw.AutoField()
87
+ collection_name = pw.CharField(max_length=255, unique=True)
88
+ name = pw.CharField(max_length=255, unique=True)
89
+ title = pw.CharField()
90
+ filename = pw.CharField()
91
+ content = pw.TextField(null=True)
92
+ user_id = pw.CharField(max_length=255)
93
+ timestamp = pw.BigIntegerField()
94
+
95
+ class Meta:
96
+ table_name = "document"
97
+
98
+ @migrator.create_model
99
+ class Modelfile(pw.Model):
100
+ id = pw.AutoField()
101
+ tag_name = pw.CharField(max_length=255, unique=True)
102
+ user_id = pw.CharField(max_length=255)
103
+ modelfile = pw.TextField()
104
+ timestamp = pw.BigIntegerField()
105
+
106
+ class Meta:
107
+ table_name = "modelfile"
108
+
109
+ @migrator.create_model
110
+ class Prompt(pw.Model):
111
+ id = pw.AutoField()
112
+ command = pw.CharField(max_length=255, unique=True)
113
+ user_id = pw.CharField(max_length=255)
114
+ title = pw.CharField()
115
+ content = pw.TextField()
116
+ timestamp = pw.BigIntegerField()
117
+
118
+ class Meta:
119
+ table_name = "prompt"
120
+
121
+ @migrator.create_model
122
+ class Tag(pw.Model):
123
+ id = pw.CharField(max_length=255, unique=True)
124
+ name = pw.CharField(max_length=255)
125
+ user_id = pw.CharField(max_length=255)
126
+ data = pw.TextField(null=True)
127
+
128
+ class Meta:
129
+ table_name = "tag"
130
+
131
+ @migrator.create_model
132
+ class User(pw.Model):
133
+ id = pw.CharField(max_length=255, unique=True)
134
+ name = pw.CharField(max_length=255)
135
+ email = pw.CharField(max_length=255)
136
+ role = pw.CharField(max_length=255)
137
+ profile_image_url = pw.CharField(max_length=255)
138
+ timestamp = pw.BigIntegerField()
139
+
140
+ class Meta:
141
+ table_name = "user"
142
+
143
+
144
+ def migrate_external(migrator: Migrator, database: pw.Database, *, fake=False):
145
+ @migrator.create_model
146
+ class Auth(pw.Model):
147
+ id = pw.CharField(max_length=255, unique=True)
148
+ email = pw.CharField(max_length=255)
149
+ password = pw.TextField()
150
+ active = pw.BooleanField()
151
+
152
+ class Meta:
153
+ table_name = "auth"
154
+
155
+ @migrator.create_model
156
+ class Chat(pw.Model):
157
+ id = pw.CharField(max_length=255, unique=True)
158
+ user_id = pw.CharField(max_length=255)
159
+ title = pw.TextField()
160
+ chat = pw.TextField()
161
+ timestamp = pw.BigIntegerField()
162
+
163
+ class Meta:
164
+ table_name = "chat"
165
+
166
+ @migrator.create_model
167
+ class ChatIdTag(pw.Model):
168
+ id = pw.CharField(max_length=255, unique=True)
169
+ tag_name = pw.CharField(max_length=255)
170
+ chat_id = pw.CharField(max_length=255)
171
+ user_id = pw.CharField(max_length=255)
172
+ timestamp = pw.BigIntegerField()
173
+
174
+ class Meta:
175
+ table_name = "chatidtag"
176
+
177
+ @migrator.create_model
178
+ class Document(pw.Model):
179
+ id = pw.AutoField()
180
+ collection_name = pw.CharField(max_length=255, unique=True)
181
+ name = pw.CharField(max_length=255, unique=True)
182
+ title = pw.TextField()
183
+ filename = pw.TextField()
184
+ content = pw.TextField(null=True)
185
+ user_id = pw.CharField(max_length=255)
186
+ timestamp = pw.BigIntegerField()
187
+
188
+ class Meta:
189
+ table_name = "document"
190
+
191
+ @migrator.create_model
192
+ class Modelfile(pw.Model):
193
+ id = pw.AutoField()
194
+ tag_name = pw.CharField(max_length=255, unique=True)
195
+ user_id = pw.CharField(max_length=255)
196
+ modelfile = pw.TextField()
197
+ timestamp = pw.BigIntegerField()
198
+
199
+ class Meta:
200
+ table_name = "modelfile"
201
+
202
+ @migrator.create_model
203
+ class Prompt(pw.Model):
204
+ id = pw.AutoField()
205
+ command = pw.CharField(max_length=255, unique=True)
206
+ user_id = pw.CharField(max_length=255)
207
+ title = pw.TextField()
208
+ content = pw.TextField()
209
+ timestamp = pw.BigIntegerField()
210
+
211
+ class Meta:
212
+ table_name = "prompt"
213
+
214
+ @migrator.create_model
215
+ class Tag(pw.Model):
216
+ id = pw.CharField(max_length=255, unique=True)
217
+ name = pw.CharField(max_length=255)
218
+ user_id = pw.CharField(max_length=255)
219
+ data = pw.TextField(null=True)
220
+
221
+ class Meta:
222
+ table_name = "tag"
223
+
224
+ @migrator.create_model
225
+ class User(pw.Model):
226
+ id = pw.CharField(max_length=255, unique=True)
227
+ name = pw.CharField(max_length=255)
228
+ email = pw.CharField(max_length=255)
229
+ role = pw.CharField(max_length=255)
230
+ profile_image_url = pw.TextField()
231
+ timestamp = pw.BigIntegerField()
232
+
233
+ class Meta:
234
+ table_name = "user"
235
+
236
+
237
+ def rollback(migrator: Migrator, database: pw.Database, *, fake=False):
238
+ """Write your rollback migrations here."""
239
+
240
+ migrator.remove_model("user")
241
+
242
+ migrator.remove_model("tag")
243
+
244
+ migrator.remove_model("prompt")
245
+
246
+ migrator.remove_model("modelfile")
247
+
248
+ migrator.remove_model("document")
249
+
250
+ migrator.remove_model("chatidtag")
251
+
252
+ migrator.remove_model("chat")
253
+
254
+ migrator.remove_model("auth")
backend/open_webui/internal/migrations/002_add_local_sharing.py ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Peewee migrations -- 002_add_local_sharing.py.
2
+
3
+ Some examples (model - class or model name)::
4
+
5
+ > Model = migrator.orm['table_name'] # Return model in current state by name
6
+ > Model = migrator.ModelClass # Return model in current state by name
7
+
8
+ > migrator.sql(sql) # Run custom SQL
9
+ > migrator.run(func, *args, **kwargs) # Run python function with the given args
10
+ > migrator.create_model(Model) # Create a model (could be used as decorator)
11
+ > migrator.remove_model(model, cascade=True) # Remove a model
12
+ > migrator.add_fields(model, **fields) # Add fields to a model
13
+ > migrator.change_fields(model, **fields) # Change fields
14
+ > migrator.remove_fields(model, *field_names, cascade=True)
15
+ > migrator.rename_field(model, old_field_name, new_field_name)
16
+ > migrator.rename_table(model, new_table_name)
17
+ > migrator.add_index(model, *col_names, unique=False)
18
+ > migrator.add_not_null(model, *field_names)
19
+ > migrator.add_default(model, field_name, default)
20
+ > migrator.add_constraint(model, name, sql)
21
+ > migrator.drop_index(model, *col_names)
22
+ > migrator.drop_not_null(model, *field_names)
23
+ > migrator.drop_constraints(model, *constraints)
24
+
25
+ """
26
+
27
+ from contextlib import suppress
28
+
29
+ import peewee as pw
30
+ from peewee_migrate import Migrator
31
+
32
+
33
+ with suppress(ImportError):
34
+ import playhouse.postgres_ext as pw_pext
35
+
36
+
37
+ def migrate(migrator: Migrator, database: pw.Database, *, fake=False):
38
+ """Write your migrations here."""
39
+
40
+ migrator.add_fields(
41
+ "chat", share_id=pw.CharField(max_length=255, null=True, unique=True)
42
+ )
43
+
44
+
45
+ def rollback(migrator: Migrator, database: pw.Database, *, fake=False):
46
+ """Write your rollback migrations here."""
47
+
48
+ migrator.remove_fields("chat", "share_id")
backend/open_webui/internal/migrations/003_add_auth_api_key.py ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Peewee migrations -- 002_add_local_sharing.py.
2
+
3
+ Some examples (model - class or model name)::
4
+
5
+ > Model = migrator.orm['table_name'] # Return model in current state by name
6
+ > Model = migrator.ModelClass # Return model in current state by name
7
+
8
+ > migrator.sql(sql) # Run custom SQL
9
+ > migrator.run(func, *args, **kwargs) # Run python function with the given args
10
+ > migrator.create_model(Model) # Create a model (could be used as decorator)
11
+ > migrator.remove_model(model, cascade=True) # Remove a model
12
+ > migrator.add_fields(model, **fields) # Add fields to a model
13
+ > migrator.change_fields(model, **fields) # Change fields
14
+ > migrator.remove_fields(model, *field_names, cascade=True)
15
+ > migrator.rename_field(model, old_field_name, new_field_name)
16
+ > migrator.rename_table(model, new_table_name)
17
+ > migrator.add_index(model, *col_names, unique=False)
18
+ > migrator.add_not_null(model, *field_names)
19
+ > migrator.add_default(model, field_name, default)
20
+ > migrator.add_constraint(model, name, sql)
21
+ > migrator.drop_index(model, *col_names)
22
+ > migrator.drop_not_null(model, *field_names)
23
+ > migrator.drop_constraints(model, *constraints)
24
+
25
+ """
26
+
27
+ from contextlib import suppress
28
+
29
+ import peewee as pw
30
+ from peewee_migrate import Migrator
31
+
32
+
33
+ with suppress(ImportError):
34
+ import playhouse.postgres_ext as pw_pext
35
+
36
+
37
+ def migrate(migrator: Migrator, database: pw.Database, *, fake=False):
38
+ """Write your migrations here."""
39
+
40
+ migrator.add_fields(
41
+ "user", api_key=pw.CharField(max_length=255, null=True, unique=True)
42
+ )
43
+
44
+
45
+ def rollback(migrator: Migrator, database: pw.Database, *, fake=False):
46
+ """Write your rollback migrations here."""
47
+
48
+ migrator.remove_fields("user", "api_key")
backend/open_webui/internal/migrations/004_add_archived.py ADDED
@@ -0,0 +1,46 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Peewee migrations -- 002_add_local_sharing.py.
2
+
3
+ Some examples (model - class or model name)::
4
+
5
+ > Model = migrator.orm['table_name'] # Return model in current state by name
6
+ > Model = migrator.ModelClass # Return model in current state by name
7
+
8
+ > migrator.sql(sql) # Run custom SQL
9
+ > migrator.run(func, *args, **kwargs) # Run python function with the given args
10
+ > migrator.create_model(Model) # Create a model (could be used as decorator)
11
+ > migrator.remove_model(model, cascade=True) # Remove a model
12
+ > migrator.add_fields(model, **fields) # Add fields to a model
13
+ > migrator.change_fields(model, **fields) # Change fields
14
+ > migrator.remove_fields(model, *field_names, cascade=True)
15
+ > migrator.rename_field(model, old_field_name, new_field_name)
16
+ > migrator.rename_table(model, new_table_name)
17
+ > migrator.add_index(model, *col_names, unique=False)
18
+ > migrator.add_not_null(model, *field_names)
19
+ > migrator.add_default(model, field_name, default)
20
+ > migrator.add_constraint(model, name, sql)
21
+ > migrator.drop_index(model, *col_names)
22
+ > migrator.drop_not_null(model, *field_names)
23
+ > migrator.drop_constraints(model, *constraints)
24
+
25
+ """
26
+
27
+ from contextlib import suppress
28
+
29
+ import peewee as pw
30
+ from peewee_migrate import Migrator
31
+
32
+
33
+ with suppress(ImportError):
34
+ import playhouse.postgres_ext as pw_pext
35
+
36
+
37
+ def migrate(migrator: Migrator, database: pw.Database, *, fake=False):
38
+ """Write your migrations here."""
39
+
40
+ migrator.add_fields("chat", archived=pw.BooleanField(default=False))
41
+
42
+
43
+ def rollback(migrator: Migrator, database: pw.Database, *, fake=False):
44
+ """Write your rollback migrations here."""
45
+
46
+ migrator.remove_fields("chat", "archived")
backend/open_webui/internal/migrations/005_add_updated_at.py ADDED
@@ -0,0 +1,130 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Peewee migrations -- 002_add_local_sharing.py.
2
+
3
+ Some examples (model - class or model name)::
4
+
5
+ > Model = migrator.orm['table_name'] # Return model in current state by name
6
+ > Model = migrator.ModelClass # Return model in current state by name
7
+
8
+ > migrator.sql(sql) # Run custom SQL
9
+ > migrator.run(func, *args, **kwargs) # Run python function with the given args
10
+ > migrator.create_model(Model) # Create a model (could be used as decorator)
11
+ > migrator.remove_model(model, cascade=True) # Remove a model
12
+ > migrator.add_fields(model, **fields) # Add fields to a model
13
+ > migrator.change_fields(model, **fields) # Change fields
14
+ > migrator.remove_fields(model, *field_names, cascade=True)
15
+ > migrator.rename_field(model, old_field_name, new_field_name)
16
+ > migrator.rename_table(model, new_table_name)
17
+ > migrator.add_index(model, *col_names, unique=False)
18
+ > migrator.add_not_null(model, *field_names)
19
+ > migrator.add_default(model, field_name, default)
20
+ > migrator.add_constraint(model, name, sql)
21
+ > migrator.drop_index(model, *col_names)
22
+ > migrator.drop_not_null(model, *field_names)
23
+ > migrator.drop_constraints(model, *constraints)
24
+
25
+ """
26
+
27
+ from contextlib import suppress
28
+
29
+ import peewee as pw
30
+ from peewee_migrate import Migrator
31
+
32
+
33
+ with suppress(ImportError):
34
+ import playhouse.postgres_ext as pw_pext
35
+
36
+
37
+ def migrate(migrator: Migrator, database: pw.Database, *, fake=False):
38
+ """Write your migrations here."""
39
+
40
+ if isinstance(database, pw.SqliteDatabase):
41
+ migrate_sqlite(migrator, database, fake=fake)
42
+ else:
43
+ migrate_external(migrator, database, fake=fake)
44
+
45
+
46
+ def migrate_sqlite(migrator: Migrator, database: pw.Database, *, fake=False):
47
+ # Adding fields created_at and updated_at to the 'chat' table
48
+ migrator.add_fields(
49
+ "chat",
50
+ created_at=pw.DateTimeField(null=True), # Allow null for transition
51
+ updated_at=pw.DateTimeField(null=True), # Allow null for transition
52
+ )
53
+
54
+ # Populate the new fields from an existing 'timestamp' field
55
+ migrator.sql(
56
+ "UPDATE chat SET created_at = timestamp, updated_at = timestamp WHERE timestamp IS NOT NULL"
57
+ )
58
+
59
+ # Now that the data has been copied, remove the original 'timestamp' field
60
+ migrator.remove_fields("chat", "timestamp")
61
+
62
+ # Update the fields to be not null now that they are populated
63
+ migrator.change_fields(
64
+ "chat",
65
+ created_at=pw.DateTimeField(null=False),
66
+ updated_at=pw.DateTimeField(null=False),
67
+ )
68
+
69
+
70
+ def migrate_external(migrator: Migrator, database: pw.Database, *, fake=False):
71
+ # Adding fields created_at and updated_at to the 'chat' table
72
+ migrator.add_fields(
73
+ "chat",
74
+ created_at=pw.BigIntegerField(null=True), # Allow null for transition
75
+ updated_at=pw.BigIntegerField(null=True), # Allow null for transition
76
+ )
77
+
78
+ # Populate the new fields from an existing 'timestamp' field
79
+ migrator.sql(
80
+ "UPDATE chat SET created_at = timestamp, updated_at = timestamp WHERE timestamp IS NOT NULL"
81
+ )
82
+
83
+ # Now that the data has been copied, remove the original 'timestamp' field
84
+ migrator.remove_fields("chat", "timestamp")
85
+
86
+ # Update the fields to be not null now that they are populated
87
+ migrator.change_fields(
88
+ "chat",
89
+ created_at=pw.BigIntegerField(null=False),
90
+ updated_at=pw.BigIntegerField(null=False),
91
+ )
92
+
93
+
94
+ def rollback(migrator: Migrator, database: pw.Database, *, fake=False):
95
+ """Write your rollback migrations here."""
96
+
97
+ if isinstance(database, pw.SqliteDatabase):
98
+ rollback_sqlite(migrator, database, fake=fake)
99
+ else:
100
+ rollback_external(migrator, database, fake=fake)
101
+
102
+
103
+ def rollback_sqlite(migrator: Migrator, database: pw.Database, *, fake=False):
104
+ # Recreate the timestamp field initially allowing null values for safe transition
105
+ migrator.add_fields("chat", timestamp=pw.DateTimeField(null=True))
106
+
107
+ # Copy the earliest created_at date back into the new timestamp field
108
+ # This assumes created_at was originally a copy of timestamp
109
+ migrator.sql("UPDATE chat SET timestamp = created_at")
110
+
111
+ # Remove the created_at and updated_at fields
112
+ migrator.remove_fields("chat", "created_at", "updated_at")
113
+
114
+ # Finally, alter the timestamp field to not allow nulls if that was the original setting
115
+ migrator.change_fields("chat", timestamp=pw.DateTimeField(null=False))
116
+
117
+
118
+ def rollback_external(migrator: Migrator, database: pw.Database, *, fake=False):
119
+ # Recreate the timestamp field initially allowing null values for safe transition
120
+ migrator.add_fields("chat", timestamp=pw.BigIntegerField(null=True))
121
+
122
+ # Copy the earliest created_at date back into the new timestamp field
123
+ # This assumes created_at was originally a copy of timestamp
124
+ migrator.sql("UPDATE chat SET timestamp = created_at")
125
+
126
+ # Remove the created_at and updated_at fields
127
+ migrator.remove_fields("chat", "created_at", "updated_at")
128
+
129
+ # Finally, alter the timestamp field to not allow nulls if that was the original setting
130
+ migrator.change_fields("chat", timestamp=pw.BigIntegerField(null=False))
backend/open_webui/internal/migrations/006_migrate_timestamps_and_charfields.py ADDED
@@ -0,0 +1,130 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Peewee migrations -- 006_migrate_timestamps_and_charfields.py.
2
+
3
+ Some examples (model - class or model name)::
4
+
5
+ > Model = migrator.orm['table_name'] # Return model in current state by name
6
+ > Model = migrator.ModelClass # Return model in current state by name
7
+
8
+ > migrator.sql(sql) # Run custom SQL
9
+ > migrator.run(func, *args, **kwargs) # Run python function with the given args
10
+ > migrator.create_model(Model) # Create a model (could be used as decorator)
11
+ > migrator.remove_model(model, cascade=True) # Remove a model
12
+ > migrator.add_fields(model, **fields) # Add fields to a model
13
+ > migrator.change_fields(model, **fields) # Change fields
14
+ > migrator.remove_fields(model, *field_names, cascade=True)
15
+ > migrator.rename_field(model, old_field_name, new_field_name)
16
+ > migrator.rename_table(model, new_table_name)
17
+ > migrator.add_index(model, *col_names, unique=False)
18
+ > migrator.add_not_null(model, *field_names)
19
+ > migrator.add_default(model, field_name, default)
20
+ > migrator.add_constraint(model, name, sql)
21
+ > migrator.drop_index(model, *col_names)
22
+ > migrator.drop_not_null(model, *field_names)
23
+ > migrator.drop_constraints(model, *constraints)
24
+
25
+ """
26
+
27
+ from contextlib import suppress
28
+
29
+ import peewee as pw
30
+ from peewee_migrate import Migrator
31
+
32
+
33
+ with suppress(ImportError):
34
+ import playhouse.postgres_ext as pw_pext
35
+
36
+
37
+ def migrate(migrator: Migrator, database: pw.Database, *, fake=False):
38
+ """Write your migrations here."""
39
+
40
+ # Alter the tables with timestamps
41
+ migrator.change_fields(
42
+ "chatidtag",
43
+ timestamp=pw.BigIntegerField(),
44
+ )
45
+ migrator.change_fields(
46
+ "document",
47
+ timestamp=pw.BigIntegerField(),
48
+ )
49
+ migrator.change_fields(
50
+ "modelfile",
51
+ timestamp=pw.BigIntegerField(),
52
+ )
53
+ migrator.change_fields(
54
+ "prompt",
55
+ timestamp=pw.BigIntegerField(),
56
+ )
57
+ migrator.change_fields(
58
+ "user",
59
+ timestamp=pw.BigIntegerField(),
60
+ )
61
+ # Alter the tables with varchar to text where necessary
62
+ migrator.change_fields(
63
+ "auth",
64
+ password=pw.TextField(),
65
+ )
66
+ migrator.change_fields(
67
+ "chat",
68
+ title=pw.TextField(),
69
+ )
70
+ migrator.change_fields(
71
+ "document",
72
+ title=pw.TextField(),
73
+ filename=pw.TextField(),
74
+ )
75
+ migrator.change_fields(
76
+ "prompt",
77
+ title=pw.TextField(),
78
+ )
79
+ migrator.change_fields(
80
+ "user",
81
+ profile_image_url=pw.TextField(),
82
+ )
83
+
84
+
85
+ def rollback(migrator: Migrator, database: pw.Database, *, fake=False):
86
+ """Write your rollback migrations here."""
87
+
88
+ if isinstance(database, pw.SqliteDatabase):
89
+ # Alter the tables with timestamps
90
+ migrator.change_fields(
91
+ "chatidtag",
92
+ timestamp=pw.DateField(),
93
+ )
94
+ migrator.change_fields(
95
+ "document",
96
+ timestamp=pw.DateField(),
97
+ )
98
+ migrator.change_fields(
99
+ "modelfile",
100
+ timestamp=pw.DateField(),
101
+ )
102
+ migrator.change_fields(
103
+ "prompt",
104
+ timestamp=pw.DateField(),
105
+ )
106
+ migrator.change_fields(
107
+ "user",
108
+ timestamp=pw.DateField(),
109
+ )
110
+ migrator.change_fields(
111
+ "auth",
112
+ password=pw.CharField(max_length=255),
113
+ )
114
+ migrator.change_fields(
115
+ "chat",
116
+ title=pw.CharField(),
117
+ )
118
+ migrator.change_fields(
119
+ "document",
120
+ title=pw.CharField(),
121
+ filename=pw.CharField(),
122
+ )
123
+ migrator.change_fields(
124
+ "prompt",
125
+ title=pw.CharField(),
126
+ )
127
+ migrator.change_fields(
128
+ "user",
129
+ profile_image_url=pw.CharField(),
130
+ )
backend/open_webui/internal/migrations/007_add_user_last_active_at.py ADDED
@@ -0,0 +1,79 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Peewee migrations -- 002_add_local_sharing.py.
2
+
3
+ Some examples (model - class or model name)::
4
+
5
+ > Model = migrator.orm['table_name'] # Return model in current state by name
6
+ > Model = migrator.ModelClass # Return model in current state by name
7
+
8
+ > migrator.sql(sql) # Run custom SQL
9
+ > migrator.run(func, *args, **kwargs) # Run python function with the given args
10
+ > migrator.create_model(Model) # Create a model (could be used as decorator)
11
+ > migrator.remove_model(model, cascade=True) # Remove a model
12
+ > migrator.add_fields(model, **fields) # Add fields to a model
13
+ > migrator.change_fields(model, **fields) # Change fields
14
+ > migrator.remove_fields(model, *field_names, cascade=True)
15
+ > migrator.rename_field(model, old_field_name, new_field_name)
16
+ > migrator.rename_table(model, new_table_name)
17
+ > migrator.add_index(model, *col_names, unique=False)
18
+ > migrator.add_not_null(model, *field_names)
19
+ > migrator.add_default(model, field_name, default)
20
+ > migrator.add_constraint(model, name, sql)
21
+ > migrator.drop_index(model, *col_names)
22
+ > migrator.drop_not_null(model, *field_names)
23
+ > migrator.drop_constraints(model, *constraints)
24
+
25
+ """
26
+
27
+ from contextlib import suppress
28
+
29
+ import peewee as pw
30
+ from peewee_migrate import Migrator
31
+
32
+
33
+ with suppress(ImportError):
34
+ import playhouse.postgres_ext as pw_pext
35
+
36
+
37
+ def migrate(migrator: Migrator, database: pw.Database, *, fake=False):
38
+ """Write your migrations here."""
39
+
40
+ # Adding fields created_at and updated_at to the 'user' table
41
+ migrator.add_fields(
42
+ "user",
43
+ created_at=pw.BigIntegerField(null=True), # Allow null for transition
44
+ updated_at=pw.BigIntegerField(null=True), # Allow null for transition
45
+ last_active_at=pw.BigIntegerField(null=True), # Allow null for transition
46
+ )
47
+
48
+ # Populate the new fields from an existing 'timestamp' field
49
+ migrator.sql(
50
+ 'UPDATE "user" SET created_at = timestamp, updated_at = timestamp, last_active_at = timestamp WHERE timestamp IS NOT NULL'
51
+ )
52
+
53
+ # Now that the data has been copied, remove the original 'timestamp' field
54
+ migrator.remove_fields("user", "timestamp")
55
+
56
+ # Update the fields to be not null now that they are populated
57
+ migrator.change_fields(
58
+ "user",
59
+ created_at=pw.BigIntegerField(null=False),
60
+ updated_at=pw.BigIntegerField(null=False),
61
+ last_active_at=pw.BigIntegerField(null=False),
62
+ )
63
+
64
+
65
+ def rollback(migrator: Migrator, database: pw.Database, *, fake=False):
66
+ """Write your rollback migrations here."""
67
+
68
+ # Recreate the timestamp field initially allowing null values for safe transition
69
+ migrator.add_fields("user", timestamp=pw.BigIntegerField(null=True))
70
+
71
+ # Copy the earliest created_at date back into the new timestamp field
72
+ # This assumes created_at was originally a copy of timestamp
73
+ migrator.sql('UPDATE "user" SET timestamp = created_at')
74
+
75
+ # Remove the created_at and updated_at fields
76
+ migrator.remove_fields("user", "created_at", "updated_at", "last_active_at")
77
+
78
+ # Finally, alter the timestamp field to not allow nulls if that was the original setting
79
+ migrator.change_fields("user", timestamp=pw.BigIntegerField(null=False))
backend/open_webui/internal/migrations/008_add_memory.py ADDED
@@ -0,0 +1,53 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Peewee migrations -- 002_add_local_sharing.py.
2
+
3
+ Some examples (model - class or model name)::
4
+
5
+ > Model = migrator.orm['table_name'] # Return model in current state by name
6
+ > Model = migrator.ModelClass # Return model in current state by name
7
+
8
+ > migrator.sql(sql) # Run custom SQL
9
+ > migrator.run(func, *args, **kwargs) # Run python function with the given args
10
+ > migrator.create_model(Model) # Create a model (could be used as decorator)
11
+ > migrator.remove_model(model, cascade=True) # Remove a model
12
+ > migrator.add_fields(model, **fields) # Add fields to a model
13
+ > migrator.change_fields(model, **fields) # Change fields
14
+ > migrator.remove_fields(model, *field_names, cascade=True)
15
+ > migrator.rename_field(model, old_field_name, new_field_name)
16
+ > migrator.rename_table(model, new_table_name)
17
+ > migrator.add_index(model, *col_names, unique=False)
18
+ > migrator.add_not_null(model, *field_names)
19
+ > migrator.add_default(model, field_name, default)
20
+ > migrator.add_constraint(model, name, sql)
21
+ > migrator.drop_index(model, *col_names)
22
+ > migrator.drop_not_null(model, *field_names)
23
+ > migrator.drop_constraints(model, *constraints)
24
+
25
+ """
26
+
27
+ from contextlib import suppress
28
+
29
+ import peewee as pw
30
+ from peewee_migrate import Migrator
31
+
32
+
33
+ with suppress(ImportError):
34
+ import playhouse.postgres_ext as pw_pext
35
+
36
+
37
+ def migrate(migrator: Migrator, database: pw.Database, *, fake=False):
38
+ @migrator.create_model
39
+ class Memory(pw.Model):
40
+ id = pw.CharField(max_length=255, unique=True)
41
+ user_id = pw.CharField(max_length=255)
42
+ content = pw.TextField(null=False)
43
+ updated_at = pw.BigIntegerField(null=False)
44
+ created_at = pw.BigIntegerField(null=False)
45
+
46
+ class Meta:
47
+ table_name = "memory"
48
+
49
+
50
+ def rollback(migrator: Migrator, database: pw.Database, *, fake=False):
51
+ """Write your rollback migrations here."""
52
+
53
+ migrator.remove_model("memory")
backend/open_webui/internal/migrations/009_add_models.py ADDED
@@ -0,0 +1,61 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Peewee migrations -- 009_add_models.py.
2
+
3
+ Some examples (model - class or model name)::
4
+
5
+ > Model = migrator.orm['table_name'] # Return model in current state by name
6
+ > Model = migrator.ModelClass # Return model in current state by name
7
+
8
+ > migrator.sql(sql) # Run custom SQL
9
+ > migrator.run(func, *args, **kwargs) # Run python function with the given args
10
+ > migrator.create_model(Model) # Create a model (could be used as decorator)
11
+ > migrator.remove_model(model, cascade=True) # Remove a model
12
+ > migrator.add_fields(model, **fields) # Add fields to a model
13
+ > migrator.change_fields(model, **fields) # Change fields
14
+ > migrator.remove_fields(model, *field_names, cascade=True)
15
+ > migrator.rename_field(model, old_field_name, new_field_name)
16
+ > migrator.rename_table(model, new_table_name)
17
+ > migrator.add_index(model, *col_names, unique=False)
18
+ > migrator.add_not_null(model, *field_names)
19
+ > migrator.add_default(model, field_name, default)
20
+ > migrator.add_constraint(model, name, sql)
21
+ > migrator.drop_index(model, *col_names)
22
+ > migrator.drop_not_null(model, *field_names)
23
+ > migrator.drop_constraints(model, *constraints)
24
+
25
+ """
26
+
27
+ from contextlib import suppress
28
+
29
+ import peewee as pw
30
+ from peewee_migrate import Migrator
31
+
32
+
33
+ with suppress(ImportError):
34
+ import playhouse.postgres_ext as pw_pext
35
+
36
+
37
+ def migrate(migrator: Migrator, database: pw.Database, *, fake=False):
38
+ """Write your migrations here."""
39
+
40
+ @migrator.create_model
41
+ class Model(pw.Model):
42
+ id = pw.TextField(unique=True)
43
+ user_id = pw.TextField()
44
+ base_model_id = pw.TextField(null=True)
45
+
46
+ name = pw.TextField()
47
+
48
+ meta = pw.TextField()
49
+ params = pw.TextField()
50
+
51
+ created_at = pw.BigIntegerField(null=False)
52
+ updated_at = pw.BigIntegerField(null=False)
53
+
54
+ class Meta:
55
+ table_name = "model"
56
+
57
+
58
+ def rollback(migrator: Migrator, database: pw.Database, *, fake=False):
59
+ """Write your rollback migrations here."""
60
+
61
+ migrator.remove_model("model")
backend/open_webui/internal/migrations/010_migrate_modelfiles_to_models.py ADDED
@@ -0,0 +1,130 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Peewee migrations -- 009_add_models.py.
2
+
3
+ Some examples (model - class or model name)::
4
+
5
+ > Model = migrator.orm['table_name'] # Return model in current state by name
6
+ > Model = migrator.ModelClass # Return model in current state by name
7
+
8
+ > migrator.sql(sql) # Run custom SQL
9
+ > migrator.run(func, *args, **kwargs) # Run python function with the given args
10
+ > migrator.create_model(Model) # Create a model (could be used as decorator)
11
+ > migrator.remove_model(model, cascade=True) # Remove a model
12
+ > migrator.add_fields(model, **fields) # Add fields to a model
13
+ > migrator.change_fields(model, **fields) # Change fields
14
+ > migrator.remove_fields(model, *field_names, cascade=True)
15
+ > migrator.rename_field(model, old_field_name, new_field_name)
16
+ > migrator.rename_table(model, new_table_name)
17
+ > migrator.add_index(model, *col_names, unique=False)
18
+ > migrator.add_not_null(model, *field_names)
19
+ > migrator.add_default(model, field_name, default)
20
+ > migrator.add_constraint(model, name, sql)
21
+ > migrator.drop_index(model, *col_names)
22
+ > migrator.drop_not_null(model, *field_names)
23
+ > migrator.drop_constraints(model, *constraints)
24
+
25
+ """
26
+
27
+ from contextlib import suppress
28
+
29
+ import peewee as pw
30
+ from peewee_migrate import Migrator
31
+ import json
32
+
33
+ from open_webui.utils.misc import parse_ollama_modelfile
34
+
35
+ with suppress(ImportError):
36
+ import playhouse.postgres_ext as pw_pext
37
+
38
+
39
+ def migrate(migrator: Migrator, database: pw.Database, *, fake=False):
40
+ """Write your migrations here."""
41
+
42
+ # Fetch data from 'modelfile' table and insert into 'model' table
43
+ migrate_modelfile_to_model(migrator, database)
44
+ # Drop the 'modelfile' table
45
+ migrator.remove_model("modelfile")
46
+
47
+
48
+ def migrate_modelfile_to_model(migrator: Migrator, database: pw.Database):
49
+ ModelFile = migrator.orm["modelfile"]
50
+ Model = migrator.orm["model"]
51
+
52
+ modelfiles = ModelFile.select()
53
+
54
+ for modelfile in modelfiles:
55
+ # Extract and transform data in Python
56
+
57
+ modelfile.modelfile = json.loads(modelfile.modelfile)
58
+ meta = json.dumps(
59
+ {
60
+ "description": modelfile.modelfile.get("desc"),
61
+ "profile_image_url": modelfile.modelfile.get("imageUrl"),
62
+ "ollama": {"modelfile": modelfile.modelfile.get("content")},
63
+ "suggestion_prompts": modelfile.modelfile.get("suggestionPrompts"),
64
+ "categories": modelfile.modelfile.get("categories"),
65
+ "user": {**modelfile.modelfile.get("user", {}), "community": True},
66
+ }
67
+ )
68
+
69
+ info = parse_ollama_modelfile(modelfile.modelfile.get("content"))
70
+
71
+ # Insert the processed data into the 'model' table
72
+ Model.create(
73
+ id=f"ollama-{modelfile.tag_name}",
74
+ user_id=modelfile.user_id,
75
+ base_model_id=info.get("base_model_id"),
76
+ name=modelfile.modelfile.get("title"),
77
+ meta=meta,
78
+ params=json.dumps(info.get("params", {})),
79
+ created_at=modelfile.timestamp,
80
+ updated_at=modelfile.timestamp,
81
+ )
82
+
83
+
84
+ def rollback(migrator: Migrator, database: pw.Database, *, fake=False):
85
+ """Write your rollback migrations here."""
86
+
87
+ recreate_modelfile_table(migrator, database)
88
+ move_data_back_to_modelfile(migrator, database)
89
+ migrator.remove_model("model")
90
+
91
+
92
+ def recreate_modelfile_table(migrator: Migrator, database: pw.Database):
93
+ query = """
94
+ CREATE TABLE IF NOT EXISTS modelfile (
95
+ user_id TEXT,
96
+ tag_name TEXT,
97
+ modelfile JSON,
98
+ timestamp BIGINT
99
+ )
100
+ """
101
+ migrator.sql(query)
102
+
103
+
104
+ def move_data_back_to_modelfile(migrator: Migrator, database: pw.Database):
105
+ Model = migrator.orm["model"]
106
+ Modelfile = migrator.orm["modelfile"]
107
+
108
+ models = Model.select()
109
+
110
+ for model in models:
111
+ # Extract and transform data in Python
112
+ meta = json.loads(model.meta)
113
+
114
+ modelfile_data = {
115
+ "title": model.name,
116
+ "desc": meta.get("description"),
117
+ "imageUrl": meta.get("profile_image_url"),
118
+ "content": meta.get("ollama", {}).get("modelfile"),
119
+ "suggestionPrompts": meta.get("suggestion_prompts"),
120
+ "categories": meta.get("categories"),
121
+ "user": {k: v for k, v in meta.get("user", {}).items() if k != "community"},
122
+ }
123
+
124
+ # Insert the processed data back into the 'modelfile' table
125
+ Modelfile.create(
126
+ user_id=model.user_id,
127
+ tag_name=model.id,
128
+ modelfile=modelfile_data,
129
+ timestamp=model.created_at,
130
+ )
backend/open_webui/internal/migrations/011_add_user_settings.py ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Peewee migrations -- 002_add_local_sharing.py.
2
+
3
+ Some examples (model - class or model name)::
4
+
5
+ > Model = migrator.orm['table_name'] # Return model in current state by name
6
+ > Model = migrator.ModelClass # Return model in current state by name
7
+
8
+ > migrator.sql(sql) # Run custom SQL
9
+ > migrator.run(func, *args, **kwargs) # Run python function with the given args
10
+ > migrator.create_model(Model) # Create a model (could be used as decorator)
11
+ > migrator.remove_model(model, cascade=True) # Remove a model
12
+ > migrator.add_fields(model, **fields) # Add fields to a model
13
+ > migrator.change_fields(model, **fields) # Change fields
14
+ > migrator.remove_fields(model, *field_names, cascade=True)
15
+ > migrator.rename_field(model, old_field_name, new_field_name)
16
+ > migrator.rename_table(model, new_table_name)
17
+ > migrator.add_index(model, *col_names, unique=False)
18
+ > migrator.add_not_null(model, *field_names)
19
+ > migrator.add_default(model, field_name, default)
20
+ > migrator.add_constraint(model, name, sql)
21
+ > migrator.drop_index(model, *col_names)
22
+ > migrator.drop_not_null(model, *field_names)
23
+ > migrator.drop_constraints(model, *constraints)
24
+
25
+ """
26
+
27
+ from contextlib import suppress
28
+
29
+ import peewee as pw
30
+ from peewee_migrate import Migrator
31
+
32
+
33
+ with suppress(ImportError):
34
+ import playhouse.postgres_ext as pw_pext
35
+
36
+
37
+ def migrate(migrator: Migrator, database: pw.Database, *, fake=False):
38
+ """Write your migrations here."""
39
+
40
+ # Adding fields settings to the 'user' table
41
+ migrator.add_fields("user", settings=pw.TextField(null=True))
42
+
43
+
44
+ def rollback(migrator: Migrator, database: pw.Database, *, fake=False):
45
+ """Write your rollback migrations here."""
46
+
47
+ # Remove the settings field
48
+ migrator.remove_fields("user", "settings")
backend/open_webui/internal/migrations/012_add_tools.py ADDED
@@ -0,0 +1,61 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Peewee migrations -- 009_add_models.py.
2
+
3
+ Some examples (model - class or model name)::
4
+
5
+ > Model = migrator.orm['table_name'] # Return model in current state by name
6
+ > Model = migrator.ModelClass # Return model in current state by name
7
+
8
+ > migrator.sql(sql) # Run custom SQL
9
+ > migrator.run(func, *args, **kwargs) # Run python function with the given args
10
+ > migrator.create_model(Model) # Create a model (could be used as decorator)
11
+ > migrator.remove_model(model, cascade=True) # Remove a model
12
+ > migrator.add_fields(model, **fields) # Add fields to a model
13
+ > migrator.change_fields(model, **fields) # Change fields
14
+ > migrator.remove_fields(model, *field_names, cascade=True)
15
+ > migrator.rename_field(model, old_field_name, new_field_name)
16
+ > migrator.rename_table(model, new_table_name)
17
+ > migrator.add_index(model, *col_names, unique=False)
18
+ > migrator.add_not_null(model, *field_names)
19
+ > migrator.add_default(model, field_name, default)
20
+ > migrator.add_constraint(model, name, sql)
21
+ > migrator.drop_index(model, *col_names)
22
+ > migrator.drop_not_null(model, *field_names)
23
+ > migrator.drop_constraints(model, *constraints)
24
+
25
+ """
26
+
27
+ from contextlib import suppress
28
+
29
+ import peewee as pw
30
+ from peewee_migrate import Migrator
31
+
32
+
33
+ with suppress(ImportError):
34
+ import playhouse.postgres_ext as pw_pext
35
+
36
+
37
+ def migrate(migrator: Migrator, database: pw.Database, *, fake=False):
38
+ """Write your migrations here."""
39
+
40
+ @migrator.create_model
41
+ class Tool(pw.Model):
42
+ id = pw.TextField(unique=True)
43
+ user_id = pw.TextField()
44
+
45
+ name = pw.TextField()
46
+ content = pw.TextField()
47
+ specs = pw.TextField()
48
+
49
+ meta = pw.TextField()
50
+
51
+ created_at = pw.BigIntegerField(null=False)
52
+ updated_at = pw.BigIntegerField(null=False)
53
+
54
+ class Meta:
55
+ table_name = "tool"
56
+
57
+
58
+ def rollback(migrator: Migrator, database: pw.Database, *, fake=False):
59
+ """Write your rollback migrations here."""
60
+
61
+ migrator.remove_model("tool")
backend/open_webui/internal/migrations/013_add_user_info.py ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Peewee migrations -- 002_add_local_sharing.py.
2
+
3
+ Some examples (model - class or model name)::
4
+
5
+ > Model = migrator.orm['table_name'] # Return model in current state by name
6
+ > Model = migrator.ModelClass # Return model in current state by name
7
+
8
+ > migrator.sql(sql) # Run custom SQL
9
+ > migrator.run(func, *args, **kwargs) # Run python function with the given args
10
+ > migrator.create_model(Model) # Create a model (could be used as decorator)
11
+ > migrator.remove_model(model, cascade=True) # Remove a model
12
+ > migrator.add_fields(model, **fields) # Add fields to a model
13
+ > migrator.change_fields(model, **fields) # Change fields
14
+ > migrator.remove_fields(model, *field_names, cascade=True)
15
+ > migrator.rename_field(model, old_field_name, new_field_name)
16
+ > migrator.rename_table(model, new_table_name)
17
+ > migrator.add_index(model, *col_names, unique=False)
18
+ > migrator.add_not_null(model, *field_names)
19
+ > migrator.add_default(model, field_name, default)
20
+ > migrator.add_constraint(model, name, sql)
21
+ > migrator.drop_index(model, *col_names)
22
+ > migrator.drop_not_null(model, *field_names)
23
+ > migrator.drop_constraints(model, *constraints)
24
+
25
+ """
26
+
27
+ from contextlib import suppress
28
+
29
+ import peewee as pw
30
+ from peewee_migrate import Migrator
31
+
32
+
33
+ with suppress(ImportError):
34
+ import playhouse.postgres_ext as pw_pext
35
+
36
+
37
+ def migrate(migrator: Migrator, database: pw.Database, *, fake=False):
38
+ """Write your migrations here."""
39
+
40
+ # Adding fields info to the 'user' table
41
+ migrator.add_fields("user", info=pw.TextField(null=True))
42
+
43
+
44
+ def rollback(migrator: Migrator, database: pw.Database, *, fake=False):
45
+ """Write your rollback migrations here."""
46
+
47
+ # Remove the settings field
48
+ migrator.remove_fields("user", "info")
backend/open_webui/internal/migrations/014_add_files.py ADDED
@@ -0,0 +1,55 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Peewee migrations -- 009_add_models.py.
2
+
3
+ Some examples (model - class or model name)::
4
+
5
+ > Model = migrator.orm['table_name'] # Return model in current state by name
6
+ > Model = migrator.ModelClass # Return model in current state by name
7
+
8
+ > migrator.sql(sql) # Run custom SQL
9
+ > migrator.run(func, *args, **kwargs) # Run python function with the given args
10
+ > migrator.create_model(Model) # Create a model (could be used as decorator)
11
+ > migrator.remove_model(model, cascade=True) # Remove a model
12
+ > migrator.add_fields(model, **fields) # Add fields to a model
13
+ > migrator.change_fields(model, **fields) # Change fields
14
+ > migrator.remove_fields(model, *field_names, cascade=True)
15
+ > migrator.rename_field(model, old_field_name, new_field_name)
16
+ > migrator.rename_table(model, new_table_name)
17
+ > migrator.add_index(model, *col_names, unique=False)
18
+ > migrator.add_not_null(model, *field_names)
19
+ > migrator.add_default(model, field_name, default)
20
+ > migrator.add_constraint(model, name, sql)
21
+ > migrator.drop_index(model, *col_names)
22
+ > migrator.drop_not_null(model, *field_names)
23
+ > migrator.drop_constraints(model, *constraints)
24
+
25
+ """
26
+
27
+ from contextlib import suppress
28
+
29
+ import peewee as pw
30
+ from peewee_migrate import Migrator
31
+
32
+
33
+ with suppress(ImportError):
34
+ import playhouse.postgres_ext as pw_pext
35
+
36
+
37
+ def migrate(migrator: Migrator, database: pw.Database, *, fake=False):
38
+ """Write your migrations here."""
39
+
40
+ @migrator.create_model
41
+ class File(pw.Model):
42
+ id = pw.TextField(unique=True)
43
+ user_id = pw.TextField()
44
+ filename = pw.TextField()
45
+ meta = pw.TextField()
46
+ created_at = pw.BigIntegerField(null=False)
47
+
48
+ class Meta:
49
+ table_name = "file"
50
+
51
+
52
+ def rollback(migrator: Migrator, database: pw.Database, *, fake=False):
53
+ """Write your rollback migrations here."""
54
+
55
+ migrator.remove_model("file")
backend/open_webui/internal/migrations/015_add_functions.py ADDED
@@ -0,0 +1,61 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Peewee migrations -- 009_add_models.py.
2
+
3
+ Some examples (model - class or model name)::
4
+
5
+ > Model = migrator.orm['table_name'] # Return model in current state by name
6
+ > Model = migrator.ModelClass # Return model in current state by name
7
+
8
+ > migrator.sql(sql) # Run custom SQL
9
+ > migrator.run(func, *args, **kwargs) # Run python function with the given args
10
+ > migrator.create_model(Model) # Create a model (could be used as decorator)
11
+ > migrator.remove_model(model, cascade=True) # Remove a model
12
+ > migrator.add_fields(model, **fields) # Add fields to a model
13
+ > migrator.change_fields(model, **fields) # Change fields
14
+ > migrator.remove_fields(model, *field_names, cascade=True)
15
+ > migrator.rename_field(model, old_field_name, new_field_name)
16
+ > migrator.rename_table(model, new_table_name)
17
+ > migrator.add_index(model, *col_names, unique=False)
18
+ > migrator.add_not_null(model, *field_names)
19
+ > migrator.add_default(model, field_name, default)
20
+ > migrator.add_constraint(model, name, sql)
21
+ > migrator.drop_index(model, *col_names)
22
+ > migrator.drop_not_null(model, *field_names)
23
+ > migrator.drop_constraints(model, *constraints)
24
+
25
+ """
26
+
27
+ from contextlib import suppress
28
+
29
+ import peewee as pw
30
+ from peewee_migrate import Migrator
31
+
32
+
33
+ with suppress(ImportError):
34
+ import playhouse.postgres_ext as pw_pext
35
+
36
+
37
+ def migrate(migrator: Migrator, database: pw.Database, *, fake=False):
38
+ """Write your migrations here."""
39
+
40
+ @migrator.create_model
41
+ class Function(pw.Model):
42
+ id = pw.TextField(unique=True)
43
+ user_id = pw.TextField()
44
+
45
+ name = pw.TextField()
46
+ type = pw.TextField()
47
+
48
+ content = pw.TextField()
49
+ meta = pw.TextField()
50
+
51
+ created_at = pw.BigIntegerField(null=False)
52
+ updated_at = pw.BigIntegerField(null=False)
53
+
54
+ class Meta:
55
+ table_name = "function"
56
+
57
+
58
+ def rollback(migrator: Migrator, database: pw.Database, *, fake=False):
59
+ """Write your rollback migrations here."""
60
+
61
+ migrator.remove_model("function")
backend/open_webui/internal/migrations/016_add_valves_and_is_active.py ADDED
@@ -0,0 +1,50 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Peewee migrations -- 009_add_models.py.
2
+
3
+ Some examples (model - class or model name)::
4
+
5
+ > Model = migrator.orm['table_name'] # Return model in current state by name
6
+ > Model = migrator.ModelClass # Return model in current state by name
7
+
8
+ > migrator.sql(sql) # Run custom SQL
9
+ > migrator.run(func, *args, **kwargs) # Run python function with the given args
10
+ > migrator.create_model(Model) # Create a model (could be used as decorator)
11
+ > migrator.remove_model(model, cascade=True) # Remove a model
12
+ > migrator.add_fields(model, **fields) # Add fields to a model
13
+ > migrator.change_fields(model, **fields) # Change fields
14
+ > migrator.remove_fields(model, *field_names, cascade=True)
15
+ > migrator.rename_field(model, old_field_name, new_field_name)
16
+ > migrator.rename_table(model, new_table_name)
17
+ > migrator.add_index(model, *col_names, unique=False)
18
+ > migrator.add_not_null(model, *field_names)
19
+ > migrator.add_default(model, field_name, default)
20
+ > migrator.add_constraint(model, name, sql)
21
+ > migrator.drop_index(model, *col_names)
22
+ > migrator.drop_not_null(model, *field_names)
23
+ > migrator.drop_constraints(model, *constraints)
24
+
25
+ """
26
+
27
+ from contextlib import suppress
28
+
29
+ import peewee as pw
30
+ from peewee_migrate import Migrator
31
+
32
+
33
+ with suppress(ImportError):
34
+ import playhouse.postgres_ext as pw_pext
35
+
36
+
37
+ def migrate(migrator: Migrator, database: pw.Database, *, fake=False):
38
+ """Write your migrations here."""
39
+
40
+ migrator.add_fields("tool", valves=pw.TextField(null=True))
41
+ migrator.add_fields("function", valves=pw.TextField(null=True))
42
+ migrator.add_fields("function", is_active=pw.BooleanField(default=False))
43
+
44
+
45
+ def rollback(migrator: Migrator, database: pw.Database, *, fake=False):
46
+ """Write your rollback migrations here."""
47
+
48
+ migrator.remove_fields("tool", "valves")
49
+ migrator.remove_fields("function", "valves")
50
+ migrator.remove_fields("function", "is_active")
backend/open_webui/internal/migrations/017_add_user_oauth_sub.py ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Peewee migrations -- 017_add_user_oauth_sub.py.
2
+ Some examples (model - class or model name)::
3
+ > Model = migrator.orm['table_name'] # Return model in current state by name
4
+ > Model = migrator.ModelClass # Return model in current state by name
5
+ > migrator.sql(sql) # Run custom SQL
6
+ > migrator.run(func, *args, **kwargs) # Run python function with the given args
7
+ > migrator.create_model(Model) # Create a model (could be used as decorator)
8
+ > migrator.remove_model(model, cascade=True) # Remove a model
9
+ > migrator.add_fields(model, **fields) # Add fields to a model
10
+ > migrator.change_fields(model, **fields) # Change fields
11
+ > migrator.remove_fields(model, *field_names, cascade=True)
12
+ > migrator.rename_field(model, old_field_name, new_field_name)
13
+ > migrator.rename_table(model, new_table_name)
14
+ > migrator.add_index(model, *col_names, unique=False)
15
+ > migrator.add_not_null(model, *field_names)
16
+ > migrator.add_default(model, field_name, default)
17
+ > migrator.add_constraint(model, name, sql)
18
+ > migrator.drop_index(model, *col_names)
19
+ > migrator.drop_not_null(model, *field_names)
20
+ > migrator.drop_constraints(model, *constraints)
21
+ """
22
+
23
+ from contextlib import suppress
24
+
25
+ import peewee as pw
26
+ from peewee_migrate import Migrator
27
+
28
+
29
+ with suppress(ImportError):
30
+ import playhouse.postgres_ext as pw_pext
31
+
32
+
33
+ def migrate(migrator: Migrator, database: pw.Database, *, fake=False):
34
+ """Write your migrations here."""
35
+
36
+ migrator.add_fields(
37
+ "user",
38
+ oauth_sub=pw.TextField(null=True, unique=True),
39
+ )
40
+
41
+
42
+ def rollback(migrator: Migrator, database: pw.Database, *, fake=False):
43
+ """Write your rollback migrations here."""
44
+
45
+ migrator.remove_fields("user", "oauth_sub")
backend/open_webui/internal/migrations/018_add_function_is_global.py ADDED
@@ -0,0 +1,49 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Peewee migrations -- 017_add_user_oauth_sub.py.
2
+
3
+ Some examples (model - class or model name)::
4
+
5
+ > Model = migrator.orm['table_name'] # Return model in current state by name
6
+ > Model = migrator.ModelClass # Return model in current state by name
7
+
8
+ > migrator.sql(sql) # Run custom SQL
9
+ > migrator.run(func, *args, **kwargs) # Run python function with the given args
10
+ > migrator.create_model(Model) # Create a model (could be used as decorator)
11
+ > migrator.remove_model(model, cascade=True) # Remove a model
12
+ > migrator.add_fields(model, **fields) # Add fields to a model
13
+ > migrator.change_fields(model, **fields) # Change fields
14
+ > migrator.remove_fields(model, *field_names, cascade=True)
15
+ > migrator.rename_field(model, old_field_name, new_field_name)
16
+ > migrator.rename_table(model, new_table_name)
17
+ > migrator.add_index(model, *col_names, unique=False)
18
+ > migrator.add_not_null(model, *field_names)
19
+ > migrator.add_default(model, field_name, default)
20
+ > migrator.add_constraint(model, name, sql)
21
+ > migrator.drop_index(model, *col_names)
22
+ > migrator.drop_not_null(model, *field_names)
23
+ > migrator.drop_constraints(model, *constraints)
24
+
25
+ """
26
+
27
+ from contextlib import suppress
28
+
29
+ import peewee as pw
30
+ from peewee_migrate import Migrator
31
+
32
+
33
+ with suppress(ImportError):
34
+ import playhouse.postgres_ext as pw_pext
35
+
36
+
37
+ def migrate(migrator: Migrator, database: pw.Database, *, fake=False):
38
+ """Write your migrations here."""
39
+
40
+ migrator.add_fields(
41
+ "function",
42
+ is_global=pw.BooleanField(default=False),
43
+ )
44
+
45
+
46
+ def rollback(migrator: Migrator, database: pw.Database, *, fake=False):
47
+ """Write your rollback migrations here."""
48
+
49
+ migrator.remove_fields("function", "is_global")
backend/open_webui/internal/wrappers.py ADDED
@@ -0,0 +1,66 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import logging
2
+ from contextvars import ContextVar
3
+
4
+ from open_webui.env import SRC_LOG_LEVELS
5
+ from peewee import *
6
+ from peewee import InterfaceError as PeeWeeInterfaceError
7
+ from peewee import PostgresqlDatabase
8
+ from playhouse.db_url import connect, parse
9
+ from playhouse.shortcuts import ReconnectMixin
10
+
11
+ log = logging.getLogger(__name__)
12
+ log.setLevel(SRC_LOG_LEVELS["DB"])
13
+
14
+ db_state_default = {"closed": None, "conn": None, "ctx": None, "transactions": None}
15
+ db_state = ContextVar("db_state", default=db_state_default.copy())
16
+
17
+
18
+ class PeeweeConnectionState(object):
19
+ def __init__(self, **kwargs):
20
+ super().__setattr__("_state", db_state)
21
+ super().__init__(**kwargs)
22
+
23
+ def __setattr__(self, name, value):
24
+ self._state.get()[name] = value
25
+
26
+ def __getattr__(self, name):
27
+ value = self._state.get()[name]
28
+ return value
29
+
30
+
31
+ class CustomReconnectMixin(ReconnectMixin):
32
+ reconnect_errors = (
33
+ # psycopg2
34
+ (OperationalError, "termin"),
35
+ (InterfaceError, "closed"),
36
+ # peewee
37
+ (PeeWeeInterfaceError, "closed"),
38
+ )
39
+
40
+
41
+ class ReconnectingPostgresqlDatabase(CustomReconnectMixin, PostgresqlDatabase):
42
+ pass
43
+
44
+
45
+ def register_connection(db_url):
46
+ db = connect(db_url, unquote_password=True)
47
+ if isinstance(db, PostgresqlDatabase):
48
+ # Enable autoconnect for SQLite databases, managed by Peewee
49
+ db.autoconnect = True
50
+ db.reuse_if_open = True
51
+ log.info("Connected to PostgreSQL database")
52
+
53
+ # Get the connection details
54
+ connection = parse(db_url, unquote_password=True)
55
+
56
+ # Use our custom database class that supports reconnection
57
+ db = ReconnectingPostgresqlDatabase(**connection)
58
+ db.connect(reuse_if_open=True)
59
+ elif isinstance(db, SqliteDatabase):
60
+ # Enable autoconnect for SQLite databases, managed by Peewee
61
+ db.autoconnect = True
62
+ db.reuse_if_open = True
63
+ log.info("Connected to SQLite database")
64
+ else:
65
+ raise ValueError("Unsupported database connection")
66
+ return db
backend/open_webui/main.py ADDED
@@ -0,0 +1,1350 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import asyncio
2
+ import inspect
3
+ import json
4
+ import logging
5
+ import mimetypes
6
+ import os
7
+ import shutil
8
+ import sys
9
+ import time
10
+ import random
11
+
12
+ from contextlib import asynccontextmanager
13
+ from urllib.parse import urlencode, parse_qs, urlparse
14
+ from pydantic import BaseModel
15
+ from sqlalchemy import text
16
+
17
+ from typing import Optional
18
+ from aiocache import cached
19
+ import aiohttp
20
+ import requests
21
+
22
+
23
+ from fastapi import (
24
+ Depends,
25
+ FastAPI,
26
+ File,
27
+ Form,
28
+ HTTPException,
29
+ Request,
30
+ UploadFile,
31
+ status,
32
+ applications,
33
+ BackgroundTasks,
34
+ )
35
+
36
+ from fastapi.openapi.docs import get_swagger_ui_html
37
+
38
+ from fastapi.middleware.cors import CORSMiddleware
39
+ from fastapi.responses import JSONResponse, RedirectResponse
40
+ from fastapi.staticfiles import StaticFiles
41
+
42
+ from starlette.exceptions import HTTPException as StarletteHTTPException
43
+ from starlette.middleware.base import BaseHTTPMiddleware
44
+ from starlette.middleware.sessions import SessionMiddleware
45
+ from starlette.responses import Response, StreamingResponse
46
+
47
+
48
+ from open_webui.socket.main import (
49
+ app as socket_app,
50
+ periodic_usage_pool_cleanup,
51
+ )
52
+ from open_webui.routers import (
53
+ audio,
54
+ images,
55
+ ollama,
56
+ openai,
57
+ retrieval,
58
+ pipelines,
59
+ tasks,
60
+ auths,
61
+ channels,
62
+ chats,
63
+ folders,
64
+ configs,
65
+ groups,
66
+ files,
67
+ functions,
68
+ memories,
69
+ models,
70
+ knowledge,
71
+ prompts,
72
+ evaluations,
73
+ tools,
74
+ users,
75
+ utils,
76
+ )
77
+
78
+ from open_webui.routers.retrieval import (
79
+ get_embedding_function,
80
+ get_ef,
81
+ get_rf,
82
+ )
83
+
84
+ from open_webui.internal.db import Session
85
+
86
+ from open_webui.models.functions import Functions
87
+ from open_webui.models.models import Models
88
+ from open_webui.models.users import UserModel, Users
89
+
90
+ from open_webui.config import (
91
+ LICENSE_KEY,
92
+ # Ollama
93
+ ENABLE_OLLAMA_API,
94
+ OLLAMA_BASE_URLS,
95
+ OLLAMA_API_CONFIGS,
96
+ # OpenAI
97
+ ENABLE_OPENAI_API,
98
+ OPENAI_API_BASE_URLS,
99
+ OPENAI_API_KEYS,
100
+ OPENAI_API_CONFIGS,
101
+ # Direct Connections
102
+ ENABLE_DIRECT_CONNECTIONS,
103
+ # Code Execution
104
+ CODE_EXECUTION_ENGINE,
105
+ CODE_EXECUTION_JUPYTER_URL,
106
+ CODE_EXECUTION_JUPYTER_AUTH,
107
+ CODE_EXECUTION_JUPYTER_AUTH_TOKEN,
108
+ CODE_EXECUTION_JUPYTER_AUTH_PASSWORD,
109
+ CODE_EXECUTION_JUPYTER_TIMEOUT,
110
+ ENABLE_CODE_INTERPRETER,
111
+ CODE_INTERPRETER_ENGINE,
112
+ CODE_INTERPRETER_PROMPT_TEMPLATE,
113
+ CODE_INTERPRETER_JUPYTER_URL,
114
+ CODE_INTERPRETER_JUPYTER_AUTH,
115
+ CODE_INTERPRETER_JUPYTER_AUTH_TOKEN,
116
+ CODE_INTERPRETER_JUPYTER_AUTH_PASSWORD,
117
+ CODE_INTERPRETER_JUPYTER_TIMEOUT,
118
+ # Image
119
+ AUTOMATIC1111_API_AUTH,
120
+ AUTOMATIC1111_BASE_URL,
121
+ AUTOMATIC1111_CFG_SCALE,
122
+ AUTOMATIC1111_SAMPLER,
123
+ AUTOMATIC1111_SCHEDULER,
124
+ COMFYUI_BASE_URL,
125
+ COMFYUI_API_KEY,
126
+ COMFYUI_WORKFLOW,
127
+ COMFYUI_WORKFLOW_NODES,
128
+ ENABLE_IMAGE_GENERATION,
129
+ ENABLE_IMAGE_PROMPT_GENERATION,
130
+ IMAGE_GENERATION_ENGINE,
131
+ IMAGE_GENERATION_MODEL,
132
+ IMAGE_SIZE,
133
+ IMAGE_STEPS,
134
+ IMAGES_OPENAI_API_BASE_URL,
135
+ IMAGES_OPENAI_API_KEY,
136
+ IMAGES_GEMINI_API_BASE_URL,
137
+ IMAGES_GEMINI_API_KEY,
138
+ # Audio
139
+ AUDIO_STT_ENGINE,
140
+ AUDIO_STT_MODEL,
141
+ AUDIO_STT_OPENAI_API_BASE_URL,
142
+ AUDIO_STT_OPENAI_API_KEY,
143
+ AUDIO_TTS_API_KEY,
144
+ AUDIO_TTS_ENGINE,
145
+ AUDIO_TTS_MODEL,
146
+ AUDIO_TTS_OPENAI_API_BASE_URL,
147
+ AUDIO_TTS_OPENAI_API_KEY,
148
+ AUDIO_TTS_SPLIT_ON,
149
+ AUDIO_TTS_VOICE,
150
+ AUDIO_TTS_AZURE_SPEECH_REGION,
151
+ AUDIO_TTS_AZURE_SPEECH_OUTPUT_FORMAT,
152
+ PLAYWRIGHT_WS_URI,
153
+ FIRECRAWL_API_BASE_URL,
154
+ FIRECRAWL_API_KEY,
155
+ RAG_WEB_LOADER_ENGINE,
156
+ WHISPER_MODEL,
157
+ DEEPGRAM_API_KEY,
158
+ WHISPER_MODEL_AUTO_UPDATE,
159
+ WHISPER_MODEL_DIR,
160
+ # Retrieval
161
+ RAG_TEMPLATE,
162
+ DEFAULT_RAG_TEMPLATE,
163
+ RAG_FULL_CONTEXT,
164
+ RAG_EMBEDDING_MODEL,
165
+ RAG_EMBEDDING_MODEL_AUTO_UPDATE,
166
+ RAG_EMBEDDING_MODEL_TRUST_REMOTE_CODE,
167
+ RAG_RERANKING_MODEL,
168
+ RAG_RERANKING_MODEL_AUTO_UPDATE,
169
+ RAG_RERANKING_MODEL_TRUST_REMOTE_CODE,
170
+ RAG_EMBEDDING_ENGINE,
171
+ RAG_EMBEDDING_BATCH_SIZE,
172
+ RAG_RELEVANCE_THRESHOLD,
173
+ RAG_FILE_MAX_COUNT,
174
+ RAG_FILE_MAX_SIZE,
175
+ RAG_OPENAI_API_BASE_URL,
176
+ RAG_OPENAI_API_KEY,
177
+ RAG_OLLAMA_BASE_URL,
178
+ RAG_OLLAMA_API_KEY,
179
+ CHUNK_OVERLAP,
180
+ CHUNK_SIZE,
181
+ CONTENT_EXTRACTION_ENGINE,
182
+ TIKA_SERVER_URL,
183
+ RAG_TOP_K,
184
+ RAG_TEXT_SPLITTER,
185
+ TIKTOKEN_ENCODING_NAME,
186
+ PDF_EXTRACT_IMAGES,
187
+ YOUTUBE_LOADER_LANGUAGE,
188
+ YOUTUBE_LOADER_PROXY_URL,
189
+ # Retrieval (Web Search)
190
+ RAG_WEB_SEARCH_ENGINE,
191
+ RAG_WEB_SEARCH_FULL_CONTEXT,
192
+ RAG_WEB_SEARCH_RESULT_COUNT,
193
+ RAG_WEB_SEARCH_CONCURRENT_REQUESTS,
194
+ RAG_WEB_SEARCH_TRUST_ENV,
195
+ RAG_WEB_SEARCH_DOMAIN_FILTER_LIST,
196
+ JINA_API_KEY,
197
+ SEARCHAPI_API_KEY,
198
+ SEARCHAPI_ENGINE,
199
+ SERPAPI_API_KEY,
200
+ SERPAPI_ENGINE,
201
+ SEARXNG_QUERY_URL,
202
+ SERPER_API_KEY,
203
+ SERPLY_API_KEY,
204
+ SERPSTACK_API_KEY,
205
+ SERPSTACK_HTTPS,
206
+ TAVILY_API_KEY,
207
+ BING_SEARCH_V7_ENDPOINT,
208
+ BING_SEARCH_V7_SUBSCRIPTION_KEY,
209
+ BRAVE_SEARCH_API_KEY,
210
+ EXA_API_KEY,
211
+ KAGI_SEARCH_API_KEY,
212
+ MOJEEK_SEARCH_API_KEY,
213
+ BOCHA_SEARCH_API_KEY,
214
+ GOOGLE_PSE_API_KEY,
215
+ GOOGLE_PSE_ENGINE_ID,
216
+ GOOGLE_DRIVE_CLIENT_ID,
217
+ GOOGLE_DRIVE_API_KEY,
218
+ ENABLE_RAG_HYBRID_SEARCH,
219
+ ENABLE_RAG_LOCAL_WEB_FETCH,
220
+ ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION,
221
+ ENABLE_RAG_WEB_SEARCH,
222
+ ENABLE_GOOGLE_DRIVE_INTEGRATION,
223
+ UPLOAD_DIR,
224
+ # WebUI
225
+ WEBUI_AUTH,
226
+ WEBUI_NAME,
227
+ WEBUI_BANNERS,
228
+ WEBHOOK_URL,
229
+ ADMIN_EMAIL,
230
+ SHOW_ADMIN_DETAILS,
231
+ JWT_EXPIRES_IN,
232
+ ENABLE_SIGNUP,
233
+ ENABLE_LOGIN_FORM,
234
+ ENABLE_API_KEY,
235
+ ENABLE_API_KEY_ENDPOINT_RESTRICTIONS,
236
+ API_KEY_ALLOWED_ENDPOINTS,
237
+ ENABLE_CHANNELS,
238
+ ENABLE_COMMUNITY_SHARING,
239
+ ENABLE_MESSAGE_RATING,
240
+ ENABLE_EVALUATION_ARENA_MODELS,
241
+ USER_PERMISSIONS,
242
+ DEFAULT_USER_ROLE,
243
+ DEFAULT_PROMPT_SUGGESTIONS,
244
+ DEFAULT_MODELS,
245
+ DEFAULT_ARENA_MODEL,
246
+ MODEL_ORDER_LIST,
247
+ EVALUATION_ARENA_MODELS,
248
+ # WebUI (OAuth)
249
+ ENABLE_OAUTH_ROLE_MANAGEMENT,
250
+ OAUTH_ROLES_CLAIM,
251
+ OAUTH_EMAIL_CLAIM,
252
+ OAUTH_PICTURE_CLAIM,
253
+ OAUTH_USERNAME_CLAIM,
254
+ OAUTH_ALLOWED_ROLES,
255
+ OAUTH_ADMIN_ROLES,
256
+ # WebUI (LDAP)
257
+ ENABLE_LDAP,
258
+ LDAP_SERVER_LABEL,
259
+ LDAP_SERVER_HOST,
260
+ LDAP_SERVER_PORT,
261
+ LDAP_ATTRIBUTE_FOR_MAIL,
262
+ LDAP_ATTRIBUTE_FOR_USERNAME,
263
+ LDAP_SEARCH_FILTERS,
264
+ LDAP_SEARCH_BASE,
265
+ LDAP_APP_DN,
266
+ LDAP_APP_PASSWORD,
267
+ LDAP_USE_TLS,
268
+ LDAP_CA_CERT_FILE,
269
+ LDAP_CIPHERS,
270
+ # Misc
271
+ ENV,
272
+ CACHE_DIR,
273
+ STATIC_DIR,
274
+ FRONTEND_BUILD_DIR,
275
+ CORS_ALLOW_ORIGIN,
276
+ DEFAULT_LOCALE,
277
+ OAUTH_PROVIDERS,
278
+ WEBUI_URL,
279
+ # Admin
280
+ ENABLE_ADMIN_CHAT_ACCESS,
281
+ ENABLE_ADMIN_EXPORT,
282
+ # Tasks
283
+ TASK_MODEL,
284
+ TASK_MODEL_EXTERNAL,
285
+ ENABLE_TAGS_GENERATION,
286
+ ENABLE_TITLE_GENERATION,
287
+ ENABLE_SEARCH_QUERY_GENERATION,
288
+ ENABLE_RETRIEVAL_QUERY_GENERATION,
289
+ ENABLE_AUTOCOMPLETE_GENERATION,
290
+ TITLE_GENERATION_PROMPT_TEMPLATE,
291
+ TAGS_GENERATION_PROMPT_TEMPLATE,
292
+ IMAGE_PROMPT_GENERATION_PROMPT_TEMPLATE,
293
+ TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE,
294
+ QUERY_GENERATION_PROMPT_TEMPLATE,
295
+ AUTOCOMPLETE_GENERATION_PROMPT_TEMPLATE,
296
+ AUTOCOMPLETE_GENERATION_INPUT_MAX_LENGTH,
297
+ AppConfig,
298
+ reset_config,
299
+ )
300
+ from open_webui.env import (
301
+ CHANGELOG,
302
+ GLOBAL_LOG_LEVEL,
303
+ SAFE_MODE,
304
+ SRC_LOG_LEVELS,
305
+ VERSION,
306
+ WEBUI_BUILD_HASH,
307
+ WEBUI_SECRET_KEY,
308
+ WEBUI_SESSION_COOKIE_SAME_SITE,
309
+ WEBUI_SESSION_COOKIE_SECURE,
310
+ WEBUI_AUTH_TRUSTED_EMAIL_HEADER,
311
+ WEBUI_AUTH_TRUSTED_NAME_HEADER,
312
+ ENABLE_WEBSOCKET_SUPPORT,
313
+ BYPASS_MODEL_ACCESS_CONTROL,
314
+ RESET_CONFIG_ON_START,
315
+ OFFLINE_MODE,
316
+ )
317
+
318
+
319
+ from open_webui.utils.models import (
320
+ get_all_models,
321
+ get_all_base_models,
322
+ check_model_access,
323
+ )
324
+ from open_webui.utils.chat import (
325
+ generate_chat_completion as chat_completion_handler,
326
+ chat_completed as chat_completed_handler,
327
+ chat_action as chat_action_handler,
328
+ )
329
+ from open_webui.utils.middleware import process_chat_payload, process_chat_response
330
+ from open_webui.utils.access_control import has_access
331
+
332
+ from open_webui.utils.auth import (
333
+ get_license_data,
334
+ decode_token,
335
+ get_admin_user,
336
+ get_verified_user,
337
+ )
338
+ from open_webui.utils.oauth import OAuthManager
339
+ from open_webui.utils.security_headers import SecurityHeadersMiddleware
340
+
341
+ from open_webui.tasks import stop_task, list_tasks # Import from tasks.py
342
+
343
+
344
+ if SAFE_MODE:
345
+ print("SAFE MODE ENABLED")
346
+ Functions.deactivate_all_functions()
347
+
348
+ logging.basicConfig(stream=sys.stdout, level=GLOBAL_LOG_LEVEL)
349
+ log = logging.getLogger(__name__)
350
+ log.setLevel(SRC_LOG_LEVELS["MAIN"])
351
+
352
+
353
+ class SPAStaticFiles(StaticFiles):
354
+ async def get_response(self, path: str, scope):
355
+ try:
356
+ return await super().get_response(path, scope)
357
+ except (HTTPException, StarletteHTTPException) as ex:
358
+ if ex.status_code == 404:
359
+ if path.endswith(".js"):
360
+ # Return 404 for javascript files
361
+ raise ex
362
+ else:
363
+ return await super().get_response("index.html", scope)
364
+ else:
365
+ raise ex
366
+
367
+
368
+ print(
369
+ rf"""
370
+ ██████╗ ██████╗ ███████╗███╗ ██╗ ██╗ ██╗███████╗██████╗ ██╗ ██╗██╗
371
+ ██╔═══██╗██╔══██╗██╔════╝████╗ ██║ ██║ ██║██╔════╝██╔══██╗██║ ██║██║
372
+ ██║ ██║██████╔╝█████╗ ██╔██╗ ██║ ██║ █╗ ██║█████╗ ██████╔╝██║ ██║██║
373
+ ██║ ██║██╔═══╝ ██╔══╝ ██║╚██╗██║ ██║███╗██║██╔══╝ ��█╔══██╗██║ ██║██║
374
+ ╚██████╔╝██║ ███████╗██║ ╚████║ ╚███╔███╔╝███████╗██████╔╝╚██████╔╝██║
375
+ ╚═════╝ ╚═╝ ╚══════╝╚═╝ ╚═══╝ ╚══╝╚══╝ ╚══════╝╚═════╝ ╚═════╝ ╚═╝
376
+
377
+
378
+ v{VERSION} - building the best open-source AI user interface.
379
+ {f"Commit: {WEBUI_BUILD_HASH}" if WEBUI_BUILD_HASH != "dev-build" else ""}
380
+ https://github.com/open-webui/open-webui
381
+ """
382
+ )
383
+
384
+
385
+ @asynccontextmanager
386
+ async def lifespan(app: FastAPI):
387
+ if RESET_CONFIG_ON_START:
388
+ reset_config()
389
+
390
+ if app.state.config.LICENSE_KEY:
391
+ get_license_data(app, app.state.config.LICENSE_KEY)
392
+
393
+ asyncio.create_task(periodic_usage_pool_cleanup())
394
+ yield
395
+
396
+
397
+ app = FastAPI(
398
+ docs_url="/docs" if ENV == "dev" else None,
399
+ openapi_url="/openapi.json" if ENV == "dev" else None,
400
+ redoc_url=None,
401
+ lifespan=lifespan,
402
+ )
403
+
404
+ oauth_manager = OAuthManager(app)
405
+
406
+ app.state.config = AppConfig()
407
+
408
+ app.state.WEBUI_NAME = WEBUI_NAME
409
+ app.state.config.LICENSE_KEY = LICENSE_KEY
410
+
411
+ ########################################
412
+ #
413
+ # OLLAMA
414
+ #
415
+ ########################################
416
+
417
+
418
+ app.state.config.ENABLE_OLLAMA_API = ENABLE_OLLAMA_API
419
+ app.state.config.OLLAMA_BASE_URLS = OLLAMA_BASE_URLS
420
+ app.state.config.OLLAMA_API_CONFIGS = OLLAMA_API_CONFIGS
421
+
422
+ app.state.OLLAMA_MODELS = {}
423
+
424
+ ########################################
425
+ #
426
+ # OPENAI
427
+ #
428
+ ########################################
429
+
430
+ app.state.config.ENABLE_OPENAI_API = ENABLE_OPENAI_API
431
+ app.state.config.OPENAI_API_BASE_URLS = OPENAI_API_BASE_URLS
432
+ app.state.config.OPENAI_API_KEYS = OPENAI_API_KEYS
433
+ app.state.config.OPENAI_API_CONFIGS = OPENAI_API_CONFIGS
434
+
435
+ app.state.OPENAI_MODELS = {}
436
+
437
+ ########################################
438
+ #
439
+ # DIRECT CONNECTIONS
440
+ #
441
+ ########################################
442
+
443
+ app.state.config.ENABLE_DIRECT_CONNECTIONS = ENABLE_DIRECT_CONNECTIONS
444
+
445
+ ########################################
446
+ #
447
+ # WEBUI
448
+ #
449
+ ########################################
450
+
451
+ app.state.config.WEBUI_URL = WEBUI_URL
452
+ app.state.config.ENABLE_SIGNUP = ENABLE_SIGNUP
453
+ app.state.config.ENABLE_LOGIN_FORM = ENABLE_LOGIN_FORM
454
+
455
+ app.state.config.ENABLE_API_KEY = ENABLE_API_KEY
456
+ app.state.config.ENABLE_API_KEY_ENDPOINT_RESTRICTIONS = (
457
+ ENABLE_API_KEY_ENDPOINT_RESTRICTIONS
458
+ )
459
+ app.state.config.API_KEY_ALLOWED_ENDPOINTS = API_KEY_ALLOWED_ENDPOINTS
460
+
461
+ app.state.config.JWT_EXPIRES_IN = JWT_EXPIRES_IN
462
+
463
+ app.state.config.SHOW_ADMIN_DETAILS = SHOW_ADMIN_DETAILS
464
+ app.state.config.ADMIN_EMAIL = ADMIN_EMAIL
465
+
466
+
467
+ app.state.config.DEFAULT_MODELS = DEFAULT_MODELS
468
+ app.state.config.DEFAULT_PROMPT_SUGGESTIONS = DEFAULT_PROMPT_SUGGESTIONS
469
+ app.state.config.DEFAULT_USER_ROLE = DEFAULT_USER_ROLE
470
+
471
+ app.state.config.USER_PERMISSIONS = USER_PERMISSIONS
472
+ app.state.config.WEBHOOK_URL = WEBHOOK_URL
473
+ app.state.config.BANNERS = WEBUI_BANNERS
474
+ app.state.config.MODEL_ORDER_LIST = MODEL_ORDER_LIST
475
+
476
+
477
+ app.state.config.ENABLE_CHANNELS = ENABLE_CHANNELS
478
+ app.state.config.ENABLE_COMMUNITY_SHARING = ENABLE_COMMUNITY_SHARING
479
+ app.state.config.ENABLE_MESSAGE_RATING = ENABLE_MESSAGE_RATING
480
+
481
+ app.state.config.ENABLE_EVALUATION_ARENA_MODELS = ENABLE_EVALUATION_ARENA_MODELS
482
+ app.state.config.EVALUATION_ARENA_MODELS = EVALUATION_ARENA_MODELS
483
+
484
+ app.state.config.OAUTH_USERNAME_CLAIM = OAUTH_USERNAME_CLAIM
485
+ app.state.config.OAUTH_PICTURE_CLAIM = OAUTH_PICTURE_CLAIM
486
+ app.state.config.OAUTH_EMAIL_CLAIM = OAUTH_EMAIL_CLAIM
487
+
488
+ app.state.config.ENABLE_OAUTH_ROLE_MANAGEMENT = ENABLE_OAUTH_ROLE_MANAGEMENT
489
+ app.state.config.OAUTH_ROLES_CLAIM = OAUTH_ROLES_CLAIM
490
+ app.state.config.OAUTH_ALLOWED_ROLES = OAUTH_ALLOWED_ROLES
491
+ app.state.config.OAUTH_ADMIN_ROLES = OAUTH_ADMIN_ROLES
492
+
493
+ app.state.config.ENABLE_LDAP = ENABLE_LDAP
494
+ app.state.config.LDAP_SERVER_LABEL = LDAP_SERVER_LABEL
495
+ app.state.config.LDAP_SERVER_HOST = LDAP_SERVER_HOST
496
+ app.state.config.LDAP_SERVER_PORT = LDAP_SERVER_PORT
497
+ app.state.config.LDAP_ATTRIBUTE_FOR_MAIL = LDAP_ATTRIBUTE_FOR_MAIL
498
+ app.state.config.LDAP_ATTRIBUTE_FOR_USERNAME = LDAP_ATTRIBUTE_FOR_USERNAME
499
+ app.state.config.LDAP_APP_DN = LDAP_APP_DN
500
+ app.state.config.LDAP_APP_PASSWORD = LDAP_APP_PASSWORD
501
+ app.state.config.LDAP_SEARCH_BASE = LDAP_SEARCH_BASE
502
+ app.state.config.LDAP_SEARCH_FILTERS = LDAP_SEARCH_FILTERS
503
+ app.state.config.LDAP_USE_TLS = LDAP_USE_TLS
504
+ app.state.config.LDAP_CA_CERT_FILE = LDAP_CA_CERT_FILE
505
+ app.state.config.LDAP_CIPHERS = LDAP_CIPHERS
506
+
507
+
508
+ app.state.AUTH_TRUSTED_EMAIL_HEADER = WEBUI_AUTH_TRUSTED_EMAIL_HEADER
509
+ app.state.AUTH_TRUSTED_NAME_HEADER = WEBUI_AUTH_TRUSTED_NAME_HEADER
510
+
511
+ app.state.USER_COUNT = None
512
+ app.state.TOOLS = {}
513
+ app.state.FUNCTIONS = {}
514
+
515
+ ########################################
516
+ #
517
+ # RETRIEVAL
518
+ #
519
+ ########################################
520
+
521
+
522
+ app.state.config.TOP_K = RAG_TOP_K
523
+ app.state.config.RELEVANCE_THRESHOLD = RAG_RELEVANCE_THRESHOLD
524
+ app.state.config.FILE_MAX_SIZE = RAG_FILE_MAX_SIZE
525
+ app.state.config.FILE_MAX_COUNT = RAG_FILE_MAX_COUNT
526
+
527
+
528
+ app.state.config.RAG_FULL_CONTEXT = RAG_FULL_CONTEXT
529
+ app.state.config.ENABLE_RAG_HYBRID_SEARCH = ENABLE_RAG_HYBRID_SEARCH
530
+ app.state.config.ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION = (
531
+ ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION
532
+ )
533
+
534
+ app.state.config.CONTENT_EXTRACTION_ENGINE = CONTENT_EXTRACTION_ENGINE
535
+ app.state.config.TIKA_SERVER_URL = TIKA_SERVER_URL
536
+
537
+ app.state.config.TEXT_SPLITTER = RAG_TEXT_SPLITTER
538
+ app.state.config.TIKTOKEN_ENCODING_NAME = TIKTOKEN_ENCODING_NAME
539
+
540
+ app.state.config.CHUNK_SIZE = CHUNK_SIZE
541
+ app.state.config.CHUNK_OVERLAP = CHUNK_OVERLAP
542
+
543
+ app.state.config.RAG_EMBEDDING_ENGINE = RAG_EMBEDDING_ENGINE
544
+ app.state.config.RAG_EMBEDDING_MODEL = RAG_EMBEDDING_MODEL
545
+ app.state.config.RAG_EMBEDDING_BATCH_SIZE = RAG_EMBEDDING_BATCH_SIZE
546
+ app.state.config.RAG_RERANKING_MODEL = RAG_RERANKING_MODEL
547
+ app.state.config.RAG_TEMPLATE = RAG_TEMPLATE
548
+
549
+ app.state.config.RAG_OPENAI_API_BASE_URL = RAG_OPENAI_API_BASE_URL
550
+ app.state.config.RAG_OPENAI_API_KEY = RAG_OPENAI_API_KEY
551
+
552
+ app.state.config.RAG_OLLAMA_BASE_URL = RAG_OLLAMA_BASE_URL
553
+ app.state.config.RAG_OLLAMA_API_KEY = RAG_OLLAMA_API_KEY
554
+
555
+ app.state.config.PDF_EXTRACT_IMAGES = PDF_EXTRACT_IMAGES
556
+
557
+ app.state.config.YOUTUBE_LOADER_LANGUAGE = YOUTUBE_LOADER_LANGUAGE
558
+ app.state.config.YOUTUBE_LOADER_PROXY_URL = YOUTUBE_LOADER_PROXY_URL
559
+
560
+
561
+ app.state.config.ENABLE_RAG_WEB_SEARCH = ENABLE_RAG_WEB_SEARCH
562
+ app.state.config.RAG_WEB_SEARCH_ENGINE = RAG_WEB_SEARCH_ENGINE
563
+ app.state.config.RAG_WEB_SEARCH_FULL_CONTEXT = RAG_WEB_SEARCH_FULL_CONTEXT
564
+ app.state.config.RAG_WEB_SEARCH_DOMAIN_FILTER_LIST = RAG_WEB_SEARCH_DOMAIN_FILTER_LIST
565
+
566
+ app.state.config.ENABLE_GOOGLE_DRIVE_INTEGRATION = ENABLE_GOOGLE_DRIVE_INTEGRATION
567
+ app.state.config.SEARXNG_QUERY_URL = SEARXNG_QUERY_URL
568
+ app.state.config.GOOGLE_PSE_API_KEY = GOOGLE_PSE_API_KEY
569
+ app.state.config.GOOGLE_PSE_ENGINE_ID = GOOGLE_PSE_ENGINE_ID
570
+ app.state.config.BRAVE_SEARCH_API_KEY = BRAVE_SEARCH_API_KEY
571
+ app.state.config.KAGI_SEARCH_API_KEY = KAGI_SEARCH_API_KEY
572
+ app.state.config.MOJEEK_SEARCH_API_KEY = MOJEEK_SEARCH_API_KEY
573
+ app.state.config.BOCHA_SEARCH_API_KEY = BOCHA_SEARCH_API_KEY
574
+ app.state.config.SERPSTACK_API_KEY = SERPSTACK_API_KEY
575
+ app.state.config.SERPSTACK_HTTPS = SERPSTACK_HTTPS
576
+ app.state.config.SERPER_API_KEY = SERPER_API_KEY
577
+ app.state.config.SERPLY_API_KEY = SERPLY_API_KEY
578
+ app.state.config.TAVILY_API_KEY = TAVILY_API_KEY
579
+ app.state.config.SEARCHAPI_API_KEY = SEARCHAPI_API_KEY
580
+ app.state.config.SEARCHAPI_ENGINE = SEARCHAPI_ENGINE
581
+ app.state.config.SERPAPI_API_KEY = SERPAPI_API_KEY
582
+ app.state.config.SERPAPI_ENGINE = SERPAPI_ENGINE
583
+ app.state.config.JINA_API_KEY = JINA_API_KEY
584
+ app.state.config.BING_SEARCH_V7_ENDPOINT = BING_SEARCH_V7_ENDPOINT
585
+ app.state.config.BING_SEARCH_V7_SUBSCRIPTION_KEY = BING_SEARCH_V7_SUBSCRIPTION_KEY
586
+ app.state.config.EXA_API_KEY = EXA_API_KEY
587
+
588
+ app.state.config.RAG_WEB_SEARCH_RESULT_COUNT = RAG_WEB_SEARCH_RESULT_COUNT
589
+ app.state.config.RAG_WEB_SEARCH_CONCURRENT_REQUESTS = RAG_WEB_SEARCH_CONCURRENT_REQUESTS
590
+ app.state.config.RAG_WEB_LOADER_ENGINE = RAG_WEB_LOADER_ENGINE
591
+ app.state.config.RAG_WEB_SEARCH_TRUST_ENV = RAG_WEB_SEARCH_TRUST_ENV
592
+ app.state.config.PLAYWRIGHT_WS_URI = PLAYWRIGHT_WS_URI
593
+ app.state.config.FIRECRAWL_API_BASE_URL = FIRECRAWL_API_BASE_URL
594
+ app.state.config.FIRECRAWL_API_KEY = FIRECRAWL_API_KEY
595
+
596
+ app.state.EMBEDDING_FUNCTION = None
597
+ app.state.ef = None
598
+ app.state.rf = None
599
+
600
+ app.state.YOUTUBE_LOADER_TRANSLATION = None
601
+
602
+
603
+ try:
604
+ app.state.ef = get_ef(
605
+ app.state.config.RAG_EMBEDDING_ENGINE,
606
+ app.state.config.RAG_EMBEDDING_MODEL,
607
+ RAG_EMBEDDING_MODEL_AUTO_UPDATE,
608
+ )
609
+
610
+ app.state.rf = get_rf(
611
+ app.state.config.RAG_RERANKING_MODEL,
612
+ RAG_RERANKING_MODEL_AUTO_UPDATE,
613
+ )
614
+ except Exception as e:
615
+ log.error(f"Error updating models: {e}")
616
+ pass
617
+
618
+
619
+ app.state.EMBEDDING_FUNCTION = get_embedding_function(
620
+ app.state.config.RAG_EMBEDDING_ENGINE,
621
+ app.state.config.RAG_EMBEDDING_MODEL,
622
+ app.state.ef,
623
+ (
624
+ app.state.config.RAG_OPENAI_API_BASE_URL
625
+ if app.state.config.RAG_EMBEDDING_ENGINE == "openai"
626
+ else app.state.config.RAG_OLLAMA_BASE_URL
627
+ ),
628
+ (
629
+ app.state.config.RAG_OPENAI_API_KEY
630
+ if app.state.config.RAG_EMBEDDING_ENGINE == "openai"
631
+ else app.state.config.RAG_OLLAMA_API_KEY
632
+ ),
633
+ app.state.config.RAG_EMBEDDING_BATCH_SIZE,
634
+ )
635
+
636
+ ########################################
637
+ #
638
+ # CODE EXECUTION
639
+ #
640
+ ########################################
641
+
642
+ app.state.config.CODE_EXECUTION_ENGINE = CODE_EXECUTION_ENGINE
643
+ app.state.config.CODE_EXECUTION_JUPYTER_URL = CODE_EXECUTION_JUPYTER_URL
644
+ app.state.config.CODE_EXECUTION_JUPYTER_AUTH = CODE_EXECUTION_JUPYTER_AUTH
645
+ app.state.config.CODE_EXECUTION_JUPYTER_AUTH_TOKEN = CODE_EXECUTION_JUPYTER_AUTH_TOKEN
646
+ app.state.config.CODE_EXECUTION_JUPYTER_AUTH_PASSWORD = (
647
+ CODE_EXECUTION_JUPYTER_AUTH_PASSWORD
648
+ )
649
+ app.state.config.CODE_EXECUTION_JUPYTER_TIMEOUT = CODE_EXECUTION_JUPYTER_TIMEOUT
650
+
651
+ app.state.config.ENABLE_CODE_INTERPRETER = ENABLE_CODE_INTERPRETER
652
+ app.state.config.CODE_INTERPRETER_ENGINE = CODE_INTERPRETER_ENGINE
653
+ app.state.config.CODE_INTERPRETER_PROMPT_TEMPLATE = CODE_INTERPRETER_PROMPT_TEMPLATE
654
+
655
+ app.state.config.CODE_INTERPRETER_JUPYTER_URL = CODE_INTERPRETER_JUPYTER_URL
656
+ app.state.config.CODE_INTERPRETER_JUPYTER_AUTH = CODE_INTERPRETER_JUPYTER_AUTH
657
+ app.state.config.CODE_INTERPRETER_JUPYTER_AUTH_TOKEN = (
658
+ CODE_INTERPRETER_JUPYTER_AUTH_TOKEN
659
+ )
660
+ app.state.config.CODE_INTERPRETER_JUPYTER_AUTH_PASSWORD = (
661
+ CODE_INTERPRETER_JUPYTER_AUTH_PASSWORD
662
+ )
663
+ app.state.config.CODE_INTERPRETER_JUPYTER_TIMEOUT = CODE_INTERPRETER_JUPYTER_TIMEOUT
664
+
665
+ ########################################
666
+ #
667
+ # IMAGES
668
+ #
669
+ ########################################
670
+
671
+ app.state.config.IMAGE_GENERATION_ENGINE = IMAGE_GENERATION_ENGINE
672
+ app.state.config.ENABLE_IMAGE_GENERATION = ENABLE_IMAGE_GENERATION
673
+ app.state.config.ENABLE_IMAGE_PROMPT_GENERATION = ENABLE_IMAGE_PROMPT_GENERATION
674
+
675
+ app.state.config.IMAGES_OPENAI_API_BASE_URL = IMAGES_OPENAI_API_BASE_URL
676
+ app.state.config.IMAGES_OPENAI_API_KEY = IMAGES_OPENAI_API_KEY
677
+
678
+ app.state.config.IMAGES_GEMINI_API_BASE_URL = IMAGES_GEMINI_API_BASE_URL
679
+ app.state.config.IMAGES_GEMINI_API_KEY = IMAGES_GEMINI_API_KEY
680
+
681
+ app.state.config.IMAGE_GENERATION_MODEL = IMAGE_GENERATION_MODEL
682
+
683
+ app.state.config.AUTOMATIC1111_BASE_URL = AUTOMATIC1111_BASE_URL
684
+ app.state.config.AUTOMATIC1111_API_AUTH = AUTOMATIC1111_API_AUTH
685
+ app.state.config.AUTOMATIC1111_CFG_SCALE = AUTOMATIC1111_CFG_SCALE
686
+ app.state.config.AUTOMATIC1111_SAMPLER = AUTOMATIC1111_SAMPLER
687
+ app.state.config.AUTOMATIC1111_SCHEDULER = AUTOMATIC1111_SCHEDULER
688
+ app.state.config.COMFYUI_BASE_URL = COMFYUI_BASE_URL
689
+ app.state.config.COMFYUI_API_KEY = COMFYUI_API_KEY
690
+ app.state.config.COMFYUI_WORKFLOW = COMFYUI_WORKFLOW
691
+ app.state.config.COMFYUI_WORKFLOW_NODES = COMFYUI_WORKFLOW_NODES
692
+
693
+ app.state.config.IMAGE_SIZE = IMAGE_SIZE
694
+ app.state.config.IMAGE_STEPS = IMAGE_STEPS
695
+
696
+
697
+ ########################################
698
+ #
699
+ # AUDIO
700
+ #
701
+ ########################################
702
+
703
+ app.state.config.STT_OPENAI_API_BASE_URL = AUDIO_STT_OPENAI_API_BASE_URL
704
+ app.state.config.STT_OPENAI_API_KEY = AUDIO_STT_OPENAI_API_KEY
705
+ app.state.config.STT_ENGINE = AUDIO_STT_ENGINE
706
+ app.state.config.STT_MODEL = AUDIO_STT_MODEL
707
+
708
+ app.state.config.WHISPER_MODEL = WHISPER_MODEL
709
+ app.state.config.DEEPGRAM_API_KEY = DEEPGRAM_API_KEY
710
+
711
+ app.state.config.TTS_OPENAI_API_BASE_URL = AUDIO_TTS_OPENAI_API_BASE_URL
712
+ app.state.config.TTS_OPENAI_API_KEY = AUDIO_TTS_OPENAI_API_KEY
713
+ app.state.config.TTS_ENGINE = AUDIO_TTS_ENGINE
714
+ app.state.config.TTS_MODEL = AUDIO_TTS_MODEL
715
+ app.state.config.TTS_VOICE = AUDIO_TTS_VOICE
716
+ app.state.config.TTS_API_KEY = AUDIO_TTS_API_KEY
717
+ app.state.config.TTS_SPLIT_ON = AUDIO_TTS_SPLIT_ON
718
+
719
+
720
+ app.state.config.TTS_AZURE_SPEECH_REGION = AUDIO_TTS_AZURE_SPEECH_REGION
721
+ app.state.config.TTS_AZURE_SPEECH_OUTPUT_FORMAT = AUDIO_TTS_AZURE_SPEECH_OUTPUT_FORMAT
722
+
723
+
724
+ app.state.faster_whisper_model = None
725
+ app.state.speech_synthesiser = None
726
+ app.state.speech_speaker_embeddings_dataset = None
727
+
728
+
729
+ ########################################
730
+ #
731
+ # TASKS
732
+ #
733
+ ########################################
734
+
735
+
736
+ app.state.config.TASK_MODEL = TASK_MODEL
737
+ app.state.config.TASK_MODEL_EXTERNAL = TASK_MODEL_EXTERNAL
738
+
739
+
740
+ app.state.config.ENABLE_SEARCH_QUERY_GENERATION = ENABLE_SEARCH_QUERY_GENERATION
741
+ app.state.config.ENABLE_RETRIEVAL_QUERY_GENERATION = ENABLE_RETRIEVAL_QUERY_GENERATION
742
+ app.state.config.ENABLE_AUTOCOMPLETE_GENERATION = ENABLE_AUTOCOMPLETE_GENERATION
743
+ app.state.config.ENABLE_TAGS_GENERATION = ENABLE_TAGS_GENERATION
744
+ app.state.config.ENABLE_TITLE_GENERATION = ENABLE_TITLE_GENERATION
745
+
746
+
747
+ app.state.config.TITLE_GENERATION_PROMPT_TEMPLATE = TITLE_GENERATION_PROMPT_TEMPLATE
748
+ app.state.config.TAGS_GENERATION_PROMPT_TEMPLATE = TAGS_GENERATION_PROMPT_TEMPLATE
749
+ app.state.config.IMAGE_PROMPT_GENERATION_PROMPT_TEMPLATE = (
750
+ IMAGE_PROMPT_GENERATION_PROMPT_TEMPLATE
751
+ )
752
+
753
+ app.state.config.TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE = (
754
+ TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE
755
+ )
756
+ app.state.config.QUERY_GENERATION_PROMPT_TEMPLATE = QUERY_GENERATION_PROMPT_TEMPLATE
757
+ app.state.config.AUTOCOMPLETE_GENERATION_PROMPT_TEMPLATE = (
758
+ AUTOCOMPLETE_GENERATION_PROMPT_TEMPLATE
759
+ )
760
+ app.state.config.AUTOCOMPLETE_GENERATION_INPUT_MAX_LENGTH = (
761
+ AUTOCOMPLETE_GENERATION_INPUT_MAX_LENGTH
762
+ )
763
+
764
+
765
+ ########################################
766
+ #
767
+ # WEBUI
768
+ #
769
+ ########################################
770
+
771
+ app.state.MODELS = {}
772
+
773
+
774
+ class RedirectMiddleware(BaseHTTPMiddleware):
775
+ async def dispatch(self, request: Request, call_next):
776
+ # Check if the request is a GET request
777
+ if request.method == "GET":
778
+ path = request.url.path
779
+ query_params = dict(parse_qs(urlparse(str(request.url)).query))
780
+
781
+ # Check for the specific watch path and the presence of 'v' parameter
782
+ if path.endswith("/watch") and "v" in query_params:
783
+ video_id = query_params["v"][0] # Extract the first 'v' parameter
784
+ encoded_video_id = urlencode({"youtube": video_id})
785
+ redirect_url = f"/?{encoded_video_id}"
786
+ return RedirectResponse(url=redirect_url)
787
+
788
+ # Proceed with the normal flow of other requests
789
+ response = await call_next(request)
790
+ return response
791
+
792
+
793
+ # Add the middleware to the app
794
+ app.add_middleware(RedirectMiddleware)
795
+ app.add_middleware(SecurityHeadersMiddleware)
796
+
797
+
798
+ @app.middleware("http")
799
+ async def commit_session_after_request(request: Request, call_next):
800
+ response = await call_next(request)
801
+ # log.debug("Commit session after request")
802
+ Session.commit()
803
+ return response
804
+
805
+
806
+ @app.middleware("http")
807
+ async def check_url(request: Request, call_next):
808
+ start_time = int(time.time())
809
+ request.state.enable_api_key = app.state.config.ENABLE_API_KEY
810
+ response = await call_next(request)
811
+ process_time = int(time.time()) - start_time
812
+ response.headers["X-Process-Time"] = str(process_time)
813
+ return response
814
+
815
+
816
+ @app.middleware("http")
817
+ async def inspect_websocket(request: Request, call_next):
818
+ if (
819
+ "/ws/socket.io" in request.url.path
820
+ and request.query_params.get("transport") == "websocket"
821
+ ):
822
+ upgrade = (request.headers.get("Upgrade") or "").lower()
823
+ connection = (request.headers.get("Connection") or "").lower().split(",")
824
+ # Check that there's the correct headers for an upgrade, else reject the connection
825
+ # This is to work around this upstream issue: https://github.com/miguelgrinberg/python-engineio/issues/367
826
+ if upgrade != "websocket" or "upgrade" not in connection:
827
+ return JSONResponse(
828
+ status_code=status.HTTP_400_BAD_REQUEST,
829
+ content={"detail": "Invalid WebSocket upgrade request"},
830
+ )
831
+ return await call_next(request)
832
+
833
+
834
+ app.add_middleware(
835
+ CORSMiddleware,
836
+ allow_origins=CORS_ALLOW_ORIGIN,
837
+ allow_credentials=True,
838
+ allow_methods=["*"],
839
+ allow_headers=["*"],
840
+ )
841
+
842
+
843
+ app.mount("/ws", socket_app)
844
+
845
+
846
+ app.include_router(ollama.router, prefix="/ollama", tags=["ollama"])
847
+ app.include_router(openai.router, prefix="/openai", tags=["openai"])
848
+
849
+
850
+ app.include_router(pipelines.router, prefix="/api/v1/pipelines", tags=["pipelines"])
851
+ app.include_router(tasks.router, prefix="/api/v1/tasks", tags=["tasks"])
852
+ app.include_router(images.router, prefix="/api/v1/images", tags=["images"])
853
+
854
+ app.include_router(audio.router, prefix="/api/v1/audio", tags=["audio"])
855
+ app.include_router(retrieval.router, prefix="/api/v1/retrieval", tags=["retrieval"])
856
+
857
+ app.include_router(configs.router, prefix="/api/v1/configs", tags=["configs"])
858
+
859
+ app.include_router(auths.router, prefix="/api/v1/auths", tags=["auths"])
860
+ app.include_router(users.router, prefix="/api/v1/users", tags=["users"])
861
+
862
+
863
+ app.include_router(channels.router, prefix="/api/v1/channels", tags=["channels"])
864
+ app.include_router(chats.router, prefix="/api/v1/chats", tags=["chats"])
865
+
866
+ app.include_router(models.router, prefix="/api/v1/models", tags=["models"])
867
+ app.include_router(knowledge.router, prefix="/api/v1/knowledge", tags=["knowledge"])
868
+ app.include_router(prompts.router, prefix="/api/v1/prompts", tags=["prompts"])
869
+ app.include_router(tools.router, prefix="/api/v1/tools", tags=["tools"])
870
+
871
+ app.include_router(memories.router, prefix="/api/v1/memories", tags=["memories"])
872
+ app.include_router(folders.router, prefix="/api/v1/folders", tags=["folders"])
873
+ app.include_router(groups.router, prefix="/api/v1/groups", tags=["groups"])
874
+ app.include_router(files.router, prefix="/api/v1/files", tags=["files"])
875
+ app.include_router(functions.router, prefix="/api/v1/functions", tags=["functions"])
876
+ app.include_router(
877
+ evaluations.router, prefix="/api/v1/evaluations", tags=["evaluations"]
878
+ )
879
+ app.include_router(utils.router, prefix="/api/v1/utils", tags=["utils"])
880
+
881
+
882
+ ##################################
883
+ #
884
+ # Chat Endpoints
885
+ #
886
+ ##################################
887
+
888
+
889
+ @app.get("/api/models")
890
+ async def get_models(request: Request, user=Depends(get_verified_user)):
891
+ def get_filtered_models(models, user):
892
+ filtered_models = []
893
+ for model in models:
894
+ if model.get("arena"):
895
+ if has_access(
896
+ user.id,
897
+ type="read",
898
+ access_control=model.get("info", {})
899
+ .get("meta", {})
900
+ .get("access_control", {}),
901
+ ):
902
+ filtered_models.append(model)
903
+ continue
904
+
905
+ model_info = Models.get_model_by_id(model["id"])
906
+ if model_info:
907
+ if user.id == model_info.user_id or has_access(
908
+ user.id, type="read", access_control=model_info.access_control
909
+ ):
910
+ filtered_models.append(model)
911
+
912
+ return filtered_models
913
+
914
+ models = await get_all_models(request)
915
+
916
+ # Filter out filter pipelines
917
+ models = [
918
+ model
919
+ for model in models
920
+ if "pipeline" not in model or model["pipeline"].get("type", None) != "filter"
921
+ ]
922
+
923
+ model_order_list = request.app.state.config.MODEL_ORDER_LIST
924
+ if model_order_list:
925
+ model_order_dict = {model_id: i for i, model_id in enumerate(model_order_list)}
926
+ # Sort models by order list priority, with fallback for those not in the list
927
+ models.sort(
928
+ key=lambda x: (model_order_dict.get(x["id"], float("inf")), x["name"])
929
+ )
930
+
931
+ # Filter out models that the user does not have access to
932
+ if user.role == "user" and not BYPASS_MODEL_ACCESS_CONTROL:
933
+ models = get_filtered_models(models, user)
934
+
935
+ log.debug(
936
+ f"/api/models returned filtered models accessible to the user: {json.dumps([model['id'] for model in models])}"
937
+ )
938
+ return {"data": models}
939
+
940
+
941
+ @app.get("/api/models/base")
942
+ async def get_base_models(request: Request, user=Depends(get_admin_user)):
943
+ models = await get_all_base_models(request)
944
+ return {"data": models}
945
+
946
+
947
+ @app.post("/api/chat/completions")
948
+ async def chat_completion(
949
+ request: Request,
950
+ form_data: dict,
951
+ user=Depends(get_verified_user),
952
+ ):
953
+ if not request.app.state.MODELS:
954
+ await get_all_models(request)
955
+
956
+ model_item = form_data.pop("model_item", {})
957
+ tasks = form_data.pop("background_tasks", None)
958
+
959
+ try:
960
+ if not model_item.get("direct", False):
961
+ model_id = form_data.get("model", None)
962
+ if model_id not in request.app.state.MODELS:
963
+ raise Exception("Model not found")
964
+
965
+ model = request.app.state.MODELS[model_id]
966
+ model_info = Models.get_model_by_id(model_id)
967
+
968
+ # Check if user has access to the model
969
+ if not BYPASS_MODEL_ACCESS_CONTROL and user.role == "user":
970
+ try:
971
+ check_model_access(user, model)
972
+ except Exception as e:
973
+ raise e
974
+ else:
975
+ model = model_item
976
+ model_info = None
977
+
978
+ request.state.direct = True
979
+ request.state.model = model
980
+
981
+ metadata = {
982
+ "user_id": user.id,
983
+ "chat_id": form_data.pop("chat_id", None),
984
+ "message_id": form_data.pop("id", None),
985
+ "session_id": form_data.pop("session_id", None),
986
+ "tool_ids": form_data.get("tool_ids", None),
987
+ "files": form_data.get("files", None),
988
+ "features": form_data.get("features", None),
989
+ "variables": form_data.get("variables", None),
990
+ "model": model_info.model_dump() if model_info else model,
991
+ "direct": model_item.get("direct", False),
992
+ **(
993
+ {"function_calling": "native"}
994
+ if form_data.get("params", {}).get("function_calling") == "native"
995
+ or (
996
+ model_info
997
+ and model_info.params.model_dump().get("function_calling")
998
+ == "native"
999
+ )
1000
+ else {}
1001
+ ),
1002
+ }
1003
+
1004
+ request.state.metadata = metadata
1005
+ form_data["metadata"] = metadata
1006
+
1007
+ form_data, metadata, events = await process_chat_payload(
1008
+ request, form_data, metadata, user, model
1009
+ )
1010
+
1011
+ except Exception as e:
1012
+ log.debug(f"Error processing chat payload: {e}")
1013
+ raise HTTPException(
1014
+ status_code=status.HTTP_400_BAD_REQUEST,
1015
+ detail=str(e),
1016
+ )
1017
+
1018
+ try:
1019
+ response = await chat_completion_handler(request, form_data, user)
1020
+
1021
+ return await process_chat_response(
1022
+ request, response, form_data, user, events, metadata, tasks
1023
+ )
1024
+ except Exception as e:
1025
+ raise HTTPException(
1026
+ status_code=status.HTTP_400_BAD_REQUEST,
1027
+ detail=str(e),
1028
+ )
1029
+
1030
+
1031
+ # Alias for chat_completion (Legacy)
1032
+ generate_chat_completions = chat_completion
1033
+ generate_chat_completion = chat_completion
1034
+
1035
+
1036
+ @app.post("/api/chat/completed")
1037
+ async def chat_completed(
1038
+ request: Request, form_data: dict, user=Depends(get_verified_user)
1039
+ ):
1040
+ try:
1041
+ model_item = form_data.pop("model_item", {})
1042
+
1043
+ if model_item.get("direct", False):
1044
+ request.state.direct = True
1045
+ request.state.model = model_item
1046
+
1047
+ return await chat_completed_handler(request, form_data, user)
1048
+ except Exception as e:
1049
+ raise HTTPException(
1050
+ status_code=status.HTTP_400_BAD_REQUEST,
1051
+ detail=str(e),
1052
+ )
1053
+
1054
+
1055
+ @app.post("/api/chat/actions/{action_id}")
1056
+ async def chat_action(
1057
+ request: Request, action_id: str, form_data: dict, user=Depends(get_verified_user)
1058
+ ):
1059
+ try:
1060
+ model_item = form_data.pop("model_item", {})
1061
+
1062
+ if model_item.get("direct", False):
1063
+ request.state.direct = True
1064
+ request.state.model = model_item
1065
+
1066
+ return await chat_action_handler(request, action_id, form_data, user)
1067
+ except Exception as e:
1068
+ raise HTTPException(
1069
+ status_code=status.HTTP_400_BAD_REQUEST,
1070
+ detail=str(e),
1071
+ )
1072
+
1073
+
1074
+ @app.post("/api/tasks/stop/{task_id}")
1075
+ async def stop_task_endpoint(task_id: str, user=Depends(get_verified_user)):
1076
+ try:
1077
+ result = await stop_task(task_id) # Use the function from tasks.py
1078
+ return result
1079
+ except ValueError as e:
1080
+ raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))
1081
+
1082
+
1083
+ @app.get("/api/tasks")
1084
+ async def list_tasks_endpoint(user=Depends(get_verified_user)):
1085
+ return {"tasks": list_tasks()} # Use the function from tasks.py
1086
+
1087
+
1088
+ ##################################
1089
+ #
1090
+ # Config Endpoints
1091
+ #
1092
+ ##################################
1093
+
1094
+
1095
+ @app.get("/api/config")
1096
+ async def get_app_config(request: Request):
1097
+ user = None
1098
+ if "token" in request.cookies:
1099
+ token = request.cookies.get("token")
1100
+ try:
1101
+ data = decode_token(token)
1102
+ except Exception as e:
1103
+ log.debug(e)
1104
+ raise HTTPException(
1105
+ status_code=status.HTTP_401_UNAUTHORIZED,
1106
+ detail="Invalid token",
1107
+ )
1108
+ if data is not None and "id" in data:
1109
+ user = Users.get_user_by_id(data["id"])
1110
+
1111
+ onboarding = False
1112
+ if user is None:
1113
+ user_count = Users.get_num_users()
1114
+ onboarding = user_count == 0
1115
+
1116
+ return {
1117
+ **({"onboarding": True} if onboarding else {}),
1118
+ "status": True,
1119
+ "name": app.state.WEBUI_NAME,
1120
+ "version": VERSION,
1121
+ "default_locale": str(DEFAULT_LOCALE),
1122
+ "oauth": {
1123
+ "providers": {
1124
+ name: config.get("name", name)
1125
+ for name, config in OAUTH_PROVIDERS.items()
1126
+ }
1127
+ },
1128
+ "features": {
1129
+ "auth": WEBUI_AUTH,
1130
+ "auth_trusted_header": bool(app.state.AUTH_TRUSTED_EMAIL_HEADER),
1131
+ "enable_ldap": app.state.config.ENABLE_LDAP,
1132
+ "enable_api_key": app.state.config.ENABLE_API_KEY,
1133
+ "enable_signup": app.state.config.ENABLE_SIGNUP,
1134
+ "enable_login_form": app.state.config.ENABLE_LOGIN_FORM,
1135
+ "enable_websocket": ENABLE_WEBSOCKET_SUPPORT,
1136
+ **(
1137
+ {
1138
+ "enable_direct_connections": app.state.config.ENABLE_DIRECT_CONNECTIONS,
1139
+ "enable_channels": app.state.config.ENABLE_CHANNELS,
1140
+ "enable_web_search": app.state.config.ENABLE_RAG_WEB_SEARCH,
1141
+ "enable_code_interpreter": app.state.config.ENABLE_CODE_INTERPRETER,
1142
+ "enable_image_generation": app.state.config.ENABLE_IMAGE_GENERATION,
1143
+ "enable_autocomplete_generation": app.state.config.ENABLE_AUTOCOMPLETE_GENERATION,
1144
+ "enable_community_sharing": app.state.config.ENABLE_COMMUNITY_SHARING,
1145
+ "enable_message_rating": app.state.config.ENABLE_MESSAGE_RATING,
1146
+ "enable_admin_export": ENABLE_ADMIN_EXPORT,
1147
+ "enable_admin_chat_access": ENABLE_ADMIN_CHAT_ACCESS,
1148
+ "enable_google_drive_integration": app.state.config.ENABLE_GOOGLE_DRIVE_INTEGRATION,
1149
+ }
1150
+ if user is not None
1151
+ else {}
1152
+ ),
1153
+ },
1154
+ **(
1155
+ {
1156
+ "default_models": app.state.config.DEFAULT_MODELS,
1157
+ "default_prompt_suggestions": app.state.config.DEFAULT_PROMPT_SUGGESTIONS,
1158
+ "code": {
1159
+ "engine": app.state.config.CODE_EXECUTION_ENGINE,
1160
+ },
1161
+ "audio": {
1162
+ "tts": {
1163
+ "engine": app.state.config.TTS_ENGINE,
1164
+ "voice": app.state.config.TTS_VOICE,
1165
+ "split_on": app.state.config.TTS_SPLIT_ON,
1166
+ },
1167
+ "stt": {
1168
+ "engine": app.state.config.STT_ENGINE,
1169
+ },
1170
+ },
1171
+ "file": {
1172
+ "max_size": app.state.config.FILE_MAX_SIZE,
1173
+ "max_count": app.state.config.FILE_MAX_COUNT,
1174
+ },
1175
+ "permissions": {**app.state.config.USER_PERMISSIONS},
1176
+ "google_drive": {
1177
+ "client_id": GOOGLE_DRIVE_CLIENT_ID.value,
1178
+ "api_key": GOOGLE_DRIVE_API_KEY.value,
1179
+ },
1180
+ }
1181
+ if user is not None
1182
+ else {}
1183
+ ),
1184
+ }
1185
+
1186
+
1187
+ class UrlForm(BaseModel):
1188
+ url: str
1189
+
1190
+
1191
+ @app.get("/api/webhook")
1192
+ async def get_webhook_url(user=Depends(get_admin_user)):
1193
+ return {
1194
+ "url": app.state.config.WEBHOOK_URL,
1195
+ }
1196
+
1197
+
1198
+ @app.post("/api/webhook")
1199
+ async def update_webhook_url(form_data: UrlForm, user=Depends(get_admin_user)):
1200
+ app.state.config.WEBHOOK_URL = form_data.url
1201
+ app.state.WEBHOOK_URL = app.state.config.WEBHOOK_URL
1202
+ return {"url": app.state.config.WEBHOOK_URL}
1203
+
1204
+
1205
+ @app.get("/api/version")
1206
+ async def get_app_version():
1207
+ return {
1208
+ "version": VERSION,
1209
+ }
1210
+
1211
+
1212
+ @app.get("/api/version/updates")
1213
+ async def get_app_latest_release_version(user=Depends(get_verified_user)):
1214
+ if OFFLINE_MODE:
1215
+ log.debug(
1216
+ f"Offline mode is enabled, returning current version as latest version"
1217
+ )
1218
+ return {"current": VERSION, "latest": VERSION}
1219
+ try:
1220
+ timeout = aiohttp.ClientTimeout(total=1)
1221
+ async with aiohttp.ClientSession(timeout=timeout, trust_env=True) as session:
1222
+ async with session.get(
1223
+ "https://api.github.com/repos/open-webui/open-webui/releases/latest"
1224
+ ) as response:
1225
+ response.raise_for_status()
1226
+ data = await response.json()
1227
+ latest_version = data["tag_name"]
1228
+
1229
+ return {"current": VERSION, "latest": latest_version[1:]}
1230
+ except Exception as e:
1231
+ log.debug(e)
1232
+ return {"current": VERSION, "latest": VERSION}
1233
+
1234
+
1235
+ @app.get("/api/changelog")
1236
+ async def get_app_changelog():
1237
+ return {key: CHANGELOG[key] for idx, key in enumerate(CHANGELOG) if idx < 5}
1238
+
1239
+
1240
+ ############################
1241
+ # OAuth Login & Callback
1242
+ ############################
1243
+
1244
+ # SessionMiddleware is used by authlib for oauth
1245
+ if len(OAUTH_PROVIDERS) > 0:
1246
+ app.add_middleware(
1247
+ SessionMiddleware,
1248
+ secret_key=WEBUI_SECRET_KEY,
1249
+ session_cookie="oui-session",
1250
+ same_site=WEBUI_SESSION_COOKIE_SAME_SITE,
1251
+ https_only=WEBUI_SESSION_COOKIE_SECURE,
1252
+ )
1253
+
1254
+
1255
+ @app.get("/oauth/{provider}/login")
1256
+ async def oauth_login(provider: str, request: Request):
1257
+ return await oauth_manager.handle_login(request, provider)
1258
+
1259
+
1260
+ # OAuth login logic is as follows:
1261
+ # 1. Attempt to find a user with matching subject ID, tied to the provider
1262
+ # 2. If OAUTH_MERGE_ACCOUNTS_BY_EMAIL is true, find a user with the email address provided via OAuth
1263
+ # - This is considered insecure in general, as OAuth providers do not always verify email addresses
1264
+ # 3. If there is no user, and ENABLE_OAUTH_SIGNUP is true, create a user
1265
+ # - Email addresses are considered unique, so we fail registration if the email address is already taken
1266
+ @app.get("/oauth/{provider}/callback")
1267
+ async def oauth_callback(provider: str, request: Request, response: Response):
1268
+ return await oauth_manager.handle_callback(request, provider, response)
1269
+
1270
+
1271
+ @app.get("/manifest.json")
1272
+ async def get_manifest_json():
1273
+ return {
1274
+ "name": app.state.WEBUI_NAME,
1275
+ "short_name": app.state.WEBUI_NAME,
1276
+ "description": "Open WebUI is an open, extensible, user-friendly interface for AI that adapts to your workflow.",
1277
+ "start_url": "/",
1278
+ "display": "standalone",
1279
+ "background_color": "#343541",
1280
+ "orientation": "natural",
1281
+ "icons": [
1282
+ {
1283
+ "src": "/static/logo.png",
1284
+ "type": "image/png",
1285
+ "sizes": "500x500",
1286
+ "purpose": "any",
1287
+ },
1288
+ {
1289
+ "src": "/static/logo.png",
1290
+ "type": "image/png",
1291
+ "sizes": "500x500",
1292
+ "purpose": "maskable",
1293
+ },
1294
+ ],
1295
+ }
1296
+
1297
+
1298
+ @app.get("/opensearch.xml")
1299
+ async def get_opensearch_xml():
1300
+ xml_content = rf"""
1301
+ <OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.1/" xmlns:moz="http://www.mozilla.org/2006/browser/search/">
1302
+ <ShortName>{app.state.WEBUI_NAME}</ShortName>
1303
+ <Description>Search {app.state.WEBUI_NAME}</Description>
1304
+ <InputEncoding>UTF-8</InputEncoding>
1305
+ <Image width="16" height="16" type="image/x-icon">{app.state.config.WEBUI_URL}/static/favicon.png</Image>
1306
+ <Url type="text/html" method="get" template="{app.state.config.WEBUI_URL}/?q={"{searchTerms}"}"/>
1307
+ <moz:SearchForm>{app.state.config.WEBUI_URL}</moz:SearchForm>
1308
+ </OpenSearchDescription>
1309
+ """
1310
+ return Response(content=xml_content, media_type="application/xml")
1311
+
1312
+
1313
+ @app.get("/health")
1314
+ async def healthcheck():
1315
+ return {"status": True}
1316
+
1317
+
1318
+ @app.get("/health/db")
1319
+ async def healthcheck_with_db():
1320
+ Session.execute(text("SELECT 1;")).all()
1321
+ return {"status": True}
1322
+
1323
+
1324
+ app.mount("/static", StaticFiles(directory=STATIC_DIR), name="static")
1325
+ app.mount("/cache", StaticFiles(directory=CACHE_DIR), name="cache")
1326
+
1327
+
1328
+ def swagger_ui_html(*args, **kwargs):
1329
+ return get_swagger_ui_html(
1330
+ *args,
1331
+ **kwargs,
1332
+ swagger_js_url="/static/swagger-ui/swagger-ui-bundle.js",
1333
+ swagger_css_url="/static/swagger-ui/swagger-ui.css",
1334
+ swagger_favicon_url="/static/swagger-ui/favicon.png",
1335
+ )
1336
+
1337
+
1338
+ applications.get_swagger_ui_html = swagger_ui_html
1339
+
1340
+ if os.path.exists(FRONTEND_BUILD_DIR):
1341
+ mimetypes.add_type("text/javascript", ".js")
1342
+ app.mount(
1343
+ "/",
1344
+ SPAStaticFiles(directory=FRONTEND_BUILD_DIR, html=True),
1345
+ name="spa-static-files",
1346
+ )
1347
+ else:
1348
+ log.warning(
1349
+ f"Frontend build directory not found at '{FRONTEND_BUILD_DIR}'. Serving API only."
1350
+ )
backend/open_webui/migrations/README ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ Generic single-database configuration.
2
+
3
+ Create new migrations with
4
+ DATABASE_URL=<replace with actual url> alembic revision --autogenerate -m "a description"