Compare commits

...

1095 Commits

Author SHA1 Message Date
Timothy Jaeryang Baek
5be97b81d6 Merge pull request #2743 from open-webui/dev
fix: changelog
2024-06-02 18:27:36 -07:00
Timothy J. Baek
42f335851f fix: changelog 2024-06-02 18:27:18 -07:00
Timothy Jaeryang Baek
0744806523 Merge pull request #2742 from open-webui/dev
0.2.2
2024-06-02 18:26:09 -07:00
Timothy J. Baek
a36555a25a chore: version bump 2024-06-02 18:26:00 -07:00
Timothy J. Baek
814fbb73f3 feat: RESET_CONFIG_ON_START 2024-06-02 18:14:36 -07:00
Timothy J. Baek
3d74c04f50 feat: mermaid rendering support 2024-06-02 18:03:30 -07:00
Timothy J. Baek
a9e5003c4f fix: filter outlet 2024-06-02 17:49:44 -07:00
Timothy J. Baek
32a61ea15d fix: pipelines 2024-06-02 16:46:33 -07:00
Timothy Jaeryang Baek
d9a3e4db39 Merge pull request #2731 from cheahjs/fix/ollama-cancellation
fix: ollama and openai stream cancellation
2024-06-02 16:27:11 -07:00
Timothy Jaeryang Baek
c5ff4c24e1 Merge branch 'dev' into fix/ollama-cancellation 2024-06-02 16:27:01 -07:00
Timothy Jaeryang Baek
9b3fdb1838 Merge pull request #2741 from open-webui/dev
fix: validate lengths for openai urls and keys
2024-06-02 16:21:28 -07:00
Timothy J. Baek
27ff3ab112 fix: validate lengths for openai urls and keys 2024-06-02 16:20:22 -07:00
Timothy Jaeryang Baek
d09164b729 Merge pull request #2739 from open-webui/dev
fix
2024-06-02 15:19:59 -07:00
Timothy Jaeryang Baek
44597314c6 Merge pull request #2737 from cheahjs/fix/docker-tag-again
fix: another attempt at fixing tag docker builds
2024-06-02 14:49:18 -07:00
Jun Siang Cheah
3176fe0c2b fix: another attempt at fixing tag docker builds 2024-06-02 22:35:59 +01:00
Timothy Jaeryang Baek
cfc78dedf0 Merge pull request #2736 from open-webui/dev
fix
2024-06-02 14:08:43 -07:00
Timothy J. Baek
642079ca29 Update CHANGELOG.md 2024-06-02 14:08:23 -07:00
Timothy J. Baek
523455a46e refac: styling 2024-06-02 14:04:37 -07:00
Timothy Jaeryang Baek
8eb81c9127 Merge pull request #2735 from open-webui/dev
0.2.1
2024-06-02 14:01:54 -07:00
Timothy J. Baek
6e818e3694 doc: changelog 2024-06-02 14:01:05 -07:00
Timothy J. Baek
6de8327665 chore: version bump 2024-06-02 13:54:11 -07:00
Timothy J. Baek
0a9f41a044 feat: single model export 2024-06-02 13:53:02 -07:00
Timothy J. Baek
2e24c2d4d3 refac 2024-06-02 13:48:14 -07:00
Timothy J. Baek
41442897e3 fix: connections 2024-06-02 13:45:04 -07:00
Timothy J. Baek
e0ba585204 feat: include num_thread in advanced params 2024-06-02 13:20:38 -07:00
Timothy Jaeryang Baek
0bae97218d Merge pull request #2722 from SimonOriginal/main
Updated Ukrainian translation to v0.2.0  🇺🇦
2024-06-02 11:45:55 -07:00
Timothy Jaeryang Baek
2ad286698b Merge pull request #2718 from cheahjs/fix/tag-docker-build
fix: docker build on tag broke due to cache
2024-06-02 11:45:21 -07:00
Timothy Jaeryang Baek
5015486fc2 Merge pull request #2715 from que-nguyen/dev
Update vi-VN/translation.json
2024-06-02 11:45:04 -07:00
Jun Siang Cheah
b5b2b70f4a fix: bad payload refactor 2024-06-02 19:40:18 +01:00
Jun Siang Cheah
7f74426a22 fix: openai streaming cancellation using aiohttp 2024-06-02 18:48:45 +01:00
Jun Siang Cheah
4dd51badfe fix: ollama streaming cancellation using aiohttp 2024-06-02 18:06:12 +01:00
Jun Siang Cheah
24c35c308d fix: stream defaults to true, return request ID 2024-06-02 17:09:15 +01:00
SimonOriginal
80c6a39fab Updated Ukrainian translation to v0.2.0 2024-06-02 14:42:15 +02:00
Jun Siang Cheah
45bff743a2 fix: docker build on tag broke due to cache 2024-06-02 10:01:01 +01:00
Que Nguyen
afd62c3b22 Update translation.json
These changes serve to complete the Vietnamese translation.
2024-06-02 13:53:38 +07:00
Timothy Jaeryang Baek
72354e06a7 Merge pull request #2476 from open-webui/dev
0.2.0
2024-06-01 22:03:02 -07:00
Timothy J. Baek
207e25035a chore: format 2024-06-01 22:01:53 -07:00
Timothy J. Baek
e99b2ecdd6 chore: version bump
Co-Authored-By: nullptr <62841684+not-nullptr@users.noreply.github.com>
2024-06-01 21:55:44 -07:00
Timothy J. Baek
1c7dd7b859 refac: openwebui.com integration 2024-06-01 21:16:32 -07:00
Timothy J. Baek
32a8884c0c fix: model create 2024-06-01 21:12:51 -07:00
Timothy J. Baek
f1855f5208 doc: typo 2024-06-01 20:50:01 -07:00
Timothy J. Baek
6895e2439c doc: code-of-conduct 2024-06-01 20:49:45 -07:00
Timothy J. Baek
1285ec1828 doc: readme 2024-06-01 20:43:10 -07:00
Timothy J. Baek
bdcfbfeb11 refac 2024-06-01 20:26:19 -07:00
Timothy J. Baek
aa7fcca4ee fix: banner styling 2024-06-01 20:21:26 -07:00
Timothy J. Baek
fc5f74cc64 chore: version bump 2024-06-01 20:16:00 -07:00
Timothy J. Baek
86510de343 chore: format 2024-06-01 20:12:11 -07:00
Timothy J. Baek
a53796270f refac: web search config 2024-06-01 20:08:08 -07:00
Timothy J. Baek
fbdfb7e4fa refac: web search 2024-06-01 19:57:00 -07:00
Timothy J. Baek
999d2bc21b refac: web search 2024-06-01 19:52:12 -07:00
Timothy J. Baek
912a704fdc refac: web search settings 2024-06-01 19:40:48 -07:00
Timothy J. Baek
ea6b8984ab refac: web search 2024-06-01 19:03:56 -07:00
Timothy J. Baek
0b8b039aae doc: changelog 2024-06-01 18:54:05 -07:00
Timothy J. Baek
dd799ebea8 refac: styling 2024-06-01 18:44:47 -07:00
Timothy Jaeryang Baek
f2e5bebdbc Merge pull request #2709 from open-webui/main
dev
2024-06-01 18:44:36 -07:00
Timothy J. Baek
abbf622fba doc: readme 2024-06-01 18:26:30 -07:00
Timothy J. Baek
e4808db281 doc: changelog 2024-06-01 18:18:51 -07:00
Timothy J. Baek
500765a778 doc: changelog 2024-06-01 18:16:17 -07:00
Timothy J. Baek
c5c33aabf9 refac: styling 2024-06-01 18:11:54 -07:00
Timothy J. Baek
ef4af86883 refac: message new line issue 2024-06-01 18:00:01 -07:00
Timothy J. Baek
d993beb18f fix: typo 2024-06-01 17:42:55 -07:00
Timothy J. Baek
53b44b75dc doc: changelog 2024-06-01 17:41:07 -07:00
Timothy J. Baek
7f2e89a8aa enh: allow label search 2024-06-01 16:34:36 -07:00
Timothy J. Baek
c6a57b1f6f feat: export litellm config.yaml 2024-06-01 16:31:39 -07:00
Timothy J. Baek
475e09be45 fix: model params 2024-06-01 15:29:47 -07:00
Timothy J. Baek
b124cd4ac5 fix: typo 2024-06-01 13:46:14 -07:00
Timothy Jaeryang Baek
d2cbb14ebf Merge pull request #2705 from cheahjs/fix/show-both-error-and-content
fix: show both message contents and error message
2024-06-01 13:05:34 -07:00
Jun Siang Cheah
1cef48730e fix: show both message contents and error message 2024-06-01 20:59:14 +01:00
Timothy J. Baek
561778d04d feat: pipelines warning message 2024-06-01 12:17:18 -07:00
Timothy J. Baek
51d2cecd36 refac: styling 2024-06-01 12:06:12 -07:00
Timothy J. Baek
6758f3d390 refac: disable sortable on mobile 2024-06-01 12:03:36 -07:00
Timothy J. Baek
6bd7c20fbb fix 2024-06-01 10:33:49 -07:00
Timothy Jaeryang Baek
572155b40c Merge pull request #2696 from cheahjs/fix/docker-layer-caching
fix: don't use BUILD_HASH build arg until needed, busts cache otherwise
2024-06-01 10:20:17 -07:00
Timothy Jaeryang Baek
950231acf7 Merge pull request #2699 from cheahjs/feat/error-message-separate
refac: error message separate from content
2024-06-01 10:19:44 -07:00
Timothy J. Baek
78b279cabc refac 2024-06-01 10:19:27 -07:00
Jun Siang Cheah
fd31b5f8d6 refac: error message separate from content 2024-06-01 15:54:02 +01:00
Jun Siang Cheah
afbea2ecea fix: don't use BUILD_HASH build arg until needed, busts cache otherwise 2024-06-01 14:16:39 +01:00
Timothy J. Baek
3730251958 refac: selector mobile styling 2024-05-31 19:51:34 -07:00
Timothy J. Baek
2dbbd1bc6f refac: pipelines 2024-05-31 19:40:58 -07:00
Timothy J. Baek
5b01d7994a fix: openai notification issue 2024-05-31 14:21:26 -07:00
Timothy J. Baek
5ef478a154 fix: read only compare messages 2024-05-31 14:19:46 -07:00
Timothy J. Baek
92700302b9 fix: edit 2024-05-31 13:35:47 -07:00
Timothy J. Baek
cb8c45d864 fix: pipelines 2024-05-31 13:30:12 -07:00
Timothy J. Baek
fc2b314c4f feat: model ordering 2024-05-31 13:11:04 -07:00
Timothy J. Baek
9c67a94542 refac: styling 2024-05-31 12:26:49 -07:00
Timothy J. Baek
cddbeb023d feat: model selector tag search 2024-05-31 12:23:29 -07:00
Timothy J. Baek
eca20b1b2c feat: model tag support 2024-05-31 12:08:25 -07:00
Timothy J. Baek
0cf9b07ec2 refac: model list styling 2024-05-31 11:24:24 -07:00
Timothy J. Baek
9dfa334a83 feat: CURRENT_DATE, USER_NAME prompt variable support 2024-05-31 11:11:28 -07:00
Timothy J. Baek
7674229e3a feat: chat clone 2024-05-31 10:30:42 -07:00
Timothy Jaeryang Baek
36e2a5e674 Create CODE_OF_CONDUCT.md 2024-05-31 11:48:56 -05:00
Timothy Jaeryang Baek
eb12d1e111 Merge pull request #2680 from aguvener/dev
i18n: incorrect translations fixed and new translations synced
2024-05-31 09:21:24 -07:00
aguvener
ffcb8da346 fix: incorrect translations fixed and new translations synced 2024-05-31 15:12:07 +03:00
Timothy J. Baek
c999c86c5e refac 2024-05-31 02:15:41 -07:00
Timothy J. Baek
9283ed1685 feat: hide/unhide model from selector 2024-05-31 02:11:25 -07:00
Timothy J. Baek
995f7bc51b fix: chat completed 2024-05-30 21:43:10 -07:00
Timothy J. Baek
e1d65065f5 fix 2024-05-30 16:31:55 -07:00
Timothy J. Baek
6922529b78 enh: keep originalContent 2024-05-30 16:23:10 -07:00
Timothy J. Baek
9cd150a048 chore: version bump 2024-05-30 02:40:17 -07:00
Timothy J. Baek
fec51342ca fix: filter outlet issue with many model chat 2024-05-30 02:37:43 -07:00
Timothy J. Baek
ef8d84296e feat: pipelines filter outlet 2024-05-30 02:04:29 -07:00
Timothy J. Baek
d9ceb31674 chore: format 2024-05-29 23:33:56 -07:00
Timothy J. Baek
c76330710f refac: pipeline filters 2024-05-29 23:29:45 -07:00
Timothy J. Baek
4715160b53 fix: pipelines 2024-05-29 22:41:51 -07:00
Timothy J. Baek
7cec88c776 fix 2024-05-29 22:27:10 -07:00
Timothy J. Baek
340b399fc2 refac: pipelines 2024-05-29 22:18:27 -07:00
Timothy J. Baek
12e60d8ebf feat: add/delete pipelines 2024-05-29 22:03:22 -07:00
Timothy J. Baek
c1cabf1415 feat: multiple pipelines server support 2024-05-29 21:26:57 -07:00
Timothy J. Baek
2981caa34b fix: import models 2024-05-29 14:05:46 -07:00
Timothy Jaeryang Baek
5c6373e057 Merge pull request #2645 from jasinliu/patch-1
enh: Update the style of ResponseMessage to change the space between paras
2024-05-29 12:46:13 -07:00
Timothy J. Baek
e427ef767b fix: openai proxy 2024-05-29 11:28:42 -07:00
Timothy J. Baek
37c87e3a14 fix: model import 2024-05-29 11:12:09 -07:00
Timothy J. Baek
7f8be4a709 refac 2024-05-29 01:42:42 -07:00
Timothy J. Baek
5cf44ac7da feat: display pipelines server in api settings 2024-05-29 01:36:13 -07:00
Timothy J. Baek
e8d4e03c0d refac: openai allow empty key 2024-05-29 01:12:25 -07:00
Liu Jiajun
5609b12852 Update ResponseMessage.svelte 2024-05-29 12:47:04 +08:00
Timothy J. Baek
943baad689 feat: allow valve pipelines edit 2024-05-28 18:24:39 -07:00
Timothy J. Baek
2bdfd85137 fix: pipelines setting 2024-05-28 16:12:54 -07:00
Timothy J. Baek
834bdf46be refac 2024-05-28 15:47:14 -07:00
Timothy Jaeryang Baek
cf1c8be85f Merge pull request #2637 from open-webui/pipelines
feat: pipeline valves
2024-05-28 13:06:46 -07:00
Timothy J. Baek
2d596d7307 feat: valves full integration 2024-05-28 13:05:31 -07:00
Timothy J. Baek
7c271734a1 fix 2024-05-28 12:36:22 -07:00
Timothy J. Baek
130d15a2fb feat: pipeline valves 2024-05-28 12:32:49 -07:00
Timothy J. Baek
0bef1b44c0 feat: pipeline valves 2024-05-28 12:04:19 -07:00
Timothy J. Baek
0383efa207 refac: pipelines 2024-05-28 11:43:48 -07:00
Timothy J. Baek
e231333bcd refac 2024-05-28 09:50:17 -07:00
Timothy Jaeryang Baek
eea8fb591a Merge pull request #2570 from silentoplayz/silentoplayz-update-readme
docs: Update README to display prominent features and link to a new dedicated features page
2024-05-28 09:24:28 -07:00
silentoplayz
0bef7fef29 Update README.md
- 🔍 **Web Search for RAG**: Perform web searches using providers like `SearXNG`, `Google PSE`, `Brave Search`, `serpstack`, and `serper`, and inject the results directly into your chat experience.
2024-05-28 05:51:25 +00:00
Timothy J. Baek
b870fd1118 fix: task model options 2024-05-27 22:24:48 -07:00
Timothy J. Baek
ec36493d61 feat: pipeline filter wildcard support 2024-05-27 20:26:24 -07:00
Timothy J. Baek
966f10e715 refac: pipeline valves -> filters 2024-05-27 19:34:05 -07:00
Timothy J. Baek
4fac99c5b3 feat: pipeline valves user field 2024-05-27 19:16:07 -07:00
Timothy J. Baek
cc6d9bb8c0 feat: pipeline valve support 2024-05-27 19:03:26 -07:00
silentoplayz
2070fa9e99 Update README.md
should be the last fix!
2024-05-28 01:18:08 +00:00
silentoplayz
0edb940dca Update README.md
PWA
2024-05-27 23:42:41 +00:00
silentoplayz
da4a057273 Update README.md
fix
2024-05-27 23:07:49 +00:00
silentoplayz
cc01c5004b Update README.md
fixes
2024-05-27 23:06:55 +00:00
Timothy Jaeryang Baek
abce172b9d Merge pull request #2602 from cheahjs/feat/openai-usage-stats
feat: add OpenAI generation stats
2024-05-27 15:26:00 -07:00
Timothy Jaeryang Baek
9d370c51e2 Merge pull request #2612 from arkohut/support-py-for-run-code
feat: support py Python for code execution
2024-05-27 15:15:48 -07:00
Timothy J. Baek
e2e96b5776 refac: citations styling 2024-05-27 15:10:38 -07:00
Timothy J. Baek
5de2b00064 refac: styling 2024-05-27 15:01:55 -07:00
Timothy Jaeryang Baek
ddc8d22e98 Merge pull request #2599 from open-webui/websearch
feat: websearch
2024-05-27 14:59:48 -07:00
Timothy J. Baek
66734a7080 refac: styling 2024-05-27 14:58:10 -07:00
Timothy J. Baek
4d64893661 refac: styling 2024-05-27 14:55:08 -07:00
Timothy J. Baek
74a8deb19f refac 2024-05-27 14:25:36 -07:00
Timothy J. Baek
f3527df644 refac: styling 2024-05-27 13:22:24 -07:00
Timothy J. Baek
4685f523b6 refac 2024-05-27 12:48:08 -07:00
Timothy J. Baek
73178cf519 refac 2024-05-27 11:08:01 -07:00
arkohut
5166e92f90 Merge branch 'dev' into support-py-for-run-code 2024-05-28 01:05:15 +08:00
arkohut
b443d61c6f feat: support py Python for code execution 2024-05-28 00:57:36 +08:00
Jun Siang Cheah
99b1661638 feat: add OpenAI generation stats 2024-05-27 10:11:53 +01:00
Timothy Jaeryang Baek
f1a7c76693 Merge pull request #2004 from cheahjs/feat/backend-web-search
feat: add user toggleable web search
2024-05-26 23:40:13 -07:00
Timothy Jaeryang Baek
bced90734b Merge branch 'websearch' into feat/backend-web-search 2024-05-26 23:40:05 -07:00
Timothy J. Baek
b6b71c08f3 fix: styling 2024-05-26 23:28:52 -07:00
Timothy J. Baek
47a9a2a190 refac: default model moved to admin settings 2024-05-26 23:04:54 -07:00
Timothy J. Baek
f2285f29b7 refac: save only to db 2024-05-26 22:52:29 -07:00
Timothy Jaeryang Baek
ee913cb6e8 Merge pull request #2597 from que-nguyen/dev
Fix errors and improve the Vietnamese translation
2024-05-26 22:49:30 -07:00
Timothy Jaeryang Baek
dd959c8f88 Merge pull request #2598 from open-webui/user-settings
feat: user settings to db
2024-05-26 22:48:13 -07:00
Timothy J. Baek
ccbafca74c feat: save user settings to db 2024-05-26 22:47:42 -07:00
silentoplayz
f98ada1776 Update README.md
Adjustment (can revert if necessary)
2024-05-27 04:54:38 +00:00
Que Nguyen
fab3f7fb1a Fix errors and improve the Vietnamese translation 2024-05-27 11:36:37 +07:00
silentoplayz
9c2bd0ef7f Update README.md 2024-05-27 02:41:57 +00:00
silentoplayz
ac0d7201e3 Update README.md
fix
2024-05-27 02:37:15 +00:00
silentoplayz
f3dd271e2c Update README.md
Fix
2024-05-27 02:34:55 +00:00
Timothy J. Baek
9d4c07b76a fix: styling 2024-05-26 18:05:49 -07:00
Timothy J. Baek
3b6acaa513 fix: styling 2024-05-26 17:47:37 -07:00
silentoplayz
839877e197 Update README.md
fix
2024-05-27 00:40:34 +00:00
silentoplayz
e39815420e Update README.md
fix
2024-05-27 00:39:31 +00:00
silentoplayz
fc59db3181 Update README.md
Revised Key Features list
2024-05-27 00:32:21 +00:00
silentoplayz
5ea47c1258 Update README.md
Revert change
2024-05-27 00:17:00 +00:00
Timothy J. Baek
225ec3d174 refac: hide banners if models > 1 2024-05-26 17:03:35 -07:00
Timothy J. Baek
b7fc37d992 fix: spinner 2024-05-26 15:21:08 -07:00
Timothy J. Baek
9fb6160010 fix: styling 2024-05-26 13:06:26 -07:00
Timothy Jaeryang Baek
c62cb43f7e Merge pull request #2579 from cheahjs/feat/disable-community-sharing
feat: toggleable community sharing
2024-05-26 10:03:42 -10:00
Timothy J. Baek
0c04b18f36 refac 2024-05-26 13:02:40 -07:00
Timothy Jaeryang Baek
78dedb3389 Merge branch 'dev' into feat/disable-community-sharing 2024-05-26 10:00:51 -10:00
Timothy J. Baek
efedd39db1 refac: styling 2024-05-26 12:55:33 -07:00
Timothy J. Baek
36f92164cd refac: styling 2024-05-26 12:53:45 -07:00
Timothy J. Baek
a8e3d80256 chore: format 2024-05-26 12:46:06 -07:00
Timothy J. Baek
e8a3fe2e07 refac: styling 2024-05-26 12:41:47 -07:00
Timothy J. Baek
d77657f0d0 refac: only show banners in landing 2024-05-26 12:39:48 -07:00
Timothy Jaeryang Baek
3a737af190 Merge pull request #2585 from open-webui/banners
feat: banners
2024-05-26 09:36:58 -10:00
Timothy J. Baek
ecfc057f25 refac: banners dismissible by default
Co-Authored-By: Jun Siang Cheah <me@jscheah.me>
2024-05-26 12:36:51 -07:00
Timothy J. Baek
fd98add6aa refac: styling
Co-Authored-By: Jun Siang Cheah <me@jscheah.me>
2024-05-26 12:32:57 -07:00
Timothy J. Baek
242d4f0c8d feat: banners
Co-Authored-By: Jun Siang Cheah <me@jscheah.me>
2024-05-26 12:18:43 -07:00
silentoplayz
2e215ca85e Update README.md
List only top 10-ish key features of Open WebUI + dedicated features page to soon be linked to README.
2024-05-26 18:21:58 +00:00
Timothy Jaeryang Baek
00f32e2651 Merge pull request #2562 from cheahjs/feat/include-git-hash-everywhere
feat: add git hash of build everywhere
2024-05-26 07:12:55 -10:00
Jun Siang Cheah
e06417f0aa feat: add admin ui for toggling community sharing 2024-05-26 17:23:24 +01:00
Jun Siang Cheah
ccff221921 refac: run admin general settings fetches in parallel 2024-05-26 17:12:52 +01:00
Jun Siang Cheah
1235714914 feat: add ENABLE_COMMUNITY_SHARING 2024-05-26 17:10:25 +01:00
Jun Siang Cheah
6c5f5fe368 refac: move ui flags into feature flags 2024-05-26 17:05:26 +01:00
Jun Siang Cheah
276b7b90b8 Merge remote-tracking branch 'upstream/dev' into feat/backend-web-search 2024-05-26 11:31:23 +01:00
Jun Siang Cheah
aba6308825 Merge remote-tracking branch 'upstream/dev' into feat/include-git-hash-everywhere 2024-05-26 11:27:02 +01:00
Timothy J. Baek
7b81271b9e refac: hide signup when ENABLE_SIGNUP false 2024-05-26 03:13:13 -07:00
Timothy J. Baek
83107c8ea5 feat: archive all 2024-05-26 02:54:35 -07:00
Timothy J. Baek
bf8c32f09f refac: admin panel 2024-05-26 02:43:52 -07:00
Timothy J. Baek
d9de426fcd feat: export all archived chats 2024-05-26 02:22:46 -07:00
Timothy J. Baek
e20bb23409 feat: access archived chats as admin 2024-05-26 02:00:31 -07:00
Timothy J. Baek
62d37f1f10 refac 2024-05-26 01:48:18 -07:00
Timothy J. Baek
555e4e32d5 feat: allow insecure pull 2024-05-26 01:31:17 -07:00
Timothy J. Baek
84bfebd05e fix 2024-05-26 01:17:57 -07:00
Timothy J. Baek
633607a5c2 fix 2024-05-26 01:16:58 -07:00
Timothy J. Baek
1fce466253 refac: folder rename web -> webui 2024-05-26 01:15:48 -07:00
Jun Siang Cheah
4fdb26fdc4 refac: rename build hash vars 2024-05-26 08:49:30 +01:00
Timothy Jaeryang Baek
6e89a481be Merge pull request #2180 from austenadler/dev
Always open links in chat in a new tab
2024-05-25 21:32:39 -10:00
Timothy J. Baek
27f034b216 feat: auto detect user language 2024-05-26 00:17:20 -07:00
Timothy J. Baek
f755f5e512 feat: model search support 2024-05-25 23:52:17 -07:00
silentoplayz
47e8465626 Update README.md
More adjustments
2024-05-26 06:38:40 +00:00
silentoplayz
e2fe87a4d0 Update README.md
Let me cook pls
2024-05-26 06:19:39 +00:00
silentoplayz
c2c7299650 Update README.md
Revamped features list into key features and more features categorized into dropdown lists
2024-05-26 05:12:43 +00:00
Timothy J. Baek
98ec38a8e7 fix 2024-05-25 17:43:28 -07:00
Timothy J. Baek
1ab01170b9 refac: loading 2024-05-25 16:26:25 -07:00
Timothy J. Baek
c1526b0022 fix 2024-05-25 15:56:47 -07:00
Timothy J. Baek
5e4a1ff6fb fix 2024-05-25 15:54:00 -07:00
Timothy J. Baek
74ca4189e2 fix 2024-05-25 15:46:22 -07:00
Timothy J. Baek
7f77b3addb rm litellm dependency 2024-05-25 14:52:08 -07:00
Timothy J. Baek
79f440f302 refac: byebye litellm 2024-05-25 14:43:35 -07:00
Timothy J. Baek
50951459c9 refac: styling 2024-05-25 14:25:53 -07:00
Timothy J. Baek
6c8997fcbc fix 2024-05-25 14:24:00 -07:00
Timothy J. Baek
9ad4a93876 fix: ollama settings 2024-05-25 14:08:41 -07:00
Timothy J. Baek
afcab78cab fix: models 2024-05-25 13:48:45 -07:00
Timothy J. Baek
3d0b3eb577 fix: styling 2024-05-25 13:13:25 -07:00
Timothy Jaeryang Baek
92e5e6c78e Merge pull request #2563 from KodeurKubik/dev
Completed French Translations (2nd time)
2024-05-25 09:49:36 -10:00
Jun Siang Cheah
b1265c9c34 Merge remote-tracking branch 'upstream/dev' into feat/backend-web-search 2024-05-25 14:55:49 +01:00
Kubik
b936c9bff7 Tabs back (idk my editor changed it to spaces) 2024-05-25 14:40:42 +03:00
Kubik
b6474aa02d Hopefully no conflicts here 2024-05-25 14:34:05 +03:00
Timothy Jaeryang Baek
e9c8341d38 Merge pull request #2561 from cheahjs/feat/generate-source-maps
feat: generate production source maps to assist debugging
2024-05-24 23:51:26 -10:00
Timothy J. Baek
1b5dfb3aac chore: format 2024-05-25 02:50:29 -07:00
Timothy Jaeryang Baek
67a5020cdc Merge pull request #2505 from open-webui/dev-models
feat: openai api compatible model presets (profiles/modelfiles)
2024-05-24 23:47:25 -10:00
Timothy J. Baek
a6af20e1eb fix: codeblock rendering issue 2024-05-25 02:47:09 -07:00
Timothy J. Baek
42dcf1b08c refac: stop param 2024-05-25 02:26:26 -07:00
Timothy J. Baek
88d053833d feat: preset backend logic 2024-05-25 02:05:05 -07:00
Timothy J. Baek
7d2ab168f1 refac: repeat_penalty renamed to frequency_penalty 2024-05-25 02:04:47 -07:00
Jun Siang Cheah
a579f5f8cf feat: add git hash of build everywhere 2024-05-25 09:19:49 +01:00
Jun Siang Cheah
3d1cc1e422 feat: generate production source maps to assist debugging 2024-05-25 09:03:04 +01:00
Timothy J. Baek
1fbcf13371 fix: model edit 2024-05-25 01:01:50 -07:00
Timothy J. Baek
bd6ccf8c15 refac: num_predict renamed to max_tokens 2024-05-25 00:58:20 -07:00
Timothy J. Baek
1aa33a4667 refac 2024-05-25 00:44:21 -07:00
Timothy J. Baek
fad07d2049 feat: import models 2024-05-25 00:36:08 -07:00
Timothy J. Baek
ea69087697 fix 2024-05-25 00:24:31 -07:00
Timothy J. Baek
331941dbc1 feat: show desc in model selector 2024-05-25 00:19:26 -07:00
Timothy J. Baek
1fb4c60233 fix: playground 2024-05-25 00:02:17 -07:00
Timothy J. Baek
0c7f0f4430 refac: suggestion prompts 2024-05-24 23:54:22 -07:00
Timothy J. Baek
bbf5e37f6f fix 2024-05-24 23:43:13 -07:00
Timothy J. Baek
96c0b8a6a6 feat: clone support 2024-05-24 23:42:27 -07:00
Timothy J. Baek
0715cd2811 feat: model capabilities 2024-05-24 23:34:58 -07:00
Timothy J. Baek
89d80b58e1 fix: presets should not be allowed as a base model 2024-05-24 22:47:44 -07:00
Timothy J. Baek
83097cd8be refac: remove modelfiles completely 2024-05-24 22:40:36 -07:00
Timothy J. Baek
c8393db97c fix 2024-05-24 22:29:26 -07:00
Timothy J. Baek
ce3d582e61 refac: responseMessage 2024-05-24 22:28:32 -07:00
Timothy J. Baek
dac9634242 feat: create model 2024-05-24 22:21:57 -07:00
Timothy J. Baek
ca3108a54d refac 2024-05-24 20:29:13 -07:00
Timothy J. Baek
e316abcfc8 refac: better migration script 2024-05-24 19:26:27 -07:00
Timothy J. Baek
8bca17ee1d refac 2024-05-24 18:32:48 -07:00
Timothy J. Baek
708d755eda feat: model update 2024-05-24 18:26:36 -07:00
Timothy J. Baek
0a48114bd2 refac 2024-05-24 03:06:57 -07:00
Timothy J. Baek
468c6398cd feat: unified models integration 2024-05-24 03:02:56 -07:00
Timothy J. Baek
e80e4c304a refac 2024-05-24 02:17:48 -07:00
Timothy J. Baek
404875cab9 fix 2024-05-24 02:11:17 -07:00
Timothy J. Baek
6dbd59c7c6 fix: migration 2024-05-24 01:43:04 -07:00
Timothy J. Baek
110ed67468 feat: unified /models endpoint 2024-05-24 01:40:48 -07:00
Timothy J. Baek
4d57e08b38 feat: modelfiles to models 2024-05-24 00:26:00 -07:00
Timothy J. Baek
17e4be49c0 feat: migrate modelfiles to models 2024-05-23 23:47:01 -07:00
Timothy J. Baek
3be0fa63ee fix 2024-05-23 22:59:11 -07:00
Timothy J. Baek
363ab562c3 refac: migration 2024-05-23 22:58:26 -07:00
Timothy J. Baek
7d159ca723 fix: styling 2024-05-22 23:34:29 -07:00
Timothy J. Baek
bfdf4f7a8a refac: #2506 2024-05-22 22:05:13 -07:00
Timothy Jaeryang Baek
f34fd3fbe1 Merge pull request #2140 from cheahjs/feat/model-config
feat: configurable model name, description and vision capability
2024-05-22 14:03:54 -10:00
Timothy J. Baek
d0d76e2ad5 refac 2024-05-22 16:11:02 -07:00
Jun Siang Cheah
60433856a2 Merge remote-tracking branch 'upstream/dev' into feat/backend-web-search 2024-05-22 22:28:45 +01:00
Timothy Jaeryang Baek
98194d97df Merge pull request #2480 from sime2408/feature/translation-fixes-croatian
feat/translation fixes croatian
2024-05-22 09:42:02 -10:00
Timothy Jaeryang Baek
1c2c244041 Merge branch 'dev' into feature/translation-fixes-croatian 2024-05-22 09:41:53 -10:00
Timothy Jaeryang Baek
f8e77ea129 Merge pull request #2490 from reiebrole30/dev
Dev: Added Cebuano Translation
2024-05-22 09:41:23 -10:00
Timothy Jaeryang Baek
bf6f0c380b Merge pull request #2501 from cheahjs/feat/speed-up-loading
refac: speed up app mount by parallelizing API requests
2024-05-22 09:39:55 -10:00
Jun Siang Cheah
929384e39a refac: speed up app mount by parallelizing API requests 2024-05-22 19:40:16 +01:00
Rei Ebrole
c7027f11d6 Update languages.json
Removed duplicate entry of ceb-PH in the lang code json
2024-05-23 02:32:42 +08:00
Timothy Jaeryang Baek
f8778333e5 Merge pull request #2487 from cheahjs/chore/upgrade-deps
chore: upgrade github actions and python dependencies
2024-05-22 06:10:36 -10:00
root
ffd157d80c adding Cebuano translation
Merge remote-tracking branch 'fork/main'
2024-05-22 10:07:04 +00:00
Rei Ebrole
b4d737c2ff Update languages.json 2024-05-22 18:00:21 +08:00
root
482d615950 Added Cebuano 2024-05-22 09:57:47 +00:00
Jun Siang Cheah
1aebf88069 chore: update python dependencies 2024-05-22 09:50:22 +01:00
Jun Siang Cheah
dcae813d05 chore: update github actions 2024-05-22 09:23:26 +01:00
Timothy Jaeryang Baek
8df0429c99 Merge branch 'dev' into feat/model-config 2024-05-21 21:37:04 -10:00
Timothy J. Baek
59602eea09 chore: format 2024-05-22 00:02:20 -07:00
Timothy J. Baek
21ca55dd30 feat: toggleable ollama 2024-05-21 23:58:42 -07:00
sime2408
ab1aeba1dd translation to croatian fixes 2024-05-22 08:08:30 +02:00
Timothy J. Baek
af022947be feat: pipeline integration 2024-05-21 21:58:02 -07:00
Timothy J. Baek
798e6e4128 chore: format 2024-05-21 21:39:45 -07:00
Timothy J. Baek
793a8603ec Update package.json 2024-05-21 21:39:30 -07:00
Timothy J. Baek
771657266a chore: format 2024-05-21 21:38:58 -07:00
Timothy Jaeryang Baek
45bd0f2a91 Merge pull request #2473 from cheahjs/fix/space-litellm-config
fix: space startup script
2024-05-21 18:28:39 -10:00
Timothy Jaeryang Baek
25c9fb6a3c Merge pull request #2474 from baptistecs/patch-1
Fix .js mimetype on Windows 11
2024-05-21 18:27:57 -10:00
Timothy Jaeryang Baek
f753db603b Merge pull request #2478 from Yanyutin753/chat-message
feat make stronger
2024-05-21 18:26:56 -10:00
Timothy J. Baek
c8a60cc1ea 0.2.0.dev1 2024-05-21 21:18:42 -07:00
Timothy J. Baek
5d356bdc6b Delete release-pypi-test.yml 2024-05-21 21:02:40 -07:00
Timothy J. Baek
d08693f4eb Create release-pypi-test.yml 2024-05-21 20:56:11 -07:00
Timothy J. Baek
44d73dd62a refac 2024-05-21 20:39:37 -07:00
Timothy J. Baek
c23a944bac rename 2024-05-21 20:36:50 -07:00
Timothy Jaeryang Baek
e645679496 Merge pull request #2245 from tcztzy/dev
changed: packaging for publishing
2024-05-21 17:33:47 -10:00
Yanyutin753
bf614068c0 feat make stronger 2024-05-22 09:35:59 +08:00
Timothy J. Baek
0cc4366f70 fix 2024-05-21 17:47:53 -07:00
Timothy J. Baek
cc47e63526 fix: chat refac 2024-05-21 17:42:39 -07:00
Baptiste Clarey Sjöstrand
ecb656af70 Fix .js mimetype on Windows 11
Failed to load module script: Expected a JavaScript module script but the server responded with a MIME type of "text/plain". Strict MIME type checking is enforced for module scripts per HTML spec.
2024-05-22 00:04:00 +02:00
Jun Siang Cheah
b20ec0586e refac: only create space admin user when configured 2024-05-21 22:09:25 +01:00
Jun Siang Cheah
889a2fd77e fix: wrong path for space litellm config 2024-05-21 22:07:51 +01:00
Jun Siang Cheah
f21c8626d6 refac: switch to meta and params, remove source 2024-05-21 22:05:16 +01:00
Timothy Jaeryang Baek
2502bd8aeb Merge pull request #2472 from cheahjs/fix/hf-space
fix: chown /app/backend/data after preloading models
2024-05-21 10:45:20 -10:00
Jun Siang Cheah
54c29f8bd3 fix: chown /app/backend/data after preloading models 2024-05-21 21:30:42 +01:00
Timothy Jaeryang Baek
19bb6e36e1 Merge pull request #2471 from open-webui/hf-space
feat: hf space
2024-05-21 10:00:20 -10:00
Timothy J. Baek
e3f8ca993d refac: dynamically add yaml 2024-05-21 12:59:53 -07:00
Timothy J. Baek
c1a97278a8 feat: deploy to hf spaces 2024-05-21 12:53:43 -07:00
Jun Siang Cheah
7ccef3e77a Merge remote-tracking branch 'upstream/dev' into feat/model-config 2024-05-21 19:17:42 +01:00
Timothy J. Baek
247af333b6 chore: disable dependabot 2024-05-21 11:09:27 -07:00
Timothy Jaeryang Baek
34253d216a Merge pull request #2469 from cheahjs/refac/consolidate-chat-into-single-component
refac: consolidate chat logic into single component
2024-05-21 07:58:08 -10:00
Timothy J. Baek
2e16fe9f85 Update languages.json 2024-05-21 10:56:50 -07:00
Timothy J. Baek
3878a09095 Update languages.json 2024-05-21 10:55:35 -07:00
Timothy Jaeryang Baek
928b791950 Merge pull request #2454 from AlyMobarak/patch-1
Fixed Arabic Translation
2024-05-21 07:52:42 -10:00
Timothy Jaeryang Baek
3aa201b7d5 Merge pull request #2416 from Yanyutin753/tem-trans
Organizes almost all languages and completes those without translations
2024-05-21 07:51:51 -10:00
Jun Siang Cheah
6d237999dc fix: formatting 2024-05-21 18:46:03 +01:00
Timothy Jaeryang Baek
b089986248 Merge pull request #2432 from cheahjs/feat/a1111-integration-test
ci: add image gen with automatic1111 to integration test
2024-05-21 07:36:33 -10:00
Timothy Jaeryang Baek
5d384272b7 Merge pull request #2433 from kroonen/dev
[fix] memory window is masked
2024-05-21 07:34:18 -10:00
Jun Siang Cheah
5eb9b00f8d refac: consolidate chat logic into single component 2024-05-21 18:30:31 +01:00
Timothy J. Baek
8fc64e45b9 refac: styling 2024-05-21 10:16:35 -07:00
Timothy Jaeryang Baek
d43ee0fc5b Merge pull request #2465 from open-webui/dev
fix
2024-05-21 07:07:03 -10:00
Timothy J. Baek
6b57d8d370 chore: format 2024-05-21 10:06:26 -07:00
Timothy Jaeryang Baek
b3de61247b Merge pull request #2453 from AlyMobarak/main
[fix] for issue #2447 (OpenAI-like API fails when a redundant, empty assistant message is sent over)
2024-05-21 07:05:07 -10:00
Aly Salah
87982fcab3 Fixed several translations and filled in most empty ones. 2024-05-21 21:48:13 +08:00
Aly Salah
2e953e6faa Fixed several translations and filled in most empty ones. 2024-05-21 14:09:59 +03:00
Aly Mobarak
b87cf0db7d Filters messages before going through to the OpenAI API to not include any empty messages. 2024-05-21 13:30:44 +03:00
Aly Mobarak
a07b89da6f Updated filter to filter null message objects as well. 2024-05-21 13:30:21 +03:00
Aly Mobarak
8116b04338 Filter message and don't send over any message that has empty contents to OpenAI's API. 2024-05-21 12:07:01 +03:00
Yanyutin753
968df25f35 fix Format & Build Frontend 2024-05-21 12:14:37 +08:00
Yanyutin753
4f0b6e39a8 feat sort by the first letter of the language 2024-05-21 12:03:35 +08:00
Yanyutin753
51b6f0d490 optimize some details 2024-05-21 10:47:07 +08:00
Yanyutin753
7d5a7c6194 optimize some details 2024-05-21 10:44:55 +08:00
Timothy Jaeryang Baek
7a556b2240 Merge pull request #2428 from cheahjs/feat/better-docker-caching
feat: cache docker layers to registry
2024-05-20 16:30:59 -10:00
Timothy Jaeryang Baek
3355577bac Merge pull request #2436 from open-webui/dev
fix: >:(
2024-05-20 16:21:31 -10:00
Timothy J. Baek
23859756d8 fix: >:( 2024-05-20 19:20:49 -07:00
Yanyutin753
9ee2f98a6a optimize traditional Chinese 2024-05-21 10:14:51 +08:00
Yanyutin753
88ee3a4205 optimize and complete the languages.json 2024-05-21 10:09:21 +08:00
Robin Kroonen
e344d3fbf7 Update docker-compose.yaml 2024-05-20 19:56:14 -04:00
Robin Kroonen
e9e17cd0a8 Update docker-compose.yaml 2024-05-20 19:53:35 -04:00
Robin Kroonen
cbec9ee6ca Merge branch 'dev' of https://github.com/kroonen/open-webui into dev 2024-05-20 19:34:11 -04:00
Jun Siang Cheah
a937f6504a feat: add image gen with automatic1111 to integration test 2024-05-20 23:03:05 +01:00
Robin Kroonen
33195b1c4b Update ManageModal.svelte
removed max-h-[22rem] to correct a bug in the UI/UX
2024-05-20 17:55:33 -04:00
Robin Kroonen
ec4c6e3918 Merge branch 'dev' of https://github.com/kroonen/open-webui into dev 2024-05-20 17:35:12 -04:00
Jun Siang Cheah
e142223ed1 feat: cache docker layers to registry 2024-05-20 20:05:53 +01:00
Jun Siang Cheah
224a578e6b Merge remote-tracking branch 'upstream/dev' into feat/backend-web-search 2024-05-20 19:53:23 +01:00
Timothy Jaeryang Baek
aea81fc988 Merge pull request #2427 from open-webui/dev
fix
2024-05-20 07:12:19 -10:00
Timothy J. Baek
54677a3374 fix 2024-05-20 10:10:35 -07:00
Jun Siang Cheah
69bac2a20e feat: use the conversation's model instead of the first model for query gen 2024-05-20 18:07:52 +01:00
Jun Siang Cheah
eb509c460a Merge remote-tracking branch 'origin/dev' into feat/backend-web-search 2024-05-20 18:01:29 +01:00
Yanyutin753
97e6c2ac04 Fixed the Ukrainian translation 2024-05-21 00:59:22 +08:00
Timothy Jaeryang Baek
175293fb63 Merge pull request #2422 from open-webui/dev
fix: rag
2024-05-20 04:23:20 -10:00
Timothy J. Baek
322db31dc9 fix: rag 2024-05-20 07:22:43 -07:00
Robin Kroonen
985ee1c261 Merge branch 'dev' of https://github.com/kroonen/open-webui into dev 2024-05-20 08:33:23 -04:00
Yanyutin753
6e6b1382ef optimize tranditional Chinese 2024-05-20 18:31:33 +08:00
Yanyutin753
5437d90a25 Fix Format & Build Frontend 2024-05-20 18:13:15 +08:00
Yanyutin753
baddd68741 translate all fr-CA 2024-05-20 18:07:57 +08:00
Yanyutin753
936401b456 translate all es-ES 2024-05-20 17:54:29 +08:00
Yanyutin753
f680817386 translate all fa-IR 2024-05-20 17:54:29 +08:00
Yanyutin753
9e23a3bde7 translate all fr-FR 2024-05-20 17:54:29 +08:00
Yanyutin753
a626796650 translate all he-IL 2024-05-20 17:54:29 +08:00
Yanyutin753
04c2db430d translate all ca-ES 2024-05-20 17:54:29 +08:00
Yanyutin753
23fef594fe translate all bn-BD 2024-05-20 17:54:29 +08:00
Yanyutin753
1ad8746f69 translate all bg-BG 2024-05-20 17:54:29 +08:00
Yanyutin753
60df8a6203 translate all hi-IN 2024-05-20 17:54:29 +08:00
Yanyutin753
92f46faf6e translate all ja-JP 2024-05-20 17:54:29 +08:00
Yanyutin753
d203a2a7c6 translate all ka-GE 2024-05-20 17:54:28 +08:00
Yanyutin753
5b96939342 translate all nl-NL 2024-05-20 17:54:28 +08:00
Yanyutin753
2684ef64c7 translate all ko-KR 2024-05-20 17:54:28 +08:00
Yanyutin753
8aef3934c4 translate all de-DE and fi-FI 2024-05-20 17:54:28 +08:00
Yanyutin753
0d43d9ef3a translate all pt-BR 2024-05-20 17:54:28 +08:00
Yanyutin753
e337c7827b translate all pt-PT 2024-05-20 17:54:28 +08:00
Yanyutin753
62b886a030 translate all ru-RU 2024-05-20 17:54:28 +08:00
Yanyutin753
f83dee792a translate all in hr-HR, It-IT and It-LT 2024-05-20 17:54:28 +08:00
Yanyutin753
a354d6043b translate all pa-IN and pl-PL 2024-05-20 17:54:28 +08:00
Yanyutin753
20b9e2abd8 translate all sv-SE 2024-05-20 17:54:28 +08:00
Yanyutin753
02a0a599ff translate all sr-RS and tr-TR 2024-05-20 17:54:28 +08:00
Yanyutin753
314bd28d21 translate all uk-UA 2024-05-20 17:54:28 +08:00
Yanyutin753
9fdaece415 translate all vi-VN 2024-05-20 17:54:28 +08:00
Yanyutin753
9d1013748f translate ar-BH, zh-CN,and zh-TW 2024-05-20 17:54:28 +08:00
Tang Ziya
b60989627e add: github action 2024-05-20 16:34:37 +08:00
Tang Ziya
a018df2734 infra: build 2024-05-20 16:34:37 +08:00
Tang Ziya
d5a4ab46f4 changed: packaging using rye and use file relative path instead of pwd relative. 2024-05-20 16:34:36 +08:00
Jun Siang Cheah
1a16f8fb1c Merge remote-tracking branch 'origin/dev' into feat/model-config 2024-05-20 09:02:41 +01:00
Timothy Jaeryang Baek
2ce6535a2d Merge pull request #2401 from open-webui/dev
fix
2024-05-19 15:33:50 -10:00
Timothy J. Baek
056c413e23 fix 2024-05-19 18:23:22 -07:00
Timothy J. Baek
3c55773bb2 fix 2024-05-19 18:17:18 -07:00
Timothy J. Baek
a600a32f18 fix 2024-05-19 17:53:21 -07:00
Timothy Jaeryang Baek
7068ea9279 Merge pull request #2397 from open-webui/dev
refac
2024-05-19 12:46:14 -10:00
Timothy J. Baek
b088b06575 refac 2024-05-19 15:45:57 -07:00
Robin Kroonen
064d307d96 Merge branch 'dev' of https://github.com/open-webui/open-webui into dev 2024-05-19 16:42:43 -04:00
Timothy Jaeryang Baek
66c2024d76 Merge pull request #2395 from open-webui/dev
fix
2024-05-19 10:25:07 -10:00
Timothy J. Baek
ab271c82ee Update ProfileImage.svelte 2024-05-19 13:24:46 -07:00
Timothy Jaeryang Baek
be5534c655 Merge pull request #2376 from open-webui/dev
0.1.125
2024-05-19 10:16:36 -10:00
Timothy J. Baek
bcc2bab623 refac 2024-05-19 13:15:41 -07:00
Timothy J. Baek
0260c714d9 refac 2024-05-19 13:11:56 -07:00
Robin Kroonen
dc6db5e8b0 Merge branch 'dev' of https://github.com/kroonen/open-webui into dev 2024-05-19 15:52:21 -04:00
Robin Kroonen
e10d029b06 Merge branch 'dev' of https://github.com/kroonen/open-webui into dev 2024-05-19 15:51:41 -04:00
Timothy Jaeryang Baek
009e85d56e Merge pull request #2394 from Yanyutin753/tem-tran
 add translations in shortcut
2024-05-19 09:20:55 -10:00
Timothy J. Baek
4d27ecef1e fix 2024-05-19 12:06:58 -07:00
Timothy J. Baek
6e6302a4a2 fix 2024-05-19 12:00:10 -07:00
Timothy J. Baek
2abbc9092e fix 2024-05-19 11:54:55 -07:00
Timothy J. Baek
4922b65753 Update Personalization.svelte 2024-05-19 11:51:32 -07:00
Timothy J. Baek
8163902ad1 fix: styling 2024-05-19 11:43:13 -07:00
Yanyutin753
0f4492114f fix Fomat & Build Frontend 2024-05-20 01:11:50 +08:00
Yanyutin753
bb6b363875 add translations in shortcut 2024-05-20 01:11:50 +08:00
Timothy J. Baek
e36be75754 Update CodeBlock.svelte 2024-05-19 10:07:43 -07:00
Timothy J. Baek
ec93fd15c3 Update CHANGELOG.md 2024-05-19 09:48:22 -07:00
Timothy J. Baek
5961fc84f4 feat: seaborn added to pyodide
Co-Authored-By: Silentoplayz <50341825+Silentoplayz@users.noreply.github.com>
2024-05-19 09:46:43 -07:00
Timothy J. Baek
a5fa881d4a Update CHANGELOG.md
Co-Authored-By: Silentoplayz <50341825+Silentoplayz@users.noreply.github.com>
2024-05-19 09:34:47 -07:00
Timothy J. Baek
4fe237da58 Update CHANGELOG.md
Co-Authored-By: Silentoplayz <50341825+Silentoplayz@users.noreply.github.com>
2024-05-19 09:30:56 -07:00
Timothy J. Baek
6139d775ef feat: clear memory 2024-05-19 09:26:24 -07:00
Timothy J. Baek
8f83f51c68 chore: format 2024-05-19 09:18:30 -07:00
Timothy J. Baek
4754e5c062 doc: changelog 2024-05-19 09:17:11 -07:00
Timothy J. Baek
5b08dc56af chore: format 2024-05-19 09:01:48 -07:00
Timothy J. Baek
8ec3934599 fix: memory 2024-05-19 08:53:28 -07:00
Timothy J. Baek
0d07d10bd8 fix 2024-05-19 08:47:37 -07:00
Timothy J. Baek
bbdfcd18e1 fix 2024-05-19 08:46:39 -07:00
Timothy J. Baek
febab58821 feat: memory integration 2024-05-19 08:40:46 -07:00
Timothy J. Baek
2638ae6a93 chore: format 2024-05-19 08:15:44 -07:00
Timothy J. Baek
a8c4588fb5 feat: memory integration 2024-05-19 08:14:04 -07:00
Timothy J. Baek
288d8a3e32 feat: memory backend 2024-05-19 08:00:07 -07:00
Timothy J. Baek
1fb5ef99e2 refac 2024-05-19 07:40:54 -07:00
Timothy J. Baek
ae35c68956 refac: styling 2024-05-19 07:37:14 -07:00
Timothy J. Baek
9ff74038b5 refac: styling 2024-05-19 07:32:19 -07:00
Jun Siang Cheah
197af1264d fix: remove stray debug log 2024-05-19 22:15:27 +08:00
Jun Siang Cheah
ae26596d0d feat: log exceptions on update_all_models 2024-05-19 22:11:28 +08:00
Timothy J. Baek
8c64f0627c fix: styling 2024-05-19 07:05:38 -07:00
Jun Siang Cheah
715a4a6c27 Merge remote-tracking branch 'upstream/dev' into feat/model-config 2024-05-19 22:01:33 +08:00
Timothy J. Baek
5376525777 refac 2024-05-19 06:51:32 -07:00
Timothy J. Baek
82bd1eb155 refac 2024-05-19 06:51:21 -07:00
Timothy J. Baek
120f8d7786 refac: regenerate behaviour 2024-05-19 05:53:43 -07:00
Timothy Jaeryang Baek
b9b25f872d Merge pull request #2391 from que-nguyen/dev
Fixed translation errors in Vietnamese locale file
2024-05-19 02:36:31 -10:00
Timothy Jaeryang Baek
123620bcc4 Merge branch 'dev' into dev 2024-05-19 02:36:18 -10:00
Timothy J. Baek
dbdf83c55f refac: styling 2024-05-19 05:22:09 -07:00
Timothy J. Baek
066789d423 refac: pyodide worker error handling 2024-05-19 05:19:48 -07:00
Timothy J. Baek
78cbdbc63f fix: code block execution 2024-05-19 05:16:31 -07:00
Timothy J. Baek
d2249921aa refac: styling 2024-05-19 05:06:13 -07:00
Timothy J. Baek
f8815b0749 chore: format 2024-05-19 05:01:57 -07:00
Timothy J. Baek
68104358d0 fix: many model regeneration 2024-05-19 05:01:29 -07:00
Timothy J. Baek
676a4dffd0 feat: many model interaction ui 2024-05-19 04:46:35 -07:00
Timothy Jaeryang Baek
0ede1d1483 Merge pull request #2388 from Yanyutin753/uploadImage
 Expend upload image file types
2024-05-19 01:40:08 -10:00
Yanyutin753
c5f13cfdda Expend upload image file types 2024-05-19 19:10:00 +08:00
Jun Siang Cheah
fa4c22492f feat: add warning icon to images when some models are not vision capable 2024-05-19 18:57:13 +08:00
Jun Siang Cheah
4002ead6af feat: store model configs in the database 2024-05-19 18:46:24 +08:00
Que Nguyen
99dd81d700 "Fixed translation errors in Vietnamese locale file
Fixed translation errors in Vietnamese locale file locales/vi-VN/translation.json.
2024-05-19 16:24:40 +07:00
Timothy J. Baek
22d23afbaf feat: memory ui frontend 2024-05-19 00:17:40 -07:00
Timothy J. Baek
b521a06b5f fix: styling
Co-Authored-By: Jannik S. <69747628+jannikstdl@users.noreply.github.com>
2024-05-19 00:14:23 -07:00
Timothy Jaeryang Baek
6d2785d58a Merge pull request #2384 from kostich/main
Update Serbian translation
2024-05-18 21:11:17 -10:00
Марко М. Костић (Marko M. Kostić)
3cd0141957 Update Serbian translation 2024-05-19 09:02:30 +02:00
Jun Siang Cheah
1bacd5d93f Merge branch 'dev' into feat/model-config 2024-05-19 13:13:17 +08:00
Timothy Jaeryang Baek
4edef53169 Merge pull request #2374 from Silentoplayz/Silentoplayz-Ineedtonamethesebetter
Update pull_request_template.md once more
2024-05-18 13:13:23 -10:00
Timothy J. Baek
9a92ae78c8 fix: hide show username interface option 2024-05-18 16:11:28 -07:00
Timothy J. Baek
8dac4e91a5 fix: share chat issue 2024-05-18 15:23:36 -07:00
Timothy J. Baek
1e6ddb2fb3 chore: format 2024-05-18 15:02:47 -07:00
Silentoplayz
e5207e098e Update pull_request_template.md
Note to first-time contributors to open a discussion post before submitting a PR
2024-05-18 21:46:38 +00:00
Timothy J. Baek
1b6700425a fix 2024-05-18 14:29:39 -07:00
Timothy J. Baek
8766d2699a refac: include packages 2024-05-18 14:28:16 -07:00
Timothy J. Baek
7f90d813bd feat: more libs for pyodide 2024-05-18 14:24:32 -07:00
Timothy J. Baek
a8b92e5e9d fix: display username in shared chats 2024-05-18 14:19:48 -07:00
Silentoplayz
0349c86448 Update pull_request_template.md
Added Screenshots or Videos section
2024-05-18 21:17:42 +00:00
Timothy J. Baek
75491b2f87 fix: rtl styling 2024-05-18 14:04:35 -07:00
Timothy Jaeryang Baek
9e60d03911 Merge pull request #2318 from idomamia/feat/rtl-layout-chat-support
Add RTL layout chat support
2024-05-18 10:57:35 -10:00
Timothy Jaeryang Baek
04925407b2 Merge pull request #2369 from kostich/main
Adds the Serbian translation
2024-05-18 10:54:40 -10:00
Timothy J. Baek
a121ae2c5d chore: format 2024-05-18 13:54:13 -07:00
Timothy Jaeryang Baek
86eafdf5e4 Merge pull request #2370 from sime2408/feature/translation-croatian
translation to croatian
2024-05-18 10:47:41 -10:00
Timothy Jaeryang Baek
12d59b6184 Merge pull request #2366 from cheahjs/refac/pyscript-npm
refac: fetch pyodide deps at build time, not checked into git
2024-05-18 10:47:34 -10:00
Timothy Jaeryang Baek
6f6b18e727 Merge pull request #2368 from cheahjs/fix/case-insensitive-model-search
fix: case insensitive model search
2024-05-18 10:44:07 -10:00
sime2408
719a8a9327 translation to croatian 2024-05-18 21:31:44 +02:00
Марко М. Костић (Marko M. Kostić)
b8b9cab529 More fixes 2024-05-18 17:29:41 +02:00
Марко М. Костић (Marko M. Kostić)
8a0fa0b493 Fix translation 2024-05-18 17:27:40 +02:00
Марко М. Костић (Marko M. Kostić)
0176edd4c7 Initial Serbian Translation 2024-05-18 14:56:08 +02:00
Jun Siang Cheah
f9587032de fix: case insensitive model search 2024-05-18 19:59:29 +08:00
Марко М. Костић (Marko M. Kostić)
4e43d31604 Initial new translation 2024-05-18 13:39:48 +02:00
Ido Henri Mamia
27c59d985f Merge branch 'dev' into feat/rtl-layout-chat-support 2024-05-18 13:58:53 +03:00
Jun Siang Cheah
781c904c91 refac: fetch pyodide deps at build time, not checked into git 2024-05-18 14:30:42 +08:00
Timothy J. Baek
400bfa5a02 fix: rag config.json 2024-05-17 19:53:38 -07:00
Timothy J. Baek
7facfa33d0 fix: litellm model filtering 2024-05-17 19:47:52 -07:00
Timothy J. Baek
3aa6b0fea9 fix: model filter issue 2024-05-17 19:11:14 -07:00
Timothy Jaeryang Baek
3890ea1490 Merge pull request #2240 from justinh-rahb/i18n-punjabi
Add Punjabi (India) language
2024-05-17 08:18:00 -10:00
Timothy J. Baek
af5b2ddb01 fix: typo 2024-05-17 11:17:17 -07:00
Timothy J. Baek
5b2369da5d fix: #2332 2024-05-17 10:56:16 -07:00
Timothy J. Baek
014e52c072 fix: connection timeout 2024-05-17 10:49:12 -07:00
Timothy J. Baek
fa54eb687e refac: switch styling 2024-05-17 10:45:50 -07:00
Timothy J. Baek
8181ae3918 fix 2024-05-17 10:35:33 -07:00
Timothy J. Baek
cdbabdfa5a refac 2024-05-17 10:30:22 -07:00
Justin Hayes
400deb90b4 Reverting to mispelling from source 2024-05-17 10:55:23 -04:00
Justin Hayes
2c44443f93 Added new strings 2024-05-17 10:52:23 -04:00
Ido Henri Mamia
e5d574307e Merge branch 'dev' of https://github.com/open-webui/open-webui into feat/rtl-layout-chat-support 2024-05-17 13:04:03 +03:00
Ido Henri Mamia
92c694e8ac Fix arrows to be in LTR layout for all layouts 2024-05-17 13:00:31 +03:00
Timothy J. Baek
6e19e46b07 refac: styling 2024-05-16 23:20:21 -10:00
Timothy J. Baek
aa9f3d0632 refac: styling 2024-05-16 23:19:31 -10:00
Timothy J. Baek
85b2a63b6d fix: matplotlib 2024-05-16 23:15:39 -10:00
Timothy J. Baek
ef300248ba feat: matplotlib support 2024-05-16 22:54:37 -10:00
Timothy J. Baek
548160c739 refac: time limit to 60sec 2024-05-16 22:39:46 -10:00
Timothy J. Baek
a4630a9825 feat: code execution time limit 2024-05-16 22:39:07 -10:00
Timothy J. Baek
c7221983a5 fix: input prompt message 2024-05-16 21:09:53 -10:00
Timothy J. Baek
c7e1d3dbc7 fix 2024-05-16 21:03:59 -10:00
Timothy J. Baek
5e3c801c44 refac: no output 2024-05-16 20:53:50 -10:00
Timothy J. Baek
b27b035caa refac: disable run button when code is running 2024-05-16 20:47:13 -10:00
Timothy Jaeryang Baek
a2933bf89d Merge pull request #2328 from cheahjs/fix/image-gen-after-config-refac
fix: image generation broken by config persistence
2024-05-16 20:39:21 -10:00
Timothy J. Baek
302a17b3ed feat: code input support 2024-05-16 20:35:25 -10:00
Timothy J. Baek
01a656cc80 feat: numpy, pandas support 2024-05-16 20:25:55 -10:00
Jun Siang Cheah
51a41c8db4 fix: image generation broken by config persistence 2024-05-17 13:52:48 +08:00
Jun Siang Cheah
5d3eddf7e9 feat: update to persistent config 2024-05-17 13:51:17 +08:00
Timothy J. Baek
6dedf9885f fix: code execution error handling 2024-05-16 19:30:09 -10:00
Timothy J. Baek
4efb8638ff refac 2024-05-16 19:22:10 -10:00
Timothy J. Baek
8f8ce26948 refac: migrated to pyodide from pyscript 2024-05-16 19:21:08 -10:00
Timothy J. Baek
0a9092156c chore: format 2024-05-16 18:21:19 -10:00
Timothy Jaeryang Baek
4950feded4 Merge pull request #2325 from open-webui/pyscript
feat: python code execution
2024-05-16 18:18:03 -10:00
Timothy Jaeryang Baek
7db4baeffa Merge pull request #2322 from cheahjs/feat/unsupported-nonroot-images
feat: optional UID/GID docker build args
2024-05-16 18:13:46 -10:00
Timothy J. Baek
18a7634f5c fix: python code execution 2024-05-16 18:05:43 -10:00
Timothy J. Baek
325fd2b97b refac: styling 2024-05-16 17:55:27 -10:00
Timothy J. Baek
1140036f20 feat: python error handling 2024-05-16 17:55:07 -10:00
Timothy J. Baek
58a13aec7e feat: python code execution 2024-05-16 17:49:28 -10:00
Jun Siang Cheah
0665703401 Merge branch 'dev' into feat/model-config 2024-05-17 11:41:37 +08:00
Jun Siang Cheah
1dc80786c1 feat: optional UID/GID docker build args 2024-05-17 09:44:52 +08:00
Timothy J. Baek
ba61f87a2f feat: settings personalization tab ui 2024-05-16 14:25:48 -10:00
Ido Henri Mamia
34dd32d48f Merge branch 'dev' of https://github.com/open-webui/open-webui into feat/rtl-layout-chat-support 2024-05-17 01:22:46 +03:00
Ido Henri Mamia
3f4eb6caa2 Merge branch 'dev' of https://github.com/open-webui/open-webui into feat/rtl-layout-chat-support 2024-05-17 01:22:23 +03:00
Ido Henri Mamia
2e77ad87cc Make the margin and avatar support both LTR and RTL 2024-05-16 23:29:33 +03:00
Ido Henri Mamia
97f1d1665a Add to the i18n the chat direction text 2024-05-16 23:28:44 +03:00
Ido Henri Mamia
4ccb5d39c7 Code blocks should be in LTR layout for all layouts 2024-05-16 23:27:33 +03:00
Ido Henri Mamia
16b218e686 Add layout direction to all the chat components 2024-05-16 23:26:36 +03:00
Ido Henri Mamia
a3a832a935 Add chat direction toggle button in the interface settings 2024-05-16 23:22:30 +03:00
Austen Adler
c9799991f2 Also replace > with &gt; 2024-05-16 16:18:42 -04:00
Ido Henri Mamia
9c389df727 Add chat direction to the settings store 2024-05-16 23:14:54 +03:00
Timothy J. Baek
e29a999dc9 fix: add user modal error handling 2024-05-15 19:01:02 -10:00
Jun Siang Cheah
b95027f182 feat: add searched urls to document 2024-05-16 11:47:53 +08:00
Jun Siang Cheah
9021f068b8 Merge remote-tracking branch 'origin/dev' into feat/backend-web-search 2024-05-16 11:36:53 +08:00
Justin Hayes
b423e442d7 run i18next-parser 2024-05-15 23:23:29 -04:00
Justin Hayes
d7ce488c42 Add newline 2024-05-15 23:12:46 -04:00
Justin Hayes
e7a7caf911 Add pa-IN to languages.json 2024-05-15 23:12:45 -04:00
Justin Hayes
4a549f0c76 Add Punjabi (India) translations 2024-05-15 23:12:45 -04:00
Timothy Jaeryang Baek
e979584298 Merge pull request #2296 from ChasLui/dev
feat: Add Chinese Simplified Translation
2024-05-15 17:06:56 -10:00
Timothy Jaeryang Baek
19203a8ed7 Merge pull request #2295 from cheahjs/fix/missing-jq
fix: add missing jq in non-ollama images
2024-05-15 17:06:46 -10:00
ChasLui
630c4d8bc6 fix: i18n zh-CN 2024-05-16 11:02:27 +08:00
Jun Siang Cheah
b6d36ab638 fix: add missing jq in non-ollama images 2024-05-16 10:45:00 +08:00
Timothy J. Baek
e6d20df463 refac: styling 2024-05-15 16:42:55 -10:00
Timothy J. Baek
de0f31680c Update General.svelte
#2292
2024-05-15 15:24:57 -10:00
Timothy J. Baek
39f91e39a7 feat: openwebui community backward compatibility 2024-05-15 14:40:52 -10:00
Timothy J. Baek
967277f1d5 refac: styling 2024-05-15 12:55:13 -10:00
Timothy J. Baek
f7fcf95689 Update UserMessage.svelte
c
2024-05-15 12:42:43 -10:00
Timothy J. Baek
f2378be6f9 feat: toggleable chat bubble 2024-05-15 12:39:41 -10:00
Timothy J. Baek
db158fc9e1 Update Dockerfile 2024-05-15 08:44:10 -10:00
Timothy Jaeryang Baek
e30166118c Merge pull request #2264 from iits-jonathan-rohde/fix/share-chat-reactive-loop
fix: prevent loop when fetching shared chat id
2024-05-15 08:40:40 -10:00
Timothy Jaeryang Baek
9381113f60 Update Menu.svelte 2024-05-15 13:40:25 -05:00
Timothy Jaeryang Baek
85dbdd23fc Merge branch 'dev' into fix/share-chat-reactive-loop 2024-05-15 08:38:59 -10:00
Timothy Jaeryang Baek
ea812a3bb8 Merge pull request #2281 from cheahjs/fix/settings-cypress
fix: settings cypress test after style changes
2024-05-15 08:34:17 -10:00
Timothy J. Baek
240cbda06c fix: draggable items in sidebar 2024-05-15 08:27:52 -10:00
Timothy J. Baek
d4c193060e fix: styling 2024-05-15 08:20:36 -10:00
Timothy J. Baek
7d62993007 refac: healthcheck endpoint 2024-05-15 08:17:18 -10:00
Timothy J. Baek
8f5c671e4c refac: sidebar breakpoint 2024-05-15 08:13:14 -10:00
Jun Siang Cheah
8d4c9c77bf fix: settings cypress test after style changes 2024-05-15 23:49:26 +08:00
Robin Kroonen
867c179e2d latest changes with upstream 2024-05-15 11:10:00 -04:00
Timothy J. Baek
b263e24e56 refac: styling 2024-05-15 00:19:10 -10:00
Timothy J. Baek
290b4e56c8 fix: styling 2024-05-14 23:57:32 -10:00
Timothy J. Baek
cf9dbea370 fix: styling 2024-05-14 23:50:50 -10:00
Timothy J. Baek
0722e4b690 refac: user message styling 2024-05-14 23:45:27 -10:00
Timothy J. Baek
308a2e1de0 refac: styling 2024-05-14 22:22:15 -10:00
Timothy J. Baek
13019acce6 refac: styling 2024-05-14 22:09:32 -10:00
Timothy J. Baek
aaaef3efd8 fix: unselectable sidebar
Co-Authored-By: Javier Leandro Arancibia <arancibiajav@gmail.com>
Co-Authored-By: Maksym Dmukhovskyy <25328501+maxdyy@users.noreply.github.com>
2024-05-14 21:29:22 -10:00
Timothy J. Baek
5079d5ec87 refac: styling 2024-05-14 21:26:54 -10:00
Timothy J. Baek
1d98099099 refac: admin panel styling 2024-05-14 20:48:46 -10:00
Timothy J. Baek
e9decf2dda refac: styling 2024-05-14 20:31:22 -10:00
Timothy J. Baek
210456e424 refac: styling 2024-05-14 20:21:22 -10:00
Timothy J. Baek
c45bb64de7 refac: styling 2024-05-14 20:16:22 -10:00
Jonathan Rohde
ea6f1a0e57 Merge branch 'dev' into fix/share-chat-reactive-loop 2024-05-15 07:59:45 +02:00
Timothy J. Baek
a57a01a598 refac: navbar styling 2024-05-14 14:50:24 -10:00
Timothy J. Baek
170361c27e refac: styling 2024-05-14 14:39:48 -10:00
Timothy J. Baek
c489d473b9 refac: styling 2024-05-14 14:38:37 -10:00
Timothy J. Baek
de75679ffb fix: sidebar model selector issue 2024-05-14 12:26:53 -10:00
Timothy J. Baek
ad67339426 fix: sidebar 2024-05-14 12:17:56 -10:00
Timothy J. Baek
d88cc6007c refac: styling 2024-05-14 12:15:01 -10:00
Timothy J. Baek
370f182430 refac: styling 2024-05-14 12:11:57 -10:00
Timothy J. Baek
39fdd95a0c refac: styling 2024-05-14 12:09:30 -10:00
Timothy Jaeryang Baek
e6ff5af38a Merge pull request #2267 from maxdyy/fix/it-translation-update
fix: added missing translations for italian it-IT
2024-05-14 08:41:13 -10:00
Timothy Jaeryang Baek
767ae3be5b Merge pull request #2161 from cheahjs/fix/handlebars-harden
feat: add {{prompt:middletruncate:<length>}} to title gen
2024-05-14 08:21:39 -10:00
Timothy Jaeryang Baek
564a3a293f Merge branch 'dev' into fix/handlebars-harden 2024-05-14 08:21:26 -10:00
Maksym Dmukhovskyy
9ad4605e57 fix: added missing translations for italian 2024-05-14 20:56:04 +04:00
Jonathan Rohde
33b9629164 fix: prevent loop when fetching shared chat id 2024-05-14 16:19:37 +02:00
Jun Siang Cheah
81a3c97069 Merge branch 'dev' into feat/backend-web-search 2024-05-14 15:20:52 +08:00
Timothy Jaeryang Baek
2290eefcb0 Merge pull request #2249 from cheahjs/fix/config-refac-broken
fix: rag, chat deletion, and webhook after config persistence change
2024-05-13 21:07:17 -10:00
Jun Siang Cheah
5369023e34 fix: rag, chat deletion, and webhook after config persistence change 2024-05-14 14:30:15 +08:00
Jun Siang Cheah
f94690386c chore: formatting 2024-05-14 14:18:01 +08:00
Jun Siang Cheah
5e1c408937 Merge branch 'dev' into feat/backend-web-search 2024-05-14 14:03:23 +08:00
Timothy Jaeryang Baek
233dcb2d95 Merge pull request #2242 from bo3bdo/main
Update Arabic language
2024-05-13 14:34:15 -10:00
Timothy Jaeryang Baek
edbe5673c5 Merge branch 'dev' into main 2024-05-13 14:33:51 -10:00
HAMAD ABDULLA
1114f9625b Update translation.json 2024-05-14 01:59:17 +03:00
HAMAD ABDULLA
f42819c6ad Update Arabic language 2024-05-14 01:49:04 +03:00
Timothy Jaeryang Baek
8b0144cd06 Merge pull request #2156 from cheahjs/feat/save-config
feat: save UI config changes to config.json
2024-05-13 11:45:30 -10:00
Timothy J. Baek
0c033b5b7b refac: rename 2024-05-13 11:32:21 -10:00
Timothy J. Baek
7e0d3496b5 fix: 1st #2208 2024-05-13 11:27:58 -10:00
Timothy Jaeryang Baek
1888327a79 Merge pull request #2150 from derek-palmer/docker-compose-version-obsolete
Removed version syntax as its no longer needed per Docker Docs
2024-05-13 10:59:54 -10:00
Timothy Jaeryang Baek
fabcbc300f Merge pull request #2211 from arsaboo/header
chore: add HTTP-Referer and X-Title headers for Open WebUI
2024-05-13 10:58:48 -10:00
Timothy Jaeryang Baek
4f87512bbc Merge pull request #2236 from akx/error-page-fix
fix: load i18n before showing error page
2024-05-13 10:58:31 -10:00
Timothy J. Baek
b5c759a059 chore: format 2024-05-13 10:56:30 -10:00
Aarni Koskela
df167d8785 fix: load i18n for /error page 2024-05-13 18:00:30 +03:00
Alok Saboo
bd82334647 chore: add additional headers only for Openrouter 2024-05-13 08:28:24 -04:00
Timothy Jaeryang Baek
8e5f96e4c3 Merge pull request #2157 from joecryptotoo/healthcheck
add healthcheck to docker
2024-05-12 14:28:59 -10:00
Timothy Jaeryang Baek
88a94cc50d Merge pull request #2183 from cheahjs/fix/streaming-error
feat: better handle openai errors, add error message to message
2024-05-12 14:28:13 -10:00
Timothy Jaeryang Baek
ba13321728 Merge pull request #2186 from cheahjs/fix/windows-prettier
fix: prettier scripts work on windows
2024-05-12 14:27:48 -10:00
Timothy Jaeryang Baek
4756511aea Merge pull request #2192 from xraysight/fix/pl-translation-update
fix: added missing translations and corrected some spelling errors
2024-05-12 14:27:14 -10:00
Timothy Jaeryang Baek
543f2652c2 Merge branch 'dev' into fix/pl-translation-update 2024-05-12 14:27:00 -10:00
Timothy Jaeryang Baek
4bd999af81 Merge pull request #2196 from Silentoplayz/PR-template-update
style: update pull_request_template.md again
2024-05-12 14:26:11 -10:00
Timothy Jaeryang Baek
807be35d9f Merge pull request #2195 from not-a-ethan/not-a-ethan-patch-1
Fixed some formatting and a link
2024-05-12 14:25:26 -10:00
Timothy Jaeryang Baek
4997c8259b Merge pull request #2201 from liorkesos/main
Hebrew language support
2024-05-12 14:23:56 -10:00
Timothy Jaeryang Baek
227a3a0d86 Merge pull request #2202 from aguvener/dev
fix: tr_TR translation synchronized with the latest updates.
2024-05-12 14:23:40 -10:00
Timothy Jaeryang Baek
e483aae1b8 Merge branch 'dev' into dev 2024-05-12 14:23:33 -10:00
Timothy Jaeryang Baek
7b864d967d Merge pull request #2203 from simonaszilinskas/patch-2
Addition of the Lithuanian language
2024-05-12 14:20:23 -10:00
Timothy Jaeryang Baek
ca985cdb07 Merge pull request #2154 from Yanyutin753/citation-translate
🎖️Added translations for Citation and No source available
2024-05-12 14:19:42 -10:00
Timothy Jaeryang Baek
eb87de9a63 Merge pull request #2124 from tcztzy/main
changed: replace deprecated `on_event` with `lifespan`
2024-05-12 14:18:35 -10:00
Alok Saboo
6671e5f170 chore: add HTTP-Referer and X-Title headers for Open WebUI 2024-05-12 14:05:38 -04:00
Simonas
5e5473c792 Create and fill translation.json for Lithuanian 2024-05-12 15:13:41 +02:00
Jun Siang Cheah
f49e1afaa6 feat: inject search result doc in the response, not the query
this is to handle when we have multiple models selected or regenerate a
response, it'll only add it to the model's response and not add dupes on
the user message
2024-05-12 20:45:46 +08:00
Jun Siang Cheah
44c8b0bb83 feat: rename title generation model to task model 2024-05-12 20:45:46 +08:00
Jun Siang Cheah
466b3e3637 feat: add support for using previous messages for query generation 2024-05-12 20:45:44 +08:00
aguvener
09357b6e81 fix: update tr_TR translation with the latest updates. 2024-05-12 12:50:03 +03:00
aguvener
b63aca534b Update translation.json 2024-05-12 12:41:08 +03:00
Jun Siang Cheah
654cc09128 feat: run i18next 2024-05-12 16:16:42 +08:00
Jun Siang Cheah
d98051862d feat: mark websearch docs differently from standard docs 2024-05-12 16:05:00 +08:00
Lior Kesos
31cb6c9857 Adding hebrew translation 2024-05-12 10:59:47 +03:00
Jun Siang Cheah
3baeda7edc feat: add in-message progress indicator for web search 2024-05-12 15:21:03 +08:00
Jun Siang Cheah
d45804d7f4 feat: web search available is inferred from env vars 2024-05-12 15:19:52 +08:00
Jun Siang Cheah
9ed1a31575 fix: continue with failures when bulk loading urls with WebBaseLoader 2024-05-12 15:19:07 +08:00
Jun Siang Cheah
5d64822c84 refac: rename WrappedConfig to PersistedConfig 2024-05-12 13:28:40 +08:00
Silentoplayz
8e94db0d1b Update pull_request_template.md
Update Open WebUI pull request template to the latest version, striving to follow KeepaChangelog and Conventional Commits guidelines.
2024-05-12 02:38:40 +00:00
Ethan
4fa8d2aa5a Fixed link/formatting 2024-05-12 01:33:49 +00:00
bulek
3a4f3e97e7 Added missing translations and corrected some spelling errors 2024-05-11 21:31:31 +02:00
Jun Siang Cheah
55060045da feat: add .gitignore to .prettierignore 2024-05-11 23:55:06 +08:00
Jun Siang Cheah
7538dc051e feat: use url as source name for citations 2024-05-11 23:51:50 +08:00
Jun Siang Cheah
77928ae141 Merge branch 'dev' of https://github.com/open-webui/open-webui into feat/web-search-toggle 2024-05-11 23:51:37 +08:00
Jun Siang Cheah
2660a6e5b8 feat: prototype frontend web search integration 2024-05-11 23:44:34 +08:00
Jun Siang Cheah
619c2f9c71 fix: toggle style 2024-05-11 23:18:59 +08:00
Jun Siang Cheah
14a902fcfa feat: add web search toggle on chat 2024-05-11 23:18:59 +08:00
Jun Siang Cheah
fb8069123e feat: add WEB_SEARCH_RESULT_COUNT to control max number of results 2024-05-11 23:18:59 +08:00
Jun Siang Cheah
e3e037e187 fix: windows invocations of prettier 2024-05-11 17:43:53 +08:00
Jun Siang Cheah
3113825895 feat: better handle openai errors, add error message to message 2024-05-11 16:34:04 +08:00
Austen Adler
3a1fbb936b Always open links in a new tab 2024-05-11 00:47:46 -04:00
Yanyutin753
d2954f8eb6 feat format 2024-05-11 09:33:31 +08:00
Jun Siang Cheah
a789b785b4 feat: add unit tests for prompt templates 2024-05-10 20:41:22 +08:00
Jun Siang Cheah
34a6d55505 feat: add {{prompt:middletruncate:<length>}} to title gen
also harden the replacement logic
2024-05-10 20:17:41 +08:00
Jun Siang Cheah
a0dceb06a5 fix: nested WrappedConfig breaks things 2024-05-10 15:20:22 +08:00
Jun Siang Cheah
298e6848b3 feat: switch to config proxy, remove config_get/set 2024-05-10 15:03:24 +08:00
Jun Siang Cheah
f712c90019 feat: raise an exception if a WrappedConfig is used as a response 2024-05-10 14:18:39 +08:00
Jun Siang Cheah
058eb76568 feat: save UI config changes to config.json 2024-05-10 13:51:50 +08:00
Yanyutin753
a374405911 🎖️Added translations for Citation and No source available 2024-05-10 12:23:12 +08:00
joecryptotoo
ef8070d35d added healthcheck 2024-05-09 12:25:26 -07:00
Derek Palmer (Creative)
93aea0a4d4 Removed version synatax as its no longer needed per Docker Docs 2024-05-09 14:54:26 -04:00
Jun Siang Cheah
0f50c12c59 fix: formatting 2024-05-09 23:57:39 +08:00
Jun Siang Cheah
fa6e21f5e0 fix: update models after deleting model info 2024-05-09 23:55:56 +08:00
Jun Siang Cheah
02a4412dfc feat: add UI support for updating model info 2024-05-09 23:54:21 +08:00
Jun Siang Cheah
0dbddebcb0 feat: add API endpoint for updating configs 2024-05-09 22:41:07 +08:00
Jun Siang Cheah
8effff6524 feat: update translation files 2024-05-09 20:52:54 +08:00
Jun Siang Cheah
9bc628ca75 fix: file input not working after rejecting images 2024-05-09 20:47:29 +08:00
Jun Siang Cheah
584385e4bf fix: revert wip code for settings UI 2024-05-09 20:27:07 +08:00
Jun Siang Cheah
e69f31267a fix: remove unused API for setting model config 2024-05-09 20:25:53 +08:00
Jun Siang Cheah
e76a444ed9 feat: allow model config via config.json 2024-05-09 20:25:30 +08:00
Timothy J. Baek
9a95767062 Merge branch 'dev' of https://github.com/open-webui/open-webui into dev 2024-05-08 23:20:53 -10:00
Timothy J. Baek
c1fc7bc764 fix: citation styling 2024-05-09 00:36:29 -07:00
Timothy J. Baek
038b6af813 refac: user menu 2024-05-09 00:34:57 -07:00
Tang Ziya
1d9c745b35 fix: replace deprecated on_event with lifespan 2024-05-09 12:04:09 +08:00
Timothy Jaeryang Baek
df3675aaf7 Merge pull request #2104 from Silentoplayz/translation-update
Updated uk-UA/translation.json
2024-05-08 17:42:07 -07:00
Timothy J. Baek
685ee9b714 chore: format 2024-05-08 17:41:55 -07:00
Timothy Jaeryang Baek
90503be2ed Merge pull request #2118 from open-webui/dev
fix
2024-05-08 17:17:38 -07:00
Timothy J. Baek
26ee547f71 fix 2024-05-08 17:17:21 -07:00
Timothy Jaeryang Baek
734e62b865 Merge pull request #2117 from open-webui/dev
fix
2024-05-08 17:16:09 -07:00
Timothy J. Baek
01e020d1ad fix 2024-05-08 17:15:54 -07:00
Timothy Jaeryang Baek
943c49a07a Merge pull request #2116 from open-webui/dev
fix
2024-05-08 16:43:08 -07:00
Timothy J. Baek
88e8f31c07 fix: webui_auth permission issue 2024-05-08 16:42:41 -07:00
Timothy J. Baek
1c2aa10796 refac: disable enable_signup if webui_auth false 2024-05-08 16:33:34 -07:00
Timothy Jaeryang Baek
78c2f16c3c Merge pull request #2115 from open-webui/dev
enhancement: youtu.be url support
2024-05-08 16:30:41 -07:00
Timothy J. Baek
bcd5853ce7 enhancement: youtu.be url support 2024-05-08 16:29:25 -07:00
Timothy Jaeryang Baek
61ffbf5b5b Merge pull request #2114 from open-webui/dev
fix: trusted header
2024-05-08 16:20:43 -07:00
Timothy J. Baek
58cfa5b9c1 fix: trusted header 2024-05-08 16:19:59 -07:00
Silentoplayz
b3c5ebb323 Updated uk-UA/translation.json 2024-05-08 20:06:52 +00:00
Timothy Jaeryang Baek
9f2b846ea7 Merge pull request #2101 from open-webui/dev
doc
2024-05-08 11:31:31 -07:00
Timothy J. Baek
1b2b05d2ea Update README.md 2024-05-08 11:29:42 -07:00
Timothy J. Baek
0db71e97bb Update README.md 2024-05-08 11:28:42 -07:00
Timothy J. Baek
c6053476ca Update dependabot.yml 2024-05-08 11:02:26 -07:00
Timothy Jaeryang Baek
b8d7fdf16e Merge pull request #1965 from open-webui/dev
0.1.124
2024-05-08 10:54:33 -07:00
Timothy J. Baek
b44ae536b9 Update CHANGELOG.md 2024-05-08 10:53:38 -07:00
Timothy J. Baek
c99e351688 Update CHANGELOG.md 2024-05-08 10:52:44 -07:00
Timothy J. Baek
37abc1513c doc: fix 2024-05-08 10:52:20 -07:00
Timothy J. Baek
73689bb26c doc: changelog 2024-05-08 10:51:51 -07:00
Timothy J. Baek
06cbe337de feat: youtube loader language env var 2024-05-08 10:51:29 -07:00
Timothy J. Baek
d3822f782c feat: non-english youtube support 2024-05-08 10:47:05 -07:00
Timothy J. Baek
87daf122db refac: styling 2024-05-08 09:43:49 -07:00
Timothy J. Baek
6023f86cf7 chore: format 2024-05-08 09:34:36 -07:00
Timothy J. Baek
c40e634391 feat: better error message 2024-05-08 09:30:14 -07:00
Timothy J. Baek
01472c071b feat: support webui_auth false
#929
2024-05-08 08:40:18 -07:00
Timothy Jaeryang Baek
0d633c0d17 Merge pull request #2079 from akx/fi
feat: add Finnish localization 🇫🇮
2024-05-08 08:04:44 -07:00
Timothy Jaeryang Baek
84ea10ab45 Merge pull request #2061 from AnkurSachdeva22/main
Added Hindi Translation and language JSON updated
2024-05-08 08:04:16 -07:00
Timothy Jaeryang Baek
675328c881 Merge pull request #2002 from Yanyutin753/addAudio
🤩 Added custom openai tts models and role variables
2024-05-08 08:03:20 -07:00
Timothy Jaeryang Baek
421a964a9b Merge pull request #2082 from cheahjs/fix/reword-secret-key
fix: reword WEBUI_SECRET_KEY generation log line
2024-05-08 08:02:49 -07:00
Jun Siang Cheah
ceecfb66a5 fix: reword WEBUI_SECRET_KEY generation log line 2024-05-08 20:03:03 +08:00
Aarni Koskela
20c4ada998 feat: add Finnish localization 🇫🇮 2024-05-08 13:33:25 +03:00
AnkurSachdeva22
9fefddd844 Merge branch 'main' of https://github.com/AnkurSachdeva22/open-webui-hindi 2024-05-08 14:32:32 +05:30
AnkurSachdeva22
7dade0715d language.json sorted by language code 2024-05-08 14:25:28 +05:30
Timothy J. Baek
9c190b56ae feat: /v1/models endpoint added for ollama proxy 2024-05-07 23:34:12 -07:00
Timothy J. Baek
72675ec15f refac: citation styling 2024-05-07 23:13:37 -07:00
Timothy Jaeryang Baek
1ea91f9060 Merge pull request #2067 from cheahjs/feat/dependabot-updates
feat: update dependabot for python and github actions
2024-05-07 22:43:21 -07:00
Jun Siang Cheah
34632b7aac fix: formatting 2024-05-08 10:50:55 +08:00
Jun Siang Cheah
2969918d19 feat: enable dependabot for github actions 2024-05-08 10:48:54 +08:00
Jun Siang Cheah
32f74c9fc7 fix: don't group dependabot updates because of conflicts 2024-05-08 10:47:59 +08:00
Ankur
0ed5405571 Merge branch 'dev' into main 2024-05-08 04:54:14 +05:30
AnkurSachdeva22
6d99ef8b4b Hindi Translation file Added and language JSON updated 2024-05-08 01:39:30 +05:30
Timothy J. Baek
c63176402c chore: format 2024-05-07 11:41:17 -07:00
Timothy J. Baek
ea69c24bf7 fix: allow message switch during regeneration
Co-Authored-By: Pandazki <onlrrr@gmail.com>
2024-05-07 11:39:32 -07:00
Timothy Jaeryang Baek
a32a752791 Merge pull request #2049 from ihavecoke/main
Adjust text formatting per Chinese copywriting guidelines
2024-05-07 11:05:13 -07:00
Timothy Jaeryang Baek
68c182db96 Merge branch 'dev' into main 2024-05-07 11:05:00 -07:00
Timothy Jaeryang Baek
876853f2b0 Merge pull request #2040 from akx/uuid4
fix: do not use hardware ID in document ID generation
2024-05-07 11:02:21 -07:00
Timothy Jaeryang Baek
838c6f8217 Merge pull request #2033 from jannikstdl/update-chat-timestamp-locale
Update chat timestamp locale
2024-05-07 11:00:40 -07:00
Timothy Jaeryang Baek
1b5d88d0c0 Merge branch 'dev' into update-chat-timestamp-locale 2024-05-07 11:00:31 -07:00
Timothy Jaeryang Baek
39bc3b508c Merge pull request #2032 from jannikstdl/update-de-locale
Added new translation strings + more missing i18n keys
2024-05-07 10:59:40 -07:00
Timothy Jaeryang Baek
5661de3221 Merge pull request #2039 from akx/py-requirement-fixes
chore: Small Python requirement fixes
2024-05-07 10:47:44 -07:00
Timothy Jaeryang Baek
3f14d8bf0b Merge pull request #2038 from akx/pr-branch
doc: note PRs should target `dev`
2024-05-07 10:47:15 -07:00
ihavecoke
242e70d181 Adjust text formatting per Chinese copywriting guidelines
Refined text formatting to follow the recommendations from: https://sparanoid.com/note/chinese-copywriting-guidelines/
2024-05-07 23:47:47 +08:00
Jannik Streidl
7ce157a8b8 fix german spelling 2024-05-07 12:00:03 +02:00
Jannik Streidl
a2d0f9ce1d fix german spelling 2024-05-07 11:56:41 +02:00
Jannik Streidl
06683f5ac0 changed german spelling 2024-05-07 11:54:34 +02:00
Jannik Streidl
9f4a71029f more keys 2024-05-07 11:44:35 +02:00
Jannik Streidl
cc72341861 found more tooltops without i18n keys 2024-05-07 11:34:15 +02:00
Aarni Koskela
61bb1f1dc8 fix: do not use hardware ID in document ID generation 2024-05-07 11:42:05 +03:00
Aarni Koskela
cbd1929f36 chore: remove uuid requirement (it's a standard library module) 2024-05-07 11:39:01 +03:00
Aarni Koskela
3723f42f1d chore: remove duplicate requirements 2024-05-07 11:39:01 +03:00
Aarni Koskela
03955d6a42 chore: pin youtube-transcript-api like the rest 2024-05-07 11:39:01 +03:00
Aarni Koskela
d27f80542c doc: note PRs should target dev 2024-05-07 11:18:40 +03:00
Jannik Streidl
f32094dd41 Added keys as comments so they dont get removed by the parser 2024-05-07 09:15:23 +02:00
Jannik Streidl
3404b8dd13 added some missing keys 2024-05-07 08:43:25 +02:00
Jannik Streidl
11dcbb2e15 fix Submit button not beeing a i18n key 2024-05-07 08:18:58 +02:00
Jannik Streidl
8d5f35a404 addition 2024-05-07 08:11:48 +02:00
Jannik Streidl
755dd85a20 Added localisation for the new chat timestamps 2024-05-07 07:59:09 +02:00
Jannik Streidl
55321a3b12 Added new translation strings + 1 i18n kkey 2024-05-07 07:34:42 +02:00
Timothy Jaeryang Baek
ab43bd17f6 Merge pull request #2030 from 0xThresh/remove-helm
Remove Helm charts, which have moved to a separate repo
2024-05-06 20:35:45 -07:00
0xThresh.eth
bfb8b08723 Remove Helm charts, which have moved to a separate repo 2024-05-06 20:39:10 -06:00
Timothy J. Baek
04a9115254 feat: esc image preview 2024-05-06 19:12:43 -07:00
Timothy J. Baek
7874bea5f4 fix: code block rendering issue 2024-05-06 19:04:36 -07:00
Timothy J. Baek
e3a49c8e9b fix: styling 2024-05-06 18:50:50 -07:00
Timothy J. Baek
f58eb0d266 feat: browser search engine support 2024-05-06 17:29:16 -07:00
Yanyutin753
5d6517c537 update the name 2024-05-07 08:28:34 +08:00
Timothy J. Baek
66e7e577df refac: citation styling 2024-05-06 17:02:04 -07:00
Timothy J. Baek
9b59039388 refac: styling 2024-05-06 17:00:50 -07:00
Timothy J. Baek
9aa689ea56 feat: title trim quotation marks 2024-05-06 16:59:54 -07:00
Timothy J. Baek
7f617225a0 refac: styling 2024-05-06 16:29:57 -07:00
Timothy Jaeryang Baek
635951b55c Merge branch 'dev' into feat/backend-web-search 2024-05-06 16:26:44 -07:00
Timothy J. Baek
bf604bc07c refac: styling 2024-05-06 16:19:48 -07:00
Timothy J. Baek
485469683e chore: format 2024-05-06 16:18:11 -07:00
Timothy Jaeryang Baek
e98cb45238 Merge pull request #2011 from cheahjs/feat/rag-citations
feat: show RAG query results as citations
2024-05-06 16:17:08 -07:00
Timothy J. Baek
c84e0aa2a3 fix: .env load 2024-05-06 16:16:26 -07:00
Timothy J. Baek
aef2a514d1 refac: citation rendering 2024-05-06 16:16:26 -07:00
Timothy J. Baek
64ed0d1089 refac: include source name to citation 2024-05-06 16:16:26 -07:00
Timothy J. Baek
4c490132ba refac: styling 2024-05-06 16:16:26 -07:00
Timothy J. Baek
38590da0b1 fix: styling 2024-05-06 16:16:26 -07:00
Timothy J. Baek
e70cdf118b refac: styling 2024-05-06 16:16:26 -07:00
Jun Siang Cheah
4c6567c46f feat: group citations by source 2024-05-06 16:16:26 -07:00
Jun Siang Cheah
c3425f3bf1 fix: update translation files 2024-05-06 16:16:26 -07:00
Jun Siang Cheah
2571f3c43b fix: remove stray debug logs 2024-05-06 16:14:10 -07:00
Jun Siang Cheah
0872bea790 feat: show RAG query results as citations 2024-05-06 16:14:10 -07:00
Timothy Jaeryang Baek
ba09fcd548 Merge pull request #2026 from cheahjs/fix/integration-test-wait-ollama
fix: wait for Ollama to come up in integration test
2024-05-06 16:01:32 -07:00
Jun Siang Cheah
546d553803 fix: wait for Ollama to come up in integration test 2024-05-07 06:17:10 +08:00
Timothy J. Baek
aa0bf79330 chore: format 2024-05-06 14:51:53 -07:00
Timothy J. Baek
cecb87b8c2 feat: web_loader_ssl_verification setting 2024-05-06 14:50:55 -07:00
Timothy J. Baek
07b39d1cb9 refac: document settings 2024-05-06 14:36:36 -07:00
Timothy J. Baek
95f579cabe feat: rag ssl verification env var
Co-Authored-By: Tobias Steidle <tobias.steidle@softwaredev.de>
2024-05-06 13:12:08 -07:00
Timothy J. Baek
7f1acf9c2d chore: format 2024-05-06 12:58:24 -07:00
Timothy Jaeryang Baek
6fa88854e3 Merge pull request #1874 from jannikstdl/update-i18n
Added some i18n keys & updated german locale
2024-05-06 12:57:42 -07:00
Jannik Streidl
2dcd9e75a1 resetted parser - only new keys 2024-05-06 21:10:46 +02:00
Jun Siang Cheah
8b3e370a6e fix: run formatter 2024-05-06 17:11:04 +08:00
Jun Siang Cheah
83f086ccdd fix: do not return raw search exception due to API keys in URLs 2024-05-06 17:09:04 +08:00
Jun Siang Cheah
99e4edd364 feat: add websearch endpoint to RAG API
fix: google PSE endpoint uses GET

fix: google PSE returns link, not url

fix: serper wrong field
2024-05-06 17:09:04 +08:00
Yanyutin753
5f7188b480 fix python test 2024-05-06 15:33:29 +08:00
Yanyutin753
761c66a8d8 🤩 Added custom openai tts models and role variables 2024-05-06 15:23:27 +08:00
Timothy Jaeryang Baek
0334ab7c5d Merge pull request #2001 from cheahjs/feat/limit-title-gen-tokens
feat: restrict title gen output to 50 tokens
2024-05-06 00:05:53 -07:00
Jannik Streidl
e31cf08188 merged conflicts in PL locale 2024-05-06 08:57:18 +02:00
Jun Siang Cheah
b8c97fa012 feat: restrict title gen output to 50 tokens 2024-05-06 14:46:50 +08:00
Timothy J. Baek
e92311b4eb fix: image download issue
Co-Authored-By: Clivia <132346501+Yanyutin753@users.noreply.github.com>
2024-05-05 22:46:22 -07:00
Jun Siang Cheah
501ff7a98b feat: backend implementation of various search APIs 2024-05-06 12:28:41 +08:00
Timothy J. Baek
594f2f264f chore: formatting 2024-05-04 14:53:21 -07:00
Timothy Jaeryang Baek
c2f429425e Merge pull request #1973 from bo3bdo/main
Adding language Arabic
2024-05-04 14:52:35 -07:00
HAMAD ABDULLA
2fec591b59 Update translation.json 2024-05-05 00:39:04 +03:00
HAMAD ABDULLA
9c1f224219 Update translation.json 2024-05-04 23:30:16 +03:00
Timothy J. Baek
af1d8470cd fix: disable template variables in prompt 2024-05-04 13:21:35 -07:00
Timothy J. Baek
3a2f1d56c8 fix: promptTemplate infinite loop 2024-05-04 13:11:58 -07:00
Timothy J. Baek
a341cfe3fd refac: getTimeRange 2024-05-04 12:59:23 -07:00
Timothy J. Baek
36d8442c92 fix: chat list time range 2024-05-04 12:53:01 -07:00
HAMAD ABDULLA
cb2e989408 Adding language Arabic
The translation has been added and the previous errors have been corrected. i hope i good now. Thank you
2024-05-04 22:15:02 +03:00
Timothy J. Baek
c0fbf1e7d0 refac: styling 2024-05-04 02:11:23 -07:00
Timothy J. Baek
1c39e5cff9 refac: styling 2024-05-04 02:05:09 -07:00
Timothy J. Baek
24c0dbec0d fix: pending permission issue 2024-05-04 01:31:08 -07:00
Timothy J. Baek
0a8dae633b refac: styling 2024-05-04 01:24:43 -07:00
Timothy J. Baek
53dae39042 feat: chat list group by time range 2024-05-04 01:23:02 -07:00
Timothy J. Baek
84ec58873a fix: tag delete issue 2024-05-03 17:27:48 -07:00
Timothy J. Baek
d4db3dd593 fix: tag delete issue 2024-05-03 16:18:11 -07:00
Timothy J. Baek
fa3c7bc218 fix 2024-05-03 15:34:08 -07:00
Timothy J. Baek
facb6edfdc refac: chat tags 2024-05-03 15:25:41 -07:00
Timothy J. Baek
e8085f80a7 fix: litellm warning message
>:(
2024-05-03 15:07:12 -07:00
Timothy J. Baek
ecde76270c chore: format 2024-05-03 14:25:40 -07:00
Timothy J. Baek
70ae053a46 fix: ascii art issue
#1918
2024-05-03 14:23:38 -07:00
Timothy J. Baek
3e6b94dbc1 fix: sidebar handle 2024-05-03 14:10:00 -07:00
Timothy Jaeryang Baek
e3a3a034c1 Merge pull request #1944 from aguvener/dev
fix: tr_TR translation synchronized with the latest updates.
2024-05-03 14:05:07 -07:00
Timothy Jaeryang Baek
46027cc390 Merge branch 'dev' into dev 2024-05-03 14:04:54 -07:00
Timothy Jaeryang Baek
e4c77628be Merge pull request #1946 from Yanyutin753/shortcuts-dev
add Symbol startup instructions in ShortcutsModal
2024-05-03 14:03:44 -07:00
Timothy J. Baek
ecfb6891f6 refac 2024-05-03 14:03:22 -07:00
Yanyutin753
115c3a38a7 add Symbol startup prompt startup instructions in ShortcutsModal 2024-05-03 17:49:58 +08:00
aguvener
9d8c23b3b2 fix: synchronized with the latest updates. 2024-05-03 11:01:37 +03:00
Timothy Jaeryang Baek
30b053116d Merge pull request #1942 from open-webui/dev
fix: drag and drop styling issue
2024-05-03 00:16:59 -07:00
Timothy J. Baek
0921317b2b fix: drag and drop styling issue 2024-05-03 00:16:43 -07:00
Timothy Jaeryang Baek
f1963c95c9 Merge pull request #1937 from open-webui/dev
fix: selector styling issue
2024-05-02 18:56:44 -07:00
Timothy J. Baek
c2c89525bb fix: selector styling issue 2024-05-02 18:56:19 -07:00
Timothy Jaeryang Baek
47565065f6 Merge pull request #1936 from open-webui/dev
fix: styling issue
2024-05-02 18:50:48 -07:00
Timothy J. Baek
ecc841c57d fix: styling issue 2024-05-02 18:50:20 -07:00
Timothy Jaeryang Baek
eaaa75387f Merge pull request #1933 from open-webui/dev
fix: selector styling
2024-05-02 17:14:59 -07:00
Timothy J. Baek
61a8f73291 fix: selector styling 2024-05-02 17:14:33 -07:00
Timothy Jaeryang Baek
9df4227db4 Merge pull request #1930 from open-webui/dev
fix
2024-05-02 16:00:05 -07:00
Timothy J. Baek
683e6ca337 fix 2024-05-02 15:59:45 -07:00
Timothy Jaeryang Baek
b1eb08b44a Merge pull request #1929 from open-webui/dev
fix
2024-05-02 15:56:02 -07:00
Timothy J. Baek
989a4d3165 fix 2024-05-02 15:55:42 -07:00
Timothy Jaeryang Baek
443a256f72 Merge pull request #1928 from open-webui/dev
fix: image gen
2024-05-02 15:54:50 -07:00
Timothy J. Baek
482f010d22 fix: image gen 2024-05-02 15:54:31 -07:00
Timothy Jaeryang Baek
fc86b20350 Merge pull request #1926 from open-webui/dev
fix
2024-05-02 15:48:49 -07:00
Timothy J. Baek
1f98c091f0 fix 2024-05-02 15:48:18 -07:00
Timothy Jaeryang Baek
dc187d35a1 Merge pull request #1925 from open-webui/dev
refac: mobile detection
2024-05-02 15:44:54 -07:00
Timothy J. Baek
e0908f9f26 refac: mobile detection 2024-05-02 15:44:31 -07:00
Timothy Jaeryang Baek
75911f6e4e Merge pull request #1920 from open-webui/dev
fix
2024-05-02 15:11:46 -07:00
Timothy J. Baek
a2ebbd02ad fix: styling 2024-05-02 15:11:20 -07:00
Timothy J. Baek
a19d4267f7 fix: integration test 2024-05-02 14:20:53 -07:00
Timothy Jaeryang Baek
444f20198a Merge pull request #1917 from open-webui/dev
fix: selector input issue
2024-05-02 13:26:08 -07:00
Timothy J. Baek
a79618ee3e fix: selector input issue 2024-05-02 13:25:44 -07:00
Timothy Jaeryang Baek
38ff3209ad Merge pull request #1881 from open-webui/dev
0.1.123
2024-05-02 13:10:28 -07:00
Timothy J. Baek
2789102d27 Update chat.cy.ts 2024-05-02 12:54:28 -07:00
Timothy J. Baek
c1f88eb0ad Update chat.cy.ts 2024-05-02 12:49:25 -07:00
Jannik Streidl
e57c0c301c resolved conflicts #2 2024-05-02 21:46:24 +02:00
Timothy J. Baek
5cf621396d refac: navbar selector styling 2024-05-02 12:33:04 -07:00
Timothy J. Baek
2bece7b4c5 fix: styling 2024-05-02 03:16:08 -07:00
Timothy J. Baek
7a96893093 refac: mobile device detection 2024-05-02 03:11:56 -07:00
Timothy J. Baek
180fd44a4d refac: gesture sensitivity 2024-05-02 03:02:15 -07:00
Timothy J. Baek
a2edd2d911 refac: styling 2024-05-02 02:59:05 -07:00
Timothy J. Baek
e6cf552aad refac: styling 2024-05-02 02:54:33 -07:00
Timothy J. Baek
be6cc1c53a chore: format 2024-05-02 02:48:56 -07:00
Timothy J. Baek
f50ffc07d7 doc: changelog 2024-05-02 02:47:16 -07:00
Timothy Jaeryang Baek
e700552f93 Merge pull request #1905 from 7a6ac0/r_score_type
fix: change r_score type to float
2024-05-02 02:22:59 -07:00
Timothy J. Baek
d680d52b85 feat:'@' model support 2024-05-02 02:20:57 -07:00
Timothy J. Baek
bf35297e4a refac: default role updated to user for add user modal 2024-05-02 01:35:00 -07:00
Timothy J. Baek
c91b9af0e0 Create logo.png 2024-05-02 01:21:44 -07:00
Timothy J. Baek
82a61e72e1 refac: hide model set as default button 2024-05-02 01:15:58 -07:00
Timothy J. Baek
52fb09ff95 fix: chat input variable styling issue 2024-05-02 00:48:07 -07:00
Timothy J. Baek
572cd22b4d feat: prompt variables from suggestions 2024-05-02 00:45:04 -07:00
Timothy J. Baek
9ee39aa07f chore: format 2024-05-02 00:35:50 -07:00
Timothy J. Baek
372284b419 refac 2024-05-02 00:34:07 -07:00
Timothy J. Baek
714bdca3f3 refac: styling 2024-05-02 00:23:32 -07:00
Timothy J. Baek
5613af032d fix: styling 2024-05-02 00:07:04 -07:00
Timothy J. Baek
8fb5e22e43 refac: placeholder fade in effect 2024-05-01 23:41:12 -07:00
Timothy J. Baek
b19e05669e fix: styling 2024-05-01 23:11:16 -07:00
Timothy J. Baek
35b5d5ba35 refac: suggestions scroll snap 2024-05-01 23:01:00 -07:00
Timothy J. Baek
84cd1b8cb8 refac: styling 2024-05-01 22:55:42 -07:00
tabacoWang
fffd283b0c fix:
fix: Change the type from int to float
2024-05-02 13:45:19 +08:00
Timothy J. Baek
6d378844ae refac: styling 2024-05-01 22:39:09 -07:00
Timothy J. Baek
68cfccedee feat: super-admin (first one to signup) 2024-05-01 19:59:05 -07:00
Timothy J. Baek
a433315346 feat: disable all actions for admins 2024-05-01 19:50:23 -07:00
Timothy J. Baek
77cc3b5470 feat: disable admin chatlist by default 2024-05-01 19:47:54 -07:00
Timothy J. Baek
dc412d21f7 refac: scrollbar styling 2024-05-01 19:37:12 -07:00
Timothy J. Baek
96d9d3447b fix: pwa icon
#1886
2024-05-01 19:32:36 -07:00
Timothy J. Baek
aed078b580 chore: format 2024-05-01 19:30:03 -07:00
Timothy J. Baek
33c337785c chore: i18n format 2024-05-01 19:29:11 -07:00
Timothy J. Baek
20a6dbbe65 feat: csv bulk import 2024-05-01 19:28:35 -07:00
Timothy J. Baek
bd3b5f1edb feat: csv user import frontend 2024-05-01 19:02:25 -07:00
Timothy J. Baek
e5703a588f fix: styling 2024-05-01 18:13:45 -07:00
Timothy J. Baek
3431a93499 fix: safari styling 2024-05-01 18:10:49 -07:00
Timothy J. Baek
8745091a16 chore: i18n 2024-05-01 18:07:29 -07:00
Timothy J. Baek
e6bcdba5ad feat: add user from admin panel 2024-05-01 18:06:02 -07:00
Timothy J. Baek
96af34f240 chore: i18n 2024-05-01 17:56:39 -07:00
Timothy J. Baek
b7fcf14f6e refac: styling 2024-05-01 17:55:18 -07:00
Timothy J. Baek
ebc6a269d3 chore: i18n 2024-05-01 17:20:21 -07:00
Timothy J. Baek
0595c04909 feat: youtube rag 2024-05-01 17:17:00 -07:00
Timothy J. Baek
e60c87d750 fix: delete chat shortcut 2024-05-01 16:08:06 -07:00
Timothy J. Baek
d5bcae6182 chore: format 2024-05-01 15:41:39 -07:00
Timothy Jaeryang Baek
dbc3aa7234 Merge pull request #1892 from Joakim-T/patch-1
Patch 1
2024-05-01 15:01:18 -07:00
Joakim
e626fb2dbf Update languages.json
Moved sv-SE to correct position alphabetically
2024-05-01 20:42:50 +02:00
Joakim
34be6de4d8 Update translation.json sv-SE
Minor changes to Swedish translations
2024-05-01 14:43:12 +02:00
Joakim
20be93217b Update languages.json to add sv-SE
Swedish translation
2024-05-01 14:35:17 +02:00
Joakim
c580d29a5d Create translation.json for sv-SE
Translation to Swedish
2024-05-01 14:30:16 +02:00
Timothy Jaeryang Baek
f337ddb267 Merge pull request #1880 from Silentoplayz/saynototelemetry
feat: Added Environment Variable ANONYMIZED_TELEMETRY=false
2024-04-30 21:19:35 -07:00
Timothy J. Baek
1e05caf809 fix: response profile image 2024-04-30 17:10:12 -07:00
Timothy J. Baek
44884a8886 feat: randomised suggestion 2024-04-30 17:07:03 -07:00
Timothy J. Baek
a01fd15812 refac: sidebar styling 2024-04-30 16:58:07 -07:00
Timothy J. Baek
d513629984 fix: styling 2024-04-30 16:55:32 -07:00
Timothy J. Baek
f1acf68bd0 fix: styling 2024-04-30 16:52:19 -07:00
Timothy J. Baek
3c9fc7858b fix: styling 2024-04-30 16:34:29 -07:00
Timothy J. Baek
bf2ff47df0 refac: styling 2024-04-30 16:00:44 -07:00
Timothy J. Baek
6318282873 refac: styling 2024-04-30 15:31:50 -07:00
Timothy J. Baek
3043e43418 refac: styling 2024-04-30 15:18:58 -07:00
Timothy J. Baek
595ebd11ac refac: styling 2024-04-30 15:17:05 -07:00
Timothy J. Baek
1157b8e12d refac: styling 2024-04-30 15:12:58 -07:00
Timothy J. Baek
f653944849 refac: snap-center removed 2024-04-30 15:11:17 -07:00
Timothy J. Baek
01c077da3d refac 2024-04-30 15:10:39 -07:00
Timothy J. Baek
27ff386115 fix: horizontal scroll issue on mobile
#1854
2024-04-30 15:08:34 -07:00
Timothy J. Baek
35437fb3a3 refac: styling 2024-04-30 14:58:11 -07:00
Timothy J. Baek
18e5c2e4cb refac: styling 2024-04-30 14:56:06 -07:00
Timothy Jaeryang Baek
2674656b49 Merge pull request #1884 from cheahjs/feat/freeze-py-and-dependabot
feat: pin all python packages and setup dependabot
2024-04-30 14:22:46 -07:00
Jun Siang Cheah
fe56c0a7ac feat: add dependabot config for keeping python packages updated 2024-04-30 21:23:52 +01:00
Jun Siang Cheah
ddac5284fe feat: specify versions for python packages 2024-04-30 21:21:00 +01:00
Silentoplayz
b2020383dd Update Dockerfile
fix
2024-04-30 20:05:11 +00:00
Silentoplayz
563377a58a fix 2024-04-30 20:04:30 +00:00
Timothy Jaeryang Baek
eadb671414 Merge pull request #1882 from cheahjs/feat/harden-streaming-parser
feat: use spec compliant SSE parser for OpenAI responses
2024-04-30 13:02:44 -07:00
Jun Siang Cheah
b3ccabd2fe fix: add missing type for splitLargeChunks 2024-04-30 20:56:58 +01:00
Jun Siang Cheah
e8bf596959 feat: switch OpenAI SSE parsing to eventsource-parser 2024-04-30 20:56:58 +01:00
Timothy J. Baek
abc9b6b3f3 fix: version pinning 2024-04-30 12:41:35 -07:00
Timothy J. Baek
79bbc2be23 chore: formatting 2024-04-30 12:34:56 -07:00
Silentoplayz
b763535c92 Added: Environment Variable ANONYMIZED_TELEMETRY=False to .env.example & Dockerfile files to prevent/disable the creation of telemetry_user_id file created by Chroma in Docker installation methods. 2024-04-30 19:10:44 +00:00
Timothy Jaeryang Baek
de62153d49 Merge pull request #1815 from Yanyutin753/new-dev
 expend the image format type after the file is downloaded
2024-04-30 11:52:30 -07:00
Timothy J. Baek
80413a7628 refac 2024-04-30 11:52:08 -07:00
Timothy Jaeryang Baek
3f0fae1d10 Merge pull request #1868 from Yanyutin753/rag-dev
📌 fixed a bug where RAG would not reply after not reading the file correctly
2024-04-30 11:40:47 -07:00
Jannik Streidl
dd52ea9d3d added and improved some german translations 2024-04-30 11:39:14 +02:00
Jannik Streidl
30d1048099 resolved the conflicts 😍 2024-04-30 11:33:05 +02:00
Jannik Streidl
142f82bae5 Added some i18n keys & updated german locale 2024-04-30 10:46:16 +02:00
Yanyutin753
c0bb32d768 📌 fixed a bug where RAG would not reply after not reading the file correctly 2024-04-30 13:51:30 +08:00
Timothy J. Baek
5e168e6fef refac: remove num_ctx limit 2024-04-29 16:10:57 -07:00
Timothy Jaeryang Baek
4bf6e64c35 Merge pull request #1865 from cheahjs/feat/run-i18n-in-ci
feat: run i18next in CI to format and catch translations diffs
2024-04-29 15:54:38 -07:00
Timothy Jaeryang Baek
1afc49c1e4 Merge pull request #1862 from cheahjs/feat/filter-local-rag-fetch
feat: add ENABLE_LOCAL_WEB_FETCH to protect against SSRF attacks
2024-04-29 15:51:17 -07:00
Timothy Jaeryang Baek
eebabd84ef Merge pull request #1864 from cheahjs/fix/format
fix: formatting
2024-04-29 15:48:03 -07:00
Timothy Jaeryang Baek
6b04380c85 Merge pull request #1863 from cheahjs/fix/make-dev-executable
fix: make backend/fix.sh executable
2024-04-29 15:47:44 -07:00
Timothy Jaeryang Baek
f3199c6510 Merge pull request #1858 from buroa/buroa/fixes
fix: various rag api calls and ui cleanup
2024-04-29 15:46:29 -07:00
Timothy Jaeryang Baek
c523126bae Merge pull request #1853 from domsleee/fix/modal-closing-on-mouseup
fix: modal should not close when dragging from inside of modal
2024-04-29 15:45:46 -07:00
Jun Siang Cheah
45f34924b0 feat: run i18next in CI to format and catch translations diffs 2024-04-29 21:10:37 +01:00
Jun Siang Cheah
25af07a0c7 fix: formatting 2024-04-29 21:07:06 +01:00
Jun Siang Cheah
bac28222fb fix: make backend/fix.sh executable 2024-04-29 21:01:54 +01:00
Jun Siang Cheah
1c4e63f71e feat: add ENABLE_LOCAL_WEB_FETCH to protect against SSRF attacks 2024-04-29 20:55:17 +01:00
Timothy J. Baek
e8abaa8bea fix: styling 2024-04-29 12:08:36 -07:00
Steven Kreitzer
5b8fd14470 fix: various api rag results 2024-04-29 12:17:36 -05:00
Dom Slee
d4339fe769 fix: modal should not close when dragging from inside of modal 2024-04-29 18:30:12 +10:00
Timothy J. Baek
877ed69004 refac: token length limit 2024-04-29 00:36:09 -07:00
Timothy Jaeryang Baek
e360d57203 Merge pull request #1839 from cheahjs/fix/fluid-setting-not-reflecting-current-setting
fix: fluid stream setting not reflecting current state on load
2024-04-29 00:31:37 -07:00
Timothy Jaeryang Baek
0235b0f6f3 Merge pull request #1838 from Maximilian-Pichler/german-locale
German locale
2024-04-29 00:21:42 -07:00
Timothy Jaeryang Baek
a128dfa06b Merge branch 'dev' into german-locale 2024-04-29 00:21:08 -07:00
Timothy Jaeryang Baek
ffc91ba6b6 Merge pull request #1841 from Yanyutin753/translate
🎖️Added translations for Fluidly stream large external response chunks and OLED Dark
2024-04-29 00:20:02 -07:00
Yanyutin753
24c000739c 🎖️Added translations for Fluidly stream large external response chunks 2024-04-29 10:26:16 +08:00
Yanyutin753
00367ef23e 🎖️Added translations for Fluidly stream large external response chunks 2024-04-29 10:04:34 +08:00
Jun Siang Cheah
527a0b329b fix: fluid stream setting not reflecting current state on load 2024-04-29 00:06:32 +01:00
Maximilian Pichler
2e4bba2374 Update translation.json 2024-04-28 22:54:20 +02:00
Maximilian Pichler
4cafea0b97 Merge branch 'dev' into german-locale 2024-04-28 22:48:33 +02:00
Maximilian Pichler
cfc9cc8a10 Merge pull request #2 from jannikstdl/german-locale
German locale -  ran i18n parser
2024-04-28 21:58:18 +02:00
Timothy Jaeryang Baek
c7fa024b8f Merge pull request #1807 from insoutt/improve-es-lang
Improve 'es' translations
2024-04-28 10:27:40 -07:00
Timothy Jaeryang Baek
cc50746043 Merge pull request #1821 from Yanyutin753/translate
🎖️Added translations for Playground and Archived Chats
2024-04-28 10:26:33 -07:00
Timothy Jaeryang Baek
bf0ea4dc5a Merge pull request #1830 from cheahjs/feat/db-migration-tests
feat: add tests for db migration on sqlite and postgres
2024-04-28 10:26:17 -07:00
Timothy Jaeryang Baek
9ddc243c52 Merge pull request #1829 from cheahjs/fix/fluid-streaming-background
fix: fluid streaming was "pausing" when tab was not visible
2024-04-28 10:24:30 -07:00
Timothy J. Baek
9832e6edba enhancement: OpenAI API
api.together.xyz, api.replicate.com
2024-04-28 10:20:40 -07:00
Jannik Streidl
afc78b0277 ran i18n parser to add the new translation keys 2024-04-28 18:47:12 +02:00
Jun Siang Cheah
f561e94244 fix: refactor db migration tests 2024-04-28 17:00:31 +01:00
Jun Siang Cheah
eed6c7194b fix: disable mysql tests 2024-04-28 16:55:53 +01:00
Jun Siang Cheah
167b5712ea feat: add tests for db migration on sqlite, postgres, and mysql 2024-04-28 16:55:16 +01:00
Timothy Jaeryang Baek
7bc17da534 Merge pull request #1825 from cheahjs/feat/allow-backend-to-run-without-frontend 2024-04-28 08:53:18 -07:00
Timothy Jaeryang Baek
24497592a8 Merge pull request #1827 from justinh-rahb/update-litellm 2024-04-28 08:52:31 -07:00
Jun Siang Cheah
ed9e99e946 fix: fluid streaming was "pausing" when tab was not visible 2024-04-28 16:51:36 +01:00
Justin Hayes
d42517b7ed Update LiteLLM (and Gunicorn) 2024-04-28 11:43:57 -04:00
Maximilian Pichler
93d4f987e7 Merge pull request #1 from jannikstdl/german-locale
German locale additions
2024-04-28 17:24:56 +02:00
Jun Siang Cheah
6a1d60b1b3 feat: warn but not exit if frontend build does not exist 2024-04-28 16:03:30 +01:00
Jannik Streidl
19843e39fd Added more german translations + added more i18n keys 2024-04-28 14:39:06 +02:00
Yanyutin753
ec18489723 🎖️Added translations for Playground and Archived Chats 2024-04-28 19:53:27 +08:00
Maximilian Pichler
9130428a55 Update translation.json 2024-04-28 13:12:41 +02:00
Maximilian Pichler
236ec28491 Update translation.json
added and corrected some translations
2024-04-28 12:58:13 +02:00
Timothy J. Baek
f070e8b7f9 chore: format 2024-04-27 21:02:15 -07:00
Yanyutin753
3321a1b922 expend the image format type after the file is downloaded 2024-04-28 12:00:52 +08:00
Timothy Jaeryang Baek
f935acc6bf Merge pull request #1813 from open-webui/doge
feat: much wow doge
2024-04-27 20:55:10 -07:00
Timothy J. Baek
6d90d130bd feat: doge i18n response message profile image 2024-04-27 20:54:31 -07:00
Timothy J. Baek
bfd33ec0df feat: doge i18n placeholder 2024-04-27 20:53:42 -07:00
Timothy Jaeryang Baek
9af6c5300b Merge pull request #1811 from open-webui/doge
feat: doge i18n
2024-04-27 20:48:20 -07:00
Timothy J. Baek
5c887d0709 feat: doge translation 2024-04-27 20:46:52 -07:00
Timothy Jaeryang Baek
c9589e2118 Merge pull request #1810 from open-webui/dev
fix: wording
2024-04-27 20:11:10 -07:00
Timothy J. Baek
75cbbbe52d fix: wording 2024-04-27 20:10:20 -07:00
Timothy Jaeryang Baek
0455b80604 Merge pull request #1809 from open-webui/dev
fix
2024-04-27 20:07:26 -07:00
Timothy J. Baek
71a75bccf5 fix 2024-04-27 20:06:07 -07:00
Timothy Jaeryang Baek
97609970b2 Merge pull request #1806 from open-webui/dev
fix
2024-04-27 19:46:05 -07:00
Timothy J. Baek
48ed701960 fix 2024-04-27 19:44:36 -07:00
Timothy Jaeryang Baek
d401d77cbd Merge pull request #1805 from open-webui/dev
fix
2024-04-27 19:42:51 -07:00
Timothy J. Baek
5e97c9927b fix 2024-04-27 19:42:19 -07:00
insoutt
76660f3e44 Improve translation 2024-04-27 21:01:25 -05:00
Timothy Jaeryang Baek
92c98eda2e Merge pull request #1781 from open-webui/dev
0.1.122
2024-04-27 18:29:10 -07:00
Timothy J. Baek
85df019c2a refac: styling 2024-04-27 21:20:43 -04:00
Timothy J. Baek
2fceeb120b refac: wording 2024-04-27 21:18:15 -04:00
Timothy J. Baek
8aa47ea6bc fix 2024-04-27 21:17:19 -04:00
Timothy J. Baek
704f7e369c Update app.html 2024-04-27 21:14:08 -04:00
Timothy J. Baek
e4dd613ed1 chore: formatting 2024-04-27 21:12:21 -04:00
Timothy J. Baek
63494a3717 Update CHANGELOG.md 2024-04-27 21:11:59 -04:00
Timothy J. Baek
5898550101 doc: changelog 2024-04-27 21:07:43 -04:00
Timothy J. Baek
f1d2340861 enhancement: disable submit via enter on mobile 2024-04-27 20:53:52 -04:00
Timothy J. Baek
e71ef42155 fix: stop seq backslash issue 2024-04-27 20:46:34 -04:00
Timothy J. Baek
7f3daa19cd fix: stop seq backslash issue
#1747
2024-04-27 20:45:09 -04:00
Timothy J. Baek
ebeaa24e9d refac 2024-04-27 19:48:46 -04:00
Timothy J. Baek
e2447dd0a7 refac: better layout loading 2024-04-27 19:47:11 -04:00
Timothy J. Baek
2feba8af94 fix: font fallback issue
#1556
2024-04-27 19:42:55 -04:00
Timothy Jaeryang Baek
386fc040b1 Merge pull request #1802 from Yanyutin753/tem-dev
feat added environment variables and sync.yml
2024-04-27 16:39:45 -07:00
Timothy J. Baek
9094536d37 feat: user last active 2024-04-27 19:38:51 -04:00
Yanyutin753
02b8a4cf11 Merge branch 'tem-dev' of https://github.com/Yanyutin753/open-webui into tem-dev 2024-04-28 07:21:36 +08:00
Yanyutin753
849a0b973d recompose the name of environment variables 2024-04-28 07:21:01 +08:00
Timothy J. Baek
01c4647dfc fix: ollama version string issue
#1800
2024-04-27 19:19:47 -04:00
Timothy Jaeryang Baek
07c110a1fc Delete .github/workflows/sync.yml 2024-04-27 18:06:40 -05:00
Timothy J. Baek
85e94cc259 chore: removed unused frontend dependency 2024-04-27 19:04:57 -04:00
Timothy J. Baek
f0da5b9ea4 refac 2024-04-27 19:02:37 -04:00
Timothy J. Baek
9ef843d492 refac: splash screen 2024-04-27 18:59:20 -04:00
Yanyutin753
b0245a7eff feat added environment variables and sync.yml 2024-04-28 06:54:26 +08:00
Timothy J. Baek
975e078f4d Update app.html 2024-04-27 18:51:59 -04:00
Timothy J. Baek
979c4290f7 feat: splash screen support 2024-04-27 18:50:26 -04:00
Timothy J. Baek
bcf78b4efa feat: show user chats in admin panel 2024-04-27 18:24:59 -04:00
Timothy J. Baek
098ac18762 refac: naming 2024-04-27 18:15:32 -04:00
Timothy J. Baek
4ea2eb7939 refac 2024-04-27 18:14:15 -04:00
Timothy J. Baek
10f27ebacf refac: naming 2024-04-27 18:12:57 -04:00
Timothy J. Baek
7b913ea032 chore: formatting 2024-04-27 17:38:48 -04:00
Timothy J. Baek
d63e52600d refac: styling 2024-04-27 17:35:20 -04:00
Timothy J. Baek
4ca7c8d548 fix: styling 2024-04-27 17:31:19 -04:00
Timothy Jaeryang Baek
69d32a94ab Merge pull request #1616 from Entaigner/patch-1
Chose between "docker-compose" and "docker compose" in Makefile
2024-04-27 14:29:15 -07:00
Timothy Jaeryang Baek
1c14b54eb9 Merge pull request #1666 from Silentoplayz/Silentoplayz-update-PR-template.md
Update Pull Request Template to Improve Checklist and Format
2024-04-27 14:28:44 -07:00
Timothy Jaeryang Baek
c1b97a723f Update pull_request_template.md 2024-04-27 16:28:14 -05:00
Timothy Jaeryang Baek
e2534e3703 Merge pull request #1794 from cheahjs/feat/external-chromadb
feat: add ability to configure a HTTP ChromaDB client
2024-04-27 14:24:45 -07:00
Timothy Jaeryang Baek
2f8164d75f Merge pull request #1798 from cheahjs/feat/abort-openai-responses-on-stop
feat: abort openai text completion when stopping responses
2024-04-27 14:15:14 -07:00
Timothy Jaeryang Baek
8215a2023e Merge pull request #1799 from karaketir16/patch-1
fix: docker gpus option "all" support in run-compose.sh
2024-04-27 14:14:30 -07:00
Jun Siang Cheah
c095a7c291 feat: abort openai text completion when stopping responses 2024-04-27 21:53:47 +01:00
Osman Karaketir
57d178456a fix: docker gpus option "all" support
"docker --gpus=all" is a valid and mostly used command. regex updated to match this.
2024-04-27 23:50:07 +03:00
Timothy Jaeryang Baek
c1d85f8a6f Merge pull request #1791 from cheahjs/feat/external-db-support
feat: add support for using postgres for the backend DB
2024-04-27 12:48:56 -07:00
Timothy Jaeryang Baek
29a750c4ca Merge pull request #1788 from cheahjs/feat/cypress-tests
feat: add basic cypress test as initial work towards e2e tests
2024-04-27 12:45:24 -07:00
Timothy Jaeryang Baek
db5c4be674 Merge pull request #1795 from cheahjs/fix/openai-handle-carriage-returns
fix: handle carriage returns in OpenAI streams
2024-04-27 12:43:33 -07:00
Timothy Jaeryang Baek
6fbddb0702 Merge pull request #1797 from cheahjs/feat/allow-overriding-host-in-docker
feat: add HOST to the backend start script
2024-04-27 12:42:06 -07:00
Timothy J. Baek
ce9a5d12e0 refac: rag pipeline 2024-04-27 15:38:50 -04:00
Jun Siang Cheah
c19429848b feat: add HOST to the backend start script 2024-04-27 20:20:48 +01:00
Timothy J. Baek
8f1563a7a5 fix: typo 2024-04-27 15:03:49 -04:00
Timothy J. Baek
2325660520 chore: formatting 2024-04-27 15:03:05 -04:00
Timothy J. Baek
9be56d68e0 refac: naming convention 2024-04-27 15:02:57 -04:00
Timothy Jaeryang Baek
99a43cc998 Merge pull request #1792 from insoutt/hide-sidebar-on-resize
Hide the sidebar when resizing the window
2024-04-27 11:59:44 -07:00
Jun Siang Cheah
be038ab878 fix: handle carriage returns in OpenAI streams 2024-04-27 19:07:41 +01:00
Jun Siang Cheah
8df7db1e17 feat: add ability to configure a HTTP ChromaDB client 2024-04-27 18:52:35 +01:00
insoutt
0c79ac0479 Hide sidevar when screen is resized to less than 1024 pixels 2024-04-27 10:16:39 -05:00
Jun Siang Cheah
47a33acfeb feat: show error toast if trying to download db on external db 2024-04-27 15:53:31 +01:00
Jun Siang Cheah
e91a49c455 feat: add support for using postgres for the backend DB 2024-04-27 15:33:20 +01:00
Jun Siang Cheah
730befce45 feat: add basic cypress test as initial work towards e2e tests 2024-04-27 14:10:10 +01:00
Timothy J. Baek
f8f9f27ae8 chore: comment 2024-04-26 22:44:10 -04:00
Timothy J. Baek
790a305c55 chore: disable unmaintained themes 2024-04-26 22:33:39 -04:00
Timothy J. Baek
f63866b72a feat: loading indicator 2024-04-26 17:43:42 -04:00
Timothy J. Baek
b22415d456 feat: litellm opt-out support 2024-04-26 17:19:50 -04:00
Timothy J. Baek
dbf7b15539 refac: naming convention
MODEL_FILTER_ENABLED -> ENABLE_MODEL_FILTER
2024-04-26 17:17:18 -04:00
Timothy J. Baek
c5eac5a1c7 fix: webui name 2024-04-26 17:00:25 -04:00
Timothy J. Baek
e399b38654 Update languages.json 2024-04-26 16:58:46 -04:00
Timothy Jaeryang Baek
b040a55591 Merge pull request #1783 from srizon/main
Added language support for Bangla (bn-BD)
2024-04-26 13:57:37 -07:00
Mamun Srizon
2968312b1c Finished remaining translation for Bangla (bn-BD) 2024-04-27 01:54:41 +06:00
Timothy J. Baek
8c97476140 chore: formatting 2024-04-26 14:49:03 -04:00
Timothy Jaeryang Baek
543707eefd Merge pull request #1756 from buroa/buroa/toggle-hybrid
feat: toggle hybrid search
2024-04-26 11:48:44 -07:00
Timothy J. Baek
cebf733b9d refac: naming convention 2024-04-26 14:41:39 -04:00
Timothy Jaeryang Baek
81fb53e757 Merge pull request #1780 from cheahjs/fix/harden-streaming
fix: harden openai streaming parsing
2024-04-26 11:06:20 -07:00
Timothy Jaeryang Baek
add5269b89 Merge pull request #1773 from Rmaan/fix-openrouter-streaming
Fixed OpenRouter heart beats breaking streaming
2024-04-26 11:05:52 -07:00
Jun Siang Cheah
615e9e348f fix: do not error out if OpenAI response has no delta 2024-04-26 18:42:43 +01:00
Jun Siang Cheah
510afab37c fix: catch any errors parsing openai sse events 2024-04-26 18:38:25 +01:00
Timothy Jaeryang Baek
e9ba8d74d2 Merge pull request #1761 from Menghuan1918/dev
Improved translation quality for Simplified Chinese
2024-04-26 10:05:27 -07:00
Timothy Jaeryang Baek
ddf06329a5 Merge pull request #1734 from darlanalves/env-docs-dir
feat: allow a docs directory coming from env
2024-04-26 09:59:34 -07:00
Mamun Srizon
cac03fbcb1 Adding bn-BD for Bangla-Bangladesh language support 2024-04-26 22:35:19 +06:00
Arman Ordookhani
4ad7ef84a4 add else 2024-04-26 15:45:29 +02:00
Arman Ordookhani
7449634290 Fix OpenRouter hearbeats breaking streaming 2024-04-26 15:37:18 +02:00
Steven Kreitzer
69822e4c25 fix: sort ranking hybrid 2024-04-26 07:56:41 -05:00
Menghuan1918
7bc6a87697 Improved translation quality for Simplified Chinese 2024-04-26 13:06:16 +08:00
Steven Kreitzer
9755cd5baa feat: toggle hybrid search 2024-04-25 17:51:38 -05:00
Timothy J. Baek
984dbf13ab revert: original rag pipeline 2024-04-25 17:03:00 -04:00
Timothy Jaeryang Baek
7d88689f51 Merge pull request #1741 from domsleee/katex-render-performance
fix: Improve katex render performance in responses
2024-04-25 13:40:27 -07:00
Timothy J. Baek
dcb92f7f74 refac: naming convention 2024-04-25 16:40:15 -04:00
Timothy Jaeryang Baek
02a241f831 Merge pull request #1753 from djismgaming/spanish-translation-v2
translation to Spanish of a new string
2024-04-25 13:37:07 -07:00
Timothy Jaeryang Baek
fafaf48cb5 Merge pull request #1744 from aguvener/dev
fix: tr_TR translation synchronized with the latest updates.
2024-04-25 13:36:43 -07:00
Timothy Jaeryang Baek
6b4f344b96 Merge pull request #1748 from velaton618/main
Ukrainian translation update
2024-04-25 13:36:30 -07:00
Timothy Jaeryang Baek
5ee2f1729a Merge pull request #1693 from buroa/buroa/hybrid-search
feat: hybrid search with reranking
2024-04-25 13:12:18 -07:00
Ismael
15657627a6 small word change in Spanish translation 2024-04-25 14:51:17 -04:00
Steven Kreitzer
1c1d2c254d fix: query collection api call 2024-04-25 13:38:18 -05:00
Steven Kreitzer
72090fab88 chore: update log line 2024-04-25 13:28:31 -05:00
Steven Kreitzer
e92680a566 chore: update changelog.md 2024-04-25 13:23:59 -05:00
Ismael
66f6123d76 translation to spanish of a new string 2024-04-25 13:43:01 -04:00
Steven Kreitzer
c9c9660459 fix: address comment in pr #1687 2024-04-25 07:50:42 -05:00
velaton
f9adfc50e4 Update translation.json
Changed to a word that is much more common
2024-04-25 17:26:56 +08:00
aguvener
ad1b461b6c fix: synchronized with the latest updates. 2024-04-25 10:16:27 +03:00
Timothy J. Baek
232de33a86 fix: toast 2024-04-24 21:50:04 -04:00
Steven Kreitzer
d5f60b119c Merge branch 'dev' into buroa/hybrid-search 2024-04-24 20:37:19 -05:00
Timothy Jaeryang Baek
1092ee9c1c Merge pull request #1739 from open-webui/dev
fix
2024-04-24 18:24:39 -07:00
Timothy J. Baek
7075837849 fix: missing import 2024-04-24 21:24:17 -04:00
Timothy Jaeryang Baek
b508f184ee Merge pull request #1738 from open-webui/main
dev
2024-04-24 18:23:15 -07:00
Dom Slee
06a6136671 fix: improve katex render performance in responses 2024-04-25 11:14:37 +10:00
Darlan Alves
89e8813188 feat: allow a docs directory coming from env
Current config assumes /data/docs to be part of the current data directory.

This allows DOCS_DIR to be mounted from a different path outside of DATA_DIR, or falls back to the previous behaviour if DOCS_DIR is not in the environment
2024-04-25 00:40:39 +02:00
Timothy Jaeryang Baek
488f448f04 Merge pull request #1732 from dannyl1u/fix/chat-link 2024-04-24 14:36:13 -07:00
Danny Liu
95295061d0 fix: config var not defined 2024-04-24 14:31:03 -07:00
Steven Kreitzer
adb009f388 Merge branch 'dev' into buroa/hybrid-search 2024-04-24 14:51:49 -05:00
Steven Kreitzer
c0259aad67 feat: hybrid search and reranking support 2024-04-24 07:55:10 -05:00
Silentoplayz
53b277e575 Update pull_request_template.md
fix
2024-04-24 06:02:25 +00:00
Silentoplayz
29c6b253ca Update pull_request_template.md
Okay, no warning then.
2024-04-23 11:17:32 +00:00
Silentoplayz
4a536484a6 Update pull_request_template.md
Added failure to follow template warning
Made more concise
2024-04-23 11:14:54 +00:00
Steven Kreitzer
db801aee79 Merge branch 'dev' into buroa/hybrid-search 2024-04-22 18:35:32 -05:00
Steven Kreitzer
4e0b32b505 feat: hybrid search 2024-04-22 18:33:43 -05:00
Silentoplayz
a615308111 Update pull_request_template.md
Fix
2024-04-22 07:19:09 +00:00
Silentoplayz
dcb32f78fe Update pull_request_template.md
Overhaul
2024-04-22 07:11:59 +00:00
Entaigner
4570f6fb0e Chose between "docker-compose" and "docker compose" in confirm_remove.sh 2024-04-19 20:19:24 +02:00
Entaigner
e1e66f708f Chose between "docker-compose" and "docker compose" in Makefile 2024-04-19 19:30:25 +02:00
299 changed files with 41611 additions and 13371 deletions

View File

@@ -10,7 +10,4 @@ OPENAI_API_KEY=''
# DO NOT TRACK
SCARF_NO_ANALYTICS=true
DO_NOT_TRACK=true
# Use locally bundled version of the LiteLLM cost map json
# to avoid repetitive startup connections
LITELLM_LOCAL_MODEL_COST_MAP="True"
ANONYMIZED_TELEMETRY=false

View File

@@ -4,6 +4,7 @@ module.exports = {
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'plugin:svelte/recommended',
'plugin:cypress/recommended',
'prettier'
],
parser: '@typescript-eslint/parser',

11
.github/dependabot.disabled vendored Normal file
View File

@@ -0,0 +1,11 @@
version: 2
updates:
- package-ecosystem: pip
directory: '/backend'
schedule:
interval: weekly
- package-ecosystem: 'github-actions'
directory: '/'
schedule:
# Check for updates to GitHub Actions every week
interval: 'weekly'

View File

@@ -1,32 +1,72 @@
## Pull Request Checklist
# Pull Request Checklist
- [ ] **Description:** Briefly describe the changes in this pull request.
### Note to first-time contributors: Please open a discussion post in [Discussions](https://github.com/open-webui/open-webui/discussions) and describe your changes before submitting a pull request.
**Before submitting, make sure you've checked the following:**
- [ ] **Target branch:** Please verify that the pull request targets the `dev` branch.
- [ ] **Description:** Provide a concise description of the changes made in this pull request.
- [ ] **Changelog:** Ensure a changelog entry following the format of [Keep a Changelog](https://keepachangelog.com/) is added at the bottom of the PR description.
- [ ] **Documentation:** Have you updated relevant documentation?
- [ ] **Documentation:** Have you updated relevant documentation [Open WebUI Docs](https://github.com/open-webui/docs), or other documentation sources?
- [ ] **Dependencies:** Are there any new dependencies? Have you updated the dependency versions in the documentation?
- [ ] **Testing:** Have you written and run sufficient tests for validating the changes?
- [ ] **Code review:** Have you performed a self-review of your code, addressing any coding standard issues and ensuring adherence to the project's coding standards?
- [ ] **Label:** To cleary categorize this pull request, assign a relevant label to the pull request title, using one of the following:
- **BREAKING CHANGE**: Significant changes that may affect compatibility
- **build**: Changes that affect the build system or external dependencies
- **ci**: Changes to our continuous integration processes or workflows
- **chore**: Refactor, cleanup, or other non-functional code changes
- **docs**: Documentation update or addition
- **feat**: Introduces a new feature or enhancement to the codebase
- **fix**: Bug fix or error correction
- **i18n**: Internationalization or localization changes
- **perf**: Performance improvement
- **refactor**: Code restructuring for better maintainability, readability, or scalability
- **style**: Changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc.)
- **test**: Adding missing tests or correcting existing tests
- **WIP**: Work in progress, a temporary label for incomplete or ongoing work
---
# Changelog Entry
## Description
### Description
[Insert a brief description of the changes made in this pull request]
---
### Changelog Entry
- [Concisely describe the changes made in this pull request, including any relevant motivation and impact (e.g., fixing a bug, adding a feature, or improving performance)]
### Added
- [List any new features or additions]
### Fixed
- [List any fixes or corrections]
- [List any new features, functionalities, or additions]
### Changed
- [List any changes or updates]
- [List any changes, updates, refactorings, or optimizations]
### Deprecated
- [List any deprecated functionality or features that have been removed]
### Removed
- [List any removed features or files]
- [List any removed features, files, or functionalities]
### Fixed
- [List any fixes, corrections, or bug fixes]
### Security
- [List any new or updated security-related changes, including vulnerability fixes]
### Breaking Changes
- **BREAKING CHANGE**: [List any breaking changes affecting compatibility or functionality]
---
### Additional Information
- [Insert any additional context, notes, or explanations for the changes]
- [Reference any related issues, commits, or other relevant information]
### Screenshots or Videos
- [Attach any relevant screenshots or videos demonstrating the changes]

View File

@@ -11,7 +11,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v2
uses: actions/checkout@v4
- name: Check for changes in package.json
run: |
@@ -36,7 +36,7 @@ jobs:
echo "::set-output name=content::$CHANGELOG_ESCAPED"
- name: Create GitHub release
uses: actions/github-script@v5
uses: actions/github-script@v7
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
@@ -51,7 +51,7 @@ jobs:
console.log(`Created release ${release.data.html_url}`)
- name: Upload package to GitHub release
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: package
path: .

View File

@@ -0,0 +1,59 @@
name: Deploy to HuggingFace Spaces
on:
push:
branches:
- dev
- main
workflow_dispatch:
jobs:
check-secret:
runs-on: ubuntu-latest
outputs:
token-set: ${{ steps.check-key.outputs.defined }}
steps:
- id: check-key
env:
HF_TOKEN: ${{ secrets.HF_TOKEN }}
if: "${{ env.HF_TOKEN != '' }}"
run: echo "defined=true" >> $GITHUB_OUTPUT
deploy:
runs-on: ubuntu-latest
needs: [check-secret]
if: needs.check-secret.outputs.token-set == 'true'
env:
HF_TOKEN: ${{ secrets.HF_TOKEN }}
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Remove git history
run: rm -rf .git
- name: Prepend YAML front matter to README.md
run: |
echo "---" > temp_readme.md
echo "title: Open WebUI" >> temp_readme.md
echo "emoji: 🐳" >> temp_readme.md
echo "colorFrom: purple" >> temp_readme.md
echo "colorTo: gray" >> temp_readme.md
echo "sdk: docker" >> temp_readme.md
echo "app_port: 8080" >> temp_readme.md
echo "---" >> temp_readme.md
cat README.md >> temp_readme.md
mv temp_readme.md README.md
- name: Configure git
run: |
git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com"
git config --global user.name "github-actions[bot]"
- name: Set up Git and push to Space
run: |
git init --initial-branch=main
git lfs track "*.ttf"
rm demo.gif
git add .
git commit -m "GitHub deploy: ${{ github.sha }}"
git push --force https://open-webui:${HF_TOKEN}@huggingface.co/spaces/open-webui/open-webui main

View File

@@ -63,6 +63,18 @@ jobs:
flavor: |
latest=${{ github.ref == 'refs/heads/main' }}
- name: Extract metadata for Docker cache
id: cache-meta
uses: docker/metadata-action@v5
with:
images: ${{ env.FULL_IMAGE_NAME }}
tags: |
type=ref,event=branch
${{ github.ref_type == 'tag' && 'type=raw,value=main' || '' }}
flavor: |
prefix=cache-${{ matrix.platform }}-
latest=false
- name: Build Docker image (latest)
uses: docker/build-push-action@v5
id: build
@@ -72,8 +84,10 @@ jobs:
platforms: ${{ matrix.platform }}
labels: ${{ steps.meta.outputs.labels }}
outputs: type=image,name=${{ env.FULL_IMAGE_NAME }},push-by-digest=true,name-canonical=true,push=true
cache-from: type=gha
cache-to: type=gha,mode=max
cache-from: type=registry,ref=${{ steps.cache-meta.outputs.tags }}
cache-to: type=registry,ref=${{ steps.cache-meta.outputs.tags }},mode=max
build-args: |
BUILD_HASH=${{ github.sha }}
- name: Export digest
run: |
@@ -123,7 +137,7 @@ jobs:
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata for Docker images (default latest tag)
- name: Extract metadata for Docker images (cuda tag)
id: meta
uses: docker/metadata-action@v5
with:
@@ -139,6 +153,18 @@ jobs:
latest=${{ github.ref == 'refs/heads/main' }}
suffix=-cuda,onlatest=true
- name: Extract metadata for Docker cache
id: cache-meta
uses: docker/metadata-action@v5
with:
images: ${{ env.FULL_IMAGE_NAME }}
tags: |
type=ref,event=branch
${{ github.ref_type == 'tag' && 'type=raw,value=main' || '' }}
flavor: |
prefix=cache-cuda-${{ matrix.platform }}-
latest=false
- name: Build Docker image (cuda)
uses: docker/build-push-action@v5
id: build
@@ -148,9 +174,11 @@ jobs:
platforms: ${{ matrix.platform }}
labels: ${{ steps.meta.outputs.labels }}
outputs: type=image,name=${{ env.FULL_IMAGE_NAME }},push-by-digest=true,name-canonical=true,push=true
cache-from: type=gha
cache-to: type=gha,mode=max
build-args: USE_CUDA=true
cache-from: type=registry,ref=${{ steps.cache-meta.outputs.tags }}
cache-to: type=registry,ref=${{ steps.cache-meta.outputs.tags }},mode=max
build-args: |
BUILD_HASH=${{ github.sha }}
USE_CUDA=true
- name: Export digest
run: |
@@ -216,6 +244,18 @@ jobs:
latest=${{ github.ref == 'refs/heads/main' }}
suffix=-ollama,onlatest=true
- name: Extract metadata for Docker cache
id: cache-meta
uses: docker/metadata-action@v5
with:
images: ${{ env.FULL_IMAGE_NAME }}
tags: |
type=ref,event=branch
${{ github.ref_type == 'tag' && 'type=raw,value=main' || '' }}
flavor: |
prefix=cache-ollama-${{ matrix.platform }}-
latest=false
- name: Build Docker image (ollama)
uses: docker/build-push-action@v5
id: build
@@ -225,9 +265,11 @@ jobs:
platforms: ${{ matrix.platform }}
labels: ${{ steps.meta.outputs.labels }}
outputs: type=image,name=${{ env.FULL_IMAGE_NAME }},push-by-digest=true,name-canonical=true,push=true
cache-from: type=gha
cache-to: type=gha,mode=max
build-args: USE_OLLAMA=true
cache-from: type=registry,ref=${{ steps.cache-meta.outputs.tags }}
cache-to: type=registry,ref=${{ steps.cache-meta.outputs.tags }},mode=max
build-args: |
BUILD_HASH=${{ github.sha }}
USE_OLLAMA=true
- name: Export digest
run: |

View File

@@ -23,7 +23,7 @@ jobs:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v2
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}

View File

@@ -19,7 +19,7 @@ jobs:
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v3
uses: actions/setup-node@v4
with:
node-version: '20' # Or specify any other version you want to use
@@ -29,8 +29,29 @@ jobs:
- name: Format Frontend
run: npm run format
- name: Run i18next
run: npm run i18n:parse
- name: Check for Changes After Format
run: git diff --exit-code
- name: Build Frontend
run: npm run build
test-frontend:
name: 'Frontend Unit Tests'
runs-on: ubuntu-latest
steps:
- name: Checkout Repository
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Install Dependencies
run: npm ci
- name: Run vitest
run: npm run test:frontend

199
.github/workflows/integration-test.yml vendored Normal file
View File

@@ -0,0 +1,199 @@
name: Integration Test
on:
push:
branches:
- main
- dev
pull_request:
branches:
- main
- dev
jobs:
cypress-run:
name: Run Cypress Integration Tests
runs-on: ubuntu-latest
steps:
- name: Checkout Repository
uses: actions/checkout@v4
- name: Build and run Compose Stack
run: |
docker compose \
--file docker-compose.yaml \
--file docker-compose.api.yaml \
--file docker-compose.a1111-test.yaml \
up --detach --build
- name: Wait for Ollama to be up
timeout-minutes: 5
run: |
until curl --output /dev/null --silent --fail http://localhost:11434; do
printf '.'
sleep 1
done
echo "Service is up!"
- name: Preload Ollama model
run: |
docker exec ollama ollama pull qwen:0.5b-chat-v1.5-q2_K
- name: Cypress run
uses: cypress-io/github-action@v6
with:
browser: chrome
wait-on: 'http://localhost:3000'
config: baseUrl=http://localhost:3000
- uses: actions/upload-artifact@v4
if: always()
name: Upload Cypress videos
with:
name: cypress-videos
path: cypress/videos
if-no-files-found: ignore
- name: Extract Compose logs
if: always()
run: |
docker compose logs > compose-logs.txt
- uses: actions/upload-artifact@v4
if: always()
name: Upload Compose logs
with:
name: compose-logs
path: compose-logs.txt
if-no-files-found: ignore
migration_test:
name: Run Migration Tests
runs-on: ubuntu-latest
services:
postgres:
image: postgres
env:
POSTGRES_PASSWORD: postgres
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 5432:5432
# mysql:
# image: mysql
# env:
# MYSQL_ROOT_PASSWORD: mysql
# MYSQL_DATABASE: mysql
# options: >-
# --health-cmd "mysqladmin ping -h localhost"
# --health-interval 10s
# --health-timeout 5s
# --health-retries 5
# ports:
# - 3306:3306
steps:
- name: Checkout Repository
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- name: Set up uv
uses: yezz123/setup-uv@v4
with:
uv-venv: venv
- name: Activate virtualenv
run: |
. venv/bin/activate
echo PATH=$PATH >> $GITHUB_ENV
- name: Install dependencies
run: |
uv pip install -r backend/requirements.txt
- name: Test backend with SQLite
id: sqlite
env:
WEBUI_SECRET_KEY: secret-key
GLOBAL_LOG_LEVEL: debug
run: |
cd backend
uvicorn main:app --port "8080" --forwarded-allow-ips '*' &
UVICORN_PID=$!
# Wait up to 20 seconds for the server to start
for i in {1..20}; do
curl -s http://localhost:8080/api/config > /dev/null && break
sleep 1
if [ $i -eq 20 ]; then
echo "Server failed to start"
kill -9 $UVICORN_PID
exit 1
fi
done
# Check that the server is still running after 5 seconds
sleep 5
if ! kill -0 $UVICORN_PID; then
echo "Server has stopped"
exit 1
fi
- name: Test backend with Postgres
if: success() || steps.sqlite.conclusion == 'failure'
env:
WEBUI_SECRET_KEY: secret-key
GLOBAL_LOG_LEVEL: debug
DATABASE_URL: postgresql://postgres:postgres@localhost:5432/postgres
run: |
cd backend
uvicorn main:app --port "8081" --forwarded-allow-ips '*' &
UVICORN_PID=$!
# Wait up to 20 seconds for the server to start
for i in {1..20}; do
curl -s http://localhost:8081/api/config > /dev/null && break
sleep 1
if [ $i -eq 20 ]; then
echo "Server failed to start"
kill -9 $UVICORN_PID
exit 1
fi
done
# Check that the server is still running after 5 seconds
sleep 5
if ! kill -0 $UVICORN_PID; then
echo "Server has stopped"
exit 1
fi
# - name: Test backend with MySQL
# if: success() || steps.sqlite.conclusion == 'failure' || steps.postgres.conclusion == 'failure'
# env:
# WEBUI_SECRET_KEY: secret-key
# GLOBAL_LOG_LEVEL: debug
# DATABASE_URL: mysql://root:mysql@localhost:3306/mysql
# run: |
# cd backend
# uvicorn main:app --port "8083" --forwarded-allow-ips '*' &
# UVICORN_PID=$!
# # Wait up to 20 seconds for the server to start
# for i in {1..20}; do
# curl -s http://localhost:8083/api/config > /dev/null && break
# sleep 1
# if [ $i -eq 20 ]; then
# echo "Server failed to start"
# kill -9 $UVICORN_PID
# exit 1
# fi
# done
# # Check that the server is still running after 5 seconds
# sleep 5
# if ! kill -0 $UVICORN_PID; then
# echo "Server has stopped"
# exit 1
# fi

31
.github/workflows/release-pypi.yml vendored Normal file
View File

@@ -0,0 +1,31 @@
name: Release to PyPI
on:
push:
branches:
- main # or whatever branch you want to use
jobs:
release:
runs-on: ubuntu-latest
environment:
name: pypi
url: https://pypi.org/p/open-webui
permissions:
id-token: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 18
- uses: actions/setup-python@v5
with:
python-version: 3.11
- name: Build
run: |
python -m pip install --upgrade pip
pip install build
python -m build .
- name: Publish package distributions to PyPI
uses: pypa/gh-action-pypi-publish@release/v1

10
.gitignore vendored
View File

@@ -16,6 +16,10 @@ __pycache__/
# C extensions
*.so
# Pyodide distribution
static/pyodide/*
!static/pyodide/pyodide-lock.json
# Distribution / packaging
.Python
build/
@@ -297,4 +301,8 @@ dist
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*
.pnp.*
# cypress artifacts
cypress/videos
cypress/screenshots

View File

@@ -1,3 +1,11 @@
# Ignore files for PNPM, NPM and YARN
pnpm-lock.yaml
package-lock.json
yarn.lock
kubernetes/
# Copy of .gitignore
.DS_Store
node_modules
/build
@@ -6,11 +14,303 @@ node_modules
.env
.env.*
!.env.example
vite.config.js.timestamp-*
vite.config.ts.timestamp-*
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# Ignore files for PNPM, NPM and YARN
pnpm-lock.yaml
package-lock.json
yarn.lock
# C extensions
*.so
# Ignore kubernetes files
kubernetes
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
.pybuilder/
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
# For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
# .python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
# poetry
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
#poetry.lock
# pdm
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
#pdm.lock
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
# in version control.
# https://pdm.fming.dev/#use-with-ide
.pdm.toml
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# pytype static type analyzer
.pytype/
# Cython debug symbols
cython_debug/
# PyCharm
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
.idea/
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
.pnpm-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Snowpack dependency directory (https://snowpack.dev/)
web_modules/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional stylelint cache
.stylelintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# Next.js build output
.next
out
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# vuepress v2.x temp and cache directory
.temp
.cache
# Docusaurus cache and generated files
.docusaurus
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*
# cypress artifacts
cypress/videos
cypress/screenshots
/static/*

View File

@@ -5,6 +5,165 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [0.2.2] - 2024-06-02
### Added
- **🌊 Mermaid Rendering Support**: We've included support for Mermaid rendering. This allows you to create beautiful diagrams and flowcharts directly within Open WebUI.
- **🔄 New Environment Variable 'RESET_CONFIG_ON_START'**: Introducing a new environment variable: 'RESET_CONFIG_ON_START'. Set this variable to reset your configuration settings upon starting the application, making it easier to revert to default settings.
### Fixed
- **🔧 Pipelines Filter Issue**: We've addressed an issue with the pipelines where filters were not functioning as expected.
## [0.2.1] - 2024-06-02
### Added
- **🖱️ Single Model Export Button**: Easily export models with just one click using the new single model export button.
- **🖥️ Advanced Parameters Support**: Added support for 'num_thread', 'use_mmap', and 'use_mlock' parameters for Ollama.
- **🌐 Improved Vietnamese Translation**: Enhanced Vietnamese language support for a better user experience for our Vietnamese-speaking community.
### Fixed
- **🔧 OpenAI URL API Save Issue**: Corrected a problem preventing the saving of OpenAI URL API settings.
- **🚫 Display Issue with Disabled Ollama API**: Fixed the display bug causing models to appear in settings when the Ollama API was disabled.
### Changed
- **💡 Versioning Update**: As a reminder from our previous update, version 0.2.y will focus primarily on bug fixes, while major updates will be designated as 0.x from now on for better version tracking.
## [0.2.0] - 2024-06-01
### Added
- **🔧 Pipelines Support**: Open WebUI now includes a plugin framework for enhanced customization and functionality (https://github.com/open-webui/pipelines). Easily add custom logic and integrate Python libraries, from AI agents to home automation APIs.
- **🔗 Function Calling via Pipelines**: Integrate function calling seamlessly through Pipelines.
- **⚖️ User Rate Limiting via Pipelines**: Implement user-specific rate limits to manage API usage efficiently.
- **📊 Usage Monitoring with Langfuse**: Track and analyze usage statistics with Langfuse integration through Pipelines.
- **🕒 Conversation Turn Limits**: Set limits on conversation turns to manage interactions better through Pipelines.
- **🛡️ Toxic Message Filtering**: Automatically filter out toxic messages to maintain a safe environment using Pipelines.
- **🔍 Web Search Support**: Introducing built-in web search capabilities via RAG API, allowing users to search using SearXNG, Google Programmatic Search Engine, Brave Search, serpstack, and serper. Activate it effortlessly by adding necessary variables from Document settings > Web Params.
- **🗂️ Models Workspace**: Create and manage model presets for both Ollama/OpenAI API. Note: The old Modelfiles workspace is deprecated.
- **🛠️ Model Builder Feature**: Build and edit all models with persistent builder mode.
- **🏷️ Model Tagging Support**: Organize models with tagging features in the models workspace.
- **📋 Model Ordering Support**: Effortlessly organize models by dragging and dropping them into the desired positions within the models workspace.
- **📈 OpenAI Generation Stats**: Access detailed generation statistics for OpenAI models.
- **📅 System Prompt Variables**: New variables added: '{{CURRENT_DATE}}' and '{{USER_NAME}}' for dynamic prompts.
- **📢 Global Banner Support**: Manage global banners from admin settings > banners.
- **🗃️ Enhanced Archived Chats Modal**: Search and export archived chats easily.
- **📂 Archive All Button**: Quickly archive all chats from settings > chats.
- **🌐 Improved Translations**: Added and improved translations for French, Croatian, Cebuano, and Vietnamese.
### Fixed
- **🔍 Archived Chats Visibility**: Resolved issue with archived chats not showing in the admin panel.
- **💬 Message Styling**: Fixed styling issues affecting message appearance.
- **🔗 Shared Chat Responses**: Corrected the issue where shared chat response messages were not readonly.
- **🖥️ UI Enhancement**: Fixed the scrollbar overlapping issue with the message box in the user interface.
### Changed
- **💾 User Settings Storage**: User settings are now saved on the backend, ensuring consistency across all devices.
- **📡 Unified API Requests**: The API request for getting models is now unified to '/api/models' for easier usage.
- **🔄 Versioning Update**: Our versioning will now follow the format 0.x for major updates and 0.x.y for patches.
- **📦 Export All Chats (All Users)**: Moved this functionality to the Admin Panel settings for better organization and accessibility.
### Removed
- **🚫 Bundled LiteLLM Support Deprecated**: Migrate your LiteLLM config.yaml to a self-hosted LiteLLM instance. LiteLLM can still be added via OpenAI Connections. Download the LiteLLM config.yaml from admin settings > database > export LiteLLM config.yaml.
## [0.1.125] - 2024-05-19
### Added
- **🔄 Updated UI**: Chat interface revamped with chat bubbles. Easily switch back to the old style via settings > interface > chat bubble UI.
- **📂 Enhanced Sidebar UI**: Model files, documents, prompts, and playground merged into Workspace for streamlined access.
- **🚀 Improved Many Model Interaction**: All responses now displayed simultaneously for a smoother experience.
- **🐍 Python Code Execution**: Execute Python code locally in the browser with libraries like 'requests', 'beautifulsoup4', 'numpy', 'pandas', 'seaborn', 'matplotlib', 'scikit-learn', 'scipy', 'regex'.
- **🧠 Experimental Memory Feature**: Manually input personal information you want LLMs to remember via settings > personalization > memory.
- **💾 Persistent Settings**: Settings now saved as config.json for convenience.
- **🩺 Health Check Endpoint**: Added for Docker deployment.
- **↕️ RTL Support**: Toggle chat direction via settings > interface > chat direction.
- **🖥️ PowerPoint Support**: RAG pipeline now supports PowerPoint documents.
- **🌐 Language Updates**: Ukrainian, Turkish, Arabic, Chinese, Serbian, Vietnamese updated; Punjabi added.
### Changed
- **👤 Shared Chat Update**: Shared chat now includes creator user information.
## [0.1.124] - 2024-05-08
### Added
- **🖼️ Improved Chat Sidebar**: Now conveniently displays time ranges and organizes chats by today, yesterday, and more.
- **📜 Citations in RAG Feature**: Easily track the context fed to the LLM with added citations in the RAG feature.
- **🔒 Auth Disable Option**: Introducing the ability to disable authentication. Set 'WEBUI_AUTH' to False to disable authentication. Note: Only applicable for fresh installations without existing users.
- **📹 Enhanced YouTube RAG Pipeline**: Now supports non-English videos for an enriched experience.
- **🔊 Specify OpenAI TTS Models**: Customize your TTS experience by specifying OpenAI TTS models.
- **🔧 Additional Environment Variables**: Discover more environment variables in our comprehensive documentation at Open WebUI Documentation (https://docs.openwebui.com).
- **🌐 Language Support**: Arabic, Finnish, and Hindi added; Improved support for German, Vietnamese, and Chinese.
### Fixed
- **🛠️ Model Selector Styling**: Addressed styling issues for improved user experience.
- **⚠️ Warning Messages**: Resolved backend warning messages.
### Changed
- **📝 Title Generation**: Limited output to 50 tokens.
- **📦 Helm Charts**: Removed Helm charts, now available in a separate repository (https://github.com/open-webui/helm-charts).
## [0.1.123] - 2024-05-02
### Added
- **🎨 New Landing Page Design**: Refreshed design for a more modern look and optimized use of screen space.
- **📹 Youtube RAG Pipeline**: Introduces dedicated RAG pipeline for Youtube videos, enabling interaction with video transcriptions directly.
- **🔧 Enhanced Admin Panel**: Streamlined user management with options to add users directly or in bulk via CSV import.
- **👥 '@' Model Integration**: Easily switch to specific models during conversations; old collaborative chat feature phased out.
- **🌐 Language Enhancements**: Swedish translation added, plus improvements to German, Spanish, and the addition of Doge translation.
### Fixed
- **🗑️ Delete Chat Shortcut**: Addressed issue where shortcut wasn't functioning.
- **🖼️ Modal Closing Bug**: Resolved unexpected closure of modal when dragging from within.
- **✏️ Edit Button Styling**: Fixed styling inconsistency with edit buttons.
- **🌐 Image Generation Compatibility Issue**: Rectified image generation compatibility issue with third-party APIs.
- **📱 iOS PWA Icon Fix**: Corrected iOS PWA home screen icon shape.
- **🔍 Scroll Gesture Bug**: Adjusted gesture sensitivity to prevent accidental activation when scrolling through code on mobile; now requires scrolling from the leftmost side to open the sidebar.
### Changed
- **🔄 Unlimited Context Length**: Advanced settings now allow unlimited max context length (previously limited to 16000).
- **👑 Super Admin Assignment**: The first signup is automatically assigned a super admin role, unchangeable by other admins.
- **🛡️ Admin User Restrictions**: User action buttons from the admin panel are now disabled for users with admin roles.
- **🔝 Default Model Selector**: Set as default model option now exclusively available on the landing page.
## [0.1.122] - 2024-04-27
### Added
- **🌟 Enhanced RAG Pipeline**: Now with hybrid searching via 'BM25', reranking powered by 'CrossEncoder', and configurable relevance score thresholds.
- **🛢️ External Database Support**: Seamlessly connect to custom SQLite or Postgres databases using the 'DATABASE_URL' environment variable.
- **🌐 Remote ChromaDB Support**: Introducing the capability to connect to remote ChromaDB servers.
- **👨‍💼 Improved Admin Panel**: Admins can now conveniently check users' chat lists and last active status directly from the admin panel.
- **🎨 Splash Screen**: Introducing a loading splash screen for a smoother user experience.
- **🌍 Language Support Expansion**: Added support for Bangla (bn-BD), along with enhancements to Chinese, Spanish, and Ukrainian translations.
- **💻 Improved LaTeX Rendering Performance**: Enjoy faster rendering times for LaTeX equations.
- **🔧 More Environment Variables**: Explore additional environment variables in our documentation (https://docs.openwebui.com), including the 'ENABLE_LITELLM' option to manage memory usage.
### Fixed
- **🔧 Ollama Compatibility**: Resolved errors occurring when Ollama server version isn't an integer, such as SHA builds or RCs.
- **🐛 Various OpenAI API Issues**: Addressed several issues related to the OpenAI API.
- **🛑 Stop Sequence Issue**: Fixed the problem where the stop sequence with a backslash '\' was not functioning.
- **🔤 Font Fallback**: Corrected font fallback issue.
### Changed
- **⌨️ Prompt Input Behavior on Mobile**: Enter key prompt submission disabled on mobile devices for improved user experience.
## [0.1.121] - 2024-04-24
### Fixed

77
CODE_OF_CONDUCT.md Normal file
View File

@@ -0,0 +1,77 @@
# Contributor Covenant Code of Conduct
## Our Pledge
We as members, contributors, and leaders pledge to make participation in our
community a harassment-free experience for everyone, regardless of age, body
size, visible or invisible disability, ethnicity, sex characteristics, gender
identity and expression, level of experience, education, socio-economic status,
nationality, personal appearance, race, religion, or sexual identity
and orientation.
We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community.
## Our Standards
Examples of behavior that contribute to a positive environment for our community include:
- Demonstrating empathy and kindness toward other people
- Being respectful of differing opinions, viewpoints, and experiences
- Giving and gracefully accepting constructive feedback
- Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience
- Focusing on what is best not just for us as individuals, but for the overall community
Examples of unacceptable behavior include:
- The use of sexualized language or imagery, and sexual attention or advances of any kind
- Trolling, insulting or derogatory comments, and personal or political attacks
- Public or private harassment
- Publishing others' private information, such as a physical or email address, without their explicit permission
- **Spamming of any kind**
- Aggressive sales tactics targeting our community members are strictly prohibited. You can mention your product if it's relevant to the discussion, but under no circumstances should you push it forcefully
- Other conduct which could reasonably be considered inappropriate in a professional setting
## Enforcement Responsibilities
Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful.
## Scope
This Code of Conduct applies within all community spaces and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event.
## Enforcement
Instances of abusive, harassing, spamming, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at hello@openwebui.com. All complaints will be reviewed and investigated promptly and fairly.
All community leaders are obligated to respect the privacy and security of the reporter of any incident.
## Enforcement Guidelines
Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct:
### 1. Temporary Ban
**Community Impact**: Any violation of community standards, including but not limited to inappropriate language, unprofessional behavior, harassment, or spamming.
**Consequence**: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban.
### 2. Permanent Ban
**Community Impact**: Repeated or severe violations of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals.
**Consequence**: A permanent ban from any sort of public interaction within the community.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
version 2.0, available at
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
Community Impact Guidelines were inspired by [Mozilla's code of conduct
enforcement ladder](https://github.com/mozilla/diversity).
[homepage]: https://www.contributor-covenant.org
For answers to common questions about this code of conduct, see the FAQ at
https://www.contributor-covenant.org/faq. Translations are available at
https://www.contributor-covenant.org/translations.

View File

@@ -8,11 +8,17 @@ ARG USE_CUDA_VER=cu121
# any sentence transformer model; models to use can be found at https://huggingface.co/models?library=sentence-transformers
# Leaderboard: https://huggingface.co/spaces/mteb/leaderboard
# for better performance and multilangauge support use "intfloat/multilingual-e5-large" (~2.5GB) or "intfloat/multilingual-e5-base" (~1.5GB)
# IMPORTANT: If you change the default 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.
# 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.
ARG USE_EMBEDDING_MODEL=sentence-transformers/all-MiniLM-L6-v2
ARG USE_RERANKING_MODEL=""
ARG BUILD_HASH=dev-build
# Override at your own risk - non-root configurations are untested
ARG UID=0
ARG GID=0
######## WebUI frontend ########
FROM --platform=$BUILDPLATFORM node:21-alpine3.19 as build
ARG BUILD_HASH
WORKDIR /app
@@ -20,6 +26,7 @@ COPY package.json package-lock.json ./
RUN npm ci
COPY . .
ENV APP_BUILD_HASH=${BUILD_HASH}
RUN npm run build
######## WebUI backend ########
@@ -30,6 +37,9 @@ ARG USE_CUDA
ARG USE_OLLAMA
ARG USE_CUDA_VER
ARG USE_EMBEDDING_MODEL
ARG USE_RERANKING_MODEL
ARG UID
ARG GID
## Basis ##
ENV ENV=prod \
@@ -38,7 +48,8 @@ ENV ENV=prod \
USE_OLLAMA_DOCKER=${USE_OLLAMA} \
USE_CUDA_DOCKER=${USE_CUDA} \
USE_CUDA_DOCKER_VER=${USE_CUDA_VER} \
USE_EMBEDDING_MODEL_DOCKER=${USE_EMBEDDING_MODEL}
USE_EMBEDDING_MODEL_DOCKER=${USE_EMBEDDING_MODEL} \
USE_RERANKING_MODEL_DOCKER=${USE_RERANKING_MODEL}
## Basis URL Config ##
ENV OLLAMA_BASE_URL="/ollama" \
@@ -48,12 +59,8 @@ ENV OLLAMA_BASE_URL="/ollama" \
ENV OPENAI_API_KEY="" \
WEBUI_SECRET_KEY="" \
SCARF_NO_ANALYTICS=true \
DO_NOT_TRACK=true
# Use locally bundled version of the LiteLLM cost map json
# to avoid repetitive startup connections
ENV LITELLM_LOCAL_MODEL_COST_MAP="True"
DO_NOT_TRACK=true \
ANONYMIZED_TELEMETRY=false
#### Other models #########################################################
## whisper TTS model settings ##
@@ -62,50 +69,69 @@ ENV WHISPER_MODEL="base" \
## RAG Embedding model settings ##
ENV RAG_EMBEDDING_MODEL="$USE_EMBEDDING_MODEL_DOCKER" \
RAG_EMBEDDING_MODEL_DIR="/app/backend/data/cache/embedding/models" \
RAG_RERANKING_MODEL="$USE_RERANKING_MODEL_DOCKER" \
SENTENCE_TRANSFORMERS_HOME="/app/backend/data/cache/embedding/models"
## Hugging Face download cache ##
ENV HF_HOME="/app/backend/data/cache/embedding/models"
#### Other models ##########################################################
WORKDIR /app/backend
ENV HOME /root
# Create user and group if not root
RUN if [ $UID -ne 0 ]; then \
if [ $GID -ne 0 ]; then \
addgroup --gid $GID app; \
fi; \
adduser --uid $UID --gid $GID --home $HOME --disabled-password --no-create-home app; \
fi
RUN mkdir -p $HOME/.cache/chroma
RUN echo -n 00000000-0000-0000-0000-000000000000 > $HOME/.cache/chroma/telemetry_user_id
# Make sure the user has access to the app and root directory
RUN chown -R $UID:$GID /app $HOME
RUN if [ "$USE_OLLAMA" = "true" ]; then \
apt-get update && \
# Install pandoc and netcat
apt-get install -y --no-install-recommends pandoc netcat-openbsd && \
# for RAG OCR
apt-get install -y --no-install-recommends ffmpeg libsm6 libxext6 && \
# install helper tools
apt-get install -y --no-install-recommends curl && \
# install ollama
curl -fsSL https://ollama.com/install.sh | sh && \
# cleanup
rm -rf /var/lib/apt/lists/*; \
apt-get update && \
# Install pandoc and netcat
apt-get install -y --no-install-recommends pandoc netcat-openbsd curl && \
# for RAG OCR
apt-get install -y --no-install-recommends ffmpeg libsm6 libxext6 && \
# install helper tools
apt-get install -y --no-install-recommends curl jq && \
# install ollama
curl -fsSL https://ollama.com/install.sh | sh && \
# cleanup
rm -rf /var/lib/apt/lists/*; \
else \
apt-get update && \
# Install pandoc and netcat
apt-get install -y --no-install-recommends pandoc netcat-openbsd && \
# for RAG OCR
apt-get install -y --no-install-recommends ffmpeg libsm6 libxext6 && \
# cleanup
rm -rf /var/lib/apt/lists/*; \
apt-get update && \
# Install pandoc and netcat
apt-get install -y --no-install-recommends pandoc netcat-openbsd curl jq && \
# for RAG OCR
apt-get install -y --no-install-recommends ffmpeg libsm6 libxext6 && \
# cleanup
rm -rf /var/lib/apt/lists/*; \
fi
# install python dependencies
COPY ./backend/requirements.txt ./requirements.txt
COPY --chown=$UID:$GID ./backend/requirements.txt ./requirements.txt
RUN pip3 install uv && \
if [ "$USE_CUDA" = "true" ]; then \
# If you use CUDA the whisper and embedding model will be downloaded on first use
pip3 install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/$USE_CUDA_DOCKER_VER --no-cache-dir && \
uv pip install --system -r requirements.txt --no-cache-dir && \
python -c "import os; from sentence_transformers import SentenceTransformer; SentenceTransformer(os.environ['RAG_EMBEDDING_MODEL'], device='cpu')" && \
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'])"; \
# If you use CUDA the whisper and embedding model will be downloaded on first use
pip3 install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/$USE_CUDA_DOCKER_VER --no-cache-dir && \
uv pip install --system -r requirements.txt --no-cache-dir && \
python -c "import os; from sentence_transformers import SentenceTransformer; SentenceTransformer(os.environ['RAG_EMBEDDING_MODEL'], device='cpu')" && \
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'])"; \
else \
pip3 install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cpu --no-cache-dir && \
uv pip install --system -r requirements.txt --no-cache-dir && \
python -c "import os; from sentence_transformers import SentenceTransformer; SentenceTransformer(os.environ['RAG_EMBEDDING_MODEL'], device='cpu')" && \
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'])"; \
fi
pip3 install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cpu --no-cache-dir && \
uv pip install --system -r requirements.txt --no-cache-dir && \
python -c "import os; from sentence_transformers import SentenceTransformer; SentenceTransformer(os.environ['RAG_EMBEDDING_MODEL'], device='cpu')" && \
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'])"; \
fi; \
chown -R $UID:$GID /app/backend/data/
@@ -114,13 +140,20 @@ RUN pip3 install uv && \
# COPY --from=build /app/onnx /root/.cache/chroma/onnx_models/all-MiniLM-L6-v2/onnx
# copy built frontend files
COPY --from=build /app/build /app/build
COPY --from=build /app/CHANGELOG.md /app/CHANGELOG.md
COPY --from=build /app/package.json /app/package.json
COPY --chown=$UID:$GID --from=build /app/build /app/build
COPY --chown=$UID:$GID --from=build /app/CHANGELOG.md /app/CHANGELOG.md
COPY --chown=$UID:$GID --from=build /app/package.json /app/package.json
# copy backend files
COPY ./backend .
COPY --chown=$UID:$GID ./backend .
EXPOSE 8080
CMD [ "bash", "start.sh"]
HEALTHCHECK CMD curl --silent --fail http://localhost:8080/health | jq -e '.status == true' || exit 1
USER $UID:$GID
ARG BUILD_HASH
ENV WEBUI_BUILD_VERSION=${BUILD_HASH}
CMD [ "bash", "start.sh"]

View File

@@ -1,27 +1,33 @@
ifneq ($(shell which docker-compose 2>/dev/null),)
DOCKER_COMPOSE := docker-compose
else
DOCKER_COMPOSE := docker compose
endif
install:
@docker-compose up -d
$(DOCKER_COMPOSE) up -d
remove:
@chmod +x confirm_remove.sh
@./confirm_remove.sh
start:
@docker-compose start
$(DOCKER_COMPOSE) start
startAndBuild:
docker-compose up -d --build
$(DOCKER_COMPOSE) up -d --build
stop:
@docker-compose stop
$(DOCKER_COMPOSE) stop
update:
# Calls the LLM update script
chmod +x update_ollama_models.sh
@./update_ollama_models.sh
@git pull
@docker-compose down
$(DOCKER_COMPOSE) down
# Make sure the ollama-webui container is stopped before rebuilding
@docker stop open-webui || true
@docker-compose up --build -d
@docker-compose start
$(DOCKER_COMPOSE) up --build -d
$(DOCKER_COMPOSE) start

152
README.md
View File

@@ -15,93 +15,39 @@ Open WebUI is an extensible, feature-rich, and user-friendly self-hosted WebUI d
![Open WebUI Demo](./demo.gif)
## Features ⭐
## Key Features of Open WebUI
- 🖥️ **Intuitive Interface**: Our chat interface takes inspiration from ChatGPT, ensuring a user-friendly experience.
- 🚀 **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.
- 📱 **Responsive Design**: Enjoy a seamless experience on both desktop and mobile devices.
- 🤝 **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**.
- **Swift Responsiveness**: Enjoy fast and responsive performance.
- 🧩 **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/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.
- 🚀 **Effortless Setup**: Install seamlessly using Docker or Kubernetes (kubectl, kustomize or helm) for a hassle-free experience.
- 📱 **Responsive Design**: Enjoy a seamless experience across Desktop PC, Laptop, and Mobile devices.
- 🌈 **Theme Customization**: Choose from a variety of themes to personalize your Open WebUI experience.
- 💻 **Code Syntax Highlighting**: Enjoy enhanced code readability with our syntax highlighting feature.
- 📱 **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.
- ✒️🔢 **Full Markdown and LaTeX Support**: Elevate your LLM experience with comprehensive Markdown and LaTeX capabilities for enriched interaction.
- 📚 **Local RAG Integration**: Dive into the future of chat interactions with the groundbreaking Retrieval Augmented Generation (RAG) support. This feature seamlessly integrates document interactions into your chat experience. You can load documents directly into the chat or add files to your document library, effortlessly accessing them using `#` command in the prompt. In its alpha phase, occasional issues may arise as we actively refine and enhance this feature to ensure optimal performance and reliability.
- 🛠️ **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.
- 🔍 **RAG Embedding Support**: Change the RAG embedding model directly in document settings, enhancing document processing. This feature supports Ollama and OpenAI models.
- 📚 **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.
- 🌐 **Web Browsing Capability**: Seamlessly integrate websites into your chat experience using the `#` command followed by the URL. This feature allows you to incorporate web content directly into your conversations, enhancing the richness and depth of your interactions.
- 🔍 **Web Search for RAG**: Perform web searches using providers like `SearXNG`, `Google PSE`, `Brave Search`, `serpstack`, and `serper`, and inject the results directly into your chat experience.
- 📜 **Prompt Preset Support**: Instantly access preset prompts using the `/` command in the chat input. Load predefined conversation starters effortlessly and expedite your interactions. Effortlessly import prompts through [Open WebUI Community](https://openwebui.com/) integration.
- 🌐 **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.
- 👍👎 **RLHF Annotation**: Empower your messages by rating them with thumbs up and thumbs down, followed by the option to provide textual feedback, facilitating the creation of datasets for Reinforcement Learning from Human Feedback (RLHF). Utilize your messages to train or fine-tune models, all while ensuring the confidentiality of locally saved data.
- 🏷️ **Conversation Tagging**: Effortlessly categorize and locate specific chats for quick reference and streamlined data collection.
- 📥🗑️ **Download/Delete Models**: Easily download or remove models directly from the web UI.
- 🔄 **Update All Ollama Models**: Easily update locally installed models all at once with a convenient button, streamlining model management.
- ⬆️ **GGUF File Model Creation**: Effortlessly create Ollama models by uploading GGUF files directly from the web UI. Streamlined process with options to upload from your machine or download GGUF files from Hugging Face.
- 🤖 **Multiple Model Support**: Seamlessly switch between different chat models for diverse interactions.
- 🔄 **Multi-Modal Support**: Seamlessly engage with models that support multimodal interactions, including images (e.g., LLava).
- 🧩 **Modelfile Builder**: Easily create Ollama modelfiles via the web UI. Create and add characters/agents, customize chat elements, and import modelfiles effortlessly through [Open WebUI Community](https://openwebui.com/) integration.
- 🎨 **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.
- ⚙️ **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.
- 💬 **Collaborative Chat**: Harness the collective intelligence of multiple models by seamlessly orchestrating group conversations. Use the `@` command to specify the model, enabling dynamic and diverse dialogues within your chat interface. Immerse yourself in the collective intelligence woven into your chat environment.
- 🗨️ **Local Chat Sharing**: Generate and share chat links seamlessly between users, enhancing collaboration and communication.
- 🔄 **Regeneration History Access**: Easily revisit and explore your entire regeneration history.
- 📜 **Chat History**: Effortlessly access and manage your conversation history.
- 📬 **Archive Chats**: Effortlessly store away completed conversations with LLMs for future reference, maintaining a tidy and clutter-free chat interface while allowing for easy retrieval and reference.
- 📤📥 **Import/Export Chat History**: Seamlessly move your chat data in and out of the platform.
- 🗣️ **Voice Input Support**: Engage with your model through voice interactions; enjoy the convenience of talking to your model directly. Additionally, explore the option for sending voice input automatically after 3 seconds of silence for a streamlined experience.
- 🔊 **Configurable Text-to-Speech Endpoint**: Customize your Text-to-Speech experience with configurable OpenAI endpoints.
- ⚙️ **Fine-Tuned Control with Advanced Parameters**: Gain a deeper level of control by adjusting parameters such as temperature and defining your system prompts to tailor the conversation to your specific preferences and needs.
- 🎨🤖 **Image Generation Integration**: Seamlessly incorporate image generation capabilities using options such as AUTOMATIC1111 API (local), ComfyUI (local), and DALL-E, enriching your chat experience with dynamic visual content.
- 🤝 **OpenAI API Integration**: Effortlessly integrate OpenAI-compatible API for versatile conversations alongside Ollama models. Customize the API Base URL to link with **LMStudio, Mistral, OpenRouter, and more**.
-**Multiple OpenAI-Compatible API Support**: Seamlessly integrate and customize various OpenAI-compatible APIs, enhancing the versatility of your chat interactions.
- 🔑 **API Key Generation Support**: Generate secret keys to leverage Open WebUI with OpenAI libraries, simplifying integration and development.
- 🔗 **External Ollama Server Connection**: Seamlessly link to an external Ollama server hosted on a different address by configuring the environment variable.
- 🔀 **Multiple Ollama Instance Load Balancing**: Effortlessly distribute chat requests across multiple Ollama instances for enhanced performance and reliability.
- 👥 **Multi-User Management**: Easily oversee and administer users via our intuitive admin panel, streamlining user management processes.
- 🔗 **Webhook Integration**: Subscribe to new user sign-up events via webhook (compatible with Google Chat and Microsoft Teams), providing real-time notifications and automation capabilities.
- 🛡️ **Model Whitelisting**: Admins can whitelist models for users with the 'user' role, enhancing security and access control.
- 📧 **Trusted Email Authentication**: Authenticate using a trusted email header, adding an additional layer of security and authentication.
- 🔐 **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.
- 🔒 **Backend Reverse Proxy Support**: Bolster security through direct communication between Open WebUI backend and Ollama. This key feature eliminates the need to expose Ollama over LAN. Requests made to the '/ollama/api' route from the web UI are seamlessly redirected to Ollama from the backend, enhancing overall system security.
- 🌐🌍 **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!
- 🌟 **Continuous Updates**: We are committed to improving Open WebUI with regular updates and new features.
- 🌟 **Continuous Updates**: We are committed to improving Open WebUI with regular updates, fixes, and new features.
Want to learn more about Open WebUI's features? Check out our [Open WebUI documentation](https://docs.openwebui.com/features) for a comprehensive overview!
## 🔗 Also Check Out Open WebUI Community!
@@ -120,22 +66,66 @@ Don't forget to explore our sibling project, [Open WebUI Community](https://open
> [!TIP]
> 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.
**If Ollama is on your computer**, use this command:
### Installation with Default Configuration
```bash
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
```
- **If Ollama is on your computer**, use this command:
**If Ollama is on a Different Server**, use this command:
```bash
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
```
To connect to Ollama on another server, change the `OLLAMA_BASE_URL` to the server's URL:
- **If Ollama is on a Different Server**, use this command:
```bash
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
```
To connect to Ollama on another server, change the `OLLAMA_BASE_URL` to the server's URL:
```bash
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
```
- **To run Open WebUI with Nvidia GPU support**, use this command:
```bash
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
```
### Installation for OpenAI API Usage Only
- **If you're only using OpenAI API**, use this command:
```bash
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
```
### Installing Open WebUI with Bundled Ollama Support
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:
- **With GPU Support**:
Utilize GPU resources by running the following command:
```bash
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
```
- **For CPU Only**:
If you're not using a GPU, use this command instead:
```bash
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
```
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.
After installation, you can access Open WebUI at [http://localhost:3000](http://localhost:3000). Enjoy! 😄
### Other Installation Methods
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.
### Troubleshooting
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).
#### Open WebUI: Server Connection Error
If you're experiencing connection issues, its 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`.
@@ -146,14 +136,6 @@ If you're experiencing connection issues, its often due to the WebUI docker c
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
```
### Other Installation Methods
We offer various installation alternatives, including non-Docker 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.
### Troubleshooting
Encountering connection issues? Our [Open WebUI Documentation](https://docs.openwebui.com/getting-started/troubleshooting/) has got you covered. For further assistance and to join our vibrant community, visit the [Open WebUI Discord](https://discord.gg/5rJgQTnV4s).
### Keeping Your Docker Installation Up-to-Date
In case you want to update your local Docker installation to the latest version, you can do it with [Watchtower](https://containrrr.dev/watchtower/):

View File

@@ -43,6 +43,9 @@ from config import (
DEVICE_TYPE,
AUDIO_OPENAI_API_BASE_URL,
AUDIO_OPENAI_API_KEY,
AUDIO_OPENAI_API_MODEL,
AUDIO_OPENAI_API_VOICE,
AppConfig,
)
log = logging.getLogger(__name__)
@@ -57,9 +60,11 @@ app.add_middleware(
allow_headers=["*"],
)
app.state.OPENAI_API_BASE_URL = AUDIO_OPENAI_API_BASE_URL
app.state.OPENAI_API_KEY = AUDIO_OPENAI_API_KEY
app.state.config = AppConfig()
app.state.config.OPENAI_API_BASE_URL = AUDIO_OPENAI_API_BASE_URL
app.state.config.OPENAI_API_KEY = AUDIO_OPENAI_API_KEY
app.state.config.OPENAI_API_MODEL = AUDIO_OPENAI_API_MODEL
app.state.config.OPENAI_API_VOICE = AUDIO_OPENAI_API_VOICE
# setting device type for whisper model
whisper_device_type = DEVICE_TYPE if DEVICE_TYPE and DEVICE_TYPE == "cuda" else "cpu"
@@ -72,13 +77,17 @@ SPEECH_CACHE_DIR.mkdir(parents=True, exist_ok=True)
class OpenAIConfigUpdateForm(BaseModel):
url: str
key: str
model: str
speaker: str
@app.get("/config")
async def get_openai_config(user=Depends(get_admin_user)):
return {
"OPENAI_API_BASE_URL": app.state.OPENAI_API_BASE_URL,
"OPENAI_API_KEY": app.state.OPENAI_API_KEY,
"OPENAI_API_BASE_URL": app.state.config.OPENAI_API_BASE_URL,
"OPENAI_API_KEY": app.state.config.OPENAI_API_KEY,
"OPENAI_API_MODEL": app.state.config.OPENAI_API_MODEL,
"OPENAI_API_VOICE": app.state.config.OPENAI_API_VOICE,
}
@@ -89,13 +98,17 @@ async def update_openai_config(
if form_data.key == "":
raise HTTPException(status_code=400, detail=ERROR_MESSAGES.API_KEY_NOT_FOUND)
app.state.OPENAI_API_BASE_URL = form_data.url
app.state.OPENAI_API_KEY = form_data.key
app.state.config.OPENAI_API_BASE_URL = form_data.url
app.state.config.OPENAI_API_KEY = form_data.key
app.state.config.OPENAI_API_MODEL = form_data.model
app.state.config.OPENAI_API_VOICE = form_data.speaker
return {
"status": True,
"OPENAI_API_BASE_URL": app.state.OPENAI_API_BASE_URL,
"OPENAI_API_KEY": app.state.OPENAI_API_KEY,
"OPENAI_API_BASE_URL": app.state.config.OPENAI_API_BASE_URL,
"OPENAI_API_KEY": app.state.config.OPENAI_API_KEY,
"OPENAI_API_MODEL": app.state.config.OPENAI_API_MODEL,
"OPENAI_API_VOICE": app.state.config.OPENAI_API_VOICE,
}
@@ -112,13 +125,13 @@ async def speech(request: Request, user=Depends(get_verified_user)):
return FileResponse(file_path)
headers = {}
headers["Authorization"] = f"Bearer {app.state.OPENAI_API_KEY}"
headers["Authorization"] = f"Bearer {app.state.config.OPENAI_API_KEY}"
headers["Content-Type"] = "application/json"
r = None
try:
r = requests.post(
url=f"{app.state.OPENAI_API_BASE_URL}/audio/speech",
url=f"{app.state.config.OPENAI_API_BASE_URL}/audio/speech",
data=body,
headers=headers,
stream=True,

View File

@@ -24,6 +24,7 @@ from utils.misc import calculate_sha256
from typing import Optional
from pydantic import BaseModel
from pathlib import Path
import mimetypes
import uuid
import base64
import json
@@ -32,11 +33,16 @@ import logging
from config import (
SRC_LOG_LEVELS,
CACHE_DIR,
IMAGE_GENERATION_ENGINE,
ENABLE_IMAGE_GENERATION,
AUTOMATIC1111_BASE_URL,
COMFYUI_BASE_URL,
IMAGES_OPENAI_API_BASE_URL,
IMAGES_OPENAI_API_KEY,
IMAGE_GENERATION_MODEL,
IMAGE_SIZE,
IMAGE_STEPS,
AppConfig,
)
@@ -55,26 +61,31 @@ app.add_middleware(
allow_headers=["*"],
)
app.state.ENGINE = ""
app.state.ENABLED = ENABLE_IMAGE_GENERATION
app.state.config = AppConfig()
app.state.OPENAI_API_BASE_URL = IMAGES_OPENAI_API_BASE_URL
app.state.OPENAI_API_KEY = IMAGES_OPENAI_API_KEY
app.state.config.ENGINE = IMAGE_GENERATION_ENGINE
app.state.config.ENABLED = ENABLE_IMAGE_GENERATION
app.state.MODEL = ""
app.state.config.OPENAI_API_BASE_URL = IMAGES_OPENAI_API_BASE_URL
app.state.config.OPENAI_API_KEY = IMAGES_OPENAI_API_KEY
app.state.config.MODEL = IMAGE_GENERATION_MODEL
app.state.AUTOMATIC1111_BASE_URL = AUTOMATIC1111_BASE_URL
app.state.COMFYUI_BASE_URL = COMFYUI_BASE_URL
app.state.config.AUTOMATIC1111_BASE_URL = AUTOMATIC1111_BASE_URL
app.state.config.COMFYUI_BASE_URL = COMFYUI_BASE_URL
app.state.IMAGE_SIZE = "512x512"
app.state.IMAGE_STEPS = 50
app.state.config.IMAGE_SIZE = IMAGE_SIZE
app.state.config.IMAGE_STEPS = IMAGE_STEPS
@app.get("/config")
async def get_config(request: Request, user=Depends(get_admin_user)):
return {"engine": app.state.ENGINE, "enabled": app.state.ENABLED}
return {
"engine": app.state.config.ENGINE,
"enabled": app.state.config.ENABLED,
}
class ConfigUpdateForm(BaseModel):
@@ -84,9 +95,12 @@ class ConfigUpdateForm(BaseModel):
@app.post("/config/update")
async def update_config(form_data: ConfigUpdateForm, user=Depends(get_admin_user)):
app.state.ENGINE = form_data.engine
app.state.ENABLED = form_data.enabled
return {"engine": app.state.ENGINE, "enabled": app.state.ENABLED}
app.state.config.ENGINE = form_data.engine
app.state.config.ENABLED = form_data.enabled
return {
"engine": app.state.config.ENGINE,
"enabled": app.state.config.ENABLED,
}
class EngineUrlUpdateForm(BaseModel):
@@ -97,8 +111,8 @@ class EngineUrlUpdateForm(BaseModel):
@app.get("/url")
async def get_engine_url(user=Depends(get_admin_user)):
return {
"AUTOMATIC1111_BASE_URL": app.state.AUTOMATIC1111_BASE_URL,
"COMFYUI_BASE_URL": app.state.COMFYUI_BASE_URL,
"AUTOMATIC1111_BASE_URL": app.state.config.AUTOMATIC1111_BASE_URL,
"COMFYUI_BASE_URL": app.state.config.COMFYUI_BASE_URL,
}
@@ -108,29 +122,29 @@ async def update_engine_url(
):
if form_data.AUTOMATIC1111_BASE_URL == None:
app.state.AUTOMATIC1111_BASE_URL = AUTOMATIC1111_BASE_URL
app.state.config.AUTOMATIC1111_BASE_URL = AUTOMATIC1111_BASE_URL
else:
url = form_data.AUTOMATIC1111_BASE_URL.strip("/")
try:
r = requests.head(url)
app.state.AUTOMATIC1111_BASE_URL = url
app.state.config.AUTOMATIC1111_BASE_URL = url
except Exception as e:
raise HTTPException(status_code=400, detail=ERROR_MESSAGES.DEFAULT(e))
if form_data.COMFYUI_BASE_URL == None:
app.state.COMFYUI_BASE_URL = COMFYUI_BASE_URL
app.state.config.COMFYUI_BASE_URL = COMFYUI_BASE_URL
else:
url = form_data.COMFYUI_BASE_URL.strip("/")
try:
r = requests.head(url)
app.state.COMFYUI_BASE_URL = url
app.state.config.COMFYUI_BASE_URL = url
except Exception as e:
raise HTTPException(status_code=400, detail=ERROR_MESSAGES.DEFAULT(e))
return {
"AUTOMATIC1111_BASE_URL": app.state.AUTOMATIC1111_BASE_URL,
"COMFYUI_BASE_URL": app.state.COMFYUI_BASE_URL,
"AUTOMATIC1111_BASE_URL": app.state.config.AUTOMATIC1111_BASE_URL,
"COMFYUI_BASE_URL": app.state.config.COMFYUI_BASE_URL,
"status": True,
}
@@ -143,8 +157,8 @@ class OpenAIConfigUpdateForm(BaseModel):
@app.get("/openai/config")
async def get_openai_config(user=Depends(get_admin_user)):
return {
"OPENAI_API_BASE_URL": app.state.OPENAI_API_BASE_URL,
"OPENAI_API_KEY": app.state.OPENAI_API_KEY,
"OPENAI_API_BASE_URL": app.state.config.OPENAI_API_BASE_URL,
"OPENAI_API_KEY": app.state.config.OPENAI_API_KEY,
}
@@ -155,13 +169,13 @@ async def update_openai_config(
if form_data.key == "":
raise HTTPException(status_code=400, detail=ERROR_MESSAGES.API_KEY_NOT_FOUND)
app.state.OPENAI_API_BASE_URL = form_data.url
app.state.OPENAI_API_KEY = form_data.key
app.state.config.OPENAI_API_BASE_URL = form_data.url
app.state.config.OPENAI_API_KEY = form_data.key
return {
"status": True,
"OPENAI_API_BASE_URL": app.state.OPENAI_API_BASE_URL,
"OPENAI_API_KEY": app.state.OPENAI_API_KEY,
"OPENAI_API_BASE_URL": app.state.config.OPENAI_API_BASE_URL,
"OPENAI_API_KEY": app.state.config.OPENAI_API_KEY,
}
@@ -171,7 +185,7 @@ class ImageSizeUpdateForm(BaseModel):
@app.get("/size")
async def get_image_size(user=Depends(get_admin_user)):
return {"IMAGE_SIZE": app.state.IMAGE_SIZE}
return {"IMAGE_SIZE": app.state.config.IMAGE_SIZE}
@app.post("/size/update")
@@ -180,9 +194,9 @@ async def update_image_size(
):
pattern = r"^\d+x\d+$" # Regular expression pattern
if re.match(pattern, form_data.size):
app.state.IMAGE_SIZE = form_data.size
app.state.config.IMAGE_SIZE = form_data.size
return {
"IMAGE_SIZE": app.state.IMAGE_SIZE,
"IMAGE_SIZE": app.state.config.IMAGE_SIZE,
"status": True,
}
else:
@@ -198,7 +212,7 @@ class ImageStepsUpdateForm(BaseModel):
@app.get("/steps")
async def get_image_size(user=Depends(get_admin_user)):
return {"IMAGE_STEPS": app.state.IMAGE_STEPS}
return {"IMAGE_STEPS": app.state.config.IMAGE_STEPS}
@app.post("/steps/update")
@@ -206,9 +220,9 @@ async def update_image_size(
form_data: ImageStepsUpdateForm, user=Depends(get_admin_user)
):
if form_data.steps >= 0:
app.state.IMAGE_STEPS = form_data.steps
app.state.config.IMAGE_STEPS = form_data.steps
return {
"IMAGE_STEPS": app.state.IMAGE_STEPS,
"IMAGE_STEPS": app.state.config.IMAGE_STEPS,
"status": True,
}
else:
@@ -221,14 +235,14 @@ async def update_image_size(
@app.get("/models")
def get_models(user=Depends(get_current_user)):
try:
if app.state.ENGINE == "openai":
if app.state.config.ENGINE == "openai":
return [
{"id": "dall-e-2", "name": "DALL·E 2"},
{"id": "dall-e-3", "name": "DALL·E 3"},
]
elif app.state.ENGINE == "comfyui":
elif app.state.config.ENGINE == "comfyui":
r = requests.get(url=f"{app.state.COMFYUI_BASE_URL}/object_info")
r = requests.get(url=f"{app.state.config.COMFYUI_BASE_URL}/object_info")
info = r.json()
return list(
@@ -240,7 +254,7 @@ def get_models(user=Depends(get_current_user)):
else:
r = requests.get(
url=f"{app.state.AUTOMATIC1111_BASE_URL}/sdapi/v1/sd-models"
url=f"{app.state.config.AUTOMATIC1111_BASE_URL}/sdapi/v1/sd-models"
)
models = r.json()
return list(
@@ -250,23 +264,29 @@ def get_models(user=Depends(get_current_user)):
)
)
except Exception as e:
app.state.ENABLED = False
app.state.config.ENABLED = False
raise HTTPException(status_code=400, detail=ERROR_MESSAGES.DEFAULT(e))
@app.get("/models/default")
async def get_default_model(user=Depends(get_admin_user)):
try:
if app.state.ENGINE == "openai":
return {"model": app.state.MODEL if app.state.MODEL else "dall-e-2"}
elif app.state.ENGINE == "comfyui":
return {"model": app.state.MODEL if app.state.MODEL else ""}
if app.state.config.ENGINE == "openai":
return {
"model": (
app.state.config.MODEL if app.state.config.MODEL else "dall-e-2"
)
}
elif app.state.config.ENGINE == "comfyui":
return {"model": (app.state.config.MODEL if app.state.config.MODEL else "")}
else:
r = requests.get(url=f"{app.state.AUTOMATIC1111_BASE_URL}/sdapi/v1/options")
r = requests.get(
url=f"{app.state.config.AUTOMATIC1111_BASE_URL}/sdapi/v1/options"
)
options = r.json()
return {"model": options["sd_model_checkpoint"]}
except Exception as e:
app.state.ENABLED = False
app.state.config.ENABLED = False
raise HTTPException(status_code=400, detail=ERROR_MESSAGES.DEFAULT(e))
@@ -275,20 +295,20 @@ class UpdateModelForm(BaseModel):
def set_model_handler(model: str):
if app.state.ENGINE == "openai":
app.state.MODEL = model
return app.state.MODEL
if app.state.ENGINE == "comfyui":
app.state.MODEL = model
return app.state.MODEL
if app.state.config.ENGINE in ["openai", "comfyui"]:
app.state.config.MODEL = model
return app.state.config.MODEL
else:
r = requests.get(url=f"{app.state.AUTOMATIC1111_BASE_URL}/sdapi/v1/options")
r = requests.get(
url=f"{app.state.config.AUTOMATIC1111_BASE_URL}/sdapi/v1/options"
)
options = r.json()
if model != options["sd_model_checkpoint"]:
options["sd_model_checkpoint"] = model
r = requests.post(
url=f"{app.state.AUTOMATIC1111_BASE_URL}/sdapi/v1/options", json=options
url=f"{app.state.config.AUTOMATIC1111_BASE_URL}/sdapi/v1/options",
json=options,
)
return options
@@ -311,35 +331,61 @@ class GenerateImageForm(BaseModel):
def save_b64_image(b64_str):
image_id = str(uuid.uuid4())
file_path = IMAGE_CACHE_DIR.joinpath(f"{image_id}.png")
try:
# Split the base64 string to get the actual image data
img_data = base64.b64decode(b64_str)
image_id = str(uuid.uuid4())
# Write the image data to a file
with open(file_path, "wb") as f:
f.write(img_data)
if "," in b64_str:
header, encoded = b64_str.split(",", 1)
mime_type = header.split(";")[0]
img_data = base64.b64decode(encoded)
image_format = mimetypes.guess_extension(mime_type)
image_filename = f"{image_id}{image_format}"
file_path = IMAGE_CACHE_DIR / f"{image_filename}"
with open(file_path, "wb") as f:
f.write(img_data)
return image_filename
else:
image_filename = f"{image_id}.png"
file_path = IMAGE_CACHE_DIR.joinpath(image_filename)
img_data = base64.b64decode(b64_str)
# Write the image data to a file
with open(file_path, "wb") as f:
f.write(img_data)
return image_filename
return image_id
except Exception as e:
log.error(f"Error saving image: {e}")
log.exception(f"Error saving image: {e}")
return None
def save_url_image(url):
image_id = str(uuid.uuid4())
file_path = IMAGE_CACHE_DIR.joinpath(f"{image_id}.png")
try:
r = requests.get(url)
r.raise_for_status()
if r.headers["content-type"].split("/")[0] == "image":
with open(file_path, "wb") as image_file:
image_file.write(r.content)
mime_type = r.headers["content-type"]
image_format = mimetypes.guess_extension(mime_type)
if not image_format:
raise ValueError("Could not determine image type from MIME type")
image_filename = f"{image_id}{image_format}"
file_path = IMAGE_CACHE_DIR.joinpath(f"{image_filename}")
with open(file_path, "wb") as image_file:
for chunk in r.iter_content(chunk_size=8192):
image_file.write(chunk)
return image_filename
else:
log.error(f"Url does not point to an image.")
return None
return image_id
except Exception as e:
log.exception(f"Error saving image: {e}")
return None
@@ -351,26 +397,32 @@ def generate_image(
user=Depends(get_current_user),
):
width, height = tuple(map(int, app.state.IMAGE_SIZE.split("x")))
width, height = tuple(map(int, app.state.config.IMAGE_SIZE.split("x")))
r = None
try:
if app.state.ENGINE == "openai":
if app.state.config.ENGINE == "openai":
headers = {}
headers["Authorization"] = f"Bearer {app.state.OPENAI_API_KEY}"
headers["Authorization"] = f"Bearer {app.state.config.OPENAI_API_KEY}"
headers["Content-Type"] = "application/json"
data = {
"model": app.state.MODEL if app.state.MODEL != "" else "dall-e-2",
"model": (
app.state.config.MODEL
if app.state.config.MODEL != ""
else "dall-e-2"
),
"prompt": form_data.prompt,
"n": form_data.n,
"size": form_data.size if form_data.size else app.state.IMAGE_SIZE,
"size": (
form_data.size if form_data.size else app.state.config.IMAGE_SIZE
),
"response_format": "b64_json",
}
r = requests.post(
url=f"{app.state.OPENAI_API_BASE_URL}/images/generations",
url=f"{app.state.config.OPENAI_API_BASE_URL}/images/generations",
json=data,
headers=headers,
)
@@ -381,16 +433,16 @@ def generate_image(
images = []
for image in res["data"]:
image_id = save_b64_image(image["b64_json"])
images.append({"url": f"/cache/image/generations/{image_id}.png"})
file_body_path = IMAGE_CACHE_DIR.joinpath(f"{image_id}.json")
image_filename = save_b64_image(image["b64_json"])
images.append({"url": f"/cache/image/generations/{image_filename}"})
file_body_path = IMAGE_CACHE_DIR.joinpath(f"{image_filename}.json")
with open(file_body_path, "w") as f:
json.dump(data, f)
return images
elif app.state.ENGINE == "comfyui":
elif app.state.config.ENGINE == "comfyui":
data = {
"prompt": form_data.prompt,
@@ -399,28 +451,28 @@ def generate_image(
"n": form_data.n,
}
if app.state.IMAGE_STEPS != None:
data["steps"] = app.state.IMAGE_STEPS
if app.state.config.IMAGE_STEPS is not None:
data["steps"] = app.state.config.IMAGE_STEPS
if form_data.negative_prompt != None:
if form_data.negative_prompt is not None:
data["negative_prompt"] = form_data.negative_prompt
data = ImageGenerationPayload(**data)
res = comfyui_generate_image(
app.state.MODEL,
app.state.config.MODEL,
data,
user.id,
app.state.COMFYUI_BASE_URL,
app.state.config.COMFYUI_BASE_URL,
)
log.debug(f"res: {res}")
images = []
for image in res["data"]:
image_id = save_url_image(image["url"])
images.append({"url": f"/cache/image/generations/{image_id}.png"})
file_body_path = IMAGE_CACHE_DIR.joinpath(f"{image_id}.json")
image_filename = save_url_image(image["url"])
images.append({"url": f"/cache/image/generations/{image_filename}"})
file_body_path = IMAGE_CACHE_DIR.joinpath(f"{image_filename}.json")
with open(file_body_path, "w") as f:
json.dump(data.model_dump(exclude_none=True), f)
@@ -438,14 +490,14 @@ def generate_image(
"height": height,
}
if app.state.IMAGE_STEPS != None:
data["steps"] = app.state.IMAGE_STEPS
if app.state.config.IMAGE_STEPS is not None:
data["steps"] = app.state.config.IMAGE_STEPS
if form_data.negative_prompt != None:
if form_data.negative_prompt is not None:
data["negative_prompt"] = form_data.negative_prompt
r = requests.post(
url=f"{app.state.AUTOMATIC1111_BASE_URL}/sdapi/v1/txt2img",
url=f"{app.state.config.AUTOMATIC1111_BASE_URL}/sdapi/v1/txt2img",
json=data,
)
@@ -456,9 +508,9 @@ def generate_image(
images = []
for image in res["images"]:
image_id = save_b64_image(image)
images.append({"url": f"/cache/image/generations/{image_id}.png"})
file_body_path = IMAGE_CACHE_DIR.joinpath(f"{image_id}.json")
image_filename = save_b64_image(image)
images.append({"url": f"/cache/image/generations/{image_filename}"})
file_body_path = IMAGE_CACHE_DIR.joinpath(f"{image_filename}.json")
with open(file_body_path, "w") as f:
json.dump({**data, "info": res["info"]}, f)

View File

@@ -1,351 +0,0 @@
import sys
from fastapi import FastAPI, Depends, HTTPException
from fastapi.routing import APIRoute
from fastapi.middleware.cors import CORSMiddleware
import logging
from fastapi import FastAPI, Request, Depends, status, Response
from fastapi.responses import JSONResponse
from starlette.middleware.base import BaseHTTPMiddleware, RequestResponseEndpoint
from starlette.responses import StreamingResponse
import json
import time
import requests
from pydantic import BaseModel, ConfigDict
from typing import Optional, List
from utils.utils import get_verified_user, get_current_user, get_admin_user
from config import SRC_LOG_LEVELS, ENV
from constants import MESSAGES
log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["LITELLM"])
from config import (
MODEL_FILTER_ENABLED,
MODEL_FILTER_LIST,
DATA_DIR,
LITELLM_PROXY_PORT,
LITELLM_PROXY_HOST,
)
from litellm.utils import get_llm_provider
import asyncio
import subprocess
import yaml
app = FastAPI()
origins = ["*"]
app.add_middleware(
CORSMiddleware,
allow_origins=origins,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
LITELLM_CONFIG_DIR = f"{DATA_DIR}/litellm/config.yaml"
with open(LITELLM_CONFIG_DIR, "r") as file:
litellm_config = yaml.safe_load(file)
app.state.CONFIG = litellm_config
# Global variable to store the subprocess reference
background_process = None
async def run_background_process(command):
global background_process
log.info("run_background_process")
try:
# Log the command to be executed
log.info(f"Executing command: {command}")
# Execute the command and create a subprocess
process = await asyncio.create_subprocess_exec(
*command, stdout=subprocess.PIPE, stderr=subprocess.PIPE
)
background_process = process
log.info("Subprocess started successfully.")
# Capture STDERR for debugging purposes
stderr_output = await process.stderr.read()
stderr_text = stderr_output.decode().strip()
if stderr_text:
log.info(f"Subprocess STDERR: {stderr_text}")
# log.info output line by line
async for line in process.stdout:
log.info(line.decode().strip())
# Wait for the process to finish
returncode = await process.wait()
log.info(f"Subprocess exited with return code {returncode}")
except Exception as e:
log.error(f"Failed to start subprocess: {e}")
raise # Optionally re-raise the exception if you want it to propagate
async def start_litellm_background():
log.info("start_litellm_background")
# Command to run in the background
command = [
"litellm",
"--port",
str(LITELLM_PROXY_PORT),
"--host",
LITELLM_PROXY_HOST,
"--telemetry",
"False",
"--config",
LITELLM_CONFIG_DIR,
]
await run_background_process(command)
async def shutdown_litellm_background():
log.info("shutdown_litellm_background")
global background_process
if background_process:
background_process.terminate()
await background_process.wait() # Ensure the process has terminated
log.info("Subprocess terminated")
background_process = None
@app.on_event("startup")
async def startup_event():
log.info("startup_event")
# TODO: Check config.yaml file and create one
asyncio.create_task(start_litellm_background())
app.state.MODEL_FILTER_ENABLED = MODEL_FILTER_ENABLED
app.state.MODEL_FILTER_LIST = MODEL_FILTER_LIST
@app.get("/")
async def get_status():
return {"status": True}
async def restart_litellm():
"""
Endpoint to restart the litellm background service.
"""
log.info("Requested restart of litellm service.")
try:
# Shut down the existing process if it is running
await shutdown_litellm_background()
log.info("litellm service shutdown complete.")
# Restart the background service
asyncio.create_task(start_litellm_background())
log.info("litellm service restart complete.")
return {
"status": "success",
"message": "litellm service restarted successfully.",
}
except Exception as e:
log.info(f"Error restarting litellm service: {e}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=str(e)
)
@app.get("/restart")
async def restart_litellm_handler(user=Depends(get_admin_user)):
return await restart_litellm()
@app.get("/config")
async def get_config(user=Depends(get_admin_user)):
return app.state.CONFIG
class LiteLLMConfigForm(BaseModel):
general_settings: Optional[dict] = None
litellm_settings: Optional[dict] = None
model_list: Optional[List[dict]] = None
router_settings: Optional[dict] = None
model_config = ConfigDict(protected_namespaces=())
@app.post("/config/update")
async def update_config(form_data: LiteLLMConfigForm, user=Depends(get_admin_user)):
app.state.CONFIG = form_data.model_dump(exclude_none=True)
with open(LITELLM_CONFIG_DIR, "w") as file:
yaml.dump(app.state.CONFIG, file)
await restart_litellm()
return app.state.CONFIG
@app.get("/models")
@app.get("/v1/models")
async def get_models(user=Depends(get_current_user)):
while not background_process:
await asyncio.sleep(0.1)
url = f"http://localhost:{LITELLM_PROXY_PORT}/v1"
r = None
try:
r = requests.request(method="GET", url=f"{url}/models")
r.raise_for_status()
data = r.json()
if app.state.MODEL_FILTER_ENABLED:
if user and user.role == "user":
data["data"] = list(
filter(
lambda model: model["id"] in app.state.MODEL_FILTER_LIST,
data["data"],
)
)
return data
except Exception as e:
log.exception(e)
error_detail = "Open WebUI: Server Connection Error"
if r is not None:
try:
res = r.json()
if "error" in res:
error_detail = f"External: {res['error']}"
except:
error_detail = f"External: {e}"
return {
"data": [
{
"id": model["model_name"],
"object": "model",
"created": int(time.time()),
"owned_by": "openai",
}
for model in app.state.CONFIG["model_list"]
],
"object": "list",
}
@app.get("/model/info")
async def get_model_list(user=Depends(get_admin_user)):
return {"data": app.state.CONFIG["model_list"]}
class AddLiteLLMModelForm(BaseModel):
model_name: str
litellm_params: dict
model_config = ConfigDict(protected_namespaces=())
@app.post("/model/new")
async def add_model_to_config(
form_data: AddLiteLLMModelForm, user=Depends(get_admin_user)
):
try:
get_llm_provider(model=form_data.model_name)
app.state.CONFIG["model_list"].append(form_data.model_dump())
with open(LITELLM_CONFIG_DIR, "w") as file:
yaml.dump(app.state.CONFIG, file)
await restart_litellm()
return {"message": MESSAGES.MODEL_ADDED(form_data.model_name)}
except Exception as e:
print(e)
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=str(e)
)
class DeleteLiteLLMModelForm(BaseModel):
id: str
@app.post("/model/delete")
async def delete_model_from_config(
form_data: DeleteLiteLLMModelForm, user=Depends(get_admin_user)
):
app.state.CONFIG["model_list"] = [
model
for model in app.state.CONFIG["model_list"]
if model["model_name"] != form_data.id
]
with open(LITELLM_CONFIG_DIR, "w") as file:
yaml.dump(app.state.CONFIG, file)
await restart_litellm()
return {"message": MESSAGES.MODEL_DELETED(form_data.id)}
@app.api_route("/{path:path}", methods=["GET", "POST", "PUT", "DELETE"])
async def proxy(path: str, request: Request, user=Depends(get_verified_user)):
body = await request.body()
url = f"http://localhost:{LITELLM_PROXY_PORT}"
target_url = f"{url}/{path}"
headers = {}
# headers["Authorization"] = f"Bearer {key}"
headers["Content-Type"] = "application/json"
r = None
try:
r = requests.request(
method=request.method,
url=target_url,
data=body,
headers=headers,
stream=True,
)
r.raise_for_status()
# Check if response is SSE
if "text/event-stream" in r.headers.get("Content-Type", ""):
return StreamingResponse(
r.iter_content(chunk_size=8192),
status_code=r.status_code,
headers=dict(r.headers),
)
else:
response_data = r.json()
return response_data
except Exception as e:
log.exception(e)
error_detail = "Open WebUI: Server Connection Error"
if r is not None:
try:
res = r.json()
if "error" in res:
error_detail = f"External: {res['error']['message'] if 'message' in res['error'] else res['error']}"
except:
error_detail = f"External: {e}"
raise HTTPException(
status_code=r.status_code if r else 500, detail=error_detail
)

File diff suppressed because it is too large Load Diff

View File

@@ -9,9 +9,10 @@ import json
import logging
from pydantic import BaseModel
from starlette.background import BackgroundTask
from apps.web.models.users import Users
from apps.webui.models.models import Models
from apps.webui.models.users import Users
from constants import ERROR_MESSAGES
from utils.utils import (
decode_token,
@@ -21,11 +22,13 @@ from utils.utils import (
)
from config import (
SRC_LOG_LEVELS,
ENABLE_OPENAI_API,
OPENAI_API_BASE_URLS,
OPENAI_API_KEYS,
CACHE_DIR,
MODEL_FILTER_ENABLED,
ENABLE_MODEL_FILTER,
MODEL_FILTER_LIST,
AppConfig,
)
from typing import List, Optional
@@ -45,11 +48,15 @@ app.add_middleware(
allow_headers=["*"],
)
app.state.MODEL_FILTER_ENABLED = MODEL_FILTER_ENABLED
app.state.MODEL_FILTER_LIST = MODEL_FILTER_LIST
app.state.OPENAI_API_BASE_URLS = OPENAI_API_BASE_URLS
app.state.OPENAI_API_KEYS = OPENAI_API_KEYS
app.state.config = AppConfig()
app.state.config.ENABLE_MODEL_FILTER = ENABLE_MODEL_FILTER
app.state.config.MODEL_FILTER_LIST = MODEL_FILTER_LIST
app.state.config.ENABLE_OPENAI_API = ENABLE_OPENAI_API
app.state.config.OPENAI_API_BASE_URLS = OPENAI_API_BASE_URLS
app.state.config.OPENAI_API_KEYS = OPENAI_API_KEYS
app.state.MODELS = {}
@@ -65,6 +72,21 @@ async def check_url(request: Request, call_next):
return response
@app.get("/config")
async def get_config(user=Depends(get_admin_user)):
return {"ENABLE_OPENAI_API": app.state.config.ENABLE_OPENAI_API}
class OpenAIConfigForm(BaseModel):
enable_openai_api: Optional[bool] = None
@app.post("/config/update")
async def update_config(form_data: OpenAIConfigForm, user=Depends(get_admin_user)):
app.state.config.ENABLE_OPENAI_API = form_data.enable_openai_api
return {"ENABLE_OPENAI_API": app.state.config.ENABLE_OPENAI_API}
class UrlsUpdateForm(BaseModel):
urls: List[str]
@@ -75,32 +97,32 @@ class KeysUpdateForm(BaseModel):
@app.get("/urls")
async def get_openai_urls(user=Depends(get_admin_user)):
return {"OPENAI_API_BASE_URLS": app.state.OPENAI_API_BASE_URLS}
return {"OPENAI_API_BASE_URLS": app.state.config.OPENAI_API_BASE_URLS}
@app.post("/urls/update")
async def update_openai_urls(form_data: UrlsUpdateForm, user=Depends(get_admin_user)):
await get_all_models()
app.state.OPENAI_API_BASE_URLS = form_data.urls
return {"OPENAI_API_BASE_URLS": app.state.OPENAI_API_BASE_URLS}
app.state.config.OPENAI_API_BASE_URLS = form_data.urls
return {"OPENAI_API_BASE_URLS": app.state.config.OPENAI_API_BASE_URLS}
@app.get("/keys")
async def get_openai_keys(user=Depends(get_admin_user)):
return {"OPENAI_API_KEYS": app.state.OPENAI_API_KEYS}
return {"OPENAI_API_KEYS": app.state.config.OPENAI_API_KEYS}
@app.post("/keys/update")
async def update_openai_key(form_data: KeysUpdateForm, user=Depends(get_admin_user)):
app.state.OPENAI_API_KEYS = form_data.keys
return {"OPENAI_API_KEYS": app.state.OPENAI_API_KEYS}
app.state.config.OPENAI_API_KEYS = form_data.keys
return {"OPENAI_API_KEYS": app.state.config.OPENAI_API_KEYS}
@app.post("/audio/speech")
async def speech(request: Request, user=Depends(get_verified_user)):
idx = None
try:
idx = app.state.OPENAI_API_BASE_URLS.index("https://api.openai.com/v1")
idx = app.state.config.OPENAI_API_BASE_URLS.index("https://api.openai.com/v1")
body = await request.body()
name = hashlib.sha256(body).hexdigest()
@@ -114,13 +136,15 @@ async def speech(request: Request, user=Depends(get_verified_user)):
return FileResponse(file_path)
headers = {}
headers["Authorization"] = f"Bearer {app.state.OPENAI_API_KEYS[idx]}"
headers["Authorization"] = f"Bearer {app.state.config.OPENAI_API_KEYS[idx]}"
headers["Content-Type"] = "application/json"
if "openrouter.ai" in app.state.config.OPENAI_API_BASE_URLS[idx]:
headers["HTTP-Referer"] = "https://openwebui.com/"
headers["X-Title"] = "Open WebUI"
r = None
try:
r = requests.post(
url=f"{app.state.OPENAI_API_BASE_URLS[idx]}/audio/speech",
url=f"{app.state.config.OPENAI_API_BASE_URLS[idx]}/audio/speech",
data=body,
headers=headers,
stream=True,
@@ -159,9 +183,10 @@ async def speech(request: Request, user=Depends(get_verified_user)):
async def fetch_url(url, key):
timeout = aiohttp.ClientTimeout(total=5)
try:
headers = {"Authorization": f"Bearer {key}"}
async with aiohttp.ClientSession() as session:
async with aiohttp.ClientSession(timeout=timeout) as session:
async with session.get(url, headers=headers) as response:
return await response.json()
except Exception as e:
@@ -170,16 +195,34 @@ async def fetch_url(url, key):
return None
async def cleanup_response(
response: Optional[aiohttp.ClientResponse],
session: Optional[aiohttp.ClientSession],
):
if response:
response.close()
if session:
await session.close()
def merge_models_lists(model_lists):
log.debug(f"merge_models_lists {model_lists}")
merged_list = []
for idx, models in enumerate(model_lists):
if models is not None and "error" not in models:
merged_list.extend(
[
{**model, "urlIdx": idx}
{
**model,
"name": model.get("name", model["id"]),
"owned_by": "openai",
"openai": model,
"urlIdx": idx,
}
for model in models
if "api.openai.com" not in app.state.OPENAI_API_BASE_URLS[idx]
if "api.openai.com"
not in app.state.config.OPENAI_API_BASE_URLS[idx]
or "gpt" in model["id"]
]
)
@@ -187,26 +230,55 @@ def merge_models_lists(model_lists):
return merged_list
async def get_all_models():
async def get_all_models(raw: bool = False):
log.info("get_all_models()")
if len(app.state.OPENAI_API_KEYS) == 1 and app.state.OPENAI_API_KEYS[0] == "":
if (
len(app.state.config.OPENAI_API_KEYS) == 1
and app.state.config.OPENAI_API_KEYS[0] == ""
) or not app.state.config.ENABLE_OPENAI_API:
models = {"data": []}
else:
# Check if API KEYS length is same than API URLS length
if len(app.state.config.OPENAI_API_KEYS) != len(
app.state.config.OPENAI_API_BASE_URLS
):
# if there are more keys than urls, remove the extra keys
if len(app.state.config.OPENAI_API_KEYS) > len(
app.state.config.OPENAI_API_BASE_URLS
):
app.state.config.OPENAI_API_KEYS = app.state.config.OPENAI_API_KEYS[
: len(app.state.config.OPENAI_API_BASE_URLS)
]
# if there are more urls than keys, add empty keys
else:
app.state.config.OPENAI_API_KEYS += [
""
for _ in range(
len(app.state.config.OPENAI_API_BASE_URLS)
- len(app.state.config.OPENAI_API_KEYS)
)
]
tasks = [
fetch_url(f"{url}/models", app.state.OPENAI_API_KEYS[idx])
for idx, url in enumerate(app.state.OPENAI_API_BASE_URLS)
fetch_url(f"{url}/models", app.state.config.OPENAI_API_KEYS[idx])
for idx, url in enumerate(app.state.config.OPENAI_API_BASE_URLS)
]
responses = await asyncio.gather(*tasks)
log.debug(f"get_all_models:responses() {responses}")
if raw:
return responses
models = {
"data": merge_models_lists(
list(
map(
lambda response: (
response["data"]
if response and "data" in response
else None
if (response and "data" in response)
else (response if isinstance(response, list) else None)
),
responses,
)
@@ -214,10 +286,10 @@ async def get_all_models():
)
}
log.info(f"models: {models}")
log.debug(f"models: {models}")
app.state.MODELS = {model["id"]: model for model in models["data"]}
return models
return models
@app.get("/models")
@@ -225,23 +297,28 @@ async def get_all_models():
async def get_models(url_idx: Optional[int] = None, user=Depends(get_current_user)):
if url_idx == None:
models = await get_all_models()
if app.state.MODEL_FILTER_ENABLED:
if app.state.config.ENABLE_MODEL_FILTER:
if user.role == "user":
models["data"] = list(
filter(
lambda model: model["id"] in app.state.MODEL_FILTER_LIST,
lambda model: model["id"] in app.state.config.MODEL_FILTER_LIST,
models["data"],
)
)
return models
return models
else:
url = app.state.OPENAI_API_BASE_URLS[url_idx]
url = app.state.config.OPENAI_API_BASE_URLS[url_idx]
key = app.state.config.OPENAI_API_KEYS[url_idx]
headers = {}
headers["Authorization"] = f"Bearer {key}"
headers["Content-Type"] = "application/json"
r = None
try:
r = requests.request(method="GET", url=f"{url}/models")
r = requests.request(method="GET", url=f"{url}/models", headers=headers)
r.raise_for_status()
response_data = r.json()
@@ -275,77 +352,154 @@ async def proxy(path: str, request: Request, user=Depends(get_verified_user)):
body = await request.body()
# TODO: Remove below after gpt-4-vision fix from Open AI
# Try to decode the body of the request from bytes to a UTF-8 string (Require add max_token to fix gpt-4-vision)
payload = None
try:
body = body.decode("utf-8")
body = json.loads(body)
if "chat/completions" in path:
body = body.decode("utf-8")
body = json.loads(body)
idx = app.state.MODELS[body.get("model")]["urlIdx"]
payload = {**body}
# Check if the model is "gpt-4-vision-preview" and set "max_tokens" to 4000
# This is a workaround until OpenAI fixes the issue with this model
if body.get("model") == "gpt-4-vision-preview":
if "max_tokens" not in body:
body["max_tokens"] = 4000
log.debug("Modified body_dict:", body)
model_id = body.get("model")
model_info = Models.get_model_by_id(model_id)
# Fix for ChatGPT calls failing because the num_ctx key is in body
if "num_ctx" in body:
# If 'num_ctx' is in the dictionary, delete it
# Leaving it there generates an error with the
# OpenAI API (Feb 2024)
del body["num_ctx"]
if model_info:
print(model_info)
if model_info.base_model_id:
payload["model"] = model_info.base_model_id
model_info.params = model_info.params.model_dump()
if model_info.params:
if model_info.params.get("temperature", None):
payload["temperature"] = int(
model_info.params.get("temperature")
)
if model_info.params.get("top_p", None):
payload["top_p"] = int(model_info.params.get("top_p", None))
if model_info.params.get("max_tokens", None):
payload["max_tokens"] = int(
model_info.params.get("max_tokens", None)
)
if model_info.params.get("frequency_penalty", None):
payload["frequency_penalty"] = int(
model_info.params.get("frequency_penalty", None)
)
if model_info.params.get("seed", None):
payload["seed"] = model_info.params.get("seed", None)
if model_info.params.get("stop", None):
payload["stop"] = (
[
bytes(stop, "utf-8").decode("unicode_escape")
for stop in model_info.params["stop"]
]
if model_info.params.get("stop", None)
else None
)
if model_info.params.get("system", None):
# Check if the payload already has a system message
# If not, add a system message to the payload
if payload.get("messages"):
for message in payload["messages"]:
if message.get("role") == "system":
message["content"] = (
model_info.params.get("system", None)
+ message["content"]
)
break
else:
payload["messages"].insert(
0,
{
"role": "system",
"content": model_info.params.get("system", None),
},
)
else:
pass
model = app.state.MODELS[payload.get("model")]
idx = model["urlIdx"]
if "pipeline" in model and model.get("pipeline"):
payload["user"] = {"name": user.name, "id": user.id}
# Check if the model is "gpt-4-vision-preview" and set "max_tokens" to 4000
# This is a workaround until OpenAI fixes the issue with this model
if payload.get("model") == "gpt-4-vision-preview":
if "max_tokens" not in payload:
payload["max_tokens"] = 4000
log.debug("Modified payload:", payload)
# Convert the modified body back to JSON
payload = json.dumps(payload)
# Convert the modified body back to JSON
body = json.dumps(body)
except json.JSONDecodeError as e:
log.error("Error loading request body into a dictionary:", e)
url = app.state.OPENAI_API_BASE_URLS[idx]
key = app.state.OPENAI_API_KEYS[idx]
print(payload)
url = app.state.config.OPENAI_API_BASE_URLS[idx]
key = app.state.config.OPENAI_API_KEYS[idx]
target_url = f"{url}/{path}"
if key == "":
raise HTTPException(status_code=401, detail=ERROR_MESSAGES.API_KEY_NOT_FOUND)
headers = {}
headers["Authorization"] = f"Bearer {key}"
headers["Content-Type"] = "application/json"
r = None
session = None
streaming = False
try:
r = requests.request(
session = aiohttp.ClientSession()
r = await session.request(
method=request.method,
url=target_url,
data=body,
data=payload if payload else body,
headers=headers,
stream=True,
)
r.raise_for_status()
# Check if response is SSE
if "text/event-stream" in r.headers.get("Content-Type", ""):
streaming = True
return StreamingResponse(
r.iter_content(chunk_size=8192),
status_code=r.status_code,
r.content,
status_code=r.status,
headers=dict(r.headers),
background=BackgroundTask(
cleanup_response, response=r, session=session
),
)
else:
response_data = r.json()
response_data = await r.json()
return response_data
except Exception as e:
log.exception(e)
error_detail = "Open WebUI: Server Connection Error"
if r is not None:
try:
res = r.json()
res = await r.json()
print(res)
if "error" in res:
error_detail = f"External: {res['error']['message'] if 'message' in res['error'] else res['error']}"
except:
error_detail = f"External: {e}"
raise HTTPException(
status_code=r.status_code if r else 500, detail=error_detail
)
raise HTTPException(status_code=r.status if r else 500, detail=error_detail)
finally:
if not streaming and session:
if r:
r.close()
await session.close()

View File

@@ -11,7 +11,7 @@ from fastapi.middleware.cors import CORSMiddleware
import os, shutil, logging, re
from pathlib import Path
from typing import List
from typing import List, Union, Sequence
from chromadb.utils.batch_utils import create_batches
@@ -28,9 +28,16 @@ from langchain_community.document_loaders import (
UnstructuredXMLLoader,
UnstructuredRSTLoader,
UnstructuredExcelLoader,
UnstructuredPowerPointLoader,
YoutubeLoader,
)
from langchain.text_splitter import RecursiveCharacterTextSplitter
import validators
import urllib.parse
import socket
from pydantic import BaseModel
from typing import Optional
import mimetypes
@@ -39,20 +46,29 @@ import json
import sentence_transformers
from apps.ollama.main import generate_ollama_embeddings, GenerateEmbeddingsForm
from apps.web.models.documents import (
from apps.webui.models.documents import (
Documents,
DocumentForm,
DocumentResponse,
)
from apps.rag.utils import (
query_embeddings_doc,
query_embeddings_collection,
generate_openai_embeddings,
get_model_path,
get_embedding_function,
query_doc,
query_doc_with_hybrid_search,
query_collection,
query_collection_with_hybrid_search,
)
from apps.rag.search.brave import search_brave
from apps.rag.search.google_pse import search_google_pse
from apps.rag.search.main import SearchResult
from apps.rag.search.searxng import search_searxng
from apps.rag.search.serper import search_serper
from apps.rag.search.serpstack import search_serpstack
from utils.misc import (
calculate_sha256,
calculate_sha256_string,
@@ -60,13 +76,24 @@ from utils.misc import (
extract_folders_after_data_docs,
)
from utils.utils import get_current_user, get_admin_user
from config import (
ENV,
SRC_LOG_LEVELS,
UPLOAD_DIR,
DOCS_DIR,
RAG_TOP_K,
RAG_RELEVANCE_THRESHOLD,
RAG_EMBEDDING_ENGINE,
RAG_EMBEDDING_MODEL,
RAG_EMBEDDING_MODEL_AUTO_UPDATE,
RAG_EMBEDDING_MODEL_TRUST_REMOTE_CODE,
ENABLE_RAG_HYBRID_SEARCH,
ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION,
RAG_RERANKING_MODEL,
PDF_EXTRACT_IMAGES,
RAG_RERANKING_MODEL_AUTO_UPDATE,
RAG_RERANKING_MODEL_TRUST_REMOTE_CODE,
RAG_OPENAI_API_BASE_URL,
RAG_OPENAI_API_KEY,
DEVICE_TYPE,
@@ -74,6 +101,20 @@ from config import (
CHUNK_SIZE,
CHUNK_OVERLAP,
RAG_TEMPLATE,
ENABLE_RAG_LOCAL_WEB_FETCH,
YOUTUBE_LOADER_LANGUAGE,
ENABLE_RAG_WEB_SEARCH,
RAG_WEB_SEARCH_ENGINE,
SEARXNG_QUERY_URL,
GOOGLE_PSE_API_KEY,
GOOGLE_PSE_ENGINE_ID,
BRAVE_SEARCH_API_KEY,
SERPSTACK_API_KEY,
SERPSTACK_HTTPS,
SERPER_API_KEY,
RAG_WEB_SEARCH_RESULT_COUNT,
RAG_WEB_SEARCH_CONCURRENT_REQUESTS,
AppConfig,
)
from constants import ERROR_MESSAGES
@@ -83,31 +124,99 @@ log.setLevel(SRC_LOG_LEVELS["RAG"])
app = FastAPI()
app.state.config = AppConfig()
app.state.TOP_K = 4
app.state.CHUNK_SIZE = CHUNK_SIZE
app.state.CHUNK_OVERLAP = CHUNK_OVERLAP
app.state.config.TOP_K = RAG_TOP_K
app.state.config.RELEVANCE_THRESHOLD = RAG_RELEVANCE_THRESHOLD
app.state.config.ENABLE_RAG_HYBRID_SEARCH = ENABLE_RAG_HYBRID_SEARCH
app.state.config.ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION = (
ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION
)
app.state.config.CHUNK_SIZE = CHUNK_SIZE
app.state.config.CHUNK_OVERLAP = CHUNK_OVERLAP
app.state.config.RAG_EMBEDDING_ENGINE = RAG_EMBEDDING_ENGINE
app.state.config.RAG_EMBEDDING_MODEL = RAG_EMBEDDING_MODEL
app.state.config.RAG_RERANKING_MODEL = RAG_RERANKING_MODEL
app.state.config.RAG_TEMPLATE = RAG_TEMPLATE
app.state.RAG_EMBEDDING_ENGINE = RAG_EMBEDDING_ENGINE
app.state.RAG_EMBEDDING_MODEL = RAG_EMBEDDING_MODEL
app.state.RAG_TEMPLATE = RAG_TEMPLATE
app.state.config.OPENAI_API_BASE_URL = RAG_OPENAI_API_BASE_URL
app.state.config.OPENAI_API_KEY = RAG_OPENAI_API_KEY
app.state.OPENAI_API_BASE_URL = RAG_OPENAI_API_BASE_URL
app.state.OPENAI_API_KEY = RAG_OPENAI_API_KEY
app.state.config.PDF_EXTRACT_IMAGES = PDF_EXTRACT_IMAGES
app.state.PDF_EXTRACT_IMAGES = False
if app.state.RAG_EMBEDDING_ENGINE == "":
app.state.sentence_transformer_ef = sentence_transformers.SentenceTransformer(
app.state.RAG_EMBEDDING_MODEL,
device=DEVICE_TYPE,
trust_remote_code=RAG_EMBEDDING_MODEL_TRUST_REMOTE_CODE,
)
app.state.config.YOUTUBE_LOADER_LANGUAGE = YOUTUBE_LOADER_LANGUAGE
app.state.YOUTUBE_LOADER_TRANSLATION = None
app.state.config.ENABLE_RAG_WEB_SEARCH = ENABLE_RAG_WEB_SEARCH
app.state.config.RAG_WEB_SEARCH_ENGINE = RAG_WEB_SEARCH_ENGINE
app.state.config.SEARXNG_QUERY_URL = SEARXNG_QUERY_URL
app.state.config.GOOGLE_PSE_API_KEY = GOOGLE_PSE_API_KEY
app.state.config.GOOGLE_PSE_ENGINE_ID = GOOGLE_PSE_ENGINE_ID
app.state.config.BRAVE_SEARCH_API_KEY = BRAVE_SEARCH_API_KEY
app.state.config.SERPSTACK_API_KEY = SERPSTACK_API_KEY
app.state.config.SERPSTACK_HTTPS = SERPSTACK_HTTPS
app.state.config.SERPER_API_KEY = SERPER_API_KEY
app.state.config.RAG_WEB_SEARCH_RESULT_COUNT = RAG_WEB_SEARCH_RESULT_COUNT
app.state.config.RAG_WEB_SEARCH_CONCURRENT_REQUESTS = RAG_WEB_SEARCH_CONCURRENT_REQUESTS
def update_embedding_model(
embedding_model: str,
update_model: bool = False,
):
if embedding_model and app.state.config.RAG_EMBEDDING_ENGINE == "":
app.state.sentence_transformer_ef = sentence_transformers.SentenceTransformer(
get_model_path(embedding_model, update_model),
device=DEVICE_TYPE,
trust_remote_code=RAG_EMBEDDING_MODEL_TRUST_REMOTE_CODE,
)
else:
app.state.sentence_transformer_ef = None
def update_reranking_model(
reranking_model: str,
update_model: bool = False,
):
if reranking_model:
app.state.sentence_transformer_rf = sentence_transformers.CrossEncoder(
get_model_path(reranking_model, update_model),
device=DEVICE_TYPE,
trust_remote_code=RAG_RERANKING_MODEL_TRUST_REMOTE_CODE,
)
else:
app.state.sentence_transformer_rf = None
update_embedding_model(
app.state.config.RAG_EMBEDDING_MODEL,
RAG_EMBEDDING_MODEL_AUTO_UPDATE,
)
update_reranking_model(
app.state.config.RAG_RERANKING_MODEL,
RAG_RERANKING_MODEL_AUTO_UPDATE,
)
app.state.EMBEDDING_FUNCTION = get_embedding_function(
app.state.config.RAG_EMBEDDING_ENGINE,
app.state.config.RAG_EMBEDDING_MODEL,
app.state.sentence_transformer_ef,
app.state.config.OPENAI_API_KEY,
app.state.config.OPENAI_API_BASE_URL,
)
origins = ["*"]
app.add_middleware(
CORSMiddleware,
allow_origins=origins,
@@ -121,19 +230,24 @@ class CollectionNameForm(BaseModel):
collection_name: Optional[str] = "test"
class StoreWebForm(CollectionNameForm):
class UrlForm(CollectionNameForm):
url: str
class SearchForm(CollectionNameForm):
query: str
@app.get("/")
async def get_status():
return {
"status": True,
"chunk_size": app.state.CHUNK_SIZE,
"chunk_overlap": app.state.CHUNK_OVERLAP,
"template": app.state.RAG_TEMPLATE,
"embedding_engine": app.state.RAG_EMBEDDING_ENGINE,
"embedding_model": app.state.RAG_EMBEDDING_MODEL,
"chunk_size": app.state.config.CHUNK_SIZE,
"chunk_overlap": app.state.config.CHUNK_OVERLAP,
"template": app.state.config.RAG_TEMPLATE,
"embedding_engine": app.state.config.RAG_EMBEDDING_ENGINE,
"embedding_model": app.state.config.RAG_EMBEDDING_MODEL,
"reranking_model": app.state.config.RAG_RERANKING_MODEL,
}
@@ -141,15 +255,23 @@ async def get_status():
async def get_embedding_config(user=Depends(get_admin_user)):
return {
"status": True,
"embedding_engine": app.state.RAG_EMBEDDING_ENGINE,
"embedding_model": app.state.RAG_EMBEDDING_MODEL,
"embedding_engine": app.state.config.RAG_EMBEDDING_ENGINE,
"embedding_model": app.state.config.RAG_EMBEDDING_MODEL,
"openai_config": {
"url": app.state.OPENAI_API_BASE_URL,
"key": app.state.OPENAI_API_KEY,
"url": app.state.config.OPENAI_API_BASE_URL,
"key": app.state.config.OPENAI_API_KEY,
},
}
@app.get("/reranking")
async def get_reraanking_config(user=Depends(get_admin_user)):
return {
"status": True,
"reranking_model": app.state.config.RAG_RERANKING_MODEL,
}
class OpenAIConfigForm(BaseModel):
url: str
key: str
@@ -166,37 +288,36 @@ async def update_embedding_config(
form_data: EmbeddingModelUpdateForm, user=Depends(get_admin_user)
):
log.info(
f"Updating embedding model: {app.state.RAG_EMBEDDING_MODEL} to {form_data.embedding_model}"
f"Updating embedding model: {app.state.config.RAG_EMBEDDING_MODEL} to {form_data.embedding_model}"
)
try:
app.state.RAG_EMBEDDING_ENGINE = form_data.embedding_engine
if app.state.RAG_EMBEDDING_ENGINE in ["ollama", "openai"]:
app.state.RAG_EMBEDDING_MODEL = form_data.embedding_model
app.state.sentence_transformer_ef = None
app.state.config.RAG_EMBEDDING_ENGINE = form_data.embedding_engine
app.state.config.RAG_EMBEDDING_MODEL = form_data.embedding_model
if app.state.config.RAG_EMBEDDING_ENGINE in ["ollama", "openai"]:
if form_data.openai_config != None:
app.state.OPENAI_API_BASE_URL = form_data.openai_config.url
app.state.OPENAI_API_KEY = form_data.openai_config.key
else:
sentence_transformer_ef = sentence_transformers.SentenceTransformer(
app.state.RAG_EMBEDDING_MODEL,
device=DEVICE_TYPE,
trust_remote_code=RAG_EMBEDDING_MODEL_TRUST_REMOTE_CODE,
)
app.state.RAG_EMBEDDING_MODEL = form_data.embedding_model
app.state.sentence_transformer_ef = sentence_transformer_ef
app.state.config.OPENAI_API_BASE_URL = form_data.openai_config.url
app.state.config.OPENAI_API_KEY = form_data.openai_config.key
update_embedding_model(app.state.config.RAG_EMBEDDING_MODEL)
app.state.EMBEDDING_FUNCTION = get_embedding_function(
app.state.config.RAG_EMBEDDING_ENGINE,
app.state.config.RAG_EMBEDDING_MODEL,
app.state.sentence_transformer_ef,
app.state.config.OPENAI_API_KEY,
app.state.config.OPENAI_API_BASE_URL,
)
return {
"status": True,
"embedding_engine": app.state.RAG_EMBEDDING_ENGINE,
"embedding_model": app.state.RAG_EMBEDDING_MODEL,
"embedding_engine": app.state.config.RAG_EMBEDDING_ENGINE,
"embedding_model": app.state.config.RAG_EMBEDDING_MODEL,
"openai_config": {
"url": app.state.OPENAI_API_BASE_URL,
"key": app.state.OPENAI_API_KEY,
"url": app.state.config.OPENAI_API_BASE_URL,
"key": app.state.config.OPENAI_API_KEY,
},
}
except Exception as e:
log.exception(f"Problem updating embedding model: {e}")
raise HTTPException(
@@ -205,14 +326,62 @@ async def update_embedding_config(
)
class RerankingModelUpdateForm(BaseModel):
reranking_model: str
@app.post("/reranking/update")
async def update_reranking_config(
form_data: RerankingModelUpdateForm, user=Depends(get_admin_user)
):
log.info(
f"Updating reranking model: {app.state.config.RAG_RERANKING_MODEL} to {form_data.reranking_model}"
)
try:
app.state.config.RAG_RERANKING_MODEL = form_data.reranking_model
update_reranking_model(app.state.config.RAG_RERANKING_MODEL), True
return {
"status": True,
"reranking_model": app.state.config.RAG_RERANKING_MODEL,
}
except Exception as e:
log.exception(f"Problem updating reranking model: {e}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=ERROR_MESSAGES.DEFAULT(e),
)
@app.get("/config")
async def get_rag_config(user=Depends(get_admin_user)):
return {
"status": True,
"pdf_extract_images": app.state.PDF_EXTRACT_IMAGES,
"pdf_extract_images": app.state.config.PDF_EXTRACT_IMAGES,
"chunk": {
"chunk_size": app.state.CHUNK_SIZE,
"chunk_overlap": app.state.CHUNK_OVERLAP,
"chunk_size": app.state.config.CHUNK_SIZE,
"chunk_overlap": app.state.config.CHUNK_OVERLAP,
},
"youtube": {
"language": app.state.config.YOUTUBE_LOADER_LANGUAGE,
"translation": app.state.YOUTUBE_LOADER_TRANSLATION,
},
"web": {
"ssl_verification": app.state.config.ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION,
"search": {
"enabled": app.state.config.ENABLE_RAG_WEB_SEARCH,
"engine": app.state.config.RAG_WEB_SEARCH_ENGINE,
"searxng_query_url": app.state.config.SEARXNG_QUERY_URL,
"google_pse_api_key": app.state.config.GOOGLE_PSE_API_KEY,
"google_pse_engine_id": app.state.config.GOOGLE_PSE_ENGINE_ID,
"brave_search_api_key": app.state.config.BRAVE_SEARCH_API_KEY,
"serpstack_api_key": app.state.config.SERPSTACK_API_KEY,
"serpstack_https": app.state.config.SERPSTACK_HTTPS,
"serper_api_key": app.state.config.SERPER_API_KEY,
"result_count": app.state.config.RAG_WEB_SEARCH_RESULT_COUNT,
"concurrent_requests": app.state.config.RAG_WEB_SEARCH_CONCURRENT_REQUESTS,
},
},
}
@@ -222,23 +391,102 @@ class ChunkParamUpdateForm(BaseModel):
chunk_overlap: int
class YoutubeLoaderConfig(BaseModel):
language: List[str]
translation: Optional[str] = None
class WebSearchConfig(BaseModel):
enabled: bool
engine: Optional[str] = None
searxng_query_url: Optional[str] = None
google_pse_api_key: Optional[str] = None
google_pse_engine_id: Optional[str] = None
brave_search_api_key: Optional[str] = None
serpstack_api_key: Optional[str] = None
serpstack_https: Optional[bool] = None
serper_api_key: Optional[str] = None
result_count: Optional[int] = None
concurrent_requests: Optional[int] = None
class WebConfig(BaseModel):
search: WebSearchConfig
web_loader_ssl_verification: Optional[bool] = None
class ConfigUpdateForm(BaseModel):
pdf_extract_images: bool
chunk: ChunkParamUpdateForm
pdf_extract_images: Optional[bool] = None
chunk: Optional[ChunkParamUpdateForm] = None
youtube: Optional[YoutubeLoaderConfig] = None
web: Optional[WebConfig] = None
@app.post("/config/update")
async def update_rag_config(form_data: ConfigUpdateForm, user=Depends(get_admin_user)):
app.state.PDF_EXTRACT_IMAGES = form_data.pdf_extract_images
app.state.CHUNK_SIZE = form_data.chunk.chunk_size
app.state.CHUNK_OVERLAP = form_data.chunk.chunk_overlap
app.state.config.PDF_EXTRACT_IMAGES = (
form_data.pdf_extract_images
if form_data.pdf_extract_images is not None
else app.state.config.PDF_EXTRACT_IMAGES
)
if form_data.chunk is not None:
app.state.config.CHUNK_SIZE = form_data.chunk.chunk_size
app.state.config.CHUNK_OVERLAP = form_data.chunk.chunk_overlap
if form_data.youtube is not None:
app.state.config.YOUTUBE_LOADER_LANGUAGE = form_data.youtube.language
app.state.YOUTUBE_LOADER_TRANSLATION = form_data.youtube.translation
if form_data.web is not None:
app.state.config.ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION = (
form_data.web.web_loader_ssl_verification
)
app.state.config.ENABLE_RAG_WEB_SEARCH = form_data.web.search.enabled
app.state.config.RAG_WEB_SEARCH_ENGINE = form_data.web.search.engine
app.state.config.SEARXNG_QUERY_URL = form_data.web.search.searxng_query_url
app.state.config.GOOGLE_PSE_API_KEY = form_data.web.search.google_pse_api_key
app.state.config.GOOGLE_PSE_ENGINE_ID = (
form_data.web.search.google_pse_engine_id
)
app.state.config.BRAVE_SEARCH_API_KEY = (
form_data.web.search.brave_search_api_key
)
app.state.config.SERPSTACK_API_KEY = form_data.web.search.serpstack_api_key
app.state.config.SERPSTACK_HTTPS = form_data.web.search.serpstack_https
app.state.config.SERPER_API_KEY = form_data.web.search.serper_api_key
app.state.config.RAG_WEB_SEARCH_RESULT_COUNT = form_data.web.search.result_count
app.state.config.RAG_WEB_SEARCH_CONCURRENT_REQUESTS = (
form_data.web.search.concurrent_requests
)
return {
"status": True,
"pdf_extract_images": app.state.PDF_EXTRACT_IMAGES,
"pdf_extract_images": app.state.config.PDF_EXTRACT_IMAGES,
"chunk": {
"chunk_size": app.state.CHUNK_SIZE,
"chunk_overlap": app.state.CHUNK_OVERLAP,
"chunk_size": app.state.config.CHUNK_SIZE,
"chunk_overlap": app.state.config.CHUNK_OVERLAP,
},
"youtube": {
"language": app.state.config.YOUTUBE_LOADER_LANGUAGE,
"translation": app.state.YOUTUBE_LOADER_TRANSLATION,
},
"web": {
"ssl_verification": app.state.config.ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION,
"search": {
"enabled": app.state.config.ENABLE_RAG_WEB_SEARCH,
"engine": app.state.config.RAG_WEB_SEARCH_ENGINE,
"searxng_query_url": app.state.config.SEARXNG_QUERY_URL,
"google_pse_api_key": app.state.config.GOOGLE_PSE_API_KEY,
"google_pse_engine_id": app.state.config.GOOGLE_PSE_ENGINE_ID,
"brave_search_api_key": app.state.config.BRAVE_SEARCH_API_KEY,
"serpstack_api_key": app.state.config.SERPSTACK_API_KEY,
"serpstack_https": app.state.config.SERPSTACK_HTTPS,
"serper_api_key": app.state.config.SERPER_API_KEY,
"result_count": app.state.config.RAG_WEB_SEARCH_RESULT_COUNT,
"concurrent_requests": app.state.config.RAG_WEB_SEARCH_CONCURRENT_REQUESTS,
},
},
}
@@ -247,7 +495,7 @@ async def update_rag_config(form_data: ConfigUpdateForm, user=Depends(get_admin_
async def get_rag_template(user=Depends(get_current_user)):
return {
"status": True,
"template": app.state.RAG_TEMPLATE,
"template": app.state.config.RAG_TEMPLATE,
}
@@ -255,29 +503,47 @@ async def get_rag_template(user=Depends(get_current_user)):
async def get_query_settings(user=Depends(get_admin_user)):
return {
"status": True,
"template": app.state.RAG_TEMPLATE,
"k": app.state.TOP_K,
"template": app.state.config.RAG_TEMPLATE,
"k": app.state.config.TOP_K,
"r": app.state.config.RELEVANCE_THRESHOLD,
"hybrid": app.state.config.ENABLE_RAG_HYBRID_SEARCH,
}
class QuerySettingsForm(BaseModel):
k: Optional[int] = None
r: Optional[float] = None
template: Optional[str] = None
hybrid: Optional[bool] = None
@app.post("/query/settings/update")
async def update_query_settings(
form_data: QuerySettingsForm, user=Depends(get_admin_user)
):
app.state.RAG_TEMPLATE = form_data.template if form_data.template else RAG_TEMPLATE
app.state.TOP_K = form_data.k if form_data.k else 4
return {"status": True, "template": app.state.RAG_TEMPLATE}
app.state.config.RAG_TEMPLATE = (
form_data.template if form_data.template else RAG_TEMPLATE
)
app.state.config.TOP_K = form_data.k if form_data.k else 4
app.state.config.RELEVANCE_THRESHOLD = form_data.r if form_data.r else 0.0
app.state.config.ENABLE_RAG_HYBRID_SEARCH = (
form_data.hybrid if form_data.hybrid else False
)
return {
"status": True,
"template": app.state.config.RAG_TEMPLATE,
"k": app.state.config.TOP_K,
"r": app.state.config.RELEVANCE_THRESHOLD,
"hybrid": app.state.config.ENABLE_RAG_HYBRID_SEARCH,
}
class QueryDocForm(BaseModel):
collection_name: str
query: str
k: Optional[int] = None
r: Optional[float] = None
hybrid: Optional[bool] = None
@app.post("/query/doc")
@@ -286,34 +552,24 @@ def query_doc_handler(
user=Depends(get_current_user),
):
try:
if app.state.RAG_EMBEDDING_ENGINE == "":
query_embeddings = app.state.sentence_transformer_ef.encode(
form_data.query
).tolist()
elif app.state.RAG_EMBEDDING_ENGINE == "ollama":
query_embeddings = generate_ollama_embeddings(
GenerateEmbeddingsForm(
**{
"model": app.state.RAG_EMBEDDING_MODEL,
"prompt": form_data.query,
}
)
if app.state.config.ENABLE_RAG_HYBRID_SEARCH:
return query_doc_with_hybrid_search(
collection_name=form_data.collection_name,
query=form_data.query,
embedding_function=app.state.EMBEDDING_FUNCTION,
k=form_data.k if form_data.k else app.state.config.TOP_K,
reranking_function=app.state.sentence_transformer_rf,
r=(
form_data.r if form_data.r else app.state.config.RELEVANCE_THRESHOLD
),
)
elif app.state.RAG_EMBEDDING_ENGINE == "openai":
query_embeddings = generate_openai_embeddings(
model=app.state.RAG_EMBEDDING_MODEL,
text=form_data.query,
key=app.state.OPENAI_API_KEY,
url=app.state.OPENAI_API_BASE_URL,
else:
return query_doc(
collection_name=form_data.collection_name,
query=form_data.query,
embedding_function=app.state.EMBEDDING_FUNCTION,
k=form_data.k if form_data.k else app.state.config.TOP_K,
)
return query_embeddings_doc(
collection_name=form_data.collection_name,
query=form_data.query,
query_embeddings=query_embeddings,
k=form_data.k if form_data.k else app.state.TOP_K,
)
except Exception as e:
log.exception(e)
raise HTTPException(
@@ -326,6 +582,8 @@ class QueryCollectionsForm(BaseModel):
collection_names: List[str]
query: str
k: Optional[int] = None
r: Optional[float] = None
hybrid: Optional[bool] = None
@app.post("/query/collection")
@@ -334,33 +592,25 @@ def query_collection_handler(
user=Depends(get_current_user),
):
try:
if app.state.RAG_EMBEDDING_ENGINE == "":
query_embeddings = app.state.sentence_transformer_ef.encode(
form_data.query
).tolist()
elif app.state.RAG_EMBEDDING_ENGINE == "ollama":
query_embeddings = generate_ollama_embeddings(
GenerateEmbeddingsForm(
**{
"model": app.state.RAG_EMBEDDING_MODEL,
"prompt": form_data.query,
}
)
if app.state.config.ENABLE_RAG_HYBRID_SEARCH:
return query_collection_with_hybrid_search(
collection_names=form_data.collection_names,
query=form_data.query,
embedding_function=app.state.EMBEDDING_FUNCTION,
k=form_data.k if form_data.k else app.state.config.TOP_K,
reranking_function=app.state.sentence_transformer_rf,
r=(
form_data.r if form_data.r else app.state.config.RELEVANCE_THRESHOLD
),
)
elif app.state.RAG_EMBEDDING_ENGINE == "openai":
query_embeddings = generate_openai_embeddings(
model=app.state.RAG_EMBEDDING_MODEL,
text=form_data.query,
key=app.state.OPENAI_API_KEY,
url=app.state.OPENAI_API_BASE_URL,
else:
return query_collection(
collection_names=form_data.collection_names,
query=form_data.query,
embedding_function=app.state.EMBEDDING_FUNCTION,
k=form_data.k if form_data.k else app.state.config.TOP_K,
)
return query_embeddings_collection(
collection_names=form_data.collection_names,
query_embeddings=query_embeddings,
k=form_data.k if form_data.k else app.state.TOP_K,
)
except Exception as e:
log.exception(e)
raise HTTPException(
@@ -369,11 +619,15 @@ def query_collection_handler(
)
@app.post("/web")
def store_web(form_data: StoreWebForm, user=Depends(get_current_user)):
# "https://www.gutenberg.org/files/1727/1727-h/1727-h.htm"
@app.post("/youtube")
def store_youtube_video(form_data: UrlForm, user=Depends(get_current_user)):
try:
loader = WebBaseLoader(form_data.url)
loader = YoutubeLoader.from_youtube_url(
form_data.url,
add_video_info=True,
language=app.state.config.YOUTUBE_LOADER_LANGUAGE,
translation=app.state.YOUTUBE_LOADER_TRANSLATION,
)
data = loader.load()
collection_name = form_data.collection_name
@@ -394,11 +648,194 @@ def store_web(form_data: StoreWebForm, user=Depends(get_current_user)):
)
@app.post("/web")
def store_web(form_data: UrlForm, user=Depends(get_current_user)):
# "https://www.gutenberg.org/files/1727/1727-h/1727-h.htm"
try:
loader = get_web_loader(
form_data.url,
verify_ssl=app.state.config.ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION,
)
data = loader.load()
collection_name = form_data.collection_name
if collection_name == "":
collection_name = calculate_sha256_string(form_data.url)[:63]
store_data_in_vector_db(data, collection_name, overwrite=True)
return {
"status": True,
"collection_name": collection_name,
"filename": form_data.url,
}
except Exception as e:
log.exception(e)
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.DEFAULT(e),
)
def get_web_loader(url: Union[str, Sequence[str]], verify_ssl: bool = True):
# Check if the URL is valid
if not validate_url(url):
raise ValueError(ERROR_MESSAGES.INVALID_URL)
return WebBaseLoader(
url,
verify_ssl=verify_ssl,
requests_per_second=RAG_WEB_SEARCH_CONCURRENT_REQUESTS,
continue_on_failure=True,
)
def validate_url(url: Union[str, Sequence[str]]):
if isinstance(url, str):
if isinstance(validators.url(url), validators.ValidationError):
raise ValueError(ERROR_MESSAGES.INVALID_URL)
if not ENABLE_RAG_LOCAL_WEB_FETCH:
# Local web fetch is disabled, filter out any URLs that resolve to private IP addresses
parsed_url = urllib.parse.urlparse(url)
# Get IPv4 and IPv6 addresses
ipv4_addresses, ipv6_addresses = resolve_hostname(parsed_url.hostname)
# Check if any of the resolved addresses are private
# This is technically still vulnerable to DNS rebinding attacks, as we don't control WebBaseLoader
for ip in ipv4_addresses:
if validators.ipv4(ip, private=True):
raise ValueError(ERROR_MESSAGES.INVALID_URL)
for ip in ipv6_addresses:
if validators.ipv6(ip, private=True):
raise ValueError(ERROR_MESSAGES.INVALID_URL)
return True
elif isinstance(url, Sequence):
return all(validate_url(u) for u in url)
else:
return False
def resolve_hostname(hostname):
# Get address information
addr_info = socket.getaddrinfo(hostname, None)
# Extract IP addresses from address information
ipv4_addresses = [info[4][0] for info in addr_info if info[0] == socket.AF_INET]
ipv6_addresses = [info[4][0] for info in addr_info if info[0] == socket.AF_INET6]
return ipv4_addresses, ipv6_addresses
def search_web(engine: str, query: str) -> list[SearchResult]:
"""Search the web using a search engine and return the results as a list of SearchResult objects.
Will look for a search engine API key in environment variables in the following order:
- SEARXNG_QUERY_URL
- GOOGLE_PSE_API_KEY + GOOGLE_PSE_ENGINE_ID
- BRAVE_SEARCH_API_KEY
- SERPSTACK_API_KEY
- SERPER_API_KEY
Args:
query (str): The query to search for
"""
# TODO: add playwright to search the web
if engine == "searxng":
if app.state.config.SEARXNG_QUERY_URL:
return search_searxng(
app.state.config.SEARXNG_QUERY_URL,
query,
app.state.config.RAG_WEB_SEARCH_RESULT_COUNT,
)
else:
raise Exception("No SEARXNG_QUERY_URL found in environment variables")
elif engine == "google_pse":
if (
app.state.config.GOOGLE_PSE_API_KEY
and app.state.config.GOOGLE_PSE_ENGINE_ID
):
return search_google_pse(
app.state.config.GOOGLE_PSE_API_KEY,
app.state.config.GOOGLE_PSE_ENGINE_ID,
query,
app.state.config.RAG_WEB_SEARCH_RESULT_COUNT,
)
else:
raise Exception(
"No GOOGLE_PSE_API_KEY or GOOGLE_PSE_ENGINE_ID found in environment variables"
)
elif engine == "brave":
if app.state.config.BRAVE_SEARCH_API_KEY:
return search_brave(
app.state.config.BRAVE_SEARCH_API_KEY,
query,
app.state.config.RAG_WEB_SEARCH_RESULT_COUNT,
)
else:
raise Exception("No BRAVE_SEARCH_API_KEY found in environment variables")
elif engine == "serpstack":
if app.state.config.SERPSTACK_API_KEY:
return search_serpstack(
app.state.config.SERPSTACK_API_KEY,
query,
app.state.config.RAG_WEB_SEARCH_RESULT_COUNT,
https_enabled=app.state.config.SERPSTACK_HTTPS,
)
else:
raise Exception("No SERPSTACK_API_KEY found in environment variables")
elif engine == "serper":
if app.state.config.SERPER_API_KEY:
return search_serper(
app.state.config.SERPER_API_KEY,
query,
app.state.config.RAG_WEB_SEARCH_RESULT_COUNT,
)
else:
raise Exception("No SERPER_API_KEY found in environment variables")
else:
raise Exception("No search engine API key found in environment variables")
@app.post("/web/search")
def store_web_search(form_data: SearchForm, user=Depends(get_current_user)):
try:
web_results = search_web(
app.state.config.RAG_WEB_SEARCH_ENGINE, form_data.query
)
except Exception as e:
log.exception(e)
print(e)
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.WEB_SEARCH_ERROR(e),
)
try:
urls = [result.link for result in web_results]
loader = get_web_loader(urls)
data = loader.load()
collection_name = form_data.collection_name
if collection_name == "":
collection_name = calculate_sha256_string(form_data.query)[:63]
store_data_in_vector_db(data, collection_name, overwrite=True)
return {
"status": True,
"collection_name": collection_name,
"filenames": urls,
}
except Exception as e:
log.exception(e)
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.DEFAULT(e),
)
def store_data_in_vector_db(data, collection_name, overwrite: bool = False) -> bool:
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=app.state.CHUNK_SIZE,
chunk_overlap=app.state.CHUNK_OVERLAP,
chunk_size=app.state.config.CHUNK_SIZE,
chunk_overlap=app.state.config.CHUNK_OVERLAP,
add_start_index=True,
)
@@ -415,8 +852,8 @@ def store_text_in_vector_db(
text, metadata, collection_name, overwrite: bool = False
) -> bool:
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=app.state.CHUNK_SIZE,
chunk_overlap=app.state.CHUNK_OVERLAP,
chunk_size=app.state.config.CHUNK_SIZE,
chunk_overlap=app.state.config.CHUNK_OVERLAP,
add_start_index=True,
)
docs = text_splitter.create_documents([text], metadatas=[metadata])
@@ -427,8 +864,6 @@ def store_docs_in_vector_db(docs, collection_name, overwrite: bool = False) -> b
log.info(f"store_docs_in_vector_db {docs} {collection_name}")
texts = [doc.page_content for doc in docs]
texts = list(map(lambda x: x.replace("\n", " "), texts))
metadatas = [doc.metadata for doc in docs]
try:
@@ -440,31 +875,20 @@ def store_docs_in_vector_db(docs, collection_name, overwrite: bool = False) -> b
collection = CHROMA_CLIENT.create_collection(name=collection_name)
if app.state.RAG_EMBEDDING_ENGINE == "":
embeddings = app.state.sentence_transformer_ef.encode(texts).tolist()
elif app.state.RAG_EMBEDDING_ENGINE == "ollama":
embeddings = [
generate_ollama_embeddings(
GenerateEmbeddingsForm(
**{"model": app.state.RAG_EMBEDDING_MODEL, "prompt": text}
)
)
for text in texts
]
elif app.state.RAG_EMBEDDING_ENGINE == "openai":
embeddings = [
generate_openai_embeddings(
model=app.state.RAG_EMBEDDING_MODEL,
text=text,
key=app.state.OPENAI_API_KEY,
url=app.state.OPENAI_API_BASE_URL,
)
for text in texts
]
embedding_func = get_embedding_function(
app.state.config.RAG_EMBEDDING_ENGINE,
app.state.config.RAG_EMBEDDING_MODEL,
app.state.sentence_transformer_ef,
app.state.config.OPENAI_API_KEY,
app.state.config.OPENAI_API_BASE_URL,
)
embedding_texts = list(map(lambda x: x.replace("\n", " "), texts))
embeddings = embedding_func(embedding_texts)
for batch in create_batches(
api=CHROMA_CLIENT,
ids=[str(uuid.uuid1()) for _ in texts],
ids=[str(uuid.uuid4()) for _ in texts],
metadatas=metadatas,
embeddings=embeddings,
documents=texts,
@@ -530,7 +954,9 @@ def get_loader(filename: str, file_content_type: str, file_path: str):
]
if file_ext == "pdf":
loader = PyPDFLoader(file_path, extract_images=app.state.PDF_EXTRACT_IMAGES)
loader = PyPDFLoader(
file_path, extract_images=app.state.config.PDF_EXTRACT_IMAGES
)
elif file_ext == "csv":
loader = CSVLoader(file_path)
elif file_ext == "rst":
@@ -554,6 +980,11 @@ def get_loader(filename: str, file_content_type: str, file_path: str):
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
] or file_ext in ["xls", "xlsx"]:
loader = UnstructuredExcelLoader(file_path)
elif file_content_type in [
"application/vnd.ms-powerpoint",
"application/vnd.openxmlformats-officedocument.presentationml.presentation",
] or file_ext in ["ppt", "pptx"]:
loader = UnstructuredPowerPointLoader(file_path)
elif file_ext in known_source_ext or (
file_content_type and file_content_type.find("text/") >= 0
):
@@ -738,3 +1169,14 @@ def reset(user=Depends(get_admin_user)) -> bool:
log.exception(e)
return True
if ENV == "dev":
@app.get("/ef")
async def get_embeddings():
return {"result": app.state.EMBEDDING_FUNCTION("hello world")}
@app.get("/ef/{text}")
async def get_embeddings_text(text: str):
return {"result": app.state.EMBEDDING_FUNCTION(text)}

View File

@@ -0,0 +1,37 @@
import logging
import requests
from apps.rag.search.main import SearchResult
from config import SRC_LOG_LEVELS
log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["RAG"])
def search_brave(api_key: str, query: str, count: int) -> list[SearchResult]:
"""Search using Brave's Search API and return the results as a list of SearchResult objects.
Args:
api_key (str): A Brave Search API key
query (str): The query to search for
"""
url = "https://api.search.brave.com/res/v1/web/search"
headers = {
"Accept": "application/json",
"Accept-Encoding": "gzip",
"X-Subscription-Token": api_key,
}
params = {"q": query, "count": count}
response = requests.get(url, headers=headers, params=params)
response.raise_for_status()
json_response = response.json()
results = json_response.get("web", {}).get("results", [])
return [
SearchResult(
link=result["url"], title=result.get("title"), snippet=result.get("snippet")
)
for result in results[:count]
]

View File

@@ -0,0 +1,45 @@
import json
import logging
import requests
from apps.rag.search.main import SearchResult
from config import SRC_LOG_LEVELS
log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["RAG"])
def search_google_pse(
api_key: str, search_engine_id: str, query: str, count: int
) -> list[SearchResult]:
"""Search using Google's Programmable Search Engine API and return the results as a list of SearchResult objects.
Args:
api_key (str): A Programmable Search Engine API key
search_engine_id (str): A Programmable Search Engine ID
query (str): The query to search for
"""
url = "https://www.googleapis.com/customsearch/v1"
headers = {"Content-Type": "application/json"}
params = {
"cx": search_engine_id,
"q": query,
"key": api_key,
"num": count,
}
response = requests.request("GET", url, headers=headers, params=params)
response.raise_for_status()
json_response = response.json()
results = json_response.get("items", [])
return [
SearchResult(
link=result["link"],
title=result.get("title"),
snippet=result.get("snippet"),
)
for result in results
]

View File

@@ -0,0 +1,9 @@
from typing import Optional
from pydantic import BaseModel
class SearchResult(BaseModel):
link: str
title: Optional[str]
snippet: Optional[str]

View File

@@ -0,0 +1,44 @@
import logging
import requests
from apps.rag.search.main import SearchResult
from config import SRC_LOG_LEVELS
log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["RAG"])
def search_searxng(query_url: str, query: str, count: int) -> list[SearchResult]:
"""Search a SearXNG instance for a query and return the results as a list of SearchResult objects.
Args:
query_url (str): The URL of the SearXNG instance to search. Must contain "<query>" as a placeholder
query (str): The query to search for
"""
url = query_url.replace("<query>", query)
if "&format=json" not in url:
url += "&format=json"
log.debug(f"searching {url}")
r = requests.get(
url,
headers={
"User-Agent": "Open WebUI (https://github.com/open-webui/open-webui) RAG Bot",
"Accept": "text/html",
"Accept-Encoding": "gzip, deflate",
"Accept-Language": "en-US,en;q=0.5",
"Connection": "keep-alive",
},
)
r.raise_for_status()
json_response = r.json()
results = json_response.get("results", [])
sorted_results = sorted(results, key=lambda x: x.get("score", 0), reverse=True)
return [
SearchResult(
link=result["url"], title=result.get("title"), snippet=result.get("content")
)
for result in sorted_results[:count]
]

View File

@@ -0,0 +1,39 @@
import json
import logging
import requests
from apps.rag.search.main import SearchResult
from config import SRC_LOG_LEVELS
log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["RAG"])
def search_serper(api_key: str, query: str, count: int) -> list[SearchResult]:
"""Search using serper.dev's API and return the results as a list of SearchResult objects.
Args:
api_key (str): A serper.dev API key
query (str): The query to search for
"""
url = "https://google.serper.dev/search"
payload = json.dumps({"q": query})
headers = {"X-API-KEY": api_key, "Content-Type": "application/json"}
response = requests.request("POST", url, headers=headers, data=payload)
response.raise_for_status()
json_response = response.json()
results = sorted(
json_response.get("organic", []), key=lambda x: x.get("position", 0)
)
return [
SearchResult(
link=result["link"],
title=result.get("title"),
snippet=result.get("description"),
)
for result in results[:count]
]

View File

@@ -0,0 +1,43 @@
import json
import logging
import requests
from apps.rag.search.main import SearchResult
from config import SRC_LOG_LEVELS
log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["RAG"])
def search_serpstack(
api_key: str, query: str, count: int, https_enabled: bool = True
) -> list[SearchResult]:
"""Search using serpstack.com's and return the results as a list of SearchResult objects.
Args:
api_key (str): A serpstack.com API key
query (str): The query to search for
https_enabled (bool): Whether to use HTTPS or HTTP for the API request
"""
url = f"{'https' if https_enabled else 'http'}://api.serpstack.com/search"
headers = {"Content-Type": "application/json"}
params = {
"access_key": api_key,
"query": query,
}
response = requests.request("POST", url, headers=headers, params=params)
response.raise_for_status()
json_response = response.json()
results = sorted(
json_response.get("organic_results", []), key=lambda x: x.get("position", 0)
)
return [
SearchResult(
link=result["url"], title=result.get("title"), snippet=result.get("snippet")
)
for result in results[:count]
]

View File

@@ -0,0 +1,998 @@
{
"query": {
"original": "python",
"show_strict_warning": false,
"is_navigational": true,
"is_news_breaking": false,
"spellcheck_off": true,
"country": "us",
"bad_results": false,
"should_fallback": false,
"postal_code": "",
"city": "",
"header_country": "",
"more_results_available": true,
"state": ""
},
"mixed": {
"type": "mixed",
"main": [
{
"type": "web",
"index": 0,
"all": false
},
{
"type": "web",
"index": 1,
"all": false
},
{
"type": "news",
"all": true
},
{
"type": "web",
"index": 2,
"all": false
},
{
"type": "videos",
"all": true
},
{
"type": "web",
"index": 3,
"all": false
},
{
"type": "web",
"index": 4,
"all": false
},
{
"type": "web",
"index": 5,
"all": false
},
{
"type": "web",
"index": 6,
"all": false
},
{
"type": "web",
"index": 7,
"all": false
},
{
"type": "web",
"index": 8,
"all": false
},
{
"type": "web",
"index": 9,
"all": false
},
{
"type": "web",
"index": 10,
"all": false
},
{
"type": "web",
"index": 11,
"all": false
},
{
"type": "web",
"index": 12,
"all": false
},
{
"type": "web",
"index": 13,
"all": false
},
{
"type": "web",
"index": 14,
"all": false
},
{
"type": "web",
"index": 15,
"all": false
},
{
"type": "web",
"index": 16,
"all": false
},
{
"type": "web",
"index": 17,
"all": false
},
{
"type": "web",
"index": 18,
"all": false
},
{
"type": "web",
"index": 19,
"all": false
}
],
"top": [],
"side": []
},
"news": {
"type": "news",
"results": [
{
"title": "Google lays off staff from Flutter, Dart and Python teams weeks before its developer conference | TechCrunch",
"url": "https://techcrunch.com/2024/05/01/google-lays-off-staff-from-flutter-dart-python-weeks-before-its-developer-conference/",
"is_source_local": false,
"is_source_both": false,
"description": "Google told TechCrunch that Flutter will have new updates to share at I/O this year.",
"page_age": "2024-05-02T17:40:05",
"family_friendly": true,
"meta_url": {
"scheme": "https",
"netloc": "techcrunch.com",
"hostname": "techcrunch.com",
"favicon": "https://imgs.search.brave.com/N6VSEVahheQOb7lqfb47dhUOB4XD-6sfQOP94sCe3Oo/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvZGI5Njk0Yzlk/YWM3ZWMwZjg1MTM1/NmIyMWEyNzBjZDZj/ZDQyNmFlNGU0NDRi/MDgyYjQwOGU0Y2Qy/ZWMwNWQ2ZC90ZWNo/Y3J1bmNoLmNvbS8",
"path": " 2024 05 01 google-lays-off-staff-from-flutter-dart-python-weeks-before-its-developer-conference"
},
"breaking": false,
"thumbnail": {
"src": "https://imgs.search.brave.com/gCI5UG8muOEOZDAx9vpu6L6r6R00mD7jOF08-biFoyQ/rs:fit:200:200:1/g:ce/aHR0cHM6Ly90ZWNo/Y3J1bmNoLmNvbS93/cC1jb250ZW50L3Vw/bG9hZHMvMjAxOC8x/MS9HZXR0eUltYWdl/cy0xMDAyNDg0NzQ2/LmpwZz9yZXNpemU9/MTIwMCw4MDA"
},
"age": "3 days ago",
"extra_snippets": [
"Ahead of Googles annual I/O developer conference in May, the tech giant has laid off staff across key teams like Flutter, Dart, Python and others, according to reports from affected employees shared on social media. Google confirmed the layoffs to TechCrunch, but not the specific teams, roles or how many people were let go.",
"In a separate post on Reddit, another commenter noted the Python team affected by the layoffs were those who managed the internal Python runtimes and toolchains and worked with OSS Python. Included in this group were “multiple current and former core devs and steering council members,” they said.",
"Meanwhile, others shared on Y Combinators Hacker News, where a Python team member detailed their specific duties on the technical front and noted that, for years, much of the work was done with fewer than 10 people. Another Hacker News commenter said their early years on the Python team were spent paying down internal technical debt accumulated from not having a strong Python strategy.",
"CNBC reports that a total of 200 people were let go across Googles “Core” teams, which included those working on Python, app platforms, and other engineering roles. Some jobs were being shifted to India and Mexico, it said, citing internal documents."
]
}
],
"mutated_by_goggles": false
},
"type": "search",
"videos": {
"type": "videos",
"results": [
{
"type": "video_result",
"url": "https://www.youtube.com/watch?v=b093aqAZiPU",
"title": "👩‍💻 Python for Beginners Tutorial - YouTube",
"description": "In this step-by-step Python for beginner's tutorial, learn how you can get started programming in Python. In this video, I assume that you are completely new...",
"age": "March 25, 2021",
"page_age": "2021-03-25T10:00:08",
"video": {},
"meta_url": {
"scheme": "https",
"netloc": "youtube.com",
"hostname": "www.youtube.com",
"favicon": "https://imgs.search.brave.com/Ux4Hee4evZhvjuTKwtapBycOGjGDci2Gvn2pbSzvbC0/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvOTkyZTZiMWU3/YzU3Nzc5YjExYzUy/N2VhZTIxOWNlYjM5/ZGVjN2MyZDY4Nzdh/ZDYzMTYxNmI5N2Rk/Y2Q3N2FkNy93d3cu/eW91dHViZS5jb20v",
"path": " watch"
},
"thumbnail": {
"src": "https://imgs.search.brave.com/tZI4Do4_EYcTCsD_MvE3Jx8FzjIXwIJ5ZuKhwiWTyZs/rs:fit:200:200:1/g:ce/aHR0cHM6Ly9pLnl0/aW1nLmNvbS92aS9i/MDkzYXFBWmlQVS9t/YXhyZXNkZWZhdWx0/LmpwZw"
}
},
{
"type": "video_result",
"url": "https://www.youtube.com/watch?v=rfscVS0vtbw",
"title": "Learn Python - Full Course for Beginners [Tutorial] - YouTube",
"description": "This course will give you a full introduction into all of the core concepts in python. Follow along with the videos and you'll be a python programmer in no t...",
"age": "July 11, 2018",
"page_age": "2018-07-11T18:00:42",
"video": {},
"meta_url": {
"scheme": "https",
"netloc": "youtube.com",
"hostname": "www.youtube.com",
"favicon": "https://imgs.search.brave.com/Ux4Hee4evZhvjuTKwtapBycOGjGDci2Gvn2pbSzvbC0/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvOTkyZTZiMWU3/YzU3Nzc5YjExYzUy/N2VhZTIxOWNlYjM5/ZGVjN2MyZDY4Nzdh/ZDYzMTYxNmI5N2Rk/Y2Q3N2FkNy93d3cu/eW91dHViZS5jb20v",
"path": " watch"
},
"thumbnail": {
"src": "https://imgs.search.brave.com/65zkx_kPU_zJb-4nmvvY-q5-ZZwzceChz-N00V8cqvk/rs:fit:200:200:1/g:ce/aHR0cHM6Ly9pLnl0/aW1nLmNvbS92aS9y/ZnNjVlMwdnRidy9t/YXhyZXNkZWZhdWx0/LmpwZw"
}
},
{
"type": "video_result",
"url": "https://www.youtube.com/watch?v=_uQrJ0TkZlc",
"title": "Python Tutorial - Python Full Course for Beginners - YouTube",
"description": "Become a Python pro! 🚀 This comprehensive tutorial takes you from beginner to hero, covering the basics, machine learning, and web development projects.🚀 W...",
"age": "February 18, 2019",
"page_age": "2019-02-18T15:00:08",
"video": {},
"meta_url": {
"scheme": "https",
"netloc": "youtube.com",
"hostname": "www.youtube.com",
"favicon": "https://imgs.search.brave.com/Ux4Hee4evZhvjuTKwtapBycOGjGDci2Gvn2pbSzvbC0/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvOTkyZTZiMWU3/YzU3Nzc5YjExYzUy/N2VhZTIxOWNlYjM5/ZGVjN2MyZDY4Nzdh/ZDYzMTYxNmI5N2Rk/Y2Q3N2FkNy93d3cu/eW91dHViZS5jb20v",
"path": " watch"
},
"thumbnail": {
"src": "https://imgs.search.brave.com/Djiv1pXLq1ClqBSE_86jQnEYR8bW8UJP6Cs7LrgyQzQ/rs:fit:200:200:1/g:ce/aHR0cHM6Ly9pLnl0/aW1nLmNvbS92aS9f/dVFySjBUa1psYy9t/YXhyZXNkZWZhdWx0/LmpwZw"
}
},
{
"type": "video_result",
"url": "https://www.youtube.com/watch?v=wRKgzC-MhIc",
"title": "[] and {} vs list() and dict(), which is better?",
"description": "Enjoy the videos and music you love, upload original content, and share it all with friends, family, and the world on YouTube.",
"video": {},
"meta_url": {
"scheme": "https",
"netloc": "youtube.com",
"hostname": "www.youtube.com",
"favicon": "https://imgs.search.brave.com/Ux4Hee4evZhvjuTKwtapBycOGjGDci2Gvn2pbSzvbC0/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvOTkyZTZiMWU3/YzU3Nzc5YjExYzUy/N2VhZTIxOWNlYjM5/ZGVjN2MyZDY4Nzdh/ZDYzMTYxNmI5N2Rk/Y2Q3N2FkNy93d3cu/eW91dHViZS5jb20v",
"path": " watch"
},
"thumbnail": {
"src": "https://imgs.search.brave.com/Hw9ep2Pio13X1VZjRw_h9R2VH_XvZFOuGlQJVnVkeq0/rs:fit:200:200:1/g:ce/aHR0cHM6Ly9pLnl0/aW1nLmNvbS92aS93/UktnekMtTWhJYy9o/cWRlZmF1bHQuanBn"
}
},
{
"type": "video_result",
"url": "https://www.youtube.com/watch?v=LWdsF79H1Pg",
"title": "print() vs. return in Python Functions - YouTube",
"description": "In this video, you will learn the differences between the return statement and the print function when they are used inside Python functions. We will see an ...",
"age": "June 11, 2022",
"page_age": "2022-06-11T21:33:26",
"video": {},
"meta_url": {
"scheme": "https",
"netloc": "youtube.com",
"hostname": "www.youtube.com",
"favicon": "https://imgs.search.brave.com/Ux4Hee4evZhvjuTKwtapBycOGjGDci2Gvn2pbSzvbC0/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvOTkyZTZiMWU3/YzU3Nzc5YjExYzUy/N2VhZTIxOWNlYjM5/ZGVjN2MyZDY4Nzdh/ZDYzMTYxNmI5N2Rk/Y2Q3N2FkNy93d3cu/eW91dHViZS5jb20v",
"path": " watch"
},
"thumbnail": {
"src": "https://imgs.search.brave.com/ebglnr5_jwHHpvon3WU-5hzt0eHdTZSVGg3Ts6R38xY/rs:fit:200:200:1/g:ce/aHR0cHM6Ly9pLnl0/aW1nLmNvbS92aS9M/V2RzRjc5SDFQZy9t/YXhyZXNkZWZhdWx0/LmpwZw"
}
},
{
"type": "video_result",
"url": "https://www.youtube.com/watch?v=AovxLr8jUH4",
"title": "Python Tutorial for Beginners 5 - Python print() and input() Function ...",
"description": "In this Video I am going to show How to use print() Function and input() Function in Python. In python The print() function is used to print the specified ...",
"age": "August 28, 2018",
"page_age": "2018-08-28T20:11:09",
"video": {},
"meta_url": {
"scheme": "https",
"netloc": "youtube.com",
"hostname": "www.youtube.com",
"favicon": "https://imgs.search.brave.com/Ux4Hee4evZhvjuTKwtapBycOGjGDci2Gvn2pbSzvbC0/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvOTkyZTZiMWU3/YzU3Nzc5YjExYzUy/N2VhZTIxOWNlYjM5/ZGVjN2MyZDY4Nzdh/ZDYzMTYxNmI5N2Rk/Y2Q3N2FkNy93d3cu/eW91dHViZS5jb20v",
"path": " watch"
},
"thumbnail": {
"src": "https://imgs.search.brave.com/nCoLEcWkKtiecprWbS6nufwGCaSbPH7o0-sMeIkFmjI/rs:fit:200:200:1/g:ce/aHR0cHM6Ly9pLnl0/aW1nLmNvbS92aS9B/b3Z4THI4alVINC9o/cWRlZmF1bHQuanBn"
}
}
],
"mutated_by_goggles": false
},
"web": {
"type": "search",
"results": [
{
"title": "Welcome to Python.org",
"url": "https://www.python.org",
"is_source_local": false,
"is_source_both": false,
"description": "The official home of the <strong>Python</strong> Programming Language",
"page_age": "2023-09-09T15:55:05",
"profile": {
"name": "Python",
"url": "https://www.python.org",
"long_name": "python.org",
"img": "https://imgs.search.brave.com/vBaRH-v6oPS4csO4cdvuKhZ7-xDVvydin3oe3zXYxAI/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvNTJjMzZjNDBj/MmIzODgwMGUyOTRj/Y2E5MjM3YjRkYTZj/YWI1Yzk1NTlmYTgw/ZDBjNzM0MGMxZjQz/YWFjNTczYy93d3cu/cHl0aG9uLm9yZy8"
},
"language": "en",
"family_friendly": true,
"type": "search_result",
"subtype": "generic",
"meta_url": {
"scheme": "https",
"netloc": "python.org",
"hostname": "www.python.org",
"favicon": "https://imgs.search.brave.com/vBaRH-v6oPS4csO4cdvuKhZ7-xDVvydin3oe3zXYxAI/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvNTJjMzZjNDBj/MmIzODgwMGUyOTRj/Y2E5MjM3YjRkYTZj/YWI1Yzk1NTlmYTgw/ZDBjNzM0MGMxZjQz/YWFjNTczYy93d3cu/cHl0aG9uLm9yZy8",
"path": ""
},
"thumbnail": {
"src": "https://imgs.search.brave.com/GGfNfe5rxJ8QWEoxXniSLc0-POLU3qPyTIpuqPdbmXk/rs:fit:200:200:1/g:ce/aHR0cHM6Ly93d3cu/cHl0aG9uLm9yZy9z/dGF0aWMvb3Blbmdy/YXBoLWljb24tMjAw/eDIwMC5wbmc",
"original": "https://www.python.org/static/opengraph-icon-200x200.png",
"logo": false
},
"age": "September 9, 2023",
"cluster_type": "generic",
"cluster": [
{
"title": "Downloads",
"url": "https://www.python.org/downloads/",
"is_source_local": false,
"is_source_both": false,
"description": "The official home of the <strong>Python</strong> Programming Language",
"family_friendly": true
},
{
"title": "Macos",
"url": "https://www.python.org/downloads/macos/",
"is_source_local": false,
"is_source_both": false,
"description": "The official home of the <strong>Python</strong> Programming Language",
"family_friendly": true
},
{
"title": "Windows",
"url": "https://www.python.org/downloads/windows/",
"is_source_local": false,
"is_source_both": false,
"description": "The official home of the <strong>Python</strong> Programming Language",
"family_friendly": true
},
{
"title": "Getting Started",
"url": "https://www.python.org/about/gettingstarted/",
"is_source_local": false,
"is_source_both": false,
"description": "The official home of the <strong>Python</strong> Programming Language",
"family_friendly": true
}
],
"extra_snippets": [
"Calculations are simple with Python, and expression syntax is straightforward: the operators +, -, * and / work as expected; parentheses () can be used for grouping. More about simple math functions in Python 3.",
"The core of extensible programming is defining functions. Python allows mandatory and optional arguments, keyword arguments, and even arbitrary argument lists. More about defining functions in Python 3",
"Lists (known as arrays in other languages) are one of the compound data types that Python understands. Lists can be indexed, sliced and manipulated with other built-in functions. More about lists in Python 3",
"# Python 3: Simple output (with Unicode) >>> print(\"Hello, I'm Python!\") Hello, I'm Python! # Input, assignment >>> name = input('What is your name?\\n') >>> print('Hi, %s.' % name) What is your name? Python Hi, Python."
]
},
{
"title": "Python (programming language) - Wikipedia",
"url": "https://en.wikipedia.org/wiki/Python_(programming_language)",
"is_source_local": false,
"is_source_both": false,
"description": "<strong>Python</strong> is a high-level, general-purpose programming language. Its design philosophy emphasizes code readability with the use of significant indentation. <strong>Python</strong> is dynamically typed and garbage-collected. It supports multiple programming paradigms, including structured (particularly procedural), ...",
"page_age": "2024-05-01T12:54:03",
"profile": {
"name": "Wikipedia",
"url": "https://en.wikipedia.org/wiki/Python_(programming_language)",
"long_name": "en.wikipedia.org",
"img": "https://imgs.search.brave.com/0kxnVOiqv-faZvOJc7zpym4Zin1CTs1f1svfNZSzmfU/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvNjQwNGZhZWY0/ZTQ1YWUzYzQ3MDUw/MmMzMGY3NTQ0ZjNj/NDUwMDk5ZTI3MWRk/NWYyNTM4N2UwOTE0/NTI3ZDQzNy9lbi53/aWtpcGVkaWEub3Jn/Lw"
},
"language": "en",
"family_friendly": true,
"type": "search_result",
"subtype": "generic",
"meta_url": {
"scheme": "https",
"netloc": "en.wikipedia.org",
"hostname": "en.wikipedia.org",
"favicon": "https://imgs.search.brave.com/0kxnVOiqv-faZvOJc7zpym4Zin1CTs1f1svfNZSzmfU/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvNjQwNGZhZWY0/ZTQ1YWUzYzQ3MDUw/MmMzMGY3NTQ0ZjNj/NDUwMDk5ZTI3MWRk/NWYyNTM4N2UwOTE0/NTI3ZDQzNy9lbi53/aWtpcGVkaWEub3Jn/Lw",
"path": " wiki Python_(programming_language)"
},
"age": "4 days ago",
"extra_snippets": [
"Python is dynamically typed and garbage-collected. It supports multiple programming paradigms, including structured (particularly procedural), object-oriented and functional programming. It is often described as a \"batteries included\" language due to its comprehensive standard library.",
"Guido van Rossum began working on Python in the late 1980s as a successor to the ABC programming language and first released it in 1991 as Python 0.9.0. Python 2.0 was released in 2000. Python 3.0, released in 2008, was a major revision not completely backward-compatible with earlier versions. Python 2.7.18, released in 2020, was the last release of Python 2.",
"Python was invented in the late 1980s by Guido van Rossum at Centrum Wiskunde & Informatica (CWI) in the Netherlands as a successor to the ABC programming language, which was inspired by SETL, capable of exception handling and interfacing with the Amoeba operating system.",
"Python consistently ranks as one of the most popular programming languages, and has gained widespread use in the machine learning community."
]
},
{
"title": "Python Tutorial",
"url": "https://www.w3schools.com/python/",
"is_source_local": false,
"is_source_both": false,
"description": "W3Schools offers free online tutorials, references and exercises in all the major languages of the web. Covering popular subjects like HTML, CSS, JavaScript, <strong>Python</strong>, SQL, Java, and many, many more.",
"page_age": "2017-12-07T00:00:00",
"profile": {
"name": "W3Schools",
"url": "https://www.w3schools.com/python/",
"long_name": "w3schools.com",
"img": "https://imgs.search.brave.com/JwO5r7z3HTBkU29vgNH_4rrSWLf2M4-8FMWNvbxrKX8/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvYjVlMGVkZDVj/ZGMyZWRmMzAwODRi/ZDAwZGE4NWI3NmU4/MjRhNjEzOGFhZWY3/ZGViMjY1OWY2ZDYw/YTZiOGUyZS93d3cu/dzNzY2hvb2xzLmNv/bS8"
},
"language": "en",
"family_friendly": true,
"type": "search_result",
"subtype": "generic",
"meta_url": {
"scheme": "https",
"netloc": "w3schools.com",
"hostname": "www.w3schools.com",
"favicon": "https://imgs.search.brave.com/JwO5r7z3HTBkU29vgNH_4rrSWLf2M4-8FMWNvbxrKX8/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvYjVlMGVkZDVj/ZGMyZWRmMzAwODRi/ZDAwZGE4NWI3NmU4/MjRhNjEzOGFhZWY3/ZGViMjY1OWY2ZDYw/YTZiOGUyZS93d3cu/dzNzY2hvb2xzLmNv/bS8",
"path": " python"
},
"thumbnail": {
"src": "https://imgs.search.brave.com/EMfp8dodbJehmj0yCJh8317RHuaumsddnHI4bujvFcg/rs:fit:200:200:1/g:ce/aHR0cHM6Ly93d3cu/dzNzY2hvb2xzLmNv/bS9pbWFnZXMvdzNz/Y2hvb2xzX2xvZ29f/NDM2XzIucG5n",
"original": "https://www.w3schools.com/images/w3schools_logo_436_2.png",
"logo": true
},
"age": "December 7, 2017",
"extra_snippets": [
"Well organized and easy to understand Web building tutorials with lots of examples of how to use HTML, CSS, JavaScript, SQL, Python, PHP, Bootstrap, Java, XML and more.",
"HTML CSS JAVASCRIPT SQL PYTHON JAVA PHP HOW TO W3.CSS C C++ C# BOOTSTRAP REACT MYSQL JQUERY EXCEL XML DJANGO NUMPY PANDAS NODEJS R TYPESCRIPT ANGULAR GIT POSTGRESQL MONGODB ASP AI GO KOTLIN SASS VUE DSA GEN AI SCIPY AWS CYBERSECURITY DATA SCIENCE",
"Python Variables Variable Names Assign Multiple Values Output Variables Global Variables Variable Exercises Python Data Types Python Numbers Python Casting Python Strings",
"Python Strings Slicing Strings Modify Strings Concatenate Strings Format Strings Escape Characters String Methods String Exercises Python Booleans Python Operators Python Lists"
]
},
{
"title": "Online Python - IDE, Editor, Compiler, Interpreter",
"url": "https://www.online-python.com/",
"is_source_local": false,
"is_source_both": false,
"description": "Build and Run your <strong>Python</strong> code instantly. Online-<strong>Python</strong> is a quick and easy tool that helps you to build, compile, test your <strong>python</strong> programs.",
"profile": {
"name": "Online-python",
"url": "https://www.online-python.com/",
"long_name": "online-python.com",
"img": "https://imgs.search.brave.com/kfaEvapwHxSsRObO52-I-otYFPHpG1h7UXJyUqDM2Ec/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvZGYxODdjNWQ0/NjZjZTNiMjk5NDY1/MWI5MTgyYjU3Y2Q3/MTI3NGM5MjUzY2Fi/OGQ3MTQ4MmIxMTQx/ZTcxNWFhMC93d3cu/b25saW5lLXB5dGhv/bi5jb20v"
},
"language": "en",
"family_friendly": true,
"type": "search_result",
"subtype": "generic",
"meta_url": {
"scheme": "https",
"netloc": "online-python.com",
"hostname": "www.online-python.com",
"favicon": "https://imgs.search.brave.com/kfaEvapwHxSsRObO52-I-otYFPHpG1h7UXJyUqDM2Ec/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvZGYxODdjNWQ0/NjZjZTNiMjk5NDY1/MWI5MTgyYjU3Y2Q3/MTI3NGM5MjUzY2Fi/OGQ3MTQ4MmIxMTQx/ZTcxNWFhMC93d3cu/b25saW5lLXB5dGhv/bi5jb20v",
"path": ""
},
"extra_snippets": [
"Build, run, and share Python code online for free with the help of online-integrated python's development environment (IDE). It is one of the most efficient, dependable, and potent online compilers for the Python programming language. It is not necessary for you to bother about establishing a Python environment in your local.",
"It is one of the most efficient, dependable, and potent online compilers for the Python programming language. It is not necessary for you to bother about establishing a Python environment in your local. Now You can immediately execute the Python code in the web browser of your choice.",
"It is not necessary for you to bother about establishing a Python environment in your local. Now You can immediately execute the Python code in the web browser of your choice. Using this Python editor is simple and quick to get up and running with. Simply type in the programme, and then press the RUN button!",
"Now You can immediately execute the Python code in the web browser of your choice. Using this Python editor is simple and quick to get up and running with. Simply type in the programme, and then press the RUN button! The code can be saved online by choosing the SHARE option, which also gives you the ability to access your code from any location providing you have internet access."
]
},
{
"title": "Python · GitHub",
"url": "https://github.com/python",
"is_source_local": false,
"is_source_both": false,
"description": "Repositories related to the <strong>Python</strong> Programming language - <strong>Python</strong>",
"page_age": "2023-03-06T00:00:00",
"profile": {
"name": "GitHub",
"url": "https://github.com/python",
"long_name": "github.com",
"img": "https://imgs.search.brave.com/v8685zI4XInM0zxlNI2s7oE_2Sb-EL7lAy81WXbkQD8/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvYWQyNWM1NjA5/ZjZmZjNlYzI2MDNk/N2VkNmJhYjE2MzZl/MDY5ZTMxMDUzZmY1/NmU3NWIzNWVmMjk0/NTBjMjJjZi9naXRo/dWIuY29tLw"
},
"language": "en",
"family_friendly": true,
"type": "search_result",
"subtype": "generic",
"meta_url": {
"scheme": "https",
"netloc": "github.com",
"hostname": "github.com",
"favicon": "https://imgs.search.brave.com/v8685zI4XInM0zxlNI2s7oE_2Sb-EL7lAy81WXbkQD8/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvYWQyNWM1NjA5/ZjZmZjNlYzI2MDNk/N2VkNmJhYjE2MzZl/MDY5ZTMxMDUzZmY1/NmU3NWIzNWVmMjk0/NTBjMjJjZi9naXRo/dWIuY29tLw",
"path": " python"
},
"thumbnail": {
"src": "https://imgs.search.brave.com/POoaRfu_7gfp-D_O3qMNJrwDqJNbiDu1HuBpNJ_MpVQ/rs:fit:200:200:1/g:ce/aHR0cHM6Ly9hdmF0/YXJzLmdpdGh1YnVz/ZXJjb250ZW50LmNv/bS91LzE1MjU5ODE_/cz0yMDAmYW1wO3Y9/NA",
"original": "https://avatars.githubusercontent.com/u/1525981?s=200&amp;v=4",
"logo": false
},
"age": "March 6, 2023",
"extra_snippets": ["Configuration for Python planets (e.g. http://planetpython.org)"]
},
{
"title": "Online Python Compiler (Interpreter)",
"url": "https://www.programiz.com/python-programming/online-compiler/",
"is_source_local": false,
"is_source_both": false,
"description": "Write and run <strong>Python</strong> code using our online compiler (interpreter). You can use <strong>Python</strong> Shell like IDLE, and take inputs from the user in our <strong>Python</strong> compiler.",
"page_age": "2020-06-02T00:00:00",
"profile": {
"name": "Programiz",
"url": "https://www.programiz.com/python-programming/online-compiler/",
"long_name": "programiz.com",
"img": "https://imgs.search.brave.com/ozj4JFayZ3Fs5c9eTp7M5g12azQ_Hblgu4dpTuHRz6U/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvMGJlN2U1YjVi/Y2M3ZDU5OGMwMWNi/M2Q3YjhjOTM1ZTFk/Y2NkZjE4NGQwOGIx/MTQ4NjI2YmNhODVj/MzFkMmJhYy93d3cu/cHJvZ3JhbWl6LmNv/bS8"
},
"language": "en",
"family_friendly": true,
"type": "search_result",
"subtype": "generic",
"meta_url": {
"scheme": "https",
"netloc": "programiz.com",
"hostname": "www.programiz.com",
"favicon": "https://imgs.search.brave.com/ozj4JFayZ3Fs5c9eTp7M5g12azQ_Hblgu4dpTuHRz6U/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvMGJlN2U1YjVi/Y2M3ZDU5OGMwMWNi/M2Q3YjhjOTM1ZTFk/Y2NkZjE4NGQwOGIx/MTQ4NjI2YmNhODVj/MzFkMmJhYy93d3cu/cHJvZ3JhbWl6LmNv/bS8",
"path": " python-programming online-compiler"
},
"age": "June 2, 2020",
"extra_snippets": [
"Python Online Compiler Online R Compiler SQL Online Editor Online HTML/CSS Editor Online Java Compiler C Online Compiler C++ Online Compiler C# Online Compiler JavaScript Online Compiler Online GoLang Compiler Online PHP Compiler Online Swift Compiler Online Rust Compiler",
"# Online Python compiler (interpreter) to run Python online. # Write Python 3 code in this online editor and run it. print(\"Try programiz.pro\")"
]
},
{
"title": "Python Developer",
"url": "https://twitter.com/Python_Dv/status/1786763460992544791",
"is_source_local": false,
"is_source_both": false,
"description": "<strong>Python</strong> Developer",
"page_age": "2024-05-04T14:30:03",
"profile": {
"name": "X",
"url": "https://twitter.com/Python_Dv/status/1786763460992544791",
"long_name": "twitter.com",
"img": "https://imgs.search.brave.com/Zq483bGX0GnSgym-1P7iyOyEDX3PkDZSNT8m56F862A/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvN2MxOTUxNzhj/OTY1ZTQ3N2I0MjJk/MTY5NGM0MTRlYWVi/MjU1YWE2NDUwYmQ2/YTA2MDFhMDlkZDEx/NTAzZGNiNi90d2l0/dGVyLmNvbS8"
},
"language": "en",
"family_friendly": true,
"type": "search_result",
"subtype": "generic",
"meta_url": {
"scheme": "https",
"netloc": "twitter.com",
"hostname": "twitter.com",
"favicon": "https://imgs.search.brave.com/Zq483bGX0GnSgym-1P7iyOyEDX3PkDZSNT8m56F862A/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvN2MxOTUxNzhj/OTY1ZTQ3N2I0MjJk/MTY5NGM0MTRlYWVi/MjU1YWE2NDUwYmQ2/YTA2MDFhMDlkZDEx/NTAzZGNiNi90d2l0/dGVyLmNvbS8",
"path": " Python_Dv status 1786763460992544791"
},
"age": "20 hours ago"
},
{
"title": "input table name? - python script - KNIME Extensions - KNIME Community Forum",
"url": "https://forum.knime.com/t/input-table-name-python-script/78978",
"is_source_local": false,
"is_source_both": false,
"description": "Hi, when running a <strong>python</strong> script node, I get the error seen on the screenshot Same happens with this code too: The script input is output from the csv reader node. How can I get the right name for that table? Best wishes, Dario",
"page_age": "2024-05-04T09:20:44",
"profile": {
"name": "Knime",
"url": "https://forum.knime.com/t/input-table-name-python-script/78978",
"long_name": "forum.knime.com",
"img": "https://imgs.search.brave.com/WQoOhAD5i6uEhJ-qXvlWMJwbGA52f2Ycc_ns36EK698/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvOTAxNzMxNjFl/MzJjNzU5NzRkOTMz/Mjg4NDU2OWUxM2Rj/YzVkOGM3MzIwNzI2/YTY1NzYxNzA1MDE5/NzQzOWU3NC9mb3J1/bS5rbmltZS5jb20v"
},
"language": "en",
"family_friendly": true,
"type": "search_result",
"subtype": "article",
"meta_url": {
"scheme": "https",
"netloc": "forum.knime.com",
"hostname": "forum.knime.com",
"favicon": "https://imgs.search.brave.com/WQoOhAD5i6uEhJ-qXvlWMJwbGA52f2Ycc_ns36EK698/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvOTAxNzMxNjFl/MzJjNzU5NzRkOTMz/Mjg4NDU2OWUxM2Rj/YzVkOGM3MzIwNzI2/YTY1NzYxNzA1MDE5/NzQzOWU3NC9mb3J1/bS5rbmltZS5jb20v",
"path": " knime extensions"
},
"thumbnail": {
"src": "https://imgs.search.brave.com/DtEl38dcvuM1kGfhN0T5HfOrsMJcztWNyriLvtDJmKI/rs:fit:200:200:1/g:ce/aHR0cHM6Ly9mb3J1/bS1jZG4ua25pbWUu/Y29tL3VwbG9hZHMv/ZGVmYXVsdC9vcmln/aW5hbC8zWC9lLzYv/ZTY0M2M2NzFlNzAz/MDg2MjkwMWY2YzJh/OWFjOWI5ZmEwM2M3/ZjMwZi5wbmc",
"original": "https://forum-cdn.knime.com/uploads/default/original/3X/e/6/e643c671e7030862901f6c2a9ac9b9fa03c7f30f.png",
"logo": false
},
"age": "1 day ago",
"extra_snippets": [
"Hi, when running a python script node, I get the error seen on the screenshot Same happens with this code too: The script input is output from the csv reader node. How can I get the right name for that table? …"
]
},
{
"title": "What does the Double Star operator mean in Python? - GeeksforGeeks",
"url": "https://www.geeksforgeeks.org/what-does-the-double-star-operator-mean-in-python/",
"is_source_local": false,
"is_source_both": false,
"description": "A Computer Science portal for geeks. It contains well written, well thought and well explained computer science and programming articles, quizzes and practice/competitive programming/company interview Questions.",
"page_age": "2023-03-14T17:15:04",
"profile": {
"name": "GeeksforGeeks",
"url": "https://www.geeksforgeeks.org/what-does-the-double-star-operator-mean-in-python/",
"long_name": "geeksforgeeks.org",
"img": "https://imgs.search.brave.com/fhzcfv5xltx6-YBvJI9RZgS7xZo0dPNaASsrB8YOsCs/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvYjBhOGQ3MmNi/ZWE5N2EwMmZjYzA1/ZTI0ZTFhMGUyMTE0/MGM0ZTBmMWZlM2Y2/Yzk2ODMxZTRhYTBi/NDdjYTE0OS93d3cu/Z2Vla3Nmb3JnZWVr/cy5vcmcv"
},
"language": "en",
"family_friendly": true,
"type": "search_result",
"subtype": "article",
"meta_url": {
"scheme": "https",
"netloc": "geeksforgeeks.org",
"hostname": "www.geeksforgeeks.org",
"favicon": "https://imgs.search.brave.com/fhzcfv5xltx6-YBvJI9RZgS7xZo0dPNaASsrB8YOsCs/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvYjBhOGQ3MmNi/ZWE5N2EwMmZjYzA1/ZTI0ZTFhMGUyMTE0/MGM0ZTBmMWZlM2Y2/Yzk2ODMxZTRhYTBi/NDdjYTE0OS93d3cu/Z2Vla3Nmb3JnZWVr/cy5vcmcv",
"path": " what-does-the-double-star-operator-mean-in-python"
},
"thumbnail": {
"src": "https://imgs.search.brave.com/GcR-j_dLbyHkbHEI3ffLMi6xpXGhF_2Z8POIoqtokhM/rs:fit:200:200:1/g:ce/aHR0cHM6Ly9tZWRp/YS5nZWVrc2Zvcmdl/ZWtzLm9yZy93cC1j/b250ZW50L3VwbG9h/ZHMvZ2ZnXzIwMFgy/MDAtMTAweDEwMC5w/bmc",
"original": "https://media.geeksforgeeks.org/wp-content/uploads/gfg_200X200-100x100.png",
"logo": false
},
"age": "March 14, 2023",
"extra_snippets": [
"Difference between / vs. // operator in Python",
"Double Star or (**) is one of the Arithmetic Operator (Like +, -, *, **, /, //, %) in Python Language. It is also known as Power Operator.",
"The time complexity of the given Python program is O(n), where n is the number of key-value pairs in the input dictionary.",
"Inplace Operators in Python | Set 2 (ixor(), iand(), ipow(),…)"
]
},
{
"title": "r/Python",
"url": "https://www.reddit.com/r/Python/",
"is_source_local": false,
"is_source_both": false,
"description": "The official <strong>Python</strong> community for Reddit! Stay up to date with the latest news, packages, and meta information relating to the <strong>Python</strong> programming language. --- If you have questions or are new to <strong>Python</strong> use r/LearnPython",
"page_age": "2022-12-30T16:25:02",
"profile": {
"name": "Reddit",
"url": "https://www.reddit.com/r/Python/",
"long_name": "reddit.com",
"img": "https://imgs.search.brave.com/mAZYEK9Wi13WLDUge7XZ8YuDTwm6DP6gBjvz1GdYZVY/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvN2ZiNTU0M2Nj/MTFhZjRiYWViZDlk/MjJiMjBjMzFjMDRk/Y2IzYWI0MGI0MjVk/OGY5NzQzOGQ5NzQ5/NWJhMWI0NC93d3cu/cmVkZGl0LmNvbS8"
},
"language": "en",
"family_friendly": true,
"type": "search_result",
"subtype": "generic",
"meta_url": {
"scheme": "https",
"netloc": "reddit.com",
"hostname": "www.reddit.com",
"favicon": "https://imgs.search.brave.com/mAZYEK9Wi13WLDUge7XZ8YuDTwm6DP6gBjvz1GdYZVY/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvN2ZiNTU0M2Nj/MTFhZjRiYWViZDlk/MjJiMjBjMzFjMDRk/Y2IzYWI0MGI0MjVk/OGY5NzQzOGQ5NzQ5/NWJhMWI0NC93d3cu/cmVkZGl0LmNvbS8",
"path": " r Python"
},
"thumbnail": {
"src": "https://imgs.search.brave.com/zWd10t3zg34ciHiAB-K5WWK3h_H4LedeDot9BVX7Ydo/rs:fit:200:200:1/g:ce/aHR0cHM6Ly9zdHls/ZXMucmVkZGl0bWVk/aWEuY29tL3Q1XzJx/aDB5L3N0eWxlcy9j/b21tdW5pdHlJY29u/X2NpZmVobDR4dDdu/YzEucG5n",
"original": "https://styles.redditmedia.com/t5_2qh0y/styles/communityIcon_cifehl4xt7nc1.png",
"logo": false
},
"age": "December 30, 2022",
"extra_snippets": [
"r/Python: The official Python community for Reddit! Stay up to date with the latest news, packages, and meta information relating to the Python…",
"By default, Python allows you to import and use anything, anywhere. Over time, this results in modules that were intended to be separate getting tightly coupled together, and domain boundaries breaking down. We experienced this first-hand at a unicorn startup, where the eng team paused development for over a year in an attempt to split up packages into independent services.",
"Hello r/Python! It's time to share what you've been working on! Whether it's a work-in-progress, a completed masterpiece, or just a rough idea, let us know what you're up to!",
"Whether it's your job, your hobby, or your passion project, all Python-related work is welcome here."
]
},
{
"title": "GitHub - python/cpython: The Python programming language",
"url": "https://github.com/python/cpython",
"is_source_local": false,
"is_source_both": false,
"description": "The <strong>Python</strong> programming language. Contribute to <strong>python</strong>/cpython development by creating an account on GitHub.",
"page_age": "2022-10-29T00:00:00",
"profile": {
"name": "GitHub",
"url": "https://github.com/python/cpython",
"long_name": "github.com",
"img": "https://imgs.search.brave.com/v8685zI4XInM0zxlNI2s7oE_2Sb-EL7lAy81WXbkQD8/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvYWQyNWM1NjA5/ZjZmZjNlYzI2MDNk/N2VkNmJhYjE2MzZl/MDY5ZTMxMDUzZmY1/NmU3NWIzNWVmMjk0/NTBjMjJjZi9naXRo/dWIuY29tLw"
},
"language": "en",
"family_friendly": true,
"type": "search_result",
"subtype": "software",
"meta_url": {
"scheme": "https",
"netloc": "github.com",
"hostname": "github.com",
"favicon": "https://imgs.search.brave.com/v8685zI4XInM0zxlNI2s7oE_2Sb-EL7lAy81WXbkQD8/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvYWQyNWM1NjA5/ZjZmZjNlYzI2MDNk/N2VkNmJhYjE2MzZl/MDY5ZTMxMDUzZmY1/NmU3NWIzNWVmMjk0/NTBjMjJjZi9naXRo/dWIuY29tLw",
"path": " python cpython"
},
"thumbnail": {
"src": "https://imgs.search.brave.com/BJbWFRUqgP-tKIyGK9ByXjuYjHO2mtYigUOEFNz_gXk/rs:fit:200:200:1/g:ce/aHR0cHM6Ly9vcGVu/Z3JhcGguZ2l0aHVi/YXNzZXRzLmNvbS82/MTY5YmJkNTQ0YzAy/NDg0MGU4NDdjYTU1/YTU3ZGZmMDA2ZDAw/YWQ1NDIzOTFmYTQ3/YmJjODg3OWM0NWYw/MTZhL3B5dGhvbi9j/cHl0aG9u",
"original": "https://opengraph.githubassets.com/6169bbd544c024840e847ca55a57dff006d00ad542391fa47bbc8879c45f016a/python/cpython",
"logo": false
},
"age": "October 29, 2022",
"extra_snippets": [
"You can pass many options to the configure script; run ./configure --help to find out more. On macOS case-insensitive file systems and on Cygwin, the executable is called python.exe; elsewhere it's just python.",
"Building a complete Python installation requires the use of various additional third-party libraries, depending on your build platform and configure options. Not all standard library modules are buildable or useable on all platforms. Refer to the Install dependencies section of the Developer Guide for current detailed information on dependencies for various Linux distributions and macOS.",
"To get an optimized build of Python, configure --enable-optimizations before you run make. This sets the default make targets up to enable Profile Guided Optimization (PGO) and may be used to auto-enable Link Time Optimization (LTO) on some platforms. For more details, see the sections below.",
"Copyright © 2001-2024 Python Software Foundation. All rights reserved."
]
},
{
"title": "5. Data Structures — Python 3.12.3 documentation",
"url": "https://docs.python.org/3/tutorial/datastructures.html",
"is_source_local": false,
"is_source_both": false,
"description": "This chapter describes some things youve learned about already in more detail, and adds some new things as well. More on Lists: The list data type has some more methods. Here are all of the method...",
"page_age": "2023-07-04T00:00:00",
"profile": {
"name": "Python documentation",
"url": "https://docs.python.org/3/tutorial/datastructures.html",
"long_name": "docs.python.org",
"img": "https://imgs.search.brave.com/F5Ym7eSElhGdGUFKLRxDj9Z_tc180ldpeMvQ2Q6ARbA/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvMTUzOTFjOGVi/YTcyOTVmODA3ODIy/YjE2NzFjY2ViMjhl/NzRlY2JhYTc5YjNm/ZjhmODAyZWI2OGUw/ZjU4NDVlNy9kb2Nz/LnB5dGhvbi5vcmcv"
},
"language": "en",
"family_friendly": true,
"type": "search_result",
"subtype": "generic",
"meta_url": {
"scheme": "https",
"netloc": "docs.python.org",
"hostname": "docs.python.org",
"favicon": "https://imgs.search.brave.com/F5Ym7eSElhGdGUFKLRxDj9Z_tc180ldpeMvQ2Q6ARbA/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvMTUzOTFjOGVi/YTcyOTVmODA3ODIy/YjE2NzFjY2ViMjhl/NzRlY2JhYTc5YjNm/ZjhmODAyZWI2OGUw/ZjU4NDVlNy9kb2Nz/LnB5dGhvbi5vcmcv",
"path": " 3 tutorial datastructures.html"
},
"thumbnail": {
"src": "https://imgs.search.brave.com/Y7GrMRF8WorDIMLuOl97XC8ltYpoOCqNwWF2pQIIKls/rs:fit:200:200:1/g:ce/aHR0cHM6Ly9kb2Nz/LnB5dGhvbi5vcmcv/My9fc3RhdGljL29n/LWltYWdlLnBuZw",
"original": "https://docs.python.org/3/_static/og-image.png",
"logo": false
},
"age": "July 4, 2023",
"extra_snippets": [
"You might have noticed that methods like insert, remove or sort that only modify the list have no return value printed they return the default None. [1] This is a design principle for all mutable data structures in Python.",
"We saw that lists and strings have many common properties, such as indexing and slicing operations. They are two examples of sequence data types (see Sequence Types — list, tuple, range). Since Python is an evolving language, other sequence data types may be added. There is also another standard sequence data type: the tuple.",
"Python also includes a data type for sets. A set is an unordered collection with no duplicate elements. Basic uses include membership testing and eliminating duplicate entries. Set objects also support mathematical operations like union, intersection, difference, and symmetric difference.",
"Another useful data type built into Python is the dictionary (see Mapping Types — dict). Dictionaries are sometimes found in other languages as “associative memories” or “associative arrays”. Unlike sequences, which are indexed by a range of numbers, dictionaries are indexed by keys, which can be any immutable type; strings and numbers can always be keys."
]
},
{
"title": "Something wrong with python packages / AUR Issues, Discussion & PKGBUILD Requests / Arch Linux Forums",
"url": "https://bbs.archlinux.org/viewtopic.php?id=295466",
"is_source_local": false,
"is_source_both": false,
"description": "Big <strong>Python</strong> updates require <strong>Python</strong> packages to be rebuild. For some reason they didn&#x27;t think a bump that made it necessary to rebuild half the official repo was a news post.",
"page_age": "2024-05-04T08:30:02",
"profile": {
"name": "Archlinux",
"url": "https://bbs.archlinux.org/viewtopic.php?id=295466",
"long_name": "bbs.archlinux.org",
"img": "https://imgs.search.brave.com/3au9oqkzSri_aLEec3jo-0bFgLuICkydrWfjFcC8lkI/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvNWNkODM1MWJl/ZmJhMzkzNzYzMDkz/NmEyMWMxNjI5MjNk/NGJmZjFhNTBlZDNl/Mzk5MzJjOGZkYjZl/MjNmY2IzNS9iYnMu/YXJjaGxpbnV4Lm9y/Zy8"
},
"language": "en",
"family_friendly": true,
"type": "search_result",
"subtype": "generic",
"meta_url": {
"scheme": "https",
"netloc": "bbs.archlinux.org",
"hostname": "bbs.archlinux.org",
"favicon": "https://imgs.search.brave.com/3au9oqkzSri_aLEec3jo-0bFgLuICkydrWfjFcC8lkI/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvNWNkODM1MWJl/ZmJhMzkzNzYzMDkz/NmEyMWMxNjI5MjNk/NGJmZjFhNTBlZDNl/Mzk5MzJjOGZkYjZl/MjNmY2IzNS9iYnMu/YXJjaGxpbnV4Lm9y/Zy8",
"path": " viewtopic.php"
},
"age": "1 day ago",
"extra_snippets": [
"Traceback (most recent call last): File \"/usr/lib/python3.12/importlib/metadata/__init__.py\", line 397, in from_name return next(cls.discover(name=name)) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ StopIteration During handling of the above exception, another exception occurred: Traceback (most recent call last): File \"/usr/bin/informant\", line 33, in <module> sys.exit(load_entry_point('informant==0.5.0', 'console_scripts', 'informant')()) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File \"/usr/bin/informant\", line 22, in importlib_load_entry_point for entry_point in distribution(dis"
]
},
{
"title": "Introduction to Python",
"url": "https://www.w3schools.com/python/python_intro.asp",
"is_source_local": false,
"is_source_both": false,
"description": "W3Schools offers free online tutorials, references and exercises in all the major languages of the web. Covering popular subjects like HTML, CSS, JavaScript, <strong>Python</strong>, SQL, Java, and many, many more.",
"profile": {
"name": "W3Schools",
"url": "https://www.w3schools.com/python/python_intro.asp",
"long_name": "w3schools.com",
"img": "https://imgs.search.brave.com/JwO5r7z3HTBkU29vgNH_4rrSWLf2M4-8FMWNvbxrKX8/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvYjVlMGVkZDVj/ZGMyZWRmMzAwODRi/ZDAwZGE4NWI3NmU4/MjRhNjEzOGFhZWY3/ZGViMjY1OWY2ZDYw/YTZiOGUyZS93d3cu/dzNzY2hvb2xzLmNv/bS8"
},
"language": "en",
"family_friendly": true,
"type": "search_result",
"subtype": "generic",
"meta_url": {
"scheme": "https",
"netloc": "w3schools.com",
"hostname": "www.w3schools.com",
"favicon": "https://imgs.search.brave.com/JwO5r7z3HTBkU29vgNH_4rrSWLf2M4-8FMWNvbxrKX8/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvYjVlMGVkZDVj/ZGMyZWRmMzAwODRi/ZDAwZGE4NWI3NmU4/MjRhNjEzOGFhZWY3/ZGViMjY1OWY2ZDYw/YTZiOGUyZS93d3cu/dzNzY2hvb2xzLmNv/bS8",
"path": " python python_intro.asp"
},
"thumbnail": {
"src": "https://imgs.search.brave.com/EMfp8dodbJehmj0yCJh8317RHuaumsddnHI4bujvFcg/rs:fit:200:200:1/g:ce/aHR0cHM6Ly93d3cu/dzNzY2hvb2xzLmNv/bS9pbWFnZXMvdzNz/Y2hvb2xzX2xvZ29f/NDM2XzIucG5n",
"original": "https://www.w3schools.com/images/w3schools_logo_436_2.png",
"logo": true
},
"extra_snippets": [
"Well organized and easy to understand Web building tutorials with lots of examples of how to use HTML, CSS, JavaScript, SQL, Python, PHP, Bootstrap, Java, XML and more.",
"HTML CSS JAVASCRIPT SQL PYTHON JAVA PHP HOW TO W3.CSS C C++ C# BOOTSTRAP REACT MYSQL JQUERY EXCEL XML DJANGO NUMPY PANDAS NODEJS R TYPESCRIPT ANGULAR GIT POSTGRESQL MONGODB ASP AI GO KOTLIN SASS VUE DSA GEN AI SCIPY AWS CYBERSECURITY DATA SCIENCE",
"Python Variables Variable Names Assign Multiple Values Output Variables Global Variables Variable Exercises Python Data Types Python Numbers Python Casting Python Strings",
"Python Strings Slicing Strings Modify Strings Concatenate Strings Format Strings Escape Characters String Methods String Exercises Python Booleans Python Operators Python Lists"
]
},
{
"title": "bug: AUR package wants to use python but does not find any preset version · Issue #1740 · asdf-vm/asdf",
"url": "https://github.com/asdf-vm/asdf/issues/1740",
"is_source_local": false,
"is_source_both": false,
"description": "Describe the Bug I am not sure why this is happening, I am trying to install tlpui from AUR and it fails, here are some logs to help: ==&gt; Making package: tlpui 2:1.6.5-1 (Mi 10 apr 2024 23:19:15 +0...",
"page_age": "2024-05-04T06:45:04",
"profile": {
"name": "GitHub",
"url": "https://github.com/asdf-vm/asdf/issues/1740",
"long_name": "github.com",
"img": "https://imgs.search.brave.com/v8685zI4XInM0zxlNI2s7oE_2Sb-EL7lAy81WXbkQD8/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvYWQyNWM1NjA5/ZjZmZjNlYzI2MDNk/N2VkNmJhYjE2MzZl/MDY5ZTMxMDUzZmY1/NmU3NWIzNWVmMjk0/NTBjMjJjZi9naXRo/dWIuY29tLw"
},
"language": "en",
"family_friendly": true,
"type": "search_result",
"subtype": "software",
"meta_url": {
"scheme": "https",
"netloc": "github.com",
"hostname": "github.com",
"favicon": "https://imgs.search.brave.com/v8685zI4XInM0zxlNI2s7oE_2Sb-EL7lAy81WXbkQD8/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvYWQyNWM1NjA5/ZjZmZjNlYzI2MDNk/N2VkNmJhYjE2MzZl/MDY5ZTMxMDUzZmY1/NmU3NWIzNWVmMjk0/NTBjMjJjZi9naXRo/dWIuY29tLw",
"path": " asdf-vm asdf issues 1740"
},
"thumbnail": {
"src": "https://imgs.search.brave.com/KrLW5s_2n4jyP8XLbc3ZPVBaLD963tQgWzG9EWPZlQs/rs:fit:200:200:1/g:ce/aHR0cHM6Ly9vcGVu/Z3JhcGguZ2l0aHVi/YXNzZXRzLmNvbS81/MTE0ZTdkOGIwODM2/YmQ2MTY3NzQ1ZGI4/MmZjMGE3OGUyMjcw/MGFlY2ZjMWZkODBl/MDYzZTNiN2ZjOWNj/NzYyL2FzZGYtdm0v/YXNkZi9pc3N1ZXMv/MTc0MA",
"original": "https://opengraph.githubassets.com/5114e7d8b0836bd6167745db82fc0a78e22700aecfc1fd80e063e3b7fc9cc762/asdf-vm/asdf/issues/1740",
"logo": false
},
"age": "1 day ago",
"extra_snippets": [
"==> Starting build()... No preset version installed for command python Please install a version by running one of the following: asdf install python 3.8 or add one of the following versions in your config file at /home/ferret/.tool-versions python 3.11.0 python 3.12.1 python 3.12.3 ==> ERROR: A failure occurred in build(). Aborting...",
"-> error making: tlpui-exit status 4 -> Failed to install the following packages. Manual intervention is required: tlpui - exit status 4 ferret@FX505DT in ~ $ cat /home/ferret/.tool-versions nodejs 21.6.0 python 3.12.3 ferret@FX505DT in ~ $ python -V Python 3.12.3 ferret@FX505DT in ~ $ which python /home/ferret/.asdf/shims/python",
"Describe the Bug I am not sure why this is happening, I am trying to install tlpui from AUR and it fails, here are some logs to help: ==> Making package: tlpui 2:1.6.5-1 (Mi 10 apr 2024 23:19:15 +0300) ==> Retrieving sources... -> Found ..."
]
},
{
"title": "What are python.exe and python3.exe, and why do they appear to point to App Installer? | Windows 11 Forum",
"url": "https://www.elevenforum.com/t/what-are-python-exe-and-python3-exe-and-why-do-they-appear-to-point-to-app-installer.24886/",
"is_source_local": false,
"is_source_both": false,
"description": "I was looking at App execution aliases (Settings &gt; Apps &gt; Advanced app settings &gt; App execution aliases) on my new computer -- my first Windows 11 computer. Why are <strong>python</strong>.exe and python3.exe listed as App Installer? I assume that App Installer refers to installation of Microsoft Store / UWP...",
"page_age": "2024-05-03T17:30:04",
"profile": {
"name": "Windows 11 Forum",
"url": "https://www.elevenforum.com/t/what-are-python-exe-and-python3-exe-and-why-do-they-appear-to-point-to-app-installer.24886/",
"long_name": "elevenforum.com",
"img": "https://imgs.search.brave.com/XVRAYMEj6Im8i7jV5RxeTwpiRPtY9IWg4wRIuh-WhEw/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvZjk5MDZkMDIw/M2U1OWIwNjM5Y2U1/M2U2NzNiNzVkNTA5/NzA5OTI1ZTFmOTc4/MzU3OTlhYzU5OTVi/ZGNjNTY4MS93d3cu/ZWxldmVuZm9ydW0u/Y29tLw"
},
"language": "en",
"family_friendly": true,
"type": "search_result",
"subtype": "generic",
"meta_url": {
"scheme": "https",
"netloc": "elevenforum.com",
"hostname": "www.elevenforum.com",
"favicon": "https://imgs.search.brave.com/XVRAYMEj6Im8i7jV5RxeTwpiRPtY9IWg4wRIuh-WhEw/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvZjk5MDZkMDIw/M2U1OWIwNjM5Y2U1/M2U2NzNiNzVkNTA5/NzA5OTI1ZTFmOTc4/MzU3OTlhYzU5OTVi/ZGNjNTY4MS93d3cu/ZWxldmVuZm9ydW0u/Y29tLw",
"path": " windows support forums apps and software"
},
"thumbnail": {
"src": "https://imgs.search.brave.com/DVoFcE6d_-lx3BVGNS-RZK_lZzxQ8VhwZVf3AVqEJFA/rs:fit:200:200:1/g:ce/aHR0cHM6Ly93d3cu/ZWxldmVuZm9ydW0u/Y29tL2RhdGEvYXNz/ZXRzL2xvZ28vbWV0/YTEtMjAxLnBuZw",
"original": "https://www.elevenforum.com/data/assets/logo/meta1-201.png",
"logo": true
},
"age": "2 days ago",
"extra_snippets": [
"Why are python.exe and python3.exe listed as App Installer? I assume that App Installer refers to installation of Microsoft Store / UWP apps, but if that's the case, then why are they called python.exe and python3.exe? Or are python.exe and python3.exe simply serving as aliases / pointers pointing to App Installer, which is itself a Microsoft Store App?",
"Or are python.exe and python3.exe simply serving as aliases / pointers pointing to App Installer, which is itself a Microsoft Store App? I wish to soon install Python, along with an integrated development editor (IDE), on my machine, so that I can code in Python.",
"I wish to soon install Python, along with an integrated development editor (IDE), on my machine, so that I can code in Python. But is a Python interpreter already on my computer as suggested, if obliquely, by the presence of python.exe and python3.exe? I kind of doubt it."
]
},
{
"title": "How to Watermark Your Images Using Python OpenCV in ...",
"url": "https://medium.com/@daily_data_prep/how-to-watermark-your-images-using-python-opencv-in-bulk-e472085389a1",
"is_source_local": false,
"is_source_both": false,
"description": "Medium is an open platform where readers find dynamic thinking, and where expert and undiscovered voices can share their writing on any topic.",
"page_age": "2024-05-03T14:05:06",
"profile": {
"name": "Medium",
"url": "https://medium.com/@daily_data_prep/how-to-watermark-your-images-using-python-opencv-in-bulk-e472085389a1",
"long_name": "medium.com",
"img": "https://imgs.search.brave.com/qvE2kIQCiAsnPv2C6P9xM5J2VVWdm55g-A-2Q_yIJ0g/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvOTZhYmQ1N2Q4/NDg4ZDcyODIyMDZi/MzFmOWNhNjE3Y2E4/Y2YzMThjNjljNDIx/ZjllZmNhYTcwODhl/YTcwNDEzYy9tZWRp/dW0uY29tLw"
},
"language": "en",
"family_friendly": true,
"type": "search_result",
"subtype": "generic",
"meta_url": {
"scheme": "https",
"netloc": "medium.com",
"hostname": "medium.com",
"favicon": "https://imgs.search.brave.com/qvE2kIQCiAsnPv2C6P9xM5J2VVWdm55g-A-2Q_yIJ0g/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvOTZhYmQ1N2Q4/NDg4ZDcyODIyMDZi/MzFmOWNhNjE3Y2E4/Y2YzMThjNjljNDIx/ZjllZmNhYTcwODhl/YTcwNDEzYy9tZWRp/dW0uY29tLw",
"path": " @daily_data_prep how-to-watermark-your-images-using-python-opencv-in-bulk-e472085389a1"
},
"age": "2 days ago"
},
{
"title": "Increment and Decrement Operators in Python?",
"url": "https://www.tutorialspoint.com/increment-and-decrement-operators-in-python",
"is_source_local": false,
"is_source_both": false,
"description": "Increment and Decrement Operators in <strong>Python</strong> - <strong>Python</strong> does not have unary increment/decrement operator (++/--). Instead to increment a value, usea += 1to decrement a value, use a -= 1Example&gt;&gt;&gt; a = 0 &gt;&gt;&gt; &gt;&gt;&gt; #Increment &gt;&gt;&gt; a +=1 &gt;&gt;&gt; &gt;&gt;&gt; #Decrement &gt;&gt;&gt; a -= 1 &gt;&gt;&gt; &gt;&gt;&gt; #value of a &gt;&gt;&gt; a 0Python ...",
"page_age": "2023-08-23T00:00:00",
"profile": {
"name": "Tutorialspoint",
"url": "https://www.tutorialspoint.com/increment-and-decrement-operators-in-python",
"long_name": "tutorialspoint.com",
"img": "https://imgs.search.brave.com/Wt8BSkivPlFwcU5yBtf7YzuvTuRExyd_502cdABCS5c/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvYjcyYjAzYmVl/ODU4MzZiMjJiYTFh/MjJhZDNmNWE4YzA5/MDgyYTZhMDg3NTYw/M2NiY2NiZTUxN2I5/MjU1MWFmMS93d3cu/dHV0b3JpYWxzcG9p/bnQuY29tLw"
},
"language": "en",
"family_friendly": true,
"type": "search_result",
"subtype": "generic",
"meta_url": {
"scheme": "https",
"netloc": "tutorialspoint.com",
"hostname": "www.tutorialspoint.com",
"favicon": "https://imgs.search.brave.com/Wt8BSkivPlFwcU5yBtf7YzuvTuRExyd_502cdABCS5c/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvYjcyYjAzYmVl/ODU4MzZiMjJiYTFh/MjJhZDNmNWE4YzA5/MDgyYTZhMDg3NTYw/M2NiY2NiZTUxN2I5/MjU1MWFmMS93d3cu/dHV0b3JpYWxzcG9p/bnQuY29tLw",
"path": " increment-and-decrement-operators-in-python"
},
"thumbnail": {
"src": "https://imgs.search.brave.com/ddG5vyZGLVudvecEbQJPeG8tGuaZ7g3Xz6Gyjdl5WA8/rs:fit:200:200:1/g:ce/aHR0cHM6Ly93d3cu/dHV0b3JpYWxzcG9p/bnQuY29tL2ltYWdl/cy90cF9sb2dvXzQz/Ni5wbmc",
"original": "https://www.tutorialspoint.com/images/tp_logo_436.png",
"logo": true
},
"age": "August 23, 2023",
"extra_snippets": [
"Increment and Decrement Operators in Python - Python does not have unary increment/decrement operator (++/--). Instead to increment a value, usea += 1to decrement a value, use a -= 1Example>>> a = 0 >>> >>> #Increment >>> a +=1 >>> >>> #Decrement >>> a -= 1 >>> >>> #value of a >>> a 0Python does not provide multiple ways to do the same thing",
"So what above statement means in python is: create an object of type int having value 1 and give the name a to it. The object is an instance of int having value 1 and the name a refers to it. The assigned name a and the object to which it refers are distinct.",
"Python does not provide multiple ways to do the same thing .",
"However, be careful if you are coming from a language like C, Python doesnt have \"variables\" in the sense that C does, instead python uses names and objects and in python integers (ints) are immutable."
]
},
{
"title": "Gumroad How not to suck at Python / SideFX Houdini | CG Persia",
"url": "https://cgpersia.com/2024/05/gumroad-how-not-to-suck-at-python-sidefx-houdini-195370.html",
"is_source_local": false,
"is_source_both": false,
"description": "Info: This course is made for artists or TD (technical director) willing to learn <strong>Python</strong> to improve their workflows inside SideFX Houdini, get faster in production and develop all the tools you always wished you had.",
"page_age": "2024-05-03T08:35:03",
"profile": {
"name": "Cgpersia",
"url": "https://cgpersia.com/2024/05/gumroad-how-not-to-suck-at-python-sidefx-houdini-195370.html",
"long_name": "cgpersia.com",
"img": "https://imgs.search.brave.com/VjyaopAm-M9sWvM7n-KnGZ3T5swIOwwE80iF5QVqQPg/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvYmE0MzQ4NmI2/NjFhMTA1ZDBiN2Iw/ZWNiNDUxNjUwYjdh/MGE5ZjQ0ZjIxNzll/NmVkZDE2YzYyMDBh/NDNiMDgwMy9jZ3Bl/cnNpYS5jb20v"
},
"language": "en",
"family_friendly": true,
"type": "search_result",
"subtype": "generic",
"meta_url": {
"scheme": "https",
"netloc": "cgpersia.com",
"hostname": "cgpersia.com",
"favicon": "https://imgs.search.brave.com/VjyaopAm-M9sWvM7n-KnGZ3T5swIOwwE80iF5QVqQPg/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvYmE0MzQ4NmI2/NjFhMTA1ZDBiN2Iw/ZWNiNDUxNjUwYjdh/MGE5ZjQ0ZjIxNzll/NmVkZDE2YzYyMDBh/NDNiMDgwMy9jZ3Bl/cnNpYS5jb20v",
"path": " 2024 05 gumroad-how-not-to-suck-at-python-sidefx-houdini-195370.html"
},
"age": "2 days ago",
"extra_snippets": [
"Posted in: 2D, CG Releases, Downloads, Learning, Tutorials, Videos. Tagged: Gumroad, Python, Sidefx. Leave a Comment",
"01 Python Fundamentals Get the Fundamentals of python before starting the fun stuff ! 02 Python Construction Part02 digging further into python concepts 03 Houdini Python Basics Applying some basic python in Houdini and starting to make tools !",
"02 Python Construction Part02 digging further into python concepts 03 Houdini Python Basics Applying some basic python in Houdini and starting to make tools ! 04 Houdini Python Intermediate Applying some more advanced python in Houdini to make tools ! 05 Houdini Python Expert Using QtDesigner in combinaison with Houdini Python/Pyside to create advanced tools."
]
},
{
"title": "How to install Python: The complete Python programmers guide",
"url": "https://www.pluralsight.com/resources/blog/software-development/python-installation-guide",
"is_source_local": false,
"is_source_both": false,
"description": "An easy guide on how set up your operating system so you can program in <strong>Python</strong>, and how to update or uninstall it. For Linux, Windows, and macOS.",
"page_age": "2024-05-02T07:30:02",
"profile": {
"name": "Pluralsight",
"url": "https://www.pluralsight.com/resources/blog/software-development/python-installation-guide",
"long_name": "pluralsight.com",
"img": "https://imgs.search.brave.com/zvwQNSVu9-jR2CRlNcsTzxjaXKPlXNuh-Jo9-0yA1OE/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvMTNkNWQyNjk3/M2Q0NzYyMmUyNDc3/ZjYwMWFlZDI5YTI4/ODhmYzc2MDkzMjAy/MjNkMWY1MDE3NTQw/MzI5NWVkZS93d3cu/cGx1cmFsc2lnaHQu/Y29tLw"
},
"language": "en",
"family_friendly": true,
"type": "search_result",
"subtype": "generic",
"meta_url": {
"scheme": "https",
"netloc": "pluralsight.com",
"hostname": "www.pluralsight.com",
"favicon": "https://imgs.search.brave.com/zvwQNSVu9-jR2CRlNcsTzxjaXKPlXNuh-Jo9-0yA1OE/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvMTNkNWQyNjk3/M2Q0NzYyMmUyNDc3/ZjYwMWFlZDI5YTI4/ODhmYzc2MDkzMjAy/MjNkMWY1MDE3NTQw/MzI5NWVkZS93d3cu/cGx1cmFsc2lnaHQu/Y29tLw",
"path": " blog blog"
},
"thumbnail": {
"src": "https://imgs.search.brave.com/xrv5PHH2Bzmq2rcIYzk__8h5RqCj6kS3I6SGCNw5dZM/rs:fit:200:200:1/g:ce/aHR0cHM6Ly93d3cu/cGx1cmFsc2lnaHQu/Y29tL2NvbnRlbnQv/ZGFtL3BzL2ltYWdl/cy9yZXNvdXJjZS1j/ZW50ZXIvYmxvZy9o/ZWFkZXItaGVyby1p/bWFnZXMvUHl0aG9u/LndlYnA",
"original": "https://www.pluralsight.com/content/dam/ps/images/resource-center/blog/header-hero-images/Python.webp",
"logo": false
},
"age": "3 days ago",
"extra_snippets": [
"Whether its your first time programming or youre a seasoned programmer, youll have to install or update Python every now and then --- or if necessary, uninstall it. In this article, you'll learn how to do just that.",
"Some systems come with Python, so to start off, well first check to see if its installed on your system before we proceed. To do that, well need to open a terminal. Since you might be new to programming, lets go over how to open a terminal for Linux, Windows, and macOS.",
"Before we dive into setting up your system so you can program in Python, lets talk terminal basics and benefits.",
"However, lets focus on why we need it for working with Python. We use a terminal, or command line, to:"
]
}
],
"family_friendly": true
}
}

View File

@@ -0,0 +1,442 @@
{
"kind": "customsearch#search",
"url": {
"type": "application/json",
"template": "https://www.googleapis.com/customsearch/v1?q={searchTerms}&num={count?}&start={startIndex?}&lr={language?}&safe={safe?}&cx={cx?}&sort={sort?}&filter={filter?}&gl={gl?}&cr={cr?}&googlehost={googleHost?}&c2coff={disableCnTwTranslation?}&hq={hq?}&hl={hl?}&siteSearch={siteSearch?}&siteSearchFilter={siteSearchFilter?}&exactTerms={exactTerms?}&excludeTerms={excludeTerms?}&linkSite={linkSite?}&orTerms={orTerms?}&dateRestrict={dateRestrict?}&lowRange={lowRange?}&highRange={highRange?}&searchType={searchType}&fileType={fileType?}&rights={rights?}&imgSize={imgSize?}&imgType={imgType?}&imgColorType={imgColorType?}&imgDominantColor={imgDominantColor?}&alt=json"
},
"queries": {
"request": [
{
"title": "Google Custom Search - lectures",
"totalResults": "2450000000",
"searchTerms": "lectures",
"count": 10,
"startIndex": 1,
"inputEncoding": "utf8",
"outputEncoding": "utf8",
"safe": "off",
"cx": "0473ef98502d44e18"
}
],
"nextPage": [
{
"title": "Google Custom Search - lectures",
"totalResults": "2450000000",
"searchTerms": "lectures",
"count": 10,
"startIndex": 11,
"inputEncoding": "utf8",
"outputEncoding": "utf8",
"safe": "off",
"cx": "0473ef98502d44e18"
}
]
},
"context": {
"title": "LLM Search"
},
"searchInformation": {
"searchTime": 0.445959,
"formattedSearchTime": "0.45",
"totalResults": "2450000000",
"formattedTotalResults": "2,450,000,000"
},
"items": [
{
"kind": "customsearch#result",
"title": "The Feynman Lectures on Physics",
"htmlTitle": "The Feynman \u003cb\u003eLectures\u003c/b\u003e on Physics",
"link": "https://www.feynmanlectures.caltech.edu/",
"displayLink": "www.feynmanlectures.caltech.edu",
"snippet": "This edition has been designed for ease of reading on devices of any size or shape; text, figures and equations can all be zoomed without degradation.",
"htmlSnippet": "This edition has been designed for ease of reading on devices of any size or shape; text, figures and equations can all be zoomed without degradation.",
"cacheId": "CyXMWYWs9UEJ",
"formattedUrl": "https://www.feynmanlectures.caltech.edu/",
"htmlFormattedUrl": "https://www.feynman\u003cb\u003electures\u003c/b\u003e.caltech.edu/",
"pagemap": {
"metatags": [
{
"viewport": "width=device-width, initial-scale=1.0"
}
]
}
},
{
"kind": "customsearch#result",
"title": "Video Lectures",
"htmlTitle": "Video \u003cb\u003eLectures\u003c/b\u003e",
"link": "https://www.reddit.com/r/lectures/",
"displayLink": "www.reddit.com",
"snippet": "r/lectures: This subreddit is all about video lectures, talks and interesting public speeches. The topics include mathematics, physics, computer…",
"htmlSnippet": "r/\u003cb\u003electures\u003c/b\u003e: This subreddit is all about video \u003cb\u003electures\u003c/b\u003e, talks and interesting public speeches. The topics include mathematics, physics, computer…",
"formattedUrl": "https://www.reddit.com/r/lectures/",
"htmlFormattedUrl": "https://www.reddit.com/r/\u003cb\u003electures\u003c/b\u003e/",
"pagemap": {
"cse_thumbnail": [
{
"src": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcTZtOjhfkgUKQbL3DZxe5F6OVsgeDNffleObjJ7n9RllKQTSsimax7VIaY&s",
"width": "192",
"height": "192"
}
],
"metatags": [
{
"og:image": "https://www.redditstatic.com/shreddit/assets/favicon/192x192.png",
"theme-color": "#000000",
"og:image:width": "256",
"og:type": "website",
"twitter:card": "summary",
"twitter:title": "r/lectures",
"og:site_name": "Reddit",
"og:title": "r/lectures",
"og:image:height": "256",
"bingbot": "noarchive",
"msapplication-navbutton-color": "#000000",
"og:description": "This subreddit is all about video lectures, talks and interesting public speeches.\n\nThe topics include mathematics, physics, computer science, programming, engineering, biology, medicine, economics, politics, social sciences, and any other subjects!",
"twitter:image": "https://www.redditstatic.com/shreddit/assets/favicon/192x192.png",
"apple-mobile-web-app-status-bar-style": "black",
"twitter:site": "@reddit",
"viewport": "width=device-width, initial-scale=1, viewport-fit=cover",
"apple-mobile-web-app-capable": "yes",
"og:ttl": "600",
"og:url": "https://www.reddit.com/r/lectures/"
}
],
"cse_image": [
{
"src": "https://www.redditstatic.com/shreddit/assets/favicon/192x192.png"
}
]
}
},
{
"kind": "customsearch#result",
"title": "Lectures & Discussions | Flint Institute of Arts",
"htmlTitle": "\u003cb\u003eLectures\u003c/b\u003e &amp; Discussions | Flint Institute of Arts",
"link": "https://flintarts.org/events/lectures",
"displayLink": "flintarts.org",
"snippet": "It will trace the intricate relationship between jewelry, attire, and the expression of personal identity, social hierarchy, and spiritual belief systems that ...",
"htmlSnippet": "It will trace the intricate relationship between jewelry, attire, and the expression of personal identity, social hierarchy, and spiritual belief systems that&nbsp;...",
"cacheId": "jvpb9DxrfxoJ",
"formattedUrl": "https://flintarts.org/events/lectures",
"htmlFormattedUrl": "https://flintarts.org/events/\u003cb\u003electures\u003c/b\u003e",
"pagemap": {
"cse_thumbnail": [
{
"src": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcS23tMtAeNhJbOWdGxShYsmnyzFdzOC9Hb7lRykA9Pw72z1IlKTkjTdZw&s",
"width": "447",
"height": "113"
}
],
"metatags": [
{
"og:image": "https://flintarts.org/uploads/images/page-headers/_headerImage/nightshot.jpg",
"og:type": "website",
"viewport": "width=device-width, initial-scale=1",
"og:title": "Lectures & Discussions | Flint Institute of Arts",
"og:description": "The Flint Institute of Arts is the second largest art museum in Michigan and one of the largest museum art schools in the nation."
}
],
"cse_image": [
{
"src": "https://flintarts.org/uploads/images/page-headers/_headerImage/nightshot.jpg"
}
]
}
},
{
"kind": "customsearch#result",
"title": "Mandel Lectures | Mandel Center for the Humanities ... - Waltham",
"htmlTitle": "Mandel \u003cb\u003eLectures\u003c/b\u003e | Mandel Center for the Humanities ... - Waltham",
"link": "https://www.brandeis.edu/mandel-center-humanities/mandel-lectures.html",
"displayLink": "www.brandeis.edu",
"snippet": "Past Lectures · Lecture 1: \"Invisible Music: The Sonic Idea of Black Revolution From Captivity to Reconstruction\" · Lecture 2: \"Solidarity in Sound: Grassroots ...",
"htmlSnippet": "Past \u003cb\u003eLectures\u003c/b\u003e &middot; \u003cb\u003eLecture\u003c/b\u003e 1: &quot;Invisible Music: The Sonic Idea of Black Revolution From Captivity to Reconstruction&quot; &middot; \u003cb\u003eLecture\u003c/b\u003e 2: &quot;Solidarity in Sound: Grassroots&nbsp;...",
"cacheId": "cQLOZr0kgEEJ",
"formattedUrl": "https://www.brandeis.edu/mandel-center-humanities/mandel-lectures.html",
"htmlFormattedUrl": "https://www.brandeis.edu/mandel-center-humanities/mandel-\u003cb\u003electures\u003c/b\u003e.html",
"pagemap": {
"cse_thumbnail": [
{
"src": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQWlU7bcJ5pIHk7RBCk2QKE-48ejF7hyPV0pr-20_cBt2BGdfKtiYXBuyw&s",
"width": "275",
"height": "183"
}
],
"metatags": [
{
"og:image": "https://www.brandeis.edu/mandel-center-humanities/events/events-images/mlhzumba",
"twitter:card": "summary_large_image",
"viewport": "width=device-width,initial-scale=1,minimum-scale=1",
"og:title": "Mandel Lectures in the Humanities",
"og:url": "https://www.brandeis.edu/mandel-center-humanities/mandel-lectures.html",
"og:description": "Annual Lecture Series",
"twitter:image": "https://www.brandeis.edu/mandel-center-humanities/events/events-images/mlhzumba"
}
],
"cse_image": [
{
"src": "https://www.brandeis.edu/mandel-center-humanities/events/events-images/mlhzumba"
}
]
}
},
{
"kind": "customsearch#result",
"title": "Brian Douglas - YouTube",
"htmlTitle": "Brian Douglas - YouTube",
"link": "https://www.youtube.com/channel/UCq0imsn84ShAe9PBOFnoIrg",
"displayLink": "www.youtube.com",
"snippet": "Welcome to Control Systems Lectures! This collection of videos is intended to supplement a first year controls class, not replace it.",
"htmlSnippet": "Welcome to Control Systems \u003cb\u003eLectures\u003c/b\u003e! This collection of videos is intended to supplement a first year controls class, not replace it.",
"cacheId": "NEROyBHolL0J",
"formattedUrl": "https://www.youtube.com/channel/UCq0imsn84ShAe9PBOFnoIrg",
"htmlFormattedUrl": "https://www.youtube.com/channel/UCq0imsn84ShAe9PBOFnoIrg",
"pagemap": {
"hcard": [
{
"fn": "Brian Douglas",
"url": "https://www.youtube.com/channel/UCq0imsn84ShAe9PBOFnoIrg"
}
],
"cse_thumbnail": [
{
"src": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcR7G0CeCBz_wVTZgjnhEr2QbiKP7f3uYzKitZYn74Mi32cDmVxvsegJoLI&s",
"width": "225",
"height": "225"
}
],
"imageobject": [
{
"width": "900",
"url": "https://yt3.googleusercontent.com/ytc/AIdro_nLo68wetImbwGUYP3stve_iKmAEccjhqB-q4o79xdInN4=s900-c-k-c0x00ffffff-no-rj",
"height": "900"
}
],
"person": [
{
"name": "Brian Douglas",
"url": "https://www.youtube.com/channel/UCq0imsn84ShAe9PBOFnoIrg"
}
],
"metatags": [
{
"apple-itunes-app": "app-id=544007664, app-argument=https://m.youtube.com/channel/UCq0imsn84ShAe9PBOFnoIrg?referring_app=com.apple.mobilesafari-smartbanner, affiliate-data=ct=smart_app_banner_polymer&pt=9008",
"og:image": "https://yt3.googleusercontent.com/ytc/AIdro_nLo68wetImbwGUYP3stve_iKmAEccjhqB-q4o79xdInN4=s900-c-k-c0x00ffffff-no-rj",
"twitter:app:url:iphone": "vnd.youtube://www.youtube.com/channel/UCq0imsn84ShAe9PBOFnoIrg",
"twitter:app:id:googleplay": "com.google.android.youtube",
"theme-color": "rgb(255, 255, 255)",
"og:image:width": "900",
"twitter:card": "summary",
"og:site_name": "YouTube",
"twitter:url": "https://www.youtube.com/channel/UCq0imsn84ShAe9PBOFnoIrg",
"twitter:app:url:ipad": "vnd.youtube://www.youtube.com/channel/UCq0imsn84ShAe9PBOFnoIrg",
"al:android:package": "com.google.android.youtube",
"twitter:app:name:googleplay": "YouTube",
"al:ios:url": "vnd.youtube://www.youtube.com/channel/UCq0imsn84ShAe9PBOFnoIrg",
"twitter:app:id:iphone": "544007664",
"og:description": "Welcome to Control Systems Lectures! This collection of videos is intended to supplement a first year controls class, not replace it. My goal is to take specific concepts in controls and expand on them in order to provide an intuitive understanding which will ultimately make you a better controls engineer. \n\nI'm glad you made it to my channel and I hope you find it useful.\n\nShoot me a message at controlsystemlectures@gmail.com, leave a comment or question and I'll get back to you if I can. Don't forget to subscribe!\n \nTwitter: @BrianBDouglas for engineering tweets and announcement of new videos.\nWebpage: http://engineeringmedia.com\n\nHere is the hardware/software I use: http://www.youtube.com/watch?v=m-M5_mIyHe4\n\nHere's a list of my favorite references: http://bit.ly/2skvmWd\n\n--Brian",
"al:ios:app_store_id": "544007664",
"twitter:image": "https://yt3.googleusercontent.com/ytc/AIdro_nLo68wetImbwGUYP3stve_iKmAEccjhqB-q4o79xdInN4=s900-c-k-c0x00ffffff-no-rj",
"twitter:site": "@youtube",
"og:type": "profile",
"twitter:title": "Brian Douglas",
"al:ios:app_name": "YouTube",
"og:title": "Brian Douglas",
"og:image:height": "900",
"twitter:app:id:ipad": "544007664",
"al:web:url": "https://www.youtube.com/channel/UCq0imsn84ShAe9PBOFnoIrg?feature=applinks",
"al:android:url": "https://www.youtube.com/channel/UCq0imsn84ShAe9PBOFnoIrg?feature=applinks",
"fb:app_id": "87741124305",
"twitter:app:url:googleplay": "https://www.youtube.com/channel/UCq0imsn84ShAe9PBOFnoIrg",
"twitter:app:name:ipad": "YouTube",
"viewport": "width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no,",
"twitter:description": "Welcome to Control Systems Lectures! This collection of videos is intended to supplement a first year controls class, not replace it. My goal is to take specific concepts in controls and expand on them in order to provide an intuitive understanding which will ultimately make you a better controls engineer. \n\nI'm glad you made it to my channel and I hope you find it useful.\n\nShoot me a message at controlsystemlectures@gmail.com, leave a comment or question and I'll get back to you if I can. Don't forget to subscribe!\n \nTwitter: @BrianBDouglas for engineering tweets and announcement of new videos.\nWebpage: http://engineeringmedia.com\n\nHere is the hardware/software I use: http://www.youtube.com/watch?v=m-M5_mIyHe4\n\nHere's a list of my favorite references: http://bit.ly/2skvmWd\n\n--Brian",
"og:url": "https://www.youtube.com/channel/UCq0imsn84ShAe9PBOFnoIrg",
"al:android:app_name": "YouTube",
"twitter:app:name:iphone": "YouTube"
}
],
"cse_image": [
{
"src": "https://yt3.googleusercontent.com/ytc/AIdro_nLo68wetImbwGUYP3stve_iKmAEccjhqB-q4o79xdInN4=s900-c-k-c0x00ffffff-no-rj"
}
]
}
},
{
"kind": "customsearch#result",
"title": "Lecture - Wikipedia",
"htmlTitle": "\u003cb\u003eLecture\u003c/b\u003e - Wikipedia",
"link": "https://en.wikipedia.org/wiki/Lecture",
"displayLink": "en.wikipedia.org",
"snippet": "Lecture ... For the academic rank, see Lecturer. A lecture (from Latin: lēctūra 'reading') is an oral presentation intended to present information or teach people ...",
"htmlSnippet": "\u003cb\u003eLecture\u003c/b\u003e ... For the academic rank, see \u003cb\u003eLecturer\u003c/b\u003e. A \u003cb\u003electure\u003c/b\u003e (from Latin: lēctūra &#39;reading&#39;) is an oral presentation intended to present information or teach people&nbsp;...",
"cacheId": "d9Pjta02fmgJ",
"formattedUrl": "https://en.wikipedia.org/wiki/Lecture",
"htmlFormattedUrl": "https://en.wikipedia.org/wiki/Lecture",
"pagemap": {
"metatags": [
{
"referrer": "origin",
"og:image": "https://upload.wikimedia.org/wikipedia/commons/thumb/2/26/ADFA_Lecture_Theatres.jpg/1200px-ADFA_Lecture_Theatres.jpg",
"theme-color": "#eaecf0",
"og:image:width": "1200",
"og:type": "website",
"viewport": "width=device-width, initial-scale=1.0, user-scalable=yes, minimum-scale=0.25, maximum-scale=5.0",
"og:title": "Lecture - Wikipedia",
"og:image:height": "799",
"format-detection": "telephone=no"
}
]
}
},
{
"kind": "customsearch#result",
"title": "Mount Wilson Observatory | Lectures",
"htmlTitle": "Mount Wilson Observatory | \u003cb\u003eLectures\u003c/b\u003e",
"link": "https://www.mtwilson.edu/lectures/",
"displayLink": "www.mtwilson.edu",
"snippet": "Talks & Telescopes: August 24, 2024 Panel: The Triumph of Hubble ... Compelling talks followed by picnicking and convivial stargazing through both the big ...",
"htmlSnippet": "Talks &amp; Telescopes: August 24, 2024 Panel: The Triumph of Hubble ... Compelling talks followed by picnicking and convivial stargazing through both the big&nbsp;...",
"cacheId": "wdXI0azqx5UJ",
"formattedUrl": "https://www.mtwilson.edu/lectures/",
"htmlFormattedUrl": "https://www.mtwilson.edu/\u003cb\u003electures\u003c/b\u003e/",
"pagemap": {
"metatags": [
{
"viewport": "width=device-width,initial-scale=1,user-scalable=no"
}
],
"webpage": [
{
"image": "http://www.mtwilson.edu/wp-content/uploads/2016/09/Logo.jpg",
"url": "https://www.facebook.com/WilsonObs"
}
]
}
},
{
"kind": "customsearch#result",
"title": "Lectures | NBER",
"htmlTitle": "\u003cb\u003eLectures\u003c/b\u003e | NBER",
"link": "https://www.nber.org/research/lectures",
"displayLink": "www.nber.org",
"snippet": "Results 1 - 50 of 354 ... Among featured events at the NBER Summer Institute are the Martin Feldstein Lecture, which examines a current issue involving economic ...",
"htmlSnippet": "Results 1 - 50 of 354 \u003cb\u003e...\u003c/b\u003e Among featured events at the NBER Summer Institute are the Martin Feldstein \u003cb\u003eLecture\u003c/b\u003e, which examines a current issue involving economic&nbsp;...",
"cacheId": "CvvP3U3nb44J",
"formattedUrl": "https://www.nber.org/research/lectures",
"htmlFormattedUrl": "https://www.nber.org/research/\u003cb\u003electures\u003c/b\u003e",
"pagemap": {
"cse_thumbnail": [
{
"src": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcTmeViEZyV1YmFEFLhcA6WdgAG3v3RV6tB93ncyxSJ5JPst_p2aWrL7D1k&s",
"width": "310",
"height": "163"
}
],
"metatags": [
{
"og:image": "https://www.nber.org/sites/default/files/2022-06/NBER-FB-Share-Tile-1200.jpg",
"og:site_name": "NBER",
"handheldfriendly": "true",
"viewport": "width=device-width, initial-scale=1.0",
"og:title": "Lectures",
"mobileoptimized": "width",
"og:url": "https://www.nber.org/research/lectures"
}
],
"cse_image": [
{
"src": "https://www.nber.org/sites/default/files/2022-06/NBER-FB-Share-Tile-1200.jpg"
}
]
}
},
{
"kind": "customsearch#result",
"title": "STUDENTS CANNOT ACCESS RECORDED LECTURES ... - Solved",
"htmlTitle": "STUDENTS CANNOT ACCESS RECORDED LECTURES ... - Solved",
"link": "https://community.canvaslms.com/t5/Canvas-Question-Forum/STUDENTS-CANNOT-ACCESS-RECORDED-LECTURES/td-p/190358",
"displayLink": "community.canvaslms.com",
"snippet": "Mar 19, 2020 ... I believe the issue is that students were not invited. Are you trying to capture your screen? If not, there is an option to just record your web ...",
"htmlSnippet": "Mar 19, 2020 \u003cb\u003e...\u003c/b\u003e I believe the issue is that students were not invited. Are you trying to capture your screen? If not, there is an option to just record your web&nbsp;...",
"cacheId": "wqrynQXX61sJ",
"formattedUrl": "https://community.canvaslms.com/t5/Canvas...LECTURES/td-p/190358",
"htmlFormattedUrl": "https://community.canvaslms.com/t5/Canvas...\u003cb\u003eLECTURES\u003c/b\u003e/td-p/190358",
"pagemap": {
"cse_thumbnail": [
{
"src": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcRUqXau3N8LfKgSD7OJOvV7xzGarLKRU-ckWXy1ZQ1p4CLPsedvLKmLMhk&s",
"width": "310",
"height": "163"
}
],
"metatags": [
{
"og:image": "https://community.canvaslms.com/html/@6A1FDD4D5FF35E4BBB4083A1022FA0DB/assets/CommunityPreview23.png",
"og:type": "article",
"article:section": "Canvas Question Forum",
"article:published_time": "2020-03-19T15:50:03.409Z",
"og:site_name": "Instructure Community",
"article:modified_time": "2020-03-19T13:55:53-07:00",
"viewport": "width=device-width, initial-scale=1.0, user-scalable=yes",
"og:title": "STUDENTS CANNOT ACCESS RECORDED LECTURES",
"og:url": "https://community.canvaslms.com/t5/Canvas-Question-Forum/STUDENTS-CANNOT-ACCESS-RECORDED-LECTURES/m-p/190358#M93667",
"og:description": "I can access and see my recorded lectures but my students can't. They have an error message when they try to open the recorded presentation or notes.",
"article:author": "https://community.canvaslms.com/t5/user/viewprofilepage/user-id/794287",
"twitter:image": "https://community.canvaslms.com/html/@6A1FDD4D5FF35E4BBB4083A1022FA0DB/assets/CommunityPreview23.png"
}
],
"cse_image": [
{
"src": "https://community.canvaslms.com/html/@6A1FDD4D5FF35E4BBB4083A1022FA0DB/assets/CommunityPreview23.png"
}
]
}
},
{
"kind": "customsearch#result",
"title": "Public Lecture Series - Sam Fox School of Design & Visual Arts",
"htmlTitle": "Public \u003cb\u003eLecture\u003c/b\u003e Series - Sam Fox School of Design &amp; Visual Arts",
"link": "https://samfoxschool.wustl.edu/calendar/series/2-public-lecture-series",
"displayLink": "samfoxschool.wustl.edu",
"snippet": "The Sam Fox School's Spring 2024 Public Lecture Series highlights design and art as catalysts for change. Renowned speakers will delve into themes like ...",
"htmlSnippet": "The Sam Fox School&#39;s Spring 2024 Public \u003cb\u003eLecture\u003c/b\u003e Series highlights design and art as catalysts for change. Renowned speakers will delve into themes like&nbsp;...",
"cacheId": "B-cgQG0j6tUJ",
"formattedUrl": "https://samfoxschool.wustl.edu/calendar/series/2-public-lecture-series",
"htmlFormattedUrl": "https://samfoxschool.wustl.edu/calendar/series/2-public-lecture-series",
"pagemap": {
"cse_thumbnail": [
{
"src": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQSmHaGianm-64m-qauYjkPK_Q0JKWe-7yom4m1ogFYTmpWArA7k6dmk0sR&s",
"width": "307",
"height": "164"
}
],
"website": [
{
"name": "Public Lecture Series - Sam Fox School of Design & Visual Arts — Washington University in St. Louis"
}
],
"metatags": [
{
"og:image": "https://dvsp0hlm0xrn3.cloudfront.net/assets/default_og_image-44e73dee4b9d1e2c6a6295901371270c8ec5899eaed48ee8167a9b12f1b0f8b3.jpg",
"og:type": "website",
"og:site_name": "Sam Fox School of Design & Visual Arts — Washington University in St. Louis",
"viewport": "width=device-width, initial-scale=1.0",
"og:title": "Public Lecture Series - Sam Fox School of Design & Visual Arts — Washington University in St. Louis",
"csrf-token": "jBQsfZGY3RH8NVs0-KVDBYB-2N2kib4UYZHYdrShfTdLkvzfSvGeOaMrRKTRdYBPRKzdcGIuP7zwm9etqX_uvg",
"csrf-param": "authenticity_token",
"og:description": "The Sam Fox School's Spring 2024 Public Lecture Series highlights design and art as catalysts for change. Renowned speakers will delve into themes like social equity, resilient cities, and the impact of emerging technologies on contemporary life. Speakers include artists, architects, designers, and critics of the highest caliber, widely recognized for their research-based practices and multidisciplinary approaches to their fields."
}
],
"cse_image": [
{
"src": "https://dvsp0hlm0xrn3.cloudfront.net/assets/default_og_image-44e73dee4b9d1e2c6a6295901371270c8ec5899eaed48ee8167a9b12f1b0f8b3.jpg"
}
]
}
}
]
}

View File

@@ -0,0 +1,476 @@
{
"query": "python",
"number_of_results": 116000000,
"results": [
{
"url": "https://www.python.org/",
"title": "Welcome to Python.org",
"content": "Python is a versatile and powerful language that lets you work quickly and integrate systems more effectively. Learn how to get started, download the latest version, access documentation, find jobs, and join the Python community.",
"engine": "bing",
"parsed_url": ["https", "www.python.org", "/", "", "", ""],
"template": "default.html",
"engines": ["bing", "qwant", "duckduckgo"],
"positions": [1, 1, 1],
"score": 9.0,
"category": "general"
},
{
"url": "https://wiki.nerdvpn.de/wiki/Python_(programming_language)",
"title": "Python (programming language) - Wikipedia",
"content": "Python is a high-level, general-purpose programming language. Its design philosophy emphasizes code readability with the use of significant indentation. Python is dynamically typed and garbage-collected. It supports multiple programming paradigms, including structured (particularly procedural), object-oriented and functional programming.",
"engine": "bing",
"parsed_url": ["https", "wiki.nerdvpn.de", "/wiki/Python_(programming_language)", "", "", ""],
"template": "default.html",
"engines": ["bing", "qwant", "duckduckgo"],
"positions": [4, 3, 2],
"score": 3.25,
"category": "general"
},
{
"url": "https://docs.python.org/3/tutorial/index.html",
"title": "The Python Tutorial \u2014 Python 3.12.3 documentation",
"content": "3 days ago \u00b7 Python is an easy to learn, powerful programming language. It has efficient high-level data structures and a simple but effective approach to object-oriented programming. Python\u2019s elegant syntax and dynamic typing, together with its interpreted nature, make it an ideal language for scripting and rapid application development in many \u2026",
"engine": "bing",
"parsed_url": ["https", "docs.python.org", "/3/tutorial/index.html", "", "", ""],
"template": "default.html",
"engines": ["bing", "qwant", "duckduckgo"],
"positions": [5, 5, 3],
"score": 2.2,
"category": "general"
},
{
"url": "https://www.python.org/downloads/",
"title": "Download Python | Python.org",
"content": "Python is a popular programming language for various purposes. Find the latest version of Python for different operating systems, download release notes, and learn about the development process.",
"engine": "bing",
"parsed_url": ["https", "www.python.org", "/downloads/", "", "", ""],
"template": "default.html",
"engines": ["bing", "duckduckgo"],
"positions": [2, 2],
"score": 2.0,
"category": "general"
},
{
"url": "https://www.python.org/about/gettingstarted/",
"title": "Python For Beginners | Python.org",
"content": "Learn the basics of Python, a popular and easy-to-use programming language, from installing it to using it for various purposes. Find out how to access online documentation, tutorials, books, code samples, and more resources to help you get started with Python.",
"engine": "bing",
"parsed_url": ["https", "www.python.org", "/about/gettingstarted/", "", "", ""],
"template": "default.html",
"engines": ["bing", "qwant", "duckduckgo"],
"positions": [9, 4, 4],
"score": 1.8333333333333333,
"category": "general"
},
{
"url": "https://www.python.org/shell/",
"title": "Welcome to Python.org",
"content": "Python is a versatile and easy-to-use programming language that lets you work quickly. Learn more about Python, download the latest version, access documentation, find jobs, and join the community.",
"engine": "bing",
"parsed_url": ["https", "www.python.org", "/shell/", "", "", ""],
"template": "default.html",
"engines": ["bing", "qwant", "duckduckgo"],
"positions": [3, 10, 8],
"score": 1.675,
"category": "general"
},
{
"url": "https://realpython.com/",
"title": "Python Tutorials \u2013 Real Python",
"content": "Real Python offers comprehensive and up-to-date tutorials, books, and courses for Python developers of all skill levels. Whether you want to learn Python basics, web development, data science, machine learning, or more, you can find clear and practical guides and code examples here.",
"engine": "bing",
"parsed_url": ["https", "realpython.com", "/", "", "", ""],
"template": "default.html",
"engines": ["bing", "qwant", "duckduckgo"],
"positions": [6, 6, 5],
"score": 1.6,
"category": "general"
},
{
"url": "https://wiki.nerdvpn.de/wiki/Python",
"title": "Python",
"content": "Topics referred to by the same term",
"engine": "wikipedia",
"parsed_url": ["https", "wiki.nerdvpn.de", "/wiki/Python", "", "", ""],
"template": "default.html",
"engines": ["wikipedia"],
"positions": [1],
"score": 1.0,
"category": "general"
},
{
"title": "Online Python - IDE, Editor, Compiler, Interpreter",
"content": "Online Python IDE is a free online tool that lets you write, execute, and share Python code in the web browser. Learn about Python, its features, and its popularity as a general-purpose programming language for web development, data science, and more.",
"url": "https://www.online-python.com/",
"engine": "duckduckgo",
"parsed_url": ["https", "www.online-python.com", "/", "", "", ""],
"template": "default.html",
"engines": ["qwant", "duckduckgo"],
"positions": [8, 6],
"score": 0.5833333333333333,
"category": "general"
},
{
"url": "https://micropython.org/",
"title": "MicroPython - Python for microcontrollers",
"content": "MicroPython is a full Python compiler and runtime that runs on the bare-metal. You get an interactive prompt (the REPL) to execute commands immediately, along ...",
"img_src": null,
"engine": "google",
"parsed_url": ["https", "micropython.org", "/", "", "", ""],
"template": "default.html",
"engines": ["google"],
"positions": [1],
"score": 1.0,
"category": "general"
},
{
"url": "https://dictionary.cambridge.org/uk/dictionary/english/python",
"title": "PYTHON | \u0417\u043d\u0430\u0447\u0435\u043d\u043d\u044f \u0432 \u0430\u043d\u0433\u043b\u0456\u0439\u0441\u044c\u043a\u0456\u0439 \u043c\u043e\u0432\u0456 - Cambridge Dictionary",
"content": "Apr 17, 2024 \u2014 \u0412\u0438\u0437\u043d\u0430\u0447\u0435\u043d\u043d\u044f PYTHON: 1. a very large snake that kills animals for food by wrapping itself around them and crushing them\u2026. \u0414\u0456\u0437\u043d\u0430\u0439\u0442\u0435\u0441\u044f \u0431\u0456\u043b\u044c\u0448\u0435.",
"img_src": null,
"engine": "google",
"parsed_url": [
"https",
"dictionary.cambridge.org",
"/uk/dictionary/english/python",
"",
"",
""
],
"template": "default.html",
"engines": ["google"],
"positions": [2],
"score": 0.5,
"category": "general"
},
{
"url": "https://www.codetoday.co.uk/code",
"title": "Web-based Python Editor (with Turtle graphics)",
"content": "Quick way of starting to write Python code, including drawing with Turtle, provided by CodeToday using Trinket.io Ideal for young children to start ...",
"img_src": null,
"engine": "google",
"parsed_url": ["https", "www.codetoday.co.uk", "/code", "", "", ""],
"template": "default.html",
"engines": ["google"],
"positions": [3],
"score": 0.3333333333333333,
"category": "general"
},
{
"url": "https://snapcraft.io/docs/python-plugin",
"title": "The python plugin | Snapcraft documentation",
"content": "The python plugin can be used by either Python 2 or Python 3 based parts using a setup.py script for building the project, or using a package published to ...",
"img_src": null,
"engine": "google",
"parsed_url": ["https", "snapcraft.io", "/docs/python-plugin", "", "", ""],
"template": "default.html",
"engines": ["google"],
"positions": [4],
"score": 0.25,
"category": "general"
},
{
"url": "https://www.developer-tech.com/categories/developer-languages/developer-languages-python/",
"title": "Latest Python Developer News",
"content": "Python's status as the primary language for AI and machine learning projects, from its extensive data-handling capabilities to its flexibility and ...",
"img_src": null,
"engine": "google",
"parsed_url": [
"https",
"www.developer-tech.com",
"/categories/developer-languages/developer-languages-python/",
"",
"",
""
],
"template": "default.html",
"engines": ["google"],
"positions": [5],
"score": 0.2,
"category": "general"
},
{
"url": "https://subjectguides.york.ac.uk/coding/python",
"title": "Coding: a Practical Guide - Python - Subject Guides",
"content": "Python is a coding language used for a wide range of things, including working with data, building systems and software, and even creating games.",
"img_src": null,
"engine": "google",
"parsed_url": ["https", "subjectguides.york.ac.uk", "/coding/python", "", "", ""],
"template": "default.html",
"engines": ["google"],
"positions": [6],
"score": 0.16666666666666666,
"category": "general"
},
{
"url": "https://hub.salford.ac.uk/psytech/python/getting-started-python/",
"title": "Getting Started - Python - Salford PsyTech Home - The Hub",
"content": "Python in itself is a very friendly programming language, when we get to grips with writing code, once you grasp the logic, it will become very intuitive.",
"img_src": null,
"engine": "google",
"parsed_url": [
"https",
"hub.salford.ac.uk",
"/psytech/python/getting-started-python/",
"",
"",
""
],
"template": "default.html",
"engines": ["google"],
"positions": [7],
"score": 0.14285714285714285,
"category": "general"
},
{
"url": "https://snapcraft.io/docs/python-apps",
"title": "Python apps | Snapcraft documentation",
"content": "Snapcraft can be used to package and distribute Python applications in a way that enables convenient installation by users. The process of creating a snap ...",
"img_src": null,
"engine": "google",
"parsed_url": ["https", "snapcraft.io", "/docs/python-apps", "", "", ""],
"template": "default.html",
"engines": ["google"],
"positions": [8],
"score": 0.125,
"category": "general"
},
{
"url": "https://anvil.works/",
"title": "Anvil | Build Web Apps with Nothing but Python",
"content": "Anvil is a free Python-based drag-and-drop web app builder.\u200eSign Up \u00b7 \u200eSign in \u00b7 \u200ePricing \u00b7 \u200eForum",
"img_src": null,
"engine": "google",
"parsed_url": ["https", "anvil.works", "/", "", "", ""],
"template": "default.html",
"engines": ["google"],
"positions": [9],
"score": 0.1111111111111111,
"category": "general"
},
{
"url": "https://docs.python.org/",
"title": "Python 3.12.3 documentation",
"content": "3 days ago \u00b7 This is the official documentation for Python 3.12.3. Documentation sections: What's new in Python 3.12? Or all \"What's new\" documents since Python 2.0. Tutorial. Start here: a tour of Python's syntax and features. Library reference. Standard library and builtins. Language reference.",
"engine": "bing",
"parsed_url": ["https", "docs.python.org", "/", "", "", ""],
"template": "default.html",
"engines": ["bing", "duckduckgo"],
"positions": [7, 13],
"score": 0.43956043956043955,
"category": "general"
},
{
"title": "How to Use Python: Your First Steps - Real Python",
"content": "Learn the basics of Python syntax, installation, error handling, and more in this tutorial. You'll also code your first Python program and test your knowledge with a quiz.",
"url": "https://realpython.com/python-first-steps/",
"engine": "duckduckgo",
"parsed_url": ["https", "realpython.com", "/python-first-steps/", "", "", ""],
"template": "default.html",
"engines": ["qwant", "duckduckgo"],
"positions": [14, 7],
"score": 0.42857142857142855,
"category": "general"
},
{
"title": "The Python Tutorial \u2014 Python 3.11.8 documentation",
"content": "This tutorial introduces the reader informally to the basic concepts and features of the Python language and system. It helps to have a Python interpreter handy for hands-on experience, but all examples are self-contained, so the tutorial can be read off-line as well. For a description of standard objects and modules, see The Python Standard ...",
"url": "https://docs.python.org/3.11/tutorial/",
"engine": "duckduckgo",
"parsed_url": ["https", "docs.python.org", "/3.11/tutorial/", "", "", ""],
"template": "default.html",
"engines": ["duckduckgo"],
"positions": [7],
"score": 0.14285714285714285,
"category": "general"
},
{
"url": "https://realpython.com/python-introduction/",
"title": "Introduction to Python 3 \u2013 Real Python",
"content": "Python programming language, including a brief history of the development of Python and reasons why you might select Python as your language of choice.",
"engine": "bing",
"parsed_url": ["https", "realpython.com", "/python-introduction/", "", "", ""],
"template": "default.html",
"engines": ["bing"],
"positions": [8],
"score": 0.125,
"category": "general"
},
{
"title": "Our Documentation | Python.org",
"content": "Find online or download Python's documentation, tutorials, and guides for beginners and advanced users. Learn how to port from Python 2 to Python 3, contribute to Python, and access Python videos and books.",
"url": "https://www.python.org/doc/",
"engine": "duckduckgo",
"parsed_url": ["https", "www.python.org", "/doc/", "", "", ""],
"template": "default.html",
"engines": ["duckduckgo"],
"positions": [9],
"score": 0.1111111111111111,
"category": "general"
},
{
"title": "Welcome to Python.org",
"url": "http://www.get-python.org/shell/",
"content": "The mission of the Python Software Foundation is to promote, protect, and advance the Python programming language, and to support and facilitate the growth of a diverse and international community of Python programmers. Learn more. Become a Member Donate to the PSF.",
"engine": "qwant",
"parsed_url": ["http", "www.get-python.org", "/shell/", "", "", ""],
"template": "default.html",
"engines": ["qwant"],
"positions": [9],
"score": 0.1111111111111111,
"category": "general"
},
{
"title": "About Python\u2122 | Python.org",
"content": "Python is a powerful, fast, and versatile programming language that runs on various platforms and is easy to learn. Learn how to get started, explore the applications, and join the community of Python programmers and users.",
"url": "https://www.python.org/about/",
"engine": "duckduckgo",
"parsed_url": ["https", "www.python.org", "/about/", "", "", ""],
"template": "default.html",
"engines": ["duckduckgo"],
"positions": [11],
"score": 0.09090909090909091,
"category": "general"
},
{
"title": "Online Python Compiler (Interpreter) - Programiz",
"content": "Write and run Python code using this online tool. You can use Python Shell like IDLE, and take inputs from the user in our Python compiler.",
"url": "https://www.programiz.com/python-programming/online-compiler/",
"engine": "duckduckgo",
"parsed_url": [
"https",
"www.programiz.com",
"/python-programming/online-compiler/",
"",
"",
""
],
"template": "default.html",
"engines": ["duckduckgo"],
"positions": [12],
"score": 0.08333333333333333,
"category": "general"
},
{
"title": "Welcome to Python.org",
"content": "Python is a versatile and powerful language that lets you work quickly and integrate systems more effectively. Download the latest version, read the documentation, find jobs, events, success stories, and more on Python.org.",
"url": "https://www.python.org/?downloads",
"engine": "duckduckgo",
"parsed_url": ["https", "www.python.org", "/", "", "downloads", ""],
"template": "default.html",
"engines": ["duckduckgo"],
"positions": [15],
"score": 0.06666666666666667,
"category": "general"
},
{
"url": "https://www.matillion.com/blog/the-importance-of-python-and-its-growing-influence-on-data-productivty-a-matillion-perspective",
"title": "The Importance of Python and its Growing Influence on ...",
"content": "Jan 30, 2024 \u2014 The synergy of low-code functionality with Python's versatility empowers data professionals to orchestrate complex transformations seamlessly.",
"img_src": null,
"engine": "google",
"parsed_url": [
"https",
"www.matillion.com",
"/blog/the-importance-of-python-and-its-growing-influence-on-data-productivty-a-matillion-perspective",
"",
"",
""
],
"template": "default.html",
"engines": ["google"],
"positions": [10],
"score": 0.1,
"category": "general"
},
{
"title": "BeginnersGuide - Python Wiki",
"content": "This is the program that reads Python programs and carries out their instructions; you need it before you can do any Python programming. Mac and Linux distributions may include an outdated version of Python (Python 2), but you should install an updated one (Python 3). See BeginnersGuide/Download for instructions to download the correct version ...",
"url": "https://wiki.python.org/moin/BeginnersGuide",
"engine": "duckduckgo",
"parsed_url": ["https", "wiki.python.org", "/moin/BeginnersGuide", "", "", ""],
"template": "default.html",
"engines": ["duckduckgo"],
"positions": [16],
"score": 0.0625,
"category": "general"
},
{
"title": "Learn Python - Free Interactive Python Tutorial",
"content": "Learn Python from scratch or improve your skills with this website that offers tutorials, exercises, tests and certification. Explore topics such as basics, data science, advanced features and more with DataCamp.",
"url": "https://www.learnpython.org/",
"engine": "duckduckgo",
"parsed_url": ["https", "www.learnpython.org", "/", "", "", ""],
"template": "default.html",
"engines": ["duckduckgo"],
"positions": [17],
"score": 0.058823529411764705,
"category": "general"
}
],
"answers": [],
"corrections": [],
"infoboxes": [
{
"infobox": "Python",
"id": "https://en.wikipedia.org/wiki/Python_(programming_language)",
"content": "general-purpose programming language",
"img_src": "https://upload.wikimedia.org/wikipedia/commons/thumb/6/6f/.PY_file_recreation.png/500px-.PY_file_recreation.png",
"urls": [
{
"title": "Official website",
"url": "https://www.python.org/",
"official": true
},
{
"title": "Wikipedia (en)",
"url": "https://en.wikipedia.org/wiki/Python_(programming_language)"
},
{
"title": "Wikidata",
"url": "http://www.wikidata.org/entity/Q28865"
}
],
"attributes": [
{
"label": "Inception",
"value": "Wednesday, February 20, 1991",
"entity": "P571"
},
{
"label": "Developer",
"value": "Python Software Foundation, Guido van Rossum",
"entity": "P178"
},
{
"label": "Copyright license",
"value": "Python Software Foundation License",
"entity": "P275"
},
{
"label": "Programmed in",
"value": "C, Python",
"entity": "P277"
},
{
"label": "Software version identifier",
"value": "3.12.3, 3.13.0a6",
"entity": "P348"
}
],
"engine": "wikidata",
"engines": ["wikidata"]
}
],
"suggestions": [
"python turtle",
"micro python tutorial",
"python docs",
"python compiler",
"snapcraft python",
"micropython vs python",
"python online",
"python download"
],
"unresponsive_engines": []
}

View File

@@ -0,0 +1,190 @@
{
"searchParameters": {
"q": "apple inc",
"gl": "us",
"hl": "en",
"autocorrect": true,
"page": 1,
"type": "search"
},
"knowledgeGraph": {
"title": "Apple",
"type": "Technology company",
"website": "http://www.apple.com/",
"imageUrl": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQwGQRv5TjjkycpctY66mOg_e2-npacrmjAb6_jAWhzlzkFE3OTjxyzbA&s=0",
"description": "Apple Inc. is an American multinational technology company specializing in consumer electronics, software and online services headquartered in Cupertino, California, United States.",
"descriptionSource": "Wikipedia",
"descriptionLink": "https://en.wikipedia.org/wiki/Apple_Inc.",
"attributes": {
"Headquarters": "Cupertino, CA",
"CEO": "Tim Cook (Aug 24, 2011)",
"Founded": "April 1, 1976, Los Altos, CA",
"Sales": "1 (800) 692-7753",
"Products": "iPhone, Apple Watch, iPad, and more",
"Founders": "Steve Jobs, Steve Wozniak, and Ronald Wayne",
"Subsidiaries": "Apple Store, Beats Electronics, Beddit, and more"
}
},
"organic": [
{
"title": "Apple",
"link": "https://www.apple.com/",
"snippet": "Discover the innovative world of Apple and shop everything iPhone, iPad, Apple Watch, Mac, and Apple TV, plus explore accessories, entertainment, ...",
"sitelinks": [
{
"title": "Support",
"link": "https://support.apple.com/"
},
{
"title": "iPhone",
"link": "https://www.apple.com/iphone/"
},
{
"title": "Apple makes business better.",
"link": "https://www.apple.com/business/"
},
{
"title": "Mac",
"link": "https://www.apple.com/mac/"
}
],
"position": 1
},
{
"title": "Apple Inc. - Wikipedia",
"link": "https://en.wikipedia.org/wiki/Apple_Inc.",
"snippet": "Apple Inc. is an American multinational technology company specializing in consumer electronics, software and online services headquartered in Cupertino, ...",
"attributes": {
"Products": "AirPods; Apple Watch; iPad; iPhone; Mac",
"Founders": "Steve Jobs; Steve Wozniak; Ronald Wayne",
"Founded": "April 1, 1976; 46 years ago in Los Altos, California, U.S",
"Industry": "Consumer electronics; Software services; Online services"
},
"sitelinks": [
{
"title": "History",
"link": "https://en.wikipedia.org/wiki/History_of_Apple_Inc."
},
{
"title": "Timeline of Apple Inc. products",
"link": "https://en.wikipedia.org/wiki/Timeline_of_Apple_Inc._products"
},
{
"title": "List of software by Apple Inc.",
"link": "https://en.wikipedia.org/wiki/List_of_software_by_Apple_Inc."
},
{
"title": "Apple Store",
"link": "https://en.wikipedia.org/wiki/Apple_Store"
}
],
"position": 2
},
{
"title": "Apple Inc. | History, Products, Headquarters, & Facts | Britannica",
"link": "https://www.britannica.com/topic/Apple-Inc",
"snippet": "Apple Inc., formerly Apple Computer, Inc., American manufacturer of personal computers, smartphones, tablet computers, computer peripherals, ...",
"date": "Aug 31, 2022",
"attributes": {
"Related People": "Steve Jobs Steve Wozniak Jony Ive Tim Cook Angela Ahrendts",
"Date": "1976 - present",
"Areas Of Involvement": "peripheral device"
},
"position": 3
},
{
"title": "AAPL: Apple Inc Stock Price Quote - NASDAQ GS - Bloomberg.com",
"link": "https://www.bloomberg.com/quote/AAPL:US",
"snippet": "Stock analysis for Apple Inc (AAPL:NASDAQ GS) including stock price, stock chart, company news, key statistics, fundamentals and company profile.",
"position": 4
},
{
"title": "Apple Inc. (AAPL) Company Profile & Facts - Yahoo Finance",
"link": "https://finance.yahoo.com/quote/AAPL/profile/",
"snippet": "Apple Inc. designs, manufactures, and markets smartphones, personal computers, tablets, wearables, and accessories worldwide. It also sells various related ...",
"position": 5
},
{
"title": "AAPL | Apple Inc. Stock Price & News - WSJ",
"link": "https://www.wsj.com/market-data/quotes/AAPL",
"snippet": "Apple, Inc. engages in the design, manufacture, and sale of smartphones, personal computers, tablets, wearables and accessories, and other varieties of ...",
"position": 6
},
{
"title": "Apple Inc Company Profile - Apple Inc Overview - GlobalData",
"link": "https://www.globaldata.com/company-profile/apple-inc/",
"snippet": "Apple Inc (Apple) designs, manufactures, and markets smartphones, tablets, personal computers (PCs), portable and wearable devices. The company also offers ...",
"position": 7
},
{
"title": "Apple Inc (AAPL) Stock Price & News - Google Finance",
"link": "https://www.google.com/finance/quote/AAPL:NASDAQ?hl=en",
"snippet": "Get the latest Apple Inc (AAPL) real-time quote, historical performance, charts, and other financial information to help you make more informed trading and ...",
"position": 8
}
],
"peopleAlsoAsk": [
{
"question": "What does Apple Inc mean?",
"snippet": "Apple Inc., formerly Apple Computer, Inc., American manufacturer of personal\ncomputers, smartphones, tablet computers, computer peripherals, and computer\nsoftware. It was the first successful personal computer company and the\npopularizer of the graphical user interface.\nAug 31, 2022",
"title": "Apple Inc. | History, Products, Headquarters, & Facts | Britannica",
"link": "https://www.britannica.com/topic/Apple-Inc"
},
{
"question": "Is Apple and Apple Inc same?",
"snippet": "Apple was founded as Apple Computer Company on April 1, 1976, by Steve Jobs,\nSteve Wozniak and Ronald Wayne to develop and sell Wozniak's Apple I personal\ncomputer. It was incorporated by Jobs and Wozniak as Apple Computer, Inc.",
"title": "Apple Inc. - Wikipedia",
"link": "https://en.wikipedia.org/wiki/Apple_Inc."
},
{
"question": "Who owns Apple Inc?",
"snippet": "Apple Inc. is owned by two main institutional investors (Vanguard Group and\nBlackRock, Inc). While its major individual shareholders comprise people like\nArt Levinson, Tim Cook, Bruce Sewell, Al Gore, Johny Sroujli, and others.",
"title": "Who Owns Apple In 2022? - FourWeekMBA",
"link": "https://fourweekmba.com/who-owns-apple/"
},
{
"question": "What products does Apple Inc offer?",
"snippet": "APPLE FOOTER\nStore.\nMac.\niPad.\niPhone.\nWatch.\nAirPods.\nTV & Home.\nAirTag.",
"title": "More items...",
"link": "https://www.apple.com/business/"
}
],
"relatedSearches": [
{
"query": "Who invented the iPhone"
},
{
"query": "Apple Inc competitors"
},
{
"query": "Apple iPad"
},
{
"query": "iPhones"
},
{
"query": "Apple Inc us"
},
{
"query": "Apple company history"
},
{
"query": "Apple Store"
},
{
"query": "Apple customer service"
},
{
"query": "Apple Watch"
},
{
"query": "Apple Inc Industry"
},
{
"query": "Apple Inc registered address"
},
{
"query": "Apple Inc Bloomberg"
}
]
}

View File

@@ -0,0 +1,276 @@
{
"request": {
"success": true,
"total_time_taken": 3.4,
"processed_timestamp": 1714968442,
"search_url": "http://www.google.com/search?q=mcdonalds\u0026gl=us\u0026hl=en\u0026safe=0\u0026num=10"
},
"search_parameters": {
"engine": "google",
"type": "web",
"device": "desktop",
"auto_location": "1",
"google_domain": "google.com",
"gl": "us",
"hl": "en",
"safe": "0",
"news_type": "all",
"exclude_autocorrected_results": "0",
"images_color": "any",
"page": "1",
"num": "10",
"output": "json",
"csv_fields": "search_parameters.query,organic_results.position,organic_results.title,organic_results.url,organic_results.domain",
"query": "mcdonalds",
"action": "search",
"access_key": "aac48e007e15c532bb94ffb34532a4b2",
"error": {}
},
"search_information": {
"total_results": 1170000000,
"time_taken_displayed": 0.49,
"detected_location": {},
"did_you_mean": {},
"no_results_for_original_query": false,
"showing_results_for": {}
},
"organic_results": [
{
"position": 1,
"title": "Our Full McDonald\u0027s Food Menu",
"snippet": "",
"prerender": false,
"cached_page_url": {},
"related_pages_url": {},
"url": "https://www.mcdonalds.com/us/en-us/full-menu.html",
"domain": "www.mcdonalds.com",
"displayed_url": "https://www.mcdonalds.com \u203a en-us \u203a full-menu"
},
{
"position": 2,
"title": "McDonald\u0027s",
"snippet": "McDonald\u0027s is the world\u0027s largest fast food restaurant chain, serving over 69 million customers daily in over 100 countries in more than 40,000 outlets as of\u00a0...",
"prerender": false,
"cached_page_url": {},
"related_pages_url": {},
"url": "https://en.wikipedia.org/wiki/McDonald%27s",
"domain": "en.wikipedia.org",
"displayed_url": "https://en.wikipedia.org \u203a wiki \u203a McDonald\u0027s"
},
{
"position": 3,
"title": "Restaurants Near Me: Nearby McDonald\u0027s Locations",
"snippet": "",
"prerender": false,
"cached_page_url": {},
"related_pages_url": {},
"url": "https://www.mcdonalds.com/us/en-us/restaurant-locator.html",
"domain": "www.mcdonalds.com",
"displayed_url": "https://www.mcdonalds.com \u203a en-us \u203a restaurant-locator"
},
{
"position": 4,
"title": "Download the McDonald\u0027s App: Deals, Promotions \u0026 ...",
"snippet": "Download the McDonald\u0027s app for Mobile Order \u0026 Pay, exclusive deals and coupons, menu information and special promotions.",
"prerender": false,
"cached_page_url": {},
"related_pages_url": {},
"url": "https://www.mcdonalds.com/us/en-us/download-app.html",
"domain": "www.mcdonalds.com",
"displayed_url": "https://www.mcdonalds.com \u203a en-us \u203a download-app"
},
{
"position": 5,
"title": "McDonald\u0027s Restaurant Careers in the US",
"snippet": "McDonald\u0027s restaurant jobs are one-of-a-kind \u2013 just like you. Restaurants are hiring across all levels, from Crew team to Management. Apply today!",
"prerender": false,
"cached_page_url": {},
"related_pages_url": {},
"url": "https://jobs.mchire.com/",
"domain": "jobs.mchire.com",
"displayed_url": "https://jobs.mchire.com"
}
],
"inline_images": [
{
"image_url": "https://serpstack-assets.apilayer.net/2418910010831954152.png",
"title": ""
}
],
"local_results": [
{
"position": 1,
"title": "McDonald\u0027s",
"coordinates": {
"latitude": 0,
"longitude": 0
},
"address": "",
"rating": 0,
"reviews": 0,
"type": "",
"price": {},
"url": 0
},
{
"position": 2,
"title": "McDonald\u0027s",
"coordinates": {
"latitude": 0,
"longitude": 0
},
"address": "",
"rating": 0,
"reviews": 0,
"type": "",
"price": {},
"url": 0
},
{
"position": 3,
"title": "McDonald\u0027s",
"coordinates": {
"latitude": 0,
"longitude": 0
},
"address": "",
"rating": 0,
"reviews": 0,
"type": "",
"price": {},
"url": 0
}
],
"top_stories": [
{
"block_position": 1,
"title": "Menu nutrition",
"url": "/search?safe=0\u0026sca_esv=c9c7fd42856085e2\u0026sca_upv=1\u0026gl=us\u0026hl=en\u0026q=mcdonald%27s+double+quarter+pounder+with+cheese\u0026stick=H4sIAAAAAAAAAONgFuLUz9U3ME-vLDBX4tVP1zc0TCsuNE0ytjTTUs5OttJPy89P0c9NzSuNLyjKL8tMSS2yAvNS80qKMlOLF7Hq5ian5Ocl5qSoFyuk5Jcm5aQqFJYmFpWkFikU5JfmATUolGeWZCgkZ6SmFqcCAM4ilJtxAAAA\u0026sa=X\u0026ved=2ahUKEwjF55alk_iFAxXlamwGHbqgAs4Qri56BAh0EAM",
"source": "",
"uploaded": "",
"uploaded_utc": "2024-05-06T04:07:22.082Z"
},
{
"block_position": 2,
"title": "Profiles",
"url": "https://www.instagram.com/McDonalds",
"source": "",
"uploaded": "",
"uploaded_utc": "2024-05-06T04:07:22.082Z"
},
{
"block_position": 3,
"title": "People also search for",
"url": "/search?safe=0\u0026sca_esv=c9c7fd42856085e2\u0026sca_upv=1\u0026gl=us\u0026hl=en\u0026si=ACC90nzx_D3_zUKRnpAjmO0UBLNxnt7EyN4YYdru6U3bxLI-L5Wg8IL2sxPFxxcDEhVbocy-LJPZIvZySijw0ho2hfZ-KtV-sSEEJ9lw7JuEkXHDnRK5y4Dm8aqbiLwugbLbslwjG3hO_gpDTFZK2VoUGZPy2nrmOBCy0G3PoOfoiEtct2GSZlUz0uufG-xP8emtNzQKQpvjkAm5Zmi57iVZueiD62upz7-x2N3dAbwtm6FkInAPRw1yR91zuT7F3lEaPblTW3LaRwCDC0bvaRCh9x4N9zHgY1OOQa_rzts2jf5WpXcuw4Y%3D\u0026q=Burger+King\u0026sa=X\u0026ved=2ahUKEwjF55alk_iFAxXlamwGHbqgAs4Qs9oBKAB6BAhzEAI",
"source": "",
"uploaded": "",
"uploaded_utc": "2024-05-06T04:07:22.082Z"
}
],
"related_questions": [
{
"question": "What\u0027s a number 7 at McDonald\u0027s?What\u0027s a number 7 at McDonald\u0027s?What\u0027s a number 7 at McDonald\u0027s?",
"answer": "",
"title": "",
"displayed_url": ""
},
{
"question": "Why is McDonald\u0027s changing their name?Why is McDonald\u0027s changing their name?Why is McDonald\u0027s changing their name?",
"answer": "",
"title": "",
"displayed_url": ""
},
{
"question": "What is the oldest still running Mcdonalds?What is the oldest still running Mcdonalds?What is the oldest still running Mcdonalds?",
"answer": "",
"title": "",
"displayed_url": ""
},
{
"question": "Why is McDonald\u0027s now WcDonald\u0027s?Why is McDonald\u0027s now WcDonald\u0027s?Why is McDonald\u0027s now WcDonald\u0027s?",
"answer": "",
"title": "",
"displayed_url": ""
}
],
"knowledge_graph": {
"title": "",
"type": "Fast-food restaurant company",
"image_urls": ["https://serpstack-assets.apilayer.net/2418910010831954152.png"],
"description": "McDonald\u0027s Corporation is an American multinational fast food chain, founded in 1940 as a restaurant operated by Richard and Maurice McDonald, in San Bernardino, California, United States.",
"source": {
"name": "Wikipedia",
"url": "https://en.wikipedia.org/wiki/McDonald\u0027s"
},
"people_also_search_for": [],
"known_attributes": [
{
"attribute": "kc:/business/business_operation:founder",
"link": "http://www.google.com/search?safe=0\u0026sca_esv=c9c7fd42856085e2\u0026sca_upv=1\u0026gl=us\u0026hl=en\u0026q=Ray+Kroc\u0026si=ACC90nzx_D3_zUKRnpAjmO0UBLNxnt7EyN4YYdru6U3bxLI-LxARWRdbk5SkoY2sDn5Qq7yOmqYGei6qZ7sfJhsjZXBPgjMlLbS7824rpJOm69GzqVWMdoNIZiFX2T4A2td14sZOn4a1BexZLtZXHU7NZdF6VsWbGMVuiSYtXdev7uaUjEJKumiwlqTAATTebOriYTEBuSzC\u0026sa=X\u0026ved=2ahUKEwjF55alk_iFAxXlamwGHbqgAs4QmxMoAHoECHgQAg",
"name": "Founder: ",
"value": "Ray Kroc"
},
{
"attribute": "kc:/organization/organization:ceo",
"link": "http://www.google.com/search?safe=0\u0026sca_esv=c9c7fd42856085e2\u0026sca_upv=1\u0026gl=us\u0026hl=en\u0026q=Chris+Kempczinski\u0026si=ACC90nwLLwns5sISZcdzuISy7t-NHozt8Cbt6G3WNQfC9ekAgKFbjdEFCDgxLbt57EDZGosYDGiZuq1AcBhA6IhTOSZxfVSySuGQ3VDwmmTA7Z93n3K3596jAuZH9VVv5h8PyvKJSuGuSsQWviJTl3eKj2UL1ZIWuDgkjyVMnC47rN7j0G9PlHRCCLdQF7VDQ1gubTiC4onXqLRBTbwAj6a--PD6Jv_NoA%3D%3D\u0026sa=X\u0026ved=2ahUKEwjF55alk_iFAxXlamwGHbqgAs4QmxMoAHoECHUQAg",
"name": "CEO: ",
"value": "Chris Kempczinski (Nov 1, 2019\u2013)"
},
{
"attribute": "kc:/business/employer:revenue",
"link": "",
"name": "Revenue: ",
"value": "25.49\u00a0billion USD (2023)"
},
{
"attribute": "kc:/organization/organization:founded",
"link": "http://www.google.com/search?safe=0\u0026sca_esv=c9c7fd42856085e2\u0026sca_upv=1\u0026gl=us\u0026hl=en\u0026q=Des+Plaines\u0026si=ACC90nyvvWro6QmnyY1IfSdgk5wwjB1r8BGd_IWRjXqmKPQqm_yqLtI_DBi5PXGOtg_Z3qrzzEP6mcih1nN7h5A7v6OefnEJiC7a8dBR-v9LxlRubfyR6vlMr3fZ3TmVKWwz9FRpvZb1eYNt-RM7KIDKQlwGEIgINvzhxjUrv6uxSmceduzxd8W7Pkz71XGwxF0F8OlSzHlx\u0026sa=X\u0026ved=2ahUKEwjF55alk_iFAxXlamwGHbqgAs4QmxMoAHoECG4QAg",
"name": "Founded: ",
"value": "April 15, 1955, Des Plaines, IL"
},
{
"attribute": "kc:/organization/organization:headquarters",
"link": "http://www.google.com/search?safe=0\u0026sca_esv=c9c7fd42856085e2\u0026sca_upv=1\u0026gl=us\u0026hl=en\u0026q=Chicago\u0026si=ACC90nyvvWro6QmnyY1IfSdgk5wwjB1r8BGd_IWRjXqmKPQqm-46AEJ_kJbUIEvsvEEZqteiYJvXVXs2ScRNDvFFpjfeAaW3dxtpTGCgcsf5RMdi6IdzOdtjJMN3ZaFwqZOmdi7tC6r0Mh1O9bnP3HrVDB9hH02m7aA6f70dCAfTdpOFnGxDU6wVMAI5MxWBE3wTugtUDOK-\u0026sa=X\u0026ved=2ahUKEwjF55alk_iFAxXlamwGHbqgAs4QmxMoAHoECHYQAg",
"name": "Headquarters: ",
"value": "Chicago, IL"
},
{
"attribute": "kc:/organization/organization:president",
"link": "http://www.google.com/search?safe=0\u0026sca_esv=c9c7fd42856085e2\u0026sca_upv=1\u0026gl=us\u0026hl=en\u0026q=Chris+Kempczinski\u0026si=ACC90nwLLwns5sISZcdzuISy7t-NHozt8Cbt6G3WNQfC9ekAgKFbjdEFCDgxLbt57EDZGosYDGiZuq1AcBhA6IhTOSZxfVSySuGQ3VDwmmTA7Z93n3K3596jAuZH9VVv5h8PyvKJSuGuSsQWviJTl3eKj2UL1ZIWuDgkjyVMnC47rN7j0G9PlHRCCLdQF7VDQ1gubTiC4onXqLRBTbwAj6a--PD6Jv_NoA%3D%3D\u0026sa=X\u0026ved=2ahUKEwjF55alk_iFAxXlamwGHbqgAs4QmxMoAHoECHEQAg",
"name": "President: ",
"value": "Chris Kempczinski"
}
],
"website": "https://www.mcdonalds.com/us/en-us.html",
"profiles": [
{
"name": "Instagram",
"url": "https://www.instagram.com/McDonalds"
},
{
"name": "X (Twitter)",
"url": "https://twitter.com/McDonalds"
},
{
"name": "Facebook",
"url": "https://www.facebook.com/McDonaldsUS"
},
{
"name": "YouTube",
"url": "https://www.youtube.com/user/McDonaldsUS"
},
{
"name": "Pinterest",
"url": "https://www.pinterest.com/mcdonalds"
}
],
"founded": "April 15, 1955, Des Plaines, IL",
"headquarters": "Chicago, IL",
"founders": [
{
"name": "Ray Kroc",
"link": "http://www.google.com/search?safe=0\u0026sca_esv=c9c7fd42856085e2\u0026sca_upv=1\u0026gl=us\u0026hl=en\u0026q=Ray+Kroc\u0026si=ACC90nzx_D3_zUKRnpAjmO0UBLNxnt7EyN4YYdru6U3bxLI-LxARWRdbk5SkoY2sDn5Qq7yOmqYGei6qZ7sfJhsjZXBPgjMlLbS7824rpJOm69GzqVWMdoNIZiFX2T4A2td14sZOn4a1BexZLtZXHU7NZdF6VsWbGMVuiSYtXdev7uaUjEJKumiwlqTAATTebOriYTEBuSzC\u0026sa=X\u0026ved=2ahUKEwjF55alk_iFAxXlamwGHbqgAs4QmxMoAHoECHgQAg"
}
]
}
}

View File

@@ -1,3 +1,4 @@
import os
import logging
import requests
@@ -8,95 +9,192 @@ from apps.ollama.main import (
GenerateEmbeddingsForm,
)
from config import SRC_LOG_LEVELS, CHROMA_CLIENT
from huggingface_hub import snapshot_download
from langchain_core.documents import Document
from langchain_community.retrievers import BM25Retriever
from langchain.retrievers import (
ContextualCompressionRetriever,
EnsembleRetriever,
)
from typing import Optional
from config import (
SRC_LOG_LEVELS,
CHROMA_CLIENT,
SEARXNG_QUERY_URL,
GOOGLE_PSE_API_KEY,
GOOGLE_PSE_ENGINE_ID,
BRAVE_SEARCH_API_KEY,
SERPSTACK_API_KEY,
SERPSTACK_HTTPS,
SERPER_API_KEY,
)
log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["RAG"])
def query_embeddings_doc(collection_name: str, query: str, query_embeddings, k: int):
def query_doc(
collection_name: str,
query: str,
embedding_function,
k: int,
):
try:
# if you use docker use the model from the environment variable
log.info(f"query_embeddings_doc {query_embeddings}")
collection = CHROMA_CLIENT.get_collection(name=collection_name)
query_embeddings = embedding_function(query)
result = collection.query(
query_embeddings=[query_embeddings],
n_results=k,
)
log.info(f"query_embeddings_doc:result {result}")
log.info(f"query_doc:result {result}")
return result
except Exception as e:
raise e
def merge_and_sort_query_results(query_results, k):
def query_doc_with_hybrid_search(
collection_name: str,
query: str,
embedding_function,
k: int,
reranking_function,
r: float,
):
try:
collection = CHROMA_CLIENT.get_collection(name=collection_name)
documents = collection.get() # get all documents
bm25_retriever = BM25Retriever.from_texts(
texts=documents.get("documents"),
metadatas=documents.get("metadatas"),
)
bm25_retriever.k = k
chroma_retriever = ChromaRetriever(
collection=collection,
embedding_function=embedding_function,
top_n=k,
)
ensemble_retriever = EnsembleRetriever(
retrievers=[bm25_retriever, chroma_retriever], weights=[0.5, 0.5]
)
compressor = RerankCompressor(
embedding_function=embedding_function,
top_n=k,
reranking_function=reranking_function,
r_score=r,
)
compression_retriever = ContextualCompressionRetriever(
base_compressor=compressor, base_retriever=ensemble_retriever
)
result = compression_retriever.invoke(query)
result = {
"distances": [[d.metadata.get("score") for d in result]],
"documents": [[d.page_content for d in result]],
"metadatas": [[d.metadata for d in result]],
}
log.info(f"query_doc_with_hybrid_search:result {result}")
return result
except Exception as e:
raise e
def merge_and_sort_query_results(query_results, k, reverse=False):
# Initialize lists to store combined data
combined_ids = []
combined_distances = []
combined_metadatas = []
combined_documents = []
combined_metadatas = []
# Combine data from each dictionary
for data in query_results:
combined_ids.extend(data["ids"][0])
combined_distances.extend(data["distances"][0])
combined_metadatas.extend(data["metadatas"][0])
combined_documents.extend(data["documents"][0])
combined_metadatas.extend(data["metadatas"][0])
# Create a list of tuples (distance, id, metadata, document)
combined = list(
zip(combined_distances, combined_ids, combined_metadatas, combined_documents)
)
# Create a list of tuples (distance, document, metadata)
combined = list(zip(combined_distances, combined_documents, combined_metadatas))
# Sort the list based on distances
combined.sort(key=lambda x: x[0])
combined.sort(key=lambda x: x[0], reverse=reverse)
# Unzip the sorted list
sorted_distances, sorted_ids, sorted_metadatas, sorted_documents = zip(*combined)
# We don't have anything :-(
if not combined:
sorted_distances = []
sorted_documents = []
sorted_metadatas = []
else:
# Unzip the sorted list
sorted_distances, sorted_documents, sorted_metadatas = zip(*combined)
# Slicing the lists to include only k elements
sorted_distances = list(sorted_distances)[:k]
sorted_ids = list(sorted_ids)[:k]
sorted_metadatas = list(sorted_metadatas)[:k]
sorted_documents = list(sorted_documents)[:k]
# Slicing the lists to include only k elements
sorted_distances = list(sorted_distances)[:k]
sorted_documents = list(sorted_documents)[:k]
sorted_metadatas = list(sorted_metadatas)[:k]
# Create the output dictionary
merged_query_results = {
"ids": [sorted_ids],
result = {
"distances": [sorted_distances],
"metadatas": [sorted_metadatas],
"documents": [sorted_documents],
"embeddings": None,
"uris": None,
"data": None,
"metadatas": [sorted_metadatas],
}
return merged_query_results
return result
def query_embeddings_collection(
collection_names: List[str], query: str, query_embeddings, k: int
def query_collection(
collection_names: List[str],
query: str,
embedding_function,
k: int,
):
results = []
log.info(f"query_embeddings_collection {query_embeddings}")
for collection_name in collection_names:
try:
result = query_embeddings_doc(
result = query_doc(
collection_name=collection_name,
query=query,
query_embeddings=query_embeddings,
k=k,
embedding_function=embedding_function,
)
results.append(result)
except:
pass
return merge_and_sort_query_results(results, k=k)
return merge_and_sort_query_results(results, k)
def query_collection_with_hybrid_search(
collection_names: List[str],
query: str,
embedding_function,
k: int,
reranking_function,
r: float,
):
results = []
for collection_name in collection_names:
try:
result = query_doc_with_hybrid_search(
collection_name=collection_name,
query=query,
embedding_function=embedding_function,
k=k,
reranking_function=reranking_function,
r=r,
)
results.append(result)
except:
pass
return merge_and_sort_query_results(results, k=k, reverse=True)
def rag_template(template: str, context: str, query: str):
@@ -105,20 +203,53 @@ def rag_template(template: str, context: str, query: str):
return template
def rag_messages(
docs,
messages,
template,
k,
def get_embedding_function(
embedding_engine,
embedding_model,
embedding_function,
openai_key,
openai_url,
):
log.debug(
f"docs: {docs} {messages} {embedding_engine} {embedding_model} {embedding_function} {openai_key} {openai_url}"
)
if embedding_engine == "":
return lambda query: embedding_function.encode(query).tolist()
elif embedding_engine in ["ollama", "openai"]:
if embedding_engine == "ollama":
func = lambda query: generate_ollama_embeddings(
GenerateEmbeddingsForm(
**{
"model": embedding_model,
"prompt": query,
}
)
)
elif embedding_engine == "openai":
func = lambda query: generate_openai_embeddings(
model=embedding_model,
text=query,
key=openai_key,
url=openai_url,
)
def generate_multiple(query, f):
if isinstance(query, list):
return [f(q) for q in query]
else:
return f(query)
return lambda query: generate_multiple(query, func)
def rag_messages(
docs,
messages,
template,
embedding_function,
k,
reranking_function,
r,
hybrid_search,
):
log.debug(f"docs: {docs} {messages} {embedding_function} {reranking_function}")
last_user_message_idx = None
for i in range(len(messages) - 1, -1, -1):
@@ -145,62 +276,74 @@ def rag_messages(
content_type = None
query = ""
extracted_collections = []
relevant_contexts = []
for doc in docs:
context = None
try:
collection_names = (
doc["collection_names"]
if doc["type"] == "collection"
else [doc["collection_name"]]
)
collection_names = set(collection_names).difference(extracted_collections)
if not collection_names:
log.debug(f"skipping {doc} as it has already been extracted")
continue
try:
if doc["type"] == "text":
context = doc["content"]
else:
if embedding_engine == "":
query_embeddings = embedding_function.encode(query).tolist()
elif embedding_engine == "ollama":
query_embeddings = generate_ollama_embeddings(
GenerateEmbeddingsForm(
**{
"model": embedding_model,
"prompt": query,
}
)
)
elif embedding_engine == "openai":
query_embeddings = generate_openai_embeddings(
model=embedding_model,
text=query,
key=openai_key,
url=openai_url,
)
if doc["type"] == "collection":
context = query_embeddings_collection(
collection_names=doc["collection_names"],
if hybrid_search:
context = query_collection_with_hybrid_search(
collection_names=collection_names,
query=query,
query_embeddings=query_embeddings,
embedding_function=embedding_function,
k=k,
reranking_function=reranking_function,
r=r,
)
else:
context = query_embeddings_doc(
collection_name=doc["collection_name"],
context = query_collection(
collection_names=collection_names,
query=query,
query_embeddings=query_embeddings,
embedding_function=embedding_function,
k=k,
)
except Exception as e:
log.exception(e)
context = None
relevant_contexts.append(context)
if context:
relevant_contexts.append({**context, "source": doc})
log.debug(f"relevant_contexts: {relevant_contexts}")
extracted_collections.extend(collection_names)
context_string = ""
citations = []
for context in relevant_contexts:
if context:
context_string += " ".join(context["documents"][0]) + "\n"
try:
if "documents" in context:
context_string += "\n\n".join(
[text for text in context["documents"][0] if text is not None]
)
if "metadatas" in context:
citations.append(
{
"source": context["source"],
"document": context["documents"][0],
"metadata": context["metadatas"][0],
}
)
except Exception as e:
log.exception(e)
context_string = context_string.strip()
ra_content = rag_template(
template=template,
@@ -208,6 +351,8 @@ def rag_messages(
query=query,
)
log.debug(f"ra_content: {ra_content}")
if content_type == "list":
new_content = []
for content_item in user_message["content"]:
@@ -226,7 +371,45 @@ def rag_messages(
messages[last_user_message_idx] = new_user_message
return messages
return messages, citations
def get_model_path(model: str, update_model: bool = False):
# Construct huggingface_hub kwargs with local_files_only to return the snapshot path
cache_dir = os.getenv("SENTENCE_TRANSFORMERS_HOME")
local_files_only = not update_model
snapshot_kwargs = {
"cache_dir": cache_dir,
"local_files_only": local_files_only,
}
log.debug(f"model: {model}")
log.debug(f"snapshot_kwargs: {snapshot_kwargs}")
# Inspiration from upstream sentence_transformers
if (
os.path.exists(model)
or ("\\" in model or model.count("/") > 1)
and local_files_only
):
# If fully qualified path exists, return input, else set repo_id
return model
elif "/" not in model:
# Set valid repo_id for model short-name
model = "sentence-transformers" + "/" + model
snapshot_kwargs["repo_id"] = model
# Attempt to query the huggingface_hub library to determine the local path and/or to update
try:
model_repo_path = snapshot_download(**snapshot_kwargs)
log.debug(f"model_repo_path: {model_repo_path}")
return model_repo_path
except Exception as e:
log.exception(f"Cannot determine model snapshot path: {e}")
return model
def generate_openai_embeddings(
@@ -250,3 +433,101 @@ def generate_openai_embeddings(
except Exception as e:
print(e)
return None
from typing import Any
from langchain_core.retrievers import BaseRetriever
from langchain_core.callbacks import CallbackManagerForRetrieverRun
class ChromaRetriever(BaseRetriever):
collection: Any
embedding_function: Any
top_n: int
def _get_relevant_documents(
self,
query: str,
*,
run_manager: CallbackManagerForRetrieverRun,
) -> List[Document]:
query_embeddings = self.embedding_function(query)
results = self.collection.query(
query_embeddings=[query_embeddings],
n_results=self.top_n,
)
ids = results["ids"][0]
metadatas = results["metadatas"][0]
documents = results["documents"][0]
results = []
for idx in range(len(ids)):
results.append(
Document(
metadata=metadatas[idx],
page_content=documents[idx],
)
)
return results
import operator
from typing import Optional, Sequence
from langchain_core.documents import BaseDocumentCompressor, Document
from langchain_core.callbacks import Callbacks
from langchain_core.pydantic_v1 import Extra
from sentence_transformers import util
class RerankCompressor(BaseDocumentCompressor):
embedding_function: Any
top_n: int
reranking_function: Any
r_score: float
class Config:
extra = Extra.forbid
arbitrary_types_allowed = True
def compress_documents(
self,
documents: Sequence[Document],
query: str,
callbacks: Optional[Callbacks] = None,
) -> Sequence[Document]:
reranking = self.reranking_function is not None
if reranking:
scores = self.reranking_function.predict(
[(query, doc.page_content) for doc in documents]
)
else:
query_embedding = self.embedding_function(query)
document_embedding = self.embedding_function(
[doc.page_content for doc in documents]
)
scores = util.cos_sim(query_embedding, document_embedding)[0]
docs_with_scores = list(zip(documents, scores.tolist()))
if self.r_score:
docs_with_scores = [
(d, s) for d, s in docs_with_scores if s >= self.r_score
]
result = sorted(docs_with_scores, key=operator.itemgetter(1), reverse=True)
final_results = []
for doc, doc_score in result[: self.top_n]:
metadata = doc.metadata
metadata["score"] = doc_score
doc = Document(
page_content=doc.page_content,
metadata=metadata,
)
final_results.append(doc)
return final_results

View File

@@ -1,22 +0,0 @@
from peewee import *
from peewee_migrate import Router
from config import SRC_LOG_LEVELS, DATA_DIR
import os
import logging
log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["DB"])
# Check if the file exists
if os.path.exists(f"{DATA_DIR}/ollama.db"):
# Rename the file
os.rename(f"{DATA_DIR}/ollama.db", f"{DATA_DIR}/webui.db")
log.info("File renamed successfully.")
else:
pass
DB = SqliteDatabase(f"{DATA_DIR}/webui.db")
router = Router(DB, migrate_dir="apps/web/internal/migrations", logger=log)
router.run()
DB.connect(reuse_if_open=True)

View File

@@ -1,136 +0,0 @@
from pydantic import BaseModel
from peewee import *
from playhouse.shortcuts import model_to_dict
from typing import List, Union, Optional
import time
from utils.utils import decode_token
from utils.misc import get_gravatar_url
from apps.web.internal.db import DB
import json
####################
# Modelfile DB Schema
####################
class Modelfile(Model):
tag_name = CharField(unique=True)
user_id = CharField()
modelfile = TextField()
timestamp = DateField()
class Meta:
database = DB
class ModelfileModel(BaseModel):
tag_name: str
user_id: str
modelfile: str
timestamp: int # timestamp in epoch
####################
# Forms
####################
class ModelfileForm(BaseModel):
modelfile: dict
class ModelfileTagNameForm(BaseModel):
tag_name: str
class ModelfileUpdateForm(ModelfileForm, ModelfileTagNameForm):
pass
class ModelfileResponse(BaseModel):
tag_name: str
user_id: str
modelfile: dict
timestamp: int # timestamp in epoch
class ModelfilesTable:
def __init__(self, db):
self.db = db
self.db.create_tables([Modelfile])
def insert_new_modelfile(
self, user_id: str, form_data: ModelfileForm
) -> Optional[ModelfileModel]:
if "tagName" in form_data.modelfile:
modelfile = ModelfileModel(
**{
"user_id": user_id,
"tag_name": form_data.modelfile["tagName"],
"modelfile": json.dumps(form_data.modelfile),
"timestamp": int(time.time()),
}
)
try:
result = Modelfile.create(**modelfile.model_dump())
if result:
return modelfile
else:
return None
except:
return None
else:
return None
def get_modelfile_by_tag_name(self, tag_name: str) -> Optional[ModelfileModel]:
try:
modelfile = Modelfile.get(Modelfile.tag_name == tag_name)
return ModelfileModel(**model_to_dict(modelfile))
except:
return None
def get_modelfiles(self, skip: int = 0, limit: int = 50) -> List[ModelfileResponse]:
return [
ModelfileResponse(
**{
**model_to_dict(modelfile),
"modelfile": json.loads(modelfile.modelfile),
}
)
for modelfile in Modelfile.select()
# .limit(limit).offset(skip)
]
def update_modelfile_by_tag_name(
self, tag_name: str, modelfile: dict
) -> Optional[ModelfileModel]:
try:
query = Modelfile.update(
modelfile=json.dumps(modelfile),
timestamp=int(time.time()),
).where(Modelfile.tag_name == tag_name)
query.execute()
modelfile = Modelfile.get(Modelfile.tag_name == tag_name)
return ModelfileModel(**model_to_dict(modelfile))
except:
return None
def delete_modelfile_by_tag_name(self, tag_name: str) -> bool:
try:
query = Modelfile.delete().where((Modelfile.tag_name == tag_name))
query.execute() # Remove the rows, return number of rows removed.
return True
except:
return False
Modelfiles = ModelfilesTable(DB)

View File

@@ -1,124 +0,0 @@
from fastapi import Depends, FastAPI, HTTPException, status
from datetime import datetime, timedelta
from typing import List, Union, Optional
from fastapi import APIRouter
from pydantic import BaseModel
import json
from apps.web.models.modelfiles import (
Modelfiles,
ModelfileForm,
ModelfileTagNameForm,
ModelfileUpdateForm,
ModelfileResponse,
)
from utils.utils import get_current_user, get_admin_user
from constants import ERROR_MESSAGES
router = APIRouter()
############################
# GetModelfiles
############################
@router.get("/", response_model=List[ModelfileResponse])
async def get_modelfiles(
skip: int = 0, limit: int = 50, user=Depends(get_current_user)
):
return Modelfiles.get_modelfiles(skip, limit)
############################
# CreateNewModelfile
############################
@router.post("/create", response_model=Optional[ModelfileResponse])
async def create_new_modelfile(form_data: ModelfileForm, user=Depends(get_admin_user)):
modelfile = Modelfiles.insert_new_modelfile(user.id, form_data)
if modelfile:
return ModelfileResponse(
**{
**modelfile.model_dump(),
"modelfile": json.loads(modelfile.modelfile),
}
)
else:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=ERROR_MESSAGES.DEFAULT(),
)
############################
# GetModelfileByTagName
############################
@router.post("/", response_model=Optional[ModelfileResponse])
async def get_modelfile_by_tag_name(
form_data: ModelfileTagNameForm, user=Depends(get_current_user)
):
modelfile = Modelfiles.get_modelfile_by_tag_name(form_data.tag_name)
if modelfile:
return ModelfileResponse(
**{
**modelfile.model_dump(),
"modelfile": json.loads(modelfile.modelfile),
}
)
else:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=ERROR_MESSAGES.NOT_FOUND,
)
############################
# UpdateModelfileByTagName
############################
@router.post("/update", response_model=Optional[ModelfileResponse])
async def update_modelfile_by_tag_name(
form_data: ModelfileUpdateForm, user=Depends(get_admin_user)
):
modelfile = Modelfiles.get_modelfile_by_tag_name(form_data.tag_name)
if modelfile:
updated_modelfile = {
**json.loads(modelfile.modelfile),
**form_data.modelfile,
}
modelfile = Modelfiles.update_modelfile_by_tag_name(
form_data.tag_name, updated_modelfile
)
return ModelfileResponse(
**{
**modelfile.model_dump(),
"modelfile": json.loads(modelfile.modelfile),
}
)
else:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
)
############################
# DeleteModelfileByTagName
############################
@router.delete("/delete", response_model=bool)
async def delete_modelfile_by_tag_name(
form_data: ModelfileTagNameForm, user=Depends(get_admin_user)
):
result = Modelfiles.delete_modelfile_by_tag_name(form_data.tag_name)
return result

View File

@@ -0,0 +1,39 @@
import json
from peewee import *
from peewee_migrate import Router
from playhouse.db_url import connect
from config import SRC_LOG_LEVELS, DATA_DIR, DATABASE_URL, BACKEND_DIR
import os
import logging
log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["DB"])
class JSONField(TextField):
def db_value(self, value):
return json.dumps(value)
def python_value(self, value):
if value is not None:
return json.loads(value)
# Check if the file exists
if os.path.exists(f"{DATA_DIR}/ollama.db"):
# Rename the file
os.rename(f"{DATA_DIR}/ollama.db", f"{DATA_DIR}/webui.db")
log.info("Database migrated from Ollama-WebUI successfully.")
else:
pass
DB = connect(DATABASE_URL)
log.info(f"Connected to a {DB.__class__.__name__} database.")
router = Router(
DB,
migrate_dir=BACKEND_DIR / "apps" / "webui" / "internal" / "migrations",
logger=log,
)
router.run()
DB.connect(reuse_if_open=True)

View File

@@ -37,6 +37,18 @@ with suppress(ImportError):
def migrate(migrator: Migrator, database: pw.Database, *, fake=False):
"""Write your migrations here."""
# We perform different migrations for SQLite and other databases
# This is because SQLite is very loose with enforcing its schema, and trying to migrate other databases like SQLite
# will require per-database SQL queries.
# Instead, we assume that because external DB support was added at a later date, it is safe to assume a newer base
# schema instead of trying to migrate from an older schema.
if isinstance(database, pw.SqliteDatabase):
migrate_sqlite(migrator, database, fake=fake)
else:
migrate_external(migrator, database, fake=fake)
def migrate_sqlite(migrator: Migrator, database: pw.Database, *, fake=False):
@migrator.create_model
class Auth(pw.Model):
id = pw.CharField(max_length=255, unique=True)
@@ -53,7 +65,7 @@ def migrate(migrator: Migrator, database: pw.Database, *, fake=False):
user_id = pw.CharField(max_length=255)
title = pw.CharField()
chat = pw.TextField()
timestamp = pw.DateField()
timestamp = pw.BigIntegerField()
class Meta:
table_name = "chat"
@@ -64,7 +76,7 @@ def migrate(migrator: Migrator, database: pw.Database, *, fake=False):
tag_name = pw.CharField(max_length=255)
chat_id = pw.CharField(max_length=255)
user_id = pw.CharField(max_length=255)
timestamp = pw.DateField()
timestamp = pw.BigIntegerField()
class Meta:
table_name = "chatidtag"
@@ -78,7 +90,7 @@ def migrate(migrator: Migrator, database: pw.Database, *, fake=False):
filename = pw.CharField()
content = pw.TextField(null=True)
user_id = pw.CharField(max_length=255)
timestamp = pw.DateField()
timestamp = pw.BigIntegerField()
class Meta:
table_name = "document"
@@ -89,7 +101,7 @@ def migrate(migrator: Migrator, database: pw.Database, *, fake=False):
tag_name = pw.CharField(max_length=255, unique=True)
user_id = pw.CharField(max_length=255)
modelfile = pw.TextField()
timestamp = pw.DateField()
timestamp = pw.BigIntegerField()
class Meta:
table_name = "modelfile"
@@ -101,7 +113,7 @@ def migrate(migrator: Migrator, database: pw.Database, *, fake=False):
user_id = pw.CharField(max_length=255)
title = pw.CharField()
content = pw.TextField()
timestamp = pw.DateField()
timestamp = pw.BigIntegerField()
class Meta:
table_name = "prompt"
@@ -123,7 +135,100 @@ def migrate(migrator: Migrator, database: pw.Database, *, fake=False):
email = pw.CharField(max_length=255)
role = pw.CharField(max_length=255)
profile_image_url = pw.CharField(max_length=255)
timestamp = pw.DateField()
timestamp = pw.BigIntegerField()
class Meta:
table_name = "user"
def migrate_external(migrator: Migrator, database: pw.Database, *, fake=False):
@migrator.create_model
class Auth(pw.Model):
id = pw.CharField(max_length=255, unique=True)
email = pw.CharField(max_length=255)
password = pw.TextField()
active = pw.BooleanField()
class Meta:
table_name = "auth"
@migrator.create_model
class Chat(pw.Model):
id = pw.CharField(max_length=255, unique=True)
user_id = pw.CharField(max_length=255)
title = pw.TextField()
chat = pw.TextField()
timestamp = pw.BigIntegerField()
class Meta:
table_name = "chat"
@migrator.create_model
class ChatIdTag(pw.Model):
id = pw.CharField(max_length=255, unique=True)
tag_name = pw.CharField(max_length=255)
chat_id = pw.CharField(max_length=255)
user_id = pw.CharField(max_length=255)
timestamp = pw.BigIntegerField()
class Meta:
table_name = "chatidtag"
@migrator.create_model
class Document(pw.Model):
id = pw.AutoField()
collection_name = pw.CharField(max_length=255, unique=True)
name = pw.CharField(max_length=255, unique=True)
title = pw.TextField()
filename = pw.TextField()
content = pw.TextField(null=True)
user_id = pw.CharField(max_length=255)
timestamp = pw.BigIntegerField()
class Meta:
table_name = "document"
@migrator.create_model
class Modelfile(pw.Model):
id = pw.AutoField()
tag_name = pw.CharField(max_length=255, unique=True)
user_id = pw.CharField(max_length=255)
modelfile = pw.TextField()
timestamp = pw.BigIntegerField()
class Meta:
table_name = "modelfile"
@migrator.create_model
class Prompt(pw.Model):
id = pw.AutoField()
command = pw.CharField(max_length=255, unique=True)
user_id = pw.CharField(max_length=255)
title = pw.TextField()
content = pw.TextField()
timestamp = pw.BigIntegerField()
class Meta:
table_name = "prompt"
@migrator.create_model
class Tag(pw.Model):
id = pw.CharField(max_length=255, unique=True)
name = pw.CharField(max_length=255)
user_id = pw.CharField(max_length=255)
data = pw.TextField(null=True)
class Meta:
table_name = "tag"
@migrator.create_model
class User(pw.Model):
id = pw.CharField(max_length=255, unique=True)
name = pw.CharField(max_length=255)
email = pw.CharField(max_length=255)
role = pw.CharField(max_length=255)
profile_image_url = pw.TextField()
timestamp = pw.BigIntegerField()
class Meta:
table_name = "user"

View File

@@ -0,0 +1,130 @@
"""Peewee migrations -- 002_add_local_sharing.py.
Some examples (model - class or model name)::
> Model = migrator.orm['table_name'] # Return model in current state by name
> Model = migrator.ModelClass # Return model in current state by name
> migrator.sql(sql) # Run custom SQL
> migrator.run(func, *args, **kwargs) # Run python function with the given args
> migrator.create_model(Model) # Create a model (could be used as decorator)
> migrator.remove_model(model, cascade=True) # Remove a model
> migrator.add_fields(model, **fields) # Add fields to a model
> migrator.change_fields(model, **fields) # Change fields
> migrator.remove_fields(model, *field_names, cascade=True)
> migrator.rename_field(model, old_field_name, new_field_name)
> migrator.rename_table(model, new_table_name)
> migrator.add_index(model, *col_names, unique=False)
> migrator.add_not_null(model, *field_names)
> migrator.add_default(model, field_name, default)
> migrator.add_constraint(model, name, sql)
> migrator.drop_index(model, *col_names)
> migrator.drop_not_null(model, *field_names)
> migrator.drop_constraints(model, *constraints)
"""
from contextlib import suppress
import peewee as pw
from peewee_migrate import Migrator
with suppress(ImportError):
import playhouse.postgres_ext as pw_pext
def migrate(migrator: Migrator, database: pw.Database, *, fake=False):
"""Write your migrations here."""
if isinstance(database, pw.SqliteDatabase):
migrate_sqlite(migrator, database, fake=fake)
else:
migrate_external(migrator, database, fake=fake)
def migrate_sqlite(migrator: Migrator, database: pw.Database, *, fake=False):
# Adding fields created_at and updated_at to the 'chat' table
migrator.add_fields(
"chat",
created_at=pw.DateTimeField(null=True), # Allow null for transition
updated_at=pw.DateTimeField(null=True), # Allow null for transition
)
# Populate the new fields from an existing 'timestamp' field
migrator.sql(
"UPDATE chat SET created_at = timestamp, updated_at = timestamp WHERE timestamp IS NOT NULL"
)
# Now that the data has been copied, remove the original 'timestamp' field
migrator.remove_fields("chat", "timestamp")
# Update the fields to be not null now that they are populated
migrator.change_fields(
"chat",
created_at=pw.DateTimeField(null=False),
updated_at=pw.DateTimeField(null=False),
)
def migrate_external(migrator: Migrator, database: pw.Database, *, fake=False):
# Adding fields created_at and updated_at to the 'chat' table
migrator.add_fields(
"chat",
created_at=pw.BigIntegerField(null=True), # Allow null for transition
updated_at=pw.BigIntegerField(null=True), # Allow null for transition
)
# Populate the new fields from an existing 'timestamp' field
migrator.sql(
"UPDATE chat SET created_at = timestamp, updated_at = timestamp WHERE timestamp IS NOT NULL"
)
# Now that the data has been copied, remove the original 'timestamp' field
migrator.remove_fields("chat", "timestamp")
# Update the fields to be not null now that they are populated
migrator.change_fields(
"chat",
created_at=pw.BigIntegerField(null=False),
updated_at=pw.BigIntegerField(null=False),
)
def rollback(migrator: Migrator, database: pw.Database, *, fake=False):
"""Write your rollback migrations here."""
if isinstance(database, pw.SqliteDatabase):
rollback_sqlite(migrator, database, fake=fake)
else:
rollback_external(migrator, database, fake=fake)
def rollback_sqlite(migrator: Migrator, database: pw.Database, *, fake=False):
# Recreate the timestamp field initially allowing null values for safe transition
migrator.add_fields("chat", timestamp=pw.DateTimeField(null=True))
# Copy the earliest created_at date back into the new timestamp field
# This assumes created_at was originally a copy of timestamp
migrator.sql("UPDATE chat SET timestamp = created_at")
# Remove the created_at and updated_at fields
migrator.remove_fields("chat", "created_at", "updated_at")
# Finally, alter the timestamp field to not allow nulls if that was the original setting
migrator.change_fields("chat", timestamp=pw.DateTimeField(null=False))
def rollback_external(migrator: Migrator, database: pw.Database, *, fake=False):
# Recreate the timestamp field initially allowing null values for safe transition
migrator.add_fields("chat", timestamp=pw.BigIntegerField(null=True))
# Copy the earliest created_at date back into the new timestamp field
# This assumes created_at was originally a copy of timestamp
migrator.sql("UPDATE chat SET timestamp = created_at")
# Remove the created_at and updated_at fields
migrator.remove_fields("chat", "created_at", "updated_at")
# Finally, alter the timestamp field to not allow nulls if that was the original setting
migrator.change_fields("chat", timestamp=pw.BigIntegerField(null=False))

View File

@@ -0,0 +1,130 @@
"""Peewee migrations -- 006_migrate_timestamps_and_charfields.py.
Some examples (model - class or model name)::
> Model = migrator.orm['table_name'] # Return model in current state by name
> Model = migrator.ModelClass # Return model in current state by name
> migrator.sql(sql) # Run custom SQL
> migrator.run(func, *args, **kwargs) # Run python function with the given args
> migrator.create_model(Model) # Create a model (could be used as decorator)
> migrator.remove_model(model, cascade=True) # Remove a model
> migrator.add_fields(model, **fields) # Add fields to a model
> migrator.change_fields(model, **fields) # Change fields
> migrator.remove_fields(model, *field_names, cascade=True)
> migrator.rename_field(model, old_field_name, new_field_name)
> migrator.rename_table(model, new_table_name)
> migrator.add_index(model, *col_names, unique=False)
> migrator.add_not_null(model, *field_names)
> migrator.add_default(model, field_name, default)
> migrator.add_constraint(model, name, sql)
> migrator.drop_index(model, *col_names)
> migrator.drop_not_null(model, *field_names)
> migrator.drop_constraints(model, *constraints)
"""
from contextlib import suppress
import peewee as pw
from peewee_migrate import Migrator
with suppress(ImportError):
import playhouse.postgres_ext as pw_pext
def migrate(migrator: Migrator, database: pw.Database, *, fake=False):
"""Write your migrations here."""
# Alter the tables with timestamps
migrator.change_fields(
"chatidtag",
timestamp=pw.BigIntegerField(),
)
migrator.change_fields(
"document",
timestamp=pw.BigIntegerField(),
)
migrator.change_fields(
"modelfile",
timestamp=pw.BigIntegerField(),
)
migrator.change_fields(
"prompt",
timestamp=pw.BigIntegerField(),
)
migrator.change_fields(
"user",
timestamp=pw.BigIntegerField(),
)
# Alter the tables with varchar to text where necessary
migrator.change_fields(
"auth",
password=pw.TextField(),
)
migrator.change_fields(
"chat",
title=pw.TextField(),
)
migrator.change_fields(
"document",
title=pw.TextField(),
filename=pw.TextField(),
)
migrator.change_fields(
"prompt",
title=pw.TextField(),
)
migrator.change_fields(
"user",
profile_image_url=pw.TextField(),
)
def rollback(migrator: Migrator, database: pw.Database, *, fake=False):
"""Write your rollback migrations here."""
if isinstance(database, pw.SqliteDatabase):
# Alter the tables with timestamps
migrator.change_fields(
"chatidtag",
timestamp=pw.DateField(),
)
migrator.change_fields(
"document",
timestamp=pw.DateField(),
)
migrator.change_fields(
"modelfile",
timestamp=pw.DateField(),
)
migrator.change_fields(
"prompt",
timestamp=pw.DateField(),
)
migrator.change_fields(
"user",
timestamp=pw.DateField(),
)
migrator.change_fields(
"auth",
password=pw.CharField(max_length=255),
)
migrator.change_fields(
"chat",
title=pw.CharField(),
)
migrator.change_fields(
"document",
title=pw.CharField(),
filename=pw.CharField(),
)
migrator.change_fields(
"prompt",
title=pw.CharField(),
)
migrator.change_fields(
"user",
profile_image_url=pw.CharField(),
)

View File

@@ -37,26 +37,28 @@ with suppress(ImportError):
def migrate(migrator: Migrator, database: pw.Database, *, fake=False):
"""Write your migrations here."""
# Adding fields created_at and updated_at to the 'chat' table
# Adding fields created_at and updated_at to the 'user' table
migrator.add_fields(
"chat",
created_at=pw.DateTimeField(null=True), # Allow null for transition
updated_at=pw.DateTimeField(null=True), # Allow null for transition
"user",
created_at=pw.BigIntegerField(null=True), # Allow null for transition
updated_at=pw.BigIntegerField(null=True), # Allow null for transition
last_active_at=pw.BigIntegerField(null=True), # Allow null for transition
)
# Populate the new fields from an existing 'timestamp' field
migrator.sql(
"UPDATE chat SET created_at = timestamp, updated_at = timestamp WHERE timestamp IS NOT NULL"
'UPDATE "user" SET created_at = timestamp, updated_at = timestamp, last_active_at = timestamp WHERE timestamp IS NOT NULL'
)
# Now that the data has been copied, remove the original 'timestamp' field
migrator.remove_fields("chat", "timestamp")
migrator.remove_fields("user", "timestamp")
# Update the fields to be not null now that they are populated
migrator.change_fields(
"chat",
created_at=pw.DateTimeField(null=False),
updated_at=pw.DateTimeField(null=False),
"user",
created_at=pw.BigIntegerField(null=False),
updated_at=pw.BigIntegerField(null=False),
last_active_at=pw.BigIntegerField(null=False),
)
@@ -64,14 +66,14 @@ def rollback(migrator: Migrator, database: pw.Database, *, fake=False):
"""Write your rollback migrations here."""
# Recreate the timestamp field initially allowing null values for safe transition
migrator.add_fields("chat", timestamp=pw.DateTimeField(null=True))
migrator.add_fields("user", timestamp=pw.BigIntegerField(null=True))
# Copy the earliest created_at date back into the new timestamp field
# This assumes created_at was originally a copy of timestamp
migrator.sql("UPDATE chat SET timestamp = created_at")
migrator.sql('UPDATE "user" SET timestamp = created_at')
# Remove the created_at and updated_at fields
migrator.remove_fields("chat", "created_at", "updated_at")
migrator.remove_fields("user", "created_at", "updated_at", "last_active_at")
# Finally, alter the timestamp field to not allow nulls if that was the original setting
migrator.change_fields("chat", timestamp=pw.DateTimeField(null=False))
migrator.change_fields("user", timestamp=pw.BigIntegerField(null=False))

View File

@@ -0,0 +1,53 @@
"""Peewee migrations -- 002_add_local_sharing.py.
Some examples (model - class or model name)::
> Model = migrator.orm['table_name'] # Return model in current state by name
> Model = migrator.ModelClass # Return model in current state by name
> migrator.sql(sql) # Run custom SQL
> migrator.run(func, *args, **kwargs) # Run python function with the given args
> migrator.create_model(Model) # Create a model (could be used as decorator)
> migrator.remove_model(model, cascade=True) # Remove a model
> migrator.add_fields(model, **fields) # Add fields to a model
> migrator.change_fields(model, **fields) # Change fields
> migrator.remove_fields(model, *field_names, cascade=True)
> migrator.rename_field(model, old_field_name, new_field_name)
> migrator.rename_table(model, new_table_name)
> migrator.add_index(model, *col_names, unique=False)
> migrator.add_not_null(model, *field_names)
> migrator.add_default(model, field_name, default)
> migrator.add_constraint(model, name, sql)
> migrator.drop_index(model, *col_names)
> migrator.drop_not_null(model, *field_names)
> migrator.drop_constraints(model, *constraints)
"""
from contextlib import suppress
import peewee as pw
from peewee_migrate import Migrator
with suppress(ImportError):
import playhouse.postgres_ext as pw_pext
def migrate(migrator: Migrator, database: pw.Database, *, fake=False):
@migrator.create_model
class Memory(pw.Model):
id = pw.CharField(max_length=255, unique=True)
user_id = pw.CharField(max_length=255)
content = pw.TextField(null=False)
updated_at = pw.BigIntegerField(null=False)
created_at = pw.BigIntegerField(null=False)
class Meta:
table_name = "memory"
def rollback(migrator: Migrator, database: pw.Database, *, fake=False):
"""Write your rollback migrations here."""
migrator.remove_model("memory")

View File

@@ -0,0 +1,61 @@
"""Peewee migrations -- 009_add_models.py.
Some examples (model - class or model name)::
> Model = migrator.orm['table_name'] # Return model in current state by name
> Model = migrator.ModelClass # Return model in current state by name
> migrator.sql(sql) # Run custom SQL
> migrator.run(func, *args, **kwargs) # Run python function with the given args
> migrator.create_model(Model) # Create a model (could be used as decorator)
> migrator.remove_model(model, cascade=True) # Remove a model
> migrator.add_fields(model, **fields) # Add fields to a model
> migrator.change_fields(model, **fields) # Change fields
> migrator.remove_fields(model, *field_names, cascade=True)
> migrator.rename_field(model, old_field_name, new_field_name)
> migrator.rename_table(model, new_table_name)
> migrator.add_index(model, *col_names, unique=False)
> migrator.add_not_null(model, *field_names)
> migrator.add_default(model, field_name, default)
> migrator.add_constraint(model, name, sql)
> migrator.drop_index(model, *col_names)
> migrator.drop_not_null(model, *field_names)
> migrator.drop_constraints(model, *constraints)
"""
from contextlib import suppress
import peewee as pw
from peewee_migrate import Migrator
with suppress(ImportError):
import playhouse.postgres_ext as pw_pext
def migrate(migrator: Migrator, database: pw.Database, *, fake=False):
"""Write your migrations here."""
@migrator.create_model
class Model(pw.Model):
id = pw.TextField(unique=True)
user_id = pw.TextField()
base_model_id = pw.TextField(null=True)
name = pw.TextField()
meta = pw.TextField()
params = pw.TextField()
created_at = pw.BigIntegerField(null=False)
updated_at = pw.BigIntegerField(null=False)
class Meta:
table_name = "model"
def rollback(migrator: Migrator, database: pw.Database, *, fake=False):
"""Write your rollback migrations here."""
migrator.remove_model("model")

View File

@@ -0,0 +1,130 @@
"""Peewee migrations -- 009_add_models.py.
Some examples (model - class or model name)::
> Model = migrator.orm['table_name'] # Return model in current state by name
> Model = migrator.ModelClass # Return model in current state by name
> migrator.sql(sql) # Run custom SQL
> migrator.run(func, *args, **kwargs) # Run python function with the given args
> migrator.create_model(Model) # Create a model (could be used as decorator)
> migrator.remove_model(model, cascade=True) # Remove a model
> migrator.add_fields(model, **fields) # Add fields to a model
> migrator.change_fields(model, **fields) # Change fields
> migrator.remove_fields(model, *field_names, cascade=True)
> migrator.rename_field(model, old_field_name, new_field_name)
> migrator.rename_table(model, new_table_name)
> migrator.add_index(model, *col_names, unique=False)
> migrator.add_not_null(model, *field_names)
> migrator.add_default(model, field_name, default)
> migrator.add_constraint(model, name, sql)
> migrator.drop_index(model, *col_names)
> migrator.drop_not_null(model, *field_names)
> migrator.drop_constraints(model, *constraints)
"""
from contextlib import suppress
import peewee as pw
from peewee_migrate import Migrator
import json
from utils.misc import parse_ollama_modelfile
with suppress(ImportError):
import playhouse.postgres_ext as pw_pext
def migrate(migrator: Migrator, database: pw.Database, *, fake=False):
"""Write your migrations here."""
# Fetch data from 'modelfile' table and insert into 'model' table
migrate_modelfile_to_model(migrator, database)
# Drop the 'modelfile' table
migrator.remove_model("modelfile")
def migrate_modelfile_to_model(migrator: Migrator, database: pw.Database):
ModelFile = migrator.orm["modelfile"]
Model = migrator.orm["model"]
modelfiles = ModelFile.select()
for modelfile in modelfiles:
# Extract and transform data in Python
modelfile.modelfile = json.loads(modelfile.modelfile)
meta = json.dumps(
{
"description": modelfile.modelfile.get("desc"),
"profile_image_url": modelfile.modelfile.get("imageUrl"),
"ollama": {"modelfile": modelfile.modelfile.get("content")},
"suggestion_prompts": modelfile.modelfile.get("suggestionPrompts"),
"categories": modelfile.modelfile.get("categories"),
"user": {**modelfile.modelfile.get("user", {}), "community": True},
}
)
info = parse_ollama_modelfile(modelfile.modelfile.get("content"))
# Insert the processed data into the 'model' table
Model.create(
id=f"ollama-{modelfile.tag_name}",
user_id=modelfile.user_id,
base_model_id=info.get("base_model_id"),
name=modelfile.modelfile.get("title"),
meta=meta,
params=json.dumps(info.get("params", {})),
created_at=modelfile.timestamp,
updated_at=modelfile.timestamp,
)
def rollback(migrator: Migrator, database: pw.Database, *, fake=False):
"""Write your rollback migrations here."""
recreate_modelfile_table(migrator, database)
move_data_back_to_modelfile(migrator, database)
migrator.remove_model("model")
def recreate_modelfile_table(migrator: Migrator, database: pw.Database):
query = """
CREATE TABLE IF NOT EXISTS modelfile (
user_id TEXT,
tag_name TEXT,
modelfile JSON,
timestamp BIGINT
)
"""
migrator.sql(query)
def move_data_back_to_modelfile(migrator: Migrator, database: pw.Database):
Model = migrator.orm["model"]
Modelfile = migrator.orm["modelfile"]
models = Model.select()
for model in models:
# Extract and transform data in Python
meta = json.loads(model.meta)
modelfile_data = {
"title": model.name,
"desc": meta.get("description"),
"imageUrl": meta.get("profile_image_url"),
"content": meta.get("ollama", {}).get("modelfile"),
"suggestionPrompts": meta.get("suggestion_prompts"),
"categories": meta.get("categories"),
"user": {k: v for k, v in meta.get("user", {}).items() if k != "community"},
}
# Insert the processed data back into the 'modelfile' table
Modelfile.create(
user_id=model.user_id,
tag_name=model.id,
modelfile=modelfile_data,
timestamp=model.created_at,
)

View File

@@ -0,0 +1,48 @@
"""Peewee migrations -- 002_add_local_sharing.py.
Some examples (model - class or model name)::
> Model = migrator.orm['table_name'] # Return model in current state by name
> Model = migrator.ModelClass # Return model in current state by name
> migrator.sql(sql) # Run custom SQL
> migrator.run(func, *args, **kwargs) # Run python function with the given args
> migrator.create_model(Model) # Create a model (could be used as decorator)
> migrator.remove_model(model, cascade=True) # Remove a model
> migrator.add_fields(model, **fields) # Add fields to a model
> migrator.change_fields(model, **fields) # Change fields
> migrator.remove_fields(model, *field_names, cascade=True)
> migrator.rename_field(model, old_field_name, new_field_name)
> migrator.rename_table(model, new_table_name)
> migrator.add_index(model, *col_names, unique=False)
> migrator.add_not_null(model, *field_names)
> migrator.add_default(model, field_name, default)
> migrator.add_constraint(model, name, sql)
> migrator.drop_index(model, *col_names)
> migrator.drop_not_null(model, *field_names)
> migrator.drop_constraints(model, *constraints)
"""
from contextlib import suppress
import peewee as pw
from peewee_migrate import Migrator
with suppress(ImportError):
import playhouse.postgres_ext as pw_pext
def migrate(migrator: Migrator, database: pw.Database, *, fake=False):
"""Write your migrations here."""
# Adding fields settings to the 'user' table
migrator.add_fields("user", settings=pw.TextField(null=True))
def rollback(migrator: Migrator, database: pw.Database, *, fake=False):
"""Write your rollback migrations here."""
# Remove the settings field
migrator.remove_fields("user", "settings")

View File

@@ -14,7 +14,7 @@ You will need to create a migration file to ensure that existing databases are u
2. Make your changes to the models.
3. From the `backend` directory, run the following command:
```bash
pw_migrate create --auto --auto-source apps.web.models --database sqlite:///${SQLITE_DB} --directory apps/web/internal/migrations ${MIGRATION_NAME}
pw_migrate create --auto --auto-source apps.webui.models --database sqlite:///${SQLITE_DB} --directory apps/web/internal/migrations ${MIGRATION_NAME}
```
- `$SQLITE_DB` should be the path to the database file.
- `$MIGRATION_NAME` should be a descriptive name for the migration.

View File

@@ -1,18 +1,19 @@
from fastapi import FastAPI, Depends
from fastapi.routing import APIRoute
from fastapi.middleware.cors import CORSMiddleware
from apps.web.routers import (
from apps.webui.routers import (
auths,
users,
chats,
documents,
modelfiles,
models,
prompts,
configs,
memories,
utils,
)
from config import (
WEBUI_VERSION,
WEBUI_BUILD_HASH,
WEBUI_AUTH,
DEFAULT_MODELS,
DEFAULT_PROMPT_SUGGESTIONS,
@@ -21,22 +22,34 @@ from config import (
USER_PERMISSIONS,
WEBHOOK_URL,
WEBUI_AUTH_TRUSTED_EMAIL_HEADER,
JWT_EXPIRES_IN,
WEBUI_BANNERS,
AppConfig,
ENABLE_COMMUNITY_SHARING,
)
app = FastAPI()
origins = ["*"]
app.state.ENABLE_SIGNUP = ENABLE_SIGNUP
app.state.JWT_EXPIRES_IN = "-1"
app.state.config = AppConfig()
app.state.DEFAULT_MODELS = DEFAULT_MODELS
app.state.DEFAULT_PROMPT_SUGGESTIONS = DEFAULT_PROMPT_SUGGESTIONS
app.state.DEFAULT_USER_ROLE = DEFAULT_USER_ROLE
app.state.USER_PERMISSIONS = USER_PERMISSIONS
app.state.WEBHOOK_URL = WEBHOOK_URL
app.state.config.ENABLE_SIGNUP = ENABLE_SIGNUP
app.state.config.JWT_EXPIRES_IN = JWT_EXPIRES_IN
app.state.config.DEFAULT_MODELS = DEFAULT_MODELS
app.state.config.DEFAULT_PROMPT_SUGGESTIONS = DEFAULT_PROMPT_SUGGESTIONS
app.state.config.DEFAULT_USER_ROLE = DEFAULT_USER_ROLE
app.state.config.USER_PERMISSIONS = USER_PERMISSIONS
app.state.config.WEBHOOK_URL = WEBHOOK_URL
app.state.config.BANNERS = WEBUI_BANNERS
app.state.config.ENABLE_COMMUNITY_SHARING = ENABLE_COMMUNITY_SHARING
app.state.MODELS = {}
app.state.AUTH_TRUSTED_EMAIL_HEADER = WEBUI_AUTH_TRUSTED_EMAIL_HEADER
app.add_middleware(
CORSMiddleware,
allow_origins=origins,
@@ -48,9 +61,11 @@ app.add_middleware(
app.include_router(auths.router, prefix="/auths", tags=["auths"])
app.include_router(users.router, prefix="/users", tags=["users"])
app.include_router(chats.router, prefix="/chats", tags=["chats"])
app.include_router(documents.router, prefix="/documents", tags=["documents"])
app.include_router(modelfiles.router, prefix="/modelfiles", tags=["modelfiles"])
app.include_router(models.router, prefix="/models", tags=["models"])
app.include_router(prompts.router, prefix="/prompts", tags=["prompts"])
app.include_router(memories.router, prefix="/memories", tags=["memories"])
app.include_router(configs.router, prefix="/configs", tags=["configs"])
app.include_router(utils.router, prefix="/utils", tags=["utils"])
@@ -61,6 +76,6 @@ async def get_status():
return {
"status": True,
"auth": WEBUI_AUTH,
"default_models": app.state.DEFAULT_MODELS,
"default_prompt_suggestions": app.state.DEFAULT_PROMPT_SUGGESTIONS,
"default_models": app.state.config.DEFAULT_MODELS,
"default_prompt_suggestions": app.state.config.DEFAULT_PROMPT_SUGGESTIONS,
}

View File

@@ -5,10 +5,10 @@ import uuid
import logging
from peewee import *
from apps.web.models.users import UserModel, Users
from apps.webui.models.users import UserModel, Users
from utils.utils import verify_password
from apps.web.internal.db import DB
from apps.webui.internal.db import DB
from config import SRC_LOG_LEVELS
@@ -23,7 +23,7 @@ log.setLevel(SRC_LOG_LEVELS["MODELS"])
class Auth(Model):
id = CharField(unique=True)
email = CharField()
password = CharField()
password = TextField()
active = BooleanField()
class Meta:
@@ -89,6 +89,10 @@ class SignupForm(BaseModel):
profile_image_url: Optional[str] = "/user.png"
class AddUserForm(SignupForm):
role: Optional[str] = "pending"
class AuthsTable:
def __init__(self, db):
self.db = db

View File

@@ -7,7 +7,7 @@ import json
import uuid
import time
from apps.web.internal.db import DB
from apps.webui.internal.db import DB
####################
# Chat DB Schema
@@ -17,11 +17,11 @@ from apps.web.internal.db import DB
class Chat(Model):
id = CharField(unique=True)
user_id = CharField()
title = CharField()
title = TextField()
chat = TextField() # Save Chat JSON as Text
created_at = DateTimeField()
updated_at = DateTimeField()
created_at = BigIntegerField()
updated_at = BigIntegerField()
share_id = CharField(null=True, unique=True)
archived = BooleanField(default=False)
@@ -191,7 +191,21 @@ class ChatTable:
except:
return None
def get_archived_chat_lists_by_user_id(
def archive_all_chats_by_user_id(self, user_id: str) -> bool:
try:
chats = self.get_chats_by_user_id(user_id)
for chat in chats:
query = Chat.update(
archived=True,
).where(Chat.id == chat.id)
query.execute()
return True
except:
return False
def get_archived_chat_list_by_user_id(
self, user_id: str, skip: int = 0, limit: int = 50
) -> List[ChatModel]:
return [
@@ -204,20 +218,34 @@ class ChatTable:
# .offset(skip)
]
def get_chat_lists_by_user_id(
self, user_id: str, skip: int = 0, limit: int = 50
def get_chat_list_by_user_id(
self,
user_id: str,
include_archived: bool = False,
skip: int = 0,
limit: int = 50,
) -> List[ChatModel]:
return [
ChatModel(**model_to_dict(chat))
for chat in Chat.select()
.where(Chat.archived == False)
.where(Chat.user_id == user_id)
.order_by(Chat.updated_at.desc())
# .limit(limit)
# .offset(skip)
]
if include_archived:
return [
ChatModel(**model_to_dict(chat))
for chat in Chat.select()
.where(Chat.user_id == user_id)
.order_by(Chat.updated_at.desc())
# .limit(limit)
# .offset(skip)
]
else:
return [
ChatModel(**model_to_dict(chat))
for chat in Chat.select()
.where(Chat.archived == False)
.where(Chat.user_id == user_id)
.order_by(Chat.updated_at.desc())
# .limit(limit)
# .offset(skip)
]
def get_chat_lists_by_chat_ids(
def get_chat_list_by_chat_ids(
self, chat_ids: List[str], skip: int = 0, limit: int = 50
) -> List[ChatModel]:
return [
@@ -228,20 +256,6 @@ class ChatTable:
.order_by(Chat.updated_at.desc())
]
def get_all_chats(self) -> List[ChatModel]:
return [
ChatModel(**model_to_dict(chat))
for chat in Chat.select().order_by(Chat.updated_at.desc())
]
def get_all_chats_by_user_id(self, user_id: str) -> List[ChatModel]:
return [
ChatModel(**model_to_dict(chat))
for chat in Chat.select()
.where(Chat.user_id == user_id)
.order_by(Chat.updated_at.desc())
]
def get_chat_by_id(self, id: str) -> Optional[ChatModel]:
try:
chat = Chat.get(Chat.id == id)
@@ -271,9 +285,28 @@ class ChatTable:
def get_chats(self, skip: int = 0, limit: int = 50) -> List[ChatModel]:
return [
ChatModel(**model_to_dict(chat))
for chat in Chat.select().limit(limit).offset(skip)
for chat in Chat.select().order_by(Chat.updated_at.desc())
# .limit(limit).offset(skip)
]
def get_chats_by_user_id(self, user_id: str) -> List[ChatModel]:
return [
ChatModel(**model_to_dict(chat))
for chat in Chat.select()
.where(Chat.user_id == user_id)
.order_by(Chat.updated_at.desc())
# .limit(limit).offset(skip)
]
def delete_chat_by_id(self, id: str) -> bool:
try:
query = Chat.delete().where((Chat.id == id))
query.execute() # Remove the rows, return number of rows removed.
return True and self.delete_shared_chat_by_chat_id(id)
except:
return False
def delete_chat_by_id_and_user_id(self, id: str, user_id: str) -> bool:
try:
query = Chat.delete().where((Chat.id == id) & (Chat.user_id == user_id))

View File

@@ -8,7 +8,7 @@ import logging
from utils.utils import decode_token
from utils.misc import get_gravatar_url
from apps.web.internal.db import DB
from apps.webui.internal.db import DB
import json
@@ -25,11 +25,11 @@ log.setLevel(SRC_LOG_LEVELS["MODELS"])
class Document(Model):
collection_name = CharField(unique=True)
name = CharField(unique=True)
title = CharField()
filename = CharField()
title = TextField()
filename = TextField()
content = TextField(null=True)
user_id = CharField()
timestamp = DateField()
timestamp = BigIntegerField()
class Meta:
database = DB

View File

@@ -0,0 +1,118 @@
from pydantic import BaseModel
from peewee import *
from playhouse.shortcuts import model_to_dict
from typing import List, Union, Optional
from apps.webui.internal.db import DB
from apps.webui.models.chats import Chats
import time
import uuid
####################
# Memory DB Schema
####################
class Memory(Model):
id = CharField(unique=True)
user_id = CharField()
content = TextField()
updated_at = BigIntegerField()
created_at = BigIntegerField()
class Meta:
database = DB
class MemoryModel(BaseModel):
id: str
user_id: str
content: str
updated_at: int # timestamp in epoch
created_at: int # timestamp in epoch
####################
# Forms
####################
class MemoriesTable:
def __init__(self, db):
self.db = db
self.db.create_tables([Memory])
def insert_new_memory(
self,
user_id: str,
content: str,
) -> Optional[MemoryModel]:
id = str(uuid.uuid4())
memory = MemoryModel(
**{
"id": id,
"user_id": user_id,
"content": content,
"created_at": int(time.time()),
"updated_at": int(time.time()),
}
)
result = Memory.create(**memory.model_dump())
if result:
return memory
else:
return None
def get_memories(self) -> List[MemoryModel]:
try:
memories = Memory.select()
return [MemoryModel(**model_to_dict(memory)) for memory in memories]
except:
return None
def get_memories_by_user_id(self, user_id: str) -> List[MemoryModel]:
try:
memories = Memory.select().where(Memory.user_id == user_id)
return [MemoryModel(**model_to_dict(memory)) for memory in memories]
except:
return None
def get_memory_by_id(self, id) -> Optional[MemoryModel]:
try:
memory = Memory.get(Memory.id == id)
return MemoryModel(**model_to_dict(memory))
except:
return None
def delete_memory_by_id(self, id: str) -> bool:
try:
query = Memory.delete().where(Memory.id == id)
query.execute() # Remove the rows, return number of rows removed.
return True
except:
return False
def delete_memories_by_user_id(self, user_id: str) -> bool:
try:
query = Memory.delete().where(Memory.user_id == user_id)
query.execute()
return True
except:
return False
def delete_memory_by_id_and_user_id(self, id: str, user_id: str) -> bool:
try:
query = Memory.delete().where(Memory.id == id, Memory.user_id == user_id)
query.execute()
return True
except:
return False
Memories = MemoriesTable(DB)

View File

@@ -0,0 +1,179 @@
import json
import logging
from typing import Optional
import peewee as pw
from peewee import *
from playhouse.shortcuts import model_to_dict
from pydantic import BaseModel, ConfigDict
from apps.webui.internal.db import DB, JSONField
from typing import List, Union, Optional
from config import SRC_LOG_LEVELS
import time
log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["MODELS"])
####################
# Models DB Schema
####################
# ModelParams is a model for the data stored in the params field of the Model table
class ModelParams(BaseModel):
model_config = ConfigDict(extra="allow")
pass
# ModelMeta is a model for the data stored in the meta field of the Model table
class ModelMeta(BaseModel):
profile_image_url: Optional[str] = "/favicon.png"
description: Optional[str] = None
"""
User-facing description of the model.
"""
capabilities: Optional[dict] = None
model_config = ConfigDict(extra="allow")
pass
class Model(pw.Model):
id = pw.TextField(unique=True)
"""
The model's id as used in the API. If set to an existing model, it will override the model.
"""
user_id = pw.TextField()
base_model_id = pw.TextField(null=True)
"""
An optional pointer to the actual model that should be used when proxying requests.
"""
name = pw.TextField()
"""
The human-readable display name of the model.
"""
params = JSONField()
"""
Holds a JSON encoded blob of parameters, see `ModelParams`.
"""
meta = JSONField()
"""
Holds a JSON encoded blob of metadata, see `ModelMeta`.
"""
updated_at = BigIntegerField()
created_at = BigIntegerField()
class Meta:
database = DB
class ModelModel(BaseModel):
id: str
user_id: str
base_model_id: Optional[str] = None
name: str
params: ModelParams
meta: ModelMeta
updated_at: int # timestamp in epoch
created_at: int # timestamp in epoch
####################
# Forms
####################
class ModelResponse(BaseModel):
id: str
name: str
meta: ModelMeta
updated_at: int # timestamp in epoch
created_at: int # timestamp in epoch
class ModelForm(BaseModel):
id: str
base_model_id: Optional[str] = None
name: str
meta: ModelMeta
params: ModelParams
class ModelsTable:
def __init__(
self,
db: pw.SqliteDatabase | pw.PostgresqlDatabase,
):
self.db = db
self.db.create_tables([Model])
def insert_new_model(
self, form_data: ModelForm, user_id: str
) -> Optional[ModelModel]:
model = ModelModel(
**{
**form_data.model_dump(),
"user_id": user_id,
"created_at": int(time.time()),
"updated_at": int(time.time()),
}
)
try:
result = Model.create(**model.model_dump())
if result:
return model
else:
return None
except Exception as e:
print(e)
return None
def get_all_models(self) -> List[ModelModel]:
return [ModelModel(**model_to_dict(model)) for model in Model.select()]
def get_model_by_id(self, id: str) -> Optional[ModelModel]:
try:
model = Model.get(Model.id == id)
return ModelModel(**model_to_dict(model))
except:
return None
def update_model_by_id(self, id: str, model: ModelForm) -> Optional[ModelModel]:
try:
# update only the fields that are present in the model
query = Model.update(**model.model_dump()).where(Model.id == id)
query.execute()
model = Model.get(Model.id == id)
return ModelModel(**model_to_dict(model))
except Exception as e:
print(e)
return None
def delete_model_by_id(self, id: str) -> bool:
try:
query = Model.delete().where(Model.id == id)
query.execute()
return True
except:
return False
Models = ModelsTable(DB)

View File

@@ -7,7 +7,7 @@ import time
from utils.utils import decode_token
from utils.misc import get_gravatar_url
from apps.web.internal.db import DB
from apps.webui.internal.db import DB
import json
@@ -19,9 +19,9 @@ import json
class Prompt(Model):
command = CharField(unique=True)
user_id = CharField()
title = CharField()
title = TextField()
content = TextField()
timestamp = DateField()
timestamp = BigIntegerField()
class Meta:
database = DB

View File

@@ -8,7 +8,7 @@ import uuid
import time
import logging
from apps.web.internal.db import DB
from apps.webui.internal.db import DB
from config import SRC_LOG_LEVELS
@@ -35,7 +35,7 @@ class ChatIdTag(Model):
tag_name = CharField()
chat_id = CharField()
user_id = CharField()
timestamp = DateField()
timestamp = BigIntegerField()
class Meta:
database = DB

View File

@@ -1,12 +1,12 @@
from pydantic import BaseModel
from pydantic import BaseModel, ConfigDict
from peewee import *
from playhouse.shortcuts import model_to_dict
from typing import List, Union, Optional
import time
from utils.misc import get_gravatar_url
from apps.web.internal.db import DB
from apps.web.models.chats import Chats
from apps.webui.internal.db import DB, JSONField
from apps.webui.models.chats import Chats
####################
# User DB Schema
@@ -18,22 +18,38 @@ class User(Model):
name = CharField()
email = CharField()
role = CharField()
profile_image_url = CharField()
timestamp = DateField()
profile_image_url = TextField()
last_active_at = BigIntegerField()
updated_at = BigIntegerField()
created_at = BigIntegerField()
api_key = CharField(null=True, unique=True)
settings = JSONField(null=True)
class Meta:
database = DB
class UserSettings(BaseModel):
ui: Optional[dict] = {}
model_config = ConfigDict(extra="allow")
pass
class UserModel(BaseModel):
id: str
name: str
email: str
role: str = "pending"
profile_image_url: str
timestamp: int # timestamp in epoch
last_active_at: int # timestamp in epoch
updated_at: int # timestamp in epoch
created_at: int # timestamp in epoch
api_key: Optional[str] = None
settings: Optional[UserSettings] = None
####################
@@ -73,7 +89,9 @@ class UsersTable:
"email": email,
"role": role,
"profile_image_url": profile_image_url,
"timestamp": int(time.time()),
"last_active_at": int(time.time()),
"created_at": int(time.time()),
"updated_at": int(time.time()),
}
)
result = User.create(**user.model_dump())
@@ -113,6 +131,13 @@ class UsersTable:
def get_num_users(self) -> Optional[int]:
return User.select().count()
def get_first_user(self) -> UserModel:
try:
user = User.select().order_by(User.created_at).first()
return UserModel(**model_to_dict(user))
except:
return None
def update_user_role_by_id(self, id: str, role: str) -> Optional[UserModel]:
try:
query = User.update(role=role).where(User.id == id)
@@ -137,6 +162,16 @@ class UsersTable:
except:
return None
def update_user_last_active_by_id(self, id: str) -> Optional[UserModel]:
try:
query = User.update(last_active_at=int(time.time())).where(User.id == id)
query.execute()
user = User.get(User.id == id)
return UserModel(**model_to_dict(user))
except:
return None
def update_user_by_id(self, id: str, updated: dict) -> Optional[UserModel]:
try:
query = User.update(**updated).where(User.id == id)

View File

@@ -1,14 +1,19 @@
from fastapi import Request
import logging
from fastapi import Request, UploadFile, File
from fastapi import Depends, HTTPException, status
from fastapi import APIRouter
from pydantic import BaseModel
import re
import uuid
import csv
from apps.web.models.auths import (
from apps.webui.models.auths import (
SigninForm,
SignupForm,
AddUserForm,
UpdateProfileForm,
UpdatePasswordForm,
UserResponse,
@@ -16,7 +21,7 @@ from apps.web.models.auths import (
Auths,
ApiKey,
)
from apps.web.models.users import Users
from apps.webui.models.users import Users
from utils.utils import (
get_password_hash,
@@ -28,7 +33,7 @@ from utils.utils import (
from utils.misc import parse_duration, validate_email_format
from utils.webhook import post_webhook
from constants import ERROR_MESSAGES, WEBHOOK_MESSAGES
from config import WEBUI_AUTH_TRUSTED_EMAIL_HEADER
from config import WEBUI_AUTH, WEBUI_AUTH_TRUSTED_EMAIL_HEADER
router = APIRouter()
@@ -113,13 +118,29 @@ async def signin(request: Request, form_data: SigninForm):
),
)
user = Auths.authenticate_user_by_trusted_header(trusted_email)
elif WEBUI_AUTH == False:
admin_email = "admin@localhost"
admin_password = "admin"
if Users.get_user_by_email(admin_email.lower()):
user = Auths.authenticate_user(admin_email.lower(), admin_password)
else:
if Users.get_num_users() != 0:
raise HTTPException(400, detail=ERROR_MESSAGES.EXISTING_USERS)
await signup(
request,
SignupForm(email=admin_email, password=admin_password, name="User"),
)
user = Auths.authenticate_user(admin_email.lower(), admin_password)
else:
user = Auths.authenticate_user(form_data.email.lower(), form_data.password)
if user:
token = create_token(
data={"id": user.id},
expires_delta=parse_duration(request.app.state.JWT_EXPIRES_IN),
expires_delta=parse_duration(request.app.state.config.JWT_EXPIRES_IN),
)
return {
@@ -142,7 +163,7 @@ async def signin(request: Request, form_data: SigninForm):
@router.post("/signup", response_model=SigninResponse)
async def signup(request: Request, form_data: SignupForm):
if not request.app.state.ENABLE_SIGNUP:
if not request.app.state.config.ENABLE_SIGNUP and WEBUI_AUTH:
raise HTTPException(
status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.ACCESS_PROHIBITED
)
@@ -159,7 +180,7 @@ async def signup(request: Request, form_data: SignupForm):
role = (
"admin"
if Users.get_num_users() == 0
else request.app.state.DEFAULT_USER_ROLE
else request.app.state.config.DEFAULT_USER_ROLE
)
hashed = get_password_hash(form_data.password)
user = Auths.insert_new_auth(
@@ -173,13 +194,13 @@ async def signup(request: Request, form_data: SignupForm):
if user:
token = create_token(
data={"id": user.id},
expires_delta=parse_duration(request.app.state.JWT_EXPIRES_IN),
expires_delta=parse_duration(request.app.state.config.JWT_EXPIRES_IN),
)
# response.set_cookie(key='token', value=token, httponly=True)
if request.app.state.WEBHOOK_URL:
if request.app.state.config.WEBHOOK_URL:
post_webhook(
request.app.state.WEBHOOK_URL,
request.app.state.config.WEBHOOK_URL,
WEBHOOK_MESSAGES.USER_SIGNUP(user.name),
{
"action": "signup",
@@ -203,6 +224,51 @@ async def signup(request: Request, form_data: SignupForm):
raise HTTPException(500, detail=ERROR_MESSAGES.DEFAULT(err))
############################
# AddUser
############################
@router.post("/add", response_model=SigninResponse)
async def add_user(form_data: AddUserForm, user=Depends(get_admin_user)):
if not validate_email_format(form_data.email.lower()):
raise HTTPException(
status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.INVALID_EMAIL_FORMAT
)
if Users.get_user_by_email(form_data.email.lower()):
raise HTTPException(400, detail=ERROR_MESSAGES.EMAIL_TAKEN)
try:
print(form_data)
hashed = get_password_hash(form_data.password)
user = Auths.insert_new_auth(
form_data.email.lower(),
hashed,
form_data.name,
form_data.profile_image_url,
form_data.role,
)
if user:
token = create_token(data={"id": user.id})
return {
"token": token,
"token_type": "Bearer",
"id": user.id,
"email": user.email,
"name": user.name,
"role": user.role,
"profile_image_url": user.profile_image_url,
}
else:
raise HTTPException(500, detail=ERROR_MESSAGES.CREATE_USER_ERROR)
except Exception as err:
raise HTTPException(500, detail=ERROR_MESSAGES.DEFAULT(err))
############################
# ToggleSignUp
############################
@@ -210,13 +276,13 @@ async def signup(request: Request, form_data: SignupForm):
@router.get("/signup/enabled", response_model=bool)
async def get_sign_up_status(request: Request, user=Depends(get_admin_user)):
return request.app.state.ENABLE_SIGNUP
return request.app.state.config.ENABLE_SIGNUP
@router.get("/signup/enabled/toggle", response_model=bool)
async def toggle_sign_up(request: Request, user=Depends(get_admin_user)):
request.app.state.ENABLE_SIGNUP = not request.app.state.ENABLE_SIGNUP
return request.app.state.ENABLE_SIGNUP
request.app.state.config.ENABLE_SIGNUP = not request.app.state.config.ENABLE_SIGNUP
return request.app.state.config.ENABLE_SIGNUP
############################
@@ -226,7 +292,7 @@ async def toggle_sign_up(request: Request, user=Depends(get_admin_user)):
@router.get("/signup/user/role")
async def get_default_user_role(request: Request, user=Depends(get_admin_user)):
return request.app.state.DEFAULT_USER_ROLE
return request.app.state.config.DEFAULT_USER_ROLE
class UpdateRoleForm(BaseModel):
@@ -238,8 +304,8 @@ async def update_default_user_role(
request: Request, form_data: UpdateRoleForm, user=Depends(get_admin_user)
):
if form_data.role in ["pending", "user", "admin"]:
request.app.state.DEFAULT_USER_ROLE = form_data.role
return request.app.state.DEFAULT_USER_ROLE
request.app.state.config.DEFAULT_USER_ROLE = form_data.role
return request.app.state.config.DEFAULT_USER_ROLE
############################
@@ -249,7 +315,7 @@ async def update_default_user_role(
@router.get("/token/expires")
async def get_token_expires_duration(request: Request, user=Depends(get_admin_user)):
return request.app.state.JWT_EXPIRES_IN
return request.app.state.config.JWT_EXPIRES_IN
class UpdateJWTExpiresDurationForm(BaseModel):
@@ -266,10 +332,10 @@ async def update_token_expires_duration(
# Check if the input string matches the pattern
if re.match(pattern, form_data.duration):
request.app.state.JWT_EXPIRES_IN = form_data.duration
return request.app.state.JWT_EXPIRES_IN
request.app.state.config.JWT_EXPIRES_IN = form_data.duration
return request.app.state.config.JWT_EXPIRES_IN
else:
return request.app.state.JWT_EXPIRES_IN
return request.app.state.config.JWT_EXPIRES_IN
############################

View File

@@ -7,8 +7,8 @@ from pydantic import BaseModel
import json
import logging
from apps.web.models.users import Users
from apps.web.models.chats import (
from apps.webui.models.users import Users
from apps.webui.models.chats import (
ChatModel,
ChatResponse,
ChatTitleForm,
@@ -18,7 +18,7 @@ from apps.web.models.chats import (
)
from apps.web.models.tags import (
from apps.webui.models.tags import (
TagModel,
ChatIdTagModel,
ChatIdTagForm,
@@ -36,58 +36,51 @@ log.setLevel(SRC_LOG_LEVELS["MODELS"])
router = APIRouter()
############################
# GetChats
# GetChatList
############################
@router.get("/", response_model=List[ChatTitleIdResponse])
async def get_user_chats(
@router.get("/list", response_model=List[ChatTitleIdResponse])
async def get_session_user_chat_list(
user=Depends(get_current_user), skip: int = 0, limit: int = 50
):
return Chats.get_chat_lists_by_user_id(user.id, skip, limit)
return Chats.get_chat_list_by_user_id(user.id, skip, limit)
############################
# GetArchivedChats
# DeleteAllChats
############################
@router.get("/archived", response_model=List[ChatTitleIdResponse])
async def get_archived_user_chats(
user=Depends(get_current_user), skip: int = 0, limit: int = 50
):
return Chats.get_archived_chat_lists_by_user_id(user.id, skip, limit)
@router.delete("/", response_model=bool)
async def delete_all_user_chats(request: Request, user=Depends(get_current_user)):
############################
# GetAllChats
############################
@router.get("/all", response_model=List[ChatResponse])
async def get_all_user_chats(user=Depends(get_current_user)):
return [
ChatResponse(**{**chat.model_dump(), "chat": json.loads(chat.chat)})
for chat in Chats.get_all_chats_by_user_id(user.id)
]
############################
# GetAllChatsInDB
############################
@router.get("/all/db", response_model=List[ChatResponse])
async def get_all_user_chats_in_db(user=Depends(get_admin_user)):
if not ENABLE_ADMIN_EXPORT:
if (
user.role == "user"
and not request.app.state.config.USER_PERMISSIONS["chat"]["deletion"]
):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
)
return [
ChatResponse(**{**chat.model_dump(), "chat": json.loads(chat.chat)})
for chat in Chats.get_all_chats()
]
result = Chats.delete_chats_by_user_id(user.id)
return result
############################
# GetUserChatList
############################
@router.get("/list/user/{user_id}", response_model=List[ChatTitleIdResponse])
async def get_user_chat_list_by_user_id(
user_id: str, user=Depends(get_admin_user), skip: int = 0, limit: int = 50
):
return Chats.get_chat_list_by_user_id(
user_id, include_archived=True, skip=skip, limit=limit
)
############################
@@ -107,6 +100,116 @@ async def create_new_chat(form_data: ChatForm, user=Depends(get_current_user)):
)
############################
# GetChats
############################
@router.get("/all", response_model=List[ChatResponse])
async def get_user_chats(user=Depends(get_current_user)):
return [
ChatResponse(**{**chat.model_dump(), "chat": json.loads(chat.chat)})
for chat in Chats.get_chats_by_user_id(user.id)
]
############################
# GetAllChatsInDB
############################
@router.get("/all/db", response_model=List[ChatResponse])
async def get_all_user_chats_in_db(user=Depends(get_admin_user)):
if not ENABLE_ADMIN_EXPORT:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
)
return [
ChatResponse(**{**chat.model_dump(), "chat": json.loads(chat.chat)})
for chat in Chats.get_chats()
]
############################
# GetArchivedChats
############################
@router.get("/archived", response_model=List[ChatTitleIdResponse])
async def get_archived_session_user_chat_list(
user=Depends(get_current_user), skip: int = 0, limit: int = 50
):
return Chats.get_archived_chat_list_by_user_id(user.id, skip, limit)
############################
# ArchiveAllChats
############################
@router.post("/archive/all", response_model=List[ChatTitleIdResponse])
async def archive_all_chats(user=Depends(get_current_user)):
return Chats.archive_all_chats_by_user_id(user.id)
############################
# GetSharedChatById
############################
@router.get("/share/{share_id}", response_model=Optional[ChatResponse])
async def get_shared_chat_by_id(share_id: str, user=Depends(get_current_user)):
if user.role == "pending":
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED, detail=ERROR_MESSAGES.NOT_FOUND
)
if user.role == "user":
chat = Chats.get_chat_by_share_id(share_id)
elif user.role == "admin":
chat = Chats.get_chat_by_id(share_id)
if chat:
return ChatResponse(**{**chat.model_dump(), "chat": json.loads(chat.chat)})
else:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED, detail=ERROR_MESSAGES.NOT_FOUND
)
############################
# GetChatsByTags
############################
class TagNameForm(BaseModel):
name: str
skip: Optional[int] = 0
limit: Optional[int] = 50
@router.post("/tags", response_model=List[ChatTitleIdResponse])
async def get_user_chat_list_by_tag_name(
form_data: TagNameForm, user=Depends(get_current_user)
):
print(form_data)
chat_ids = [
chat_id_tag.chat_id
for chat_id_tag in Tags.get_chat_ids_by_tag_name_and_user_id(
form_data.name, user.id
)
]
chats = Chats.get_chat_list_by_chat_ids(chat_ids, form_data.skip, form_data.limit)
if len(chats) == 0:
Tags.delete_tag_by_tag_name_and_user_id(form_data.name, user.id)
return chats
############################
# GetAllTags
############################
@@ -124,28 +227,6 @@ async def get_all_tags(user=Depends(get_current_user)):
)
############################
# GetChatsByTags
############################
@router.get("/tags/tag/{tag_name}", response_model=List[ChatTitleIdResponse])
async def get_user_chats_by_tag_name(
tag_name: str, user=Depends(get_current_user), skip: int = 0, limit: int = 50
):
chat_ids = [
chat_id_tag.chat_id
for chat_id_tag in Tags.get_chat_ids_by_tag_name_and_user_id(tag_name, user.id)
]
chats = Chats.get_chat_lists_by_chat_ids(chat_ids, skip, limit)
if len(chats) == 0:
Tags.delete_tag_by_tag_name_and_user_id(tag_name, user.id)
return chats
############################
# GetChatById
############################
@@ -193,17 +274,44 @@ async def update_chat_by_id(
@router.delete("/{id}", response_model=bool)
async def delete_chat_by_id(request: Request, id: str, user=Depends(get_current_user)):
if (
user.role == "user"
and not request.app.state.USER_PERMISSIONS["chat"]["deletion"]
):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
)
if user.role == "admin":
result = Chats.delete_chat_by_id(id)
return result
else:
if not request.app.state.config.USER_PERMISSIONS["chat"]["deletion"]:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
)
result = Chats.delete_chat_by_id_and_user_id(id, user.id)
return result
result = Chats.delete_chat_by_id_and_user_id(id, user.id)
return result
############################
# CloneChat
############################
@router.get("/{id}/clone", response_model=Optional[ChatResponse])
async def clone_chat_by_id(id: str, user=Depends(get_current_user)):
chat = Chats.get_chat_by_id_and_user_id(id, user.id)
if chat:
chat_body = json.loads(chat.chat)
updated_chat = {
**chat_body,
"originalChatId": chat.id,
"branchPointMessageId": chat_body["history"]["currentId"],
"title": f"Clone of {chat.title}",
}
chat = Chats.insert_new_chat(user.id, ChatForm(**{"chat": updated_chat}))
return ChatResponse(**{**chat.model_dump(), "chat": json.loads(chat.chat)})
else:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED, detail=ERROR_MESSAGES.DEFAULT()
)
############################
@@ -278,31 +386,6 @@ async def delete_shared_chat_by_id(id: str, user=Depends(get_current_user)):
)
############################
# GetSharedChatById
############################
@router.get("/share/{share_id}", response_model=Optional[ChatResponse])
async def get_shared_chat_by_id(share_id: str, user=Depends(get_current_user)):
if user.role == "pending":
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED, detail=ERROR_MESSAGES.NOT_FOUND
)
if user.role == "user":
chat = Chats.get_chat_by_share_id(share_id)
elif user.role == "admin":
chat = Chats.get_chat_by_id(share_id)
if chat:
return ChatResponse(**{**chat.model_dump(), "chat": json.loads(chat.chat)})
else:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED, detail=ERROR_MESSAGES.NOT_FOUND
)
############################
# GetChatTagsById
############################
@@ -383,24 +466,3 @@ async def delete_all_chat_tags_by_id(id: str, user=Depends(get_current_user)):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED, detail=ERROR_MESSAGES.NOT_FOUND
)
############################
# DeleteAllChats
############################
@router.delete("/", response_model=bool)
async def delete_all_user_chats(request: Request, user=Depends(get_current_user)):
if (
user.role == "user"
and not request.app.state.USER_PERMISSIONS["chat"]["deletion"]
):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
)
result = Chats.delete_chats_by_user_id(user.id)
return result

View File

@@ -8,7 +8,9 @@ from pydantic import BaseModel
import time
import uuid
from apps.web.models.users import Users
from config import BannerModel
from apps.webui.models.users import Users
from utils.utils import (
get_password_hash,
@@ -44,8 +46,8 @@ class SetDefaultSuggestionsForm(BaseModel):
async def set_global_default_models(
request: Request, form_data: SetDefaultModelsForm, user=Depends(get_admin_user)
):
request.app.state.DEFAULT_MODELS = form_data.models
return request.app.state.DEFAULT_MODELS
request.app.state.config.DEFAULT_MODELS = form_data.models
return request.app.state.config.DEFAULT_MODELS
@router.post("/default/suggestions", response_model=List[PromptSuggestion])
@@ -55,5 +57,33 @@ async def set_global_default_suggestions(
user=Depends(get_admin_user),
):
data = form_data.model_dump()
request.app.state.DEFAULT_PROMPT_SUGGESTIONS = data["suggestions"]
return request.app.state.DEFAULT_PROMPT_SUGGESTIONS
request.app.state.config.DEFAULT_PROMPT_SUGGESTIONS = data["suggestions"]
return request.app.state.config.DEFAULT_PROMPT_SUGGESTIONS
############################
# SetBanners
############################
class SetBannersForm(BaseModel):
banners: List[BannerModel]
@router.post("/banners", response_model=List[BannerModel])
async def set_banners(
request: Request,
form_data: SetBannersForm,
user=Depends(get_admin_user),
):
data = form_data.model_dump()
request.app.state.config.BANNERS = data["banners"]
return request.app.state.config.BANNERS
@router.get("/banners", response_model=List[BannerModel])
async def get_banners(
request: Request,
user=Depends(get_current_user),
):
return request.app.state.config.BANNERS

View File

@@ -6,7 +6,7 @@ from fastapi import APIRouter
from pydantic import BaseModel
import json
from apps.web.models.documents import (
from apps.webui.models.documents import (
Documents,
DocumentForm,
DocumentUpdateForm,

View File

@@ -0,0 +1,145 @@
from fastapi import Response, Request
from fastapi import Depends, FastAPI, HTTPException, status
from datetime import datetime, timedelta
from typing import List, Union, Optional
from fastapi import APIRouter
from pydantic import BaseModel
import logging
from apps.webui.models.memories import Memories, MemoryModel
from utils.utils import get_verified_user
from constants import ERROR_MESSAGES
from config import SRC_LOG_LEVELS, CHROMA_CLIENT
log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["MODELS"])
router = APIRouter()
@router.get("/ef")
async def get_embeddings(request: Request):
return {"result": request.app.state.EMBEDDING_FUNCTION("hello world")}
############################
# GetMemories
############################
@router.get("/", response_model=List[MemoryModel])
async def get_memories(user=Depends(get_verified_user)):
return Memories.get_memories_by_user_id(user.id)
############################
# AddMemory
############################
class AddMemoryForm(BaseModel):
content: str
@router.post("/add", response_model=Optional[MemoryModel])
async def add_memory(
request: Request, form_data: AddMemoryForm, user=Depends(get_verified_user)
):
memory = Memories.insert_new_memory(user.id, form_data.content)
memory_embedding = request.app.state.EMBEDDING_FUNCTION(memory.content)
collection = CHROMA_CLIENT.get_or_create_collection(name=f"user-memory-{user.id}")
collection.upsert(
documents=[memory.content],
ids=[memory.id],
embeddings=[memory_embedding],
metadatas=[{"created_at": memory.created_at}],
)
return memory
############################
# QueryMemory
############################
class QueryMemoryForm(BaseModel):
content: str
@router.post("/query")
async def query_memory(
request: Request, form_data: QueryMemoryForm, user=Depends(get_verified_user)
):
query_embedding = request.app.state.EMBEDDING_FUNCTION(form_data.content)
collection = CHROMA_CLIENT.get_or_create_collection(name=f"user-memory-{user.id}")
results = collection.query(
query_embeddings=[query_embedding],
n_results=1, # how many results to return
)
return results
############################
# ResetMemoryFromVectorDB
############################
@router.get("/reset", response_model=bool)
async def reset_memory_from_vector_db(
request: Request, user=Depends(get_verified_user)
):
CHROMA_CLIENT.delete_collection(f"user-memory-{user.id}")
collection = CHROMA_CLIENT.get_or_create_collection(name=f"user-memory-{user.id}")
memories = Memories.get_memories_by_user_id(user.id)
for memory in memories:
memory_embedding = request.app.state.EMBEDDING_FUNCTION(memory.content)
collection.upsert(
documents=[memory.content],
ids=[memory.id],
embeddings=[memory_embedding],
)
return True
############################
# DeleteMemoriesByUserId
############################
@router.delete("/user", response_model=bool)
async def delete_memory_by_user_id(user=Depends(get_verified_user)):
result = Memories.delete_memories_by_user_id(user.id)
if result:
try:
CHROMA_CLIENT.delete_collection(f"user-memory-{user.id}")
except Exception as e:
log.error(e)
return True
return False
############################
# DeleteMemoryById
############################
@router.delete("/{memory_id}", response_model=bool)
async def delete_memory_by_id(memory_id: str, user=Depends(get_verified_user)):
result = Memories.delete_memory_by_id_and_user_id(memory_id, user.id)
if result:
collection = CHROMA_CLIENT.get_or_create_collection(
name=f"user-memory-{user.id}"
)
collection.delete(ids=[memory_id])
return True
return False

View File

@@ -0,0 +1,107 @@
from fastapi import Depends, FastAPI, HTTPException, status, Request
from datetime import datetime, timedelta
from typing import List, Union, Optional
from fastapi import APIRouter
from pydantic import BaseModel
import json
from apps.webui.models.models import Models, ModelModel, ModelForm, ModelResponse
from utils.utils import get_verified_user, get_admin_user
from constants import ERROR_MESSAGES
router = APIRouter()
###########################
# getModels
###########################
@router.get("/", response_model=List[ModelResponse])
async def get_models(user=Depends(get_verified_user)):
return Models.get_all_models()
############################
# AddNewModel
############################
@router.post("/add", response_model=Optional[ModelModel])
async def add_new_model(
request: Request, form_data: ModelForm, user=Depends(get_admin_user)
):
if form_data.id in request.app.state.MODELS:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=ERROR_MESSAGES.MODEL_ID_TAKEN,
)
else:
model = Models.insert_new_model(form_data, user.id)
if model:
return model
else:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=ERROR_MESSAGES.DEFAULT(),
)
############################
# GetModelById
############################
@router.get("/", response_model=Optional[ModelModel])
async def get_model_by_id(id: str, user=Depends(get_verified_user)):
model = Models.get_model_by_id(id)
if model:
return model
else:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=ERROR_MESSAGES.NOT_FOUND,
)
############################
# UpdateModelById
############################
@router.post("/update", response_model=Optional[ModelModel])
async def update_model_by_id(
request: Request, id: str, form_data: ModelForm, user=Depends(get_admin_user)
):
model = Models.get_model_by_id(id)
if model:
model = Models.update_model_by_id(id, form_data)
return model
else:
if form_data.id in request.app.state.MODELS:
model = Models.insert_new_model(form_data, user.id)
if model:
return model
else:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=ERROR_MESSAGES.DEFAULT(),
)
else:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=ERROR_MESSAGES.DEFAULT(),
)
############################
# DeleteModelById
############################
@router.delete("/delete", response_model=bool)
async def delete_model_by_id(id: str, user=Depends(get_admin_user)):
result = Models.delete_model_by_id(id)
return result

View File

@@ -6,7 +6,7 @@ from fastapi import APIRouter
from pydantic import BaseModel
import json
from apps.web.models.prompts import Prompts, PromptForm, PromptModel
from apps.webui.models.prompts import Prompts, PromptForm, PromptModel
from utils.utils import get_current_user, get_admin_user
from constants import ERROR_MESSAGES

View File

@@ -9,10 +9,17 @@ import time
import uuid
import logging
from apps.web.models.users import UserModel, UserUpdateForm, UserRoleUpdateForm, Users
from apps.web.models.auths import Auths
from apps.webui.models.users import (
UserModel,
UserUpdateForm,
UserRoleUpdateForm,
UserSettings,
Users,
)
from apps.webui.models.auths import Auths
from apps.webui.models.chats import Chats
from utils.utils import get_current_user, get_password_hash, get_admin_user
from utils.utils import get_verified_user, get_password_hash, get_admin_user
from constants import ERROR_MESSAGES
from config import SRC_LOG_LEVELS
@@ -39,15 +46,15 @@ async def get_users(skip: int = 0, limit: int = 50, user=Depends(get_admin_user)
@router.get("/permissions/user")
async def get_user_permissions(request: Request, user=Depends(get_admin_user)):
return request.app.state.USER_PERMISSIONS
return request.app.state.config.USER_PERMISSIONS
@router.post("/permissions/user")
async def update_user_permissions(
request: Request, form_data: dict, user=Depends(get_admin_user)
):
request.app.state.USER_PERMISSIONS = form_data
return request.app.state.USER_PERMISSIONS
request.app.state.config.USER_PERMISSIONS = form_data
return request.app.state.config.USER_PERMISSIONS
############################
@@ -58,7 +65,7 @@ async def update_user_permissions(
@router.post("/update/role", response_model=Optional[UserModel])
async def update_user_role(form_data: UserRoleUpdateForm, user=Depends(get_admin_user)):
if user.id != form_data.id:
if user.id != form_data.id and form_data.id != Users.get_first_user().id:
return Users.update_user_role_by_id(form_data.id, form_data.role)
raise HTTPException(
@@ -67,6 +74,79 @@ async def update_user_role(form_data: UserRoleUpdateForm, user=Depends(get_admin
)
############################
# GetUserSettingsBySessionUser
############################
@router.get("/user/settings", response_model=Optional[UserSettings])
async def get_user_settings_by_session_user(user=Depends(get_verified_user)):
user = Users.get_user_by_id(user.id)
if user:
return user.settings
else:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.USER_NOT_FOUND,
)
############################
# UpdateUserSettingsBySessionUser
############################
@router.post("/user/settings/update", response_model=UserSettings)
async def update_user_settings_by_session_user(
form_data: UserSettings, user=Depends(get_verified_user)
):
user = Users.update_user_by_id(user.id, {"settings": form_data.model_dump()})
if user:
return user.settings
else:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.USER_NOT_FOUND,
)
############################
# GetUserById
############################
class UserResponse(BaseModel):
name: str
profile_image_url: str
@router.get("/{user_id}", response_model=UserResponse)
async def get_user_by_id(user_id: str, user=Depends(get_verified_user)):
# Check if user_id is a shared chat
# If it is, get the user_id from the chat
if user_id.startswith("shared-"):
chat_id = user_id.replace("shared-", "")
chat = Chats.get_chat_by_id(chat_id)
if chat:
user_id = chat.user_id
else:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.USER_NOT_FOUND,
)
user = Users.get_user_by_id(user_id)
if user:
return UserResponse(name=user.name, profile_image_url=user.profile_image_url)
else:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.USER_NOT_FOUND,
)
############################
# UpdateUserById
############################

View File

@@ -1,5 +1,6 @@
from fastapi import APIRouter, UploadFile, File, Response
from fastapi import Depends, HTTPException, status
from peewee import SqliteDatabase
from starlette.responses import StreamingResponse, FileResponse
from pydantic import BaseModel
@@ -7,11 +8,11 @@ from pydantic import BaseModel
from fpdf import FPDF
import markdown
from apps.webui.internal.db import DB
from utils.utils import get_admin_user
from utils.misc import calculate_sha256, get_gravatar_url
from config import OLLAMA_BASE_URLS, DATA_DIR, UPLOAD_DIR
from config import OLLAMA_BASE_URLS, DATA_DIR, UPLOAD_DIR, ENABLE_ADMIN_EXPORT
from constants import ERROR_MESSAGES
from typing import List
@@ -96,8 +97,22 @@ async def download_db(user=Depends(get_admin_user)):
status_code=status.HTTP_401_UNAUTHORIZED,
detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
)
if not isinstance(DB, SqliteDatabase):
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.DB_NOT_SQLITE,
)
return FileResponse(
f"{DATA_DIR}/webui.db",
DB.database,
media_type="application/octet-stream",
filename="webui.db",
)
@router.get("/litellm/config")
async def download_litellm_config_yaml(user=Depends(get_admin_user)):
return FileResponse(
f"{DATA_DIR}/litellm/config.yaml",
media_type="application/octet-stream",
filename="config.yaml",
)

View File

@@ -1,10 +1,15 @@
import os
import sys
import logging
import importlib.metadata
import pkgutil
import chromadb
from chromadb import Settings
from base64 import b64encode
from bs4 import BeautifulSoup
from typing import TypeVar, Generic, Union
from pydantic import BaseModel
from typing import Optional
from pathlib import Path
import json
@@ -17,6 +22,22 @@ import shutil
from secrets import token_bytes
from constants import ERROR_MESSAGES
####################################
# Load .env file
####################################
BACKEND_DIR = Path(__file__).parent # the path containing this file
BASE_DIR = BACKEND_DIR.parent # the path containing the backend/
print(BASE_DIR)
try:
from dotenv import load_dotenv, find_dotenv
load_dotenv(find_dotenv(str(BASE_DIR / ".env")))
except ImportError:
print("dotenv not installed, skipping...")
####################################
# LOGGING
@@ -39,7 +60,6 @@ log_sources = [
"CONFIG",
"DB",
"IMAGES",
"LITELLM",
"MAIN",
"MODELS",
"OLLAMA",
@@ -59,20 +79,15 @@ for source in log_sources:
log.setLevel(SRC_LOG_LEVELS["CONFIG"])
####################################
# Load .env file
####################################
try:
from dotenv import load_dotenv, find_dotenv
load_dotenv(find_dotenv("../.env"))
except ImportError:
log.warning("dotenv not installed, skipping...")
WEBUI_NAME = os.environ.get("WEBUI_NAME", "Open WebUI")
if WEBUI_NAME != "Open WebUI":
WEBUI_NAME += " (Open WebUI)"
WEBUI_URL = os.environ.get("WEBUI_URL", "http://localhost:3000")
WEBUI_FAVICON_URL = "https://openwebui.com/favicon.png"
####################################
# ENV (dev,test,prod)
####################################
@@ -80,10 +95,12 @@ WEBUI_FAVICON_URL = "https://openwebui.com/favicon.png"
ENV = os.environ.get("ENV", "dev")
try:
with open(f"../package.json", "r") as f:
PACKAGE_DATA = json.load(f)
PACKAGE_DATA = json.loads((BASE_DIR / "package.json").read_text())
except:
PACKAGE_DATA = {"version": "0.0.0"}
try:
PACKAGE_DATA = {"version": importlib.metadata.version("open-webui")}
except importlib.metadata.PackageNotFoundError:
PACKAGE_DATA = {"version": "0.0.0"}
VERSION = PACKAGE_DATA["version"]
@@ -108,10 +125,13 @@ def parse_section(section):
try:
with open("../CHANGELOG.md", "r") as file:
changelog_path = BASE_DIR / "CHANGELOG.md"
with open(str(changelog_path.absolute()), "r", encoding="utf8") as file:
changelog_content = file.read()
except:
changelog_content = ""
changelog_content = (pkgutil.get_data("open_webui", "CHANGELOG.md") or b"").decode()
# Convert markdown content to HTML
html_content = markdown.markdown(changelog_content)
@@ -146,26 +166,149 @@ for version in soup.find_all("h2"):
CHANGELOG = changelog_json
####################################
# WEBUI_BUILD_HASH
####################################
WEBUI_BUILD_HASH = os.environ.get("WEBUI_BUILD_HASH", "dev-build")
####################################
# DATA/FRONTEND BUILD DIR
####################################
DATA_DIR = str(Path(os.getenv("DATA_DIR", "./data")).resolve())
FRONTEND_BUILD_DIR = str(Path(os.getenv("FRONTEND_BUILD_DIR", "../build")))
DATA_DIR = Path(os.getenv("DATA_DIR", BACKEND_DIR / "data")).resolve()
FRONTEND_BUILD_DIR = Path(os.getenv("FRONTEND_BUILD_DIR", BASE_DIR / "build")).resolve()
RESET_CONFIG_ON_START = (
os.environ.get("RESET_CONFIG_ON_START", "False").lower() == "true"
)
if RESET_CONFIG_ON_START:
try:
os.remove(f"{DATA_DIR}/config.json")
with open(f"{DATA_DIR}/config.json", "w") as f:
f.write("{}")
except:
pass
try:
with open(f"{DATA_DIR}/config.json", "r") as f:
CONFIG_DATA = json.load(f)
CONFIG_DATA = json.loads((DATA_DIR / "config.json").read_text())
except:
CONFIG_DATA = {}
####################################
# Config helpers
####################################
def save_config():
try:
with open(f"{DATA_DIR}/config.json", "w") as f:
json.dump(CONFIG_DATA, f, indent="\t")
except Exception as e:
log.exception(e)
def get_config_value(config_path: str):
path_parts = config_path.split(".")
cur_config = CONFIG_DATA
for key in path_parts:
if key in cur_config:
cur_config = cur_config[key]
else:
return None
return cur_config
T = TypeVar("T")
class PersistentConfig(Generic[T]):
def __init__(self, env_name: str, config_path: str, env_value: T):
self.env_name = env_name
self.config_path = config_path
self.env_value = env_value
self.config_value = get_config_value(config_path)
if self.config_value is not None:
log.info(f"'{env_name}' loaded from config.json")
self.value = self.config_value
else:
self.value = env_value
def __str__(self):
return str(self.value)
@property
def __dict__(self):
raise TypeError(
"PersistentConfig object cannot be converted to dict, use config_get or .value instead."
)
def __getattribute__(self, item):
if item == "__dict__":
raise TypeError(
"PersistentConfig object cannot be converted to dict, use config_get or .value instead."
)
return super().__getattribute__(item)
def save(self):
# Don't save if the value is the same as the env value and the config value
if self.env_value == self.value:
if self.config_value == self.value:
return
log.info(f"Saving '{self.env_name}' to config.json")
path_parts = self.config_path.split(".")
config = CONFIG_DATA
for key in path_parts[:-1]:
if key not in config:
config[key] = {}
config = config[key]
config[path_parts[-1]] = self.value
save_config()
self.config_value = self.value
class AppConfig:
_state: dict[str, PersistentConfig]
def __init__(self):
super().__setattr__("_state", {})
def __setattr__(self, key, value):
if isinstance(value, PersistentConfig):
self._state[key] = value
else:
self._state[key].value = value
self._state[key].save()
def __getattr__(self, key):
return self._state[key].value
####################################
# WEBUI_AUTH (Required for security)
####################################
WEBUI_AUTH = os.environ.get("WEBUI_AUTH", "True").lower() == "true"
WEBUI_AUTH_TRUSTED_EMAIL_HEADER = os.environ.get(
"WEBUI_AUTH_TRUSTED_EMAIL_HEADER", None
)
JWT_EXPIRES_IN = PersistentConfig(
"JWT_EXPIRES_IN", "auth.jwt_expiry", os.environ.get("JWT_EXPIRES_IN", "-1")
)
####################################
# Static DIR
####################################
STATIC_DIR = str(Path(os.getenv("STATIC_DIR", "./static")).resolve())
STATIC_DIR = Path(os.getenv("STATIC_DIR", BACKEND_DIR / "static")).resolve()
shutil.copyfile(f"{FRONTEND_BUILD_DIR}/favicon.png", f"{STATIC_DIR}/favicon.png")
frontend_favicon = FRONTEND_BUILD_DIR / "favicon.png"
if frontend_favicon.exists():
shutil.copyfile(frontend_favicon, STATIC_DIR / "favicon.png")
else:
logging.warning(f"Frontend favicon not found at {frontend_favicon}")
####################################
# CUSTOM_NAME
@@ -195,9 +338,6 @@ if CUSTOM_NAME:
except Exception as e:
log.exception(e)
pass
else:
if WEBUI_NAME != "Open WebUI":
WEBUI_NAME += " (Open WebUI)"
####################################
@@ -220,7 +360,7 @@ Path(CACHE_DIR).mkdir(parents=True, exist_ok=True)
# Docs DIR
####################################
DOCS_DIR = f"{DATA_DIR}/docs"
DOCS_DIR = os.getenv("DOCS_DIR", f"{DATA_DIR}/docs")
Path(DOCS_DIR).mkdir(parents=True, exist_ok=True)
@@ -251,16 +391,23 @@ def create_config_file(file_path):
LITELLM_CONFIG_PATH = f"{DATA_DIR}/litellm/config.yaml"
if not os.path.exists(LITELLM_CONFIG_PATH):
log.info("Config file doesn't exist. Creating...")
create_config_file(LITELLM_CONFIG_PATH)
log.info("Config file created successfully.")
# if not os.path.exists(LITELLM_CONFIG_PATH):
# log.info("Config file doesn't exist. Creating...")
# create_config_file(LITELLM_CONFIG_PATH)
# log.info("Config file created successfully.")
####################################
# OLLAMA_BASE_URL
####################################
ENABLE_OLLAMA_API = PersistentConfig(
"ENABLE_OLLAMA_API",
"ollama.enable",
os.environ.get("ENABLE_OLLAMA_API", "True").lower() == "true",
)
OLLAMA_API_BASE_URL = os.environ.get(
"OLLAMA_API_BASE_URL", "http://localhost:11434/api"
)
@@ -292,12 +439,22 @@ OLLAMA_BASE_URLS = os.environ.get("OLLAMA_BASE_URLS", "")
OLLAMA_BASE_URLS = OLLAMA_BASE_URLS if OLLAMA_BASE_URLS != "" else OLLAMA_BASE_URL
OLLAMA_BASE_URLS = [url.strip() for url in OLLAMA_BASE_URLS.split(";")]
OLLAMA_BASE_URLS = PersistentConfig(
"OLLAMA_BASE_URLS", "ollama.base_urls", OLLAMA_BASE_URLS
)
####################################
# OPENAI_API
####################################
ENABLE_OPENAI_API = PersistentConfig(
"ENABLE_OPENAI_API",
"openai.enable",
os.environ.get("ENABLE_OPENAI_API", "True").lower() == "true",
)
OPENAI_API_KEY = os.environ.get("OPENAI_API_KEY", "")
OPENAI_API_BASE_URL = os.environ.get("OPENAI_API_BASE_URL", "")
@@ -309,7 +466,9 @@ OPENAI_API_KEYS = os.environ.get("OPENAI_API_KEYS", "")
OPENAI_API_KEYS = OPENAI_API_KEYS if OPENAI_API_KEYS != "" else OPENAI_API_KEY
OPENAI_API_KEYS = [url.strip() for url in OPENAI_API_KEYS.split(";")]
OPENAI_API_KEYS = PersistentConfig(
"OPENAI_API_KEYS", "openai.api_keys", OPENAI_API_KEYS
)
OPENAI_API_BASE_URLS = os.environ.get("OPENAI_API_BASE_URLS", "")
OPENAI_API_BASE_URLS = (
@@ -320,33 +479,42 @@ OPENAI_API_BASE_URLS = [
url.strip() if url != "" else "https://api.openai.com/v1"
for url in OPENAI_API_BASE_URLS.split(";")
]
OPENAI_API_BASE_URLS = PersistentConfig(
"OPENAI_API_BASE_URLS", "openai.api_base_urls", OPENAI_API_BASE_URLS
)
OPENAI_API_KEY = ""
try:
OPENAI_API_KEY = OPENAI_API_KEYS[
OPENAI_API_BASE_URLS.index("https://api.openai.com/v1")
OPENAI_API_KEY = OPENAI_API_KEYS.value[
OPENAI_API_BASE_URLS.value.index("https://api.openai.com/v1")
]
except:
pass
OPENAI_API_BASE_URL = "https://api.openai.com/v1"
####################################
# WEBUI
####################################
ENABLE_SIGNUP = os.environ.get("ENABLE_SIGNUP", "True").lower() == "true"
DEFAULT_MODELS = os.environ.get("DEFAULT_MODELS", None)
ENABLE_SIGNUP = PersistentConfig(
"ENABLE_SIGNUP",
"ui.enable_signup",
(
False
if not WEBUI_AUTH
else os.environ.get("ENABLE_SIGNUP", "True").lower() == "true"
),
)
DEFAULT_MODELS = PersistentConfig(
"DEFAULT_MODELS", "ui.default_models", os.environ.get("DEFAULT_MODELS", None)
)
DEFAULT_PROMPT_SUGGESTIONS = (
CONFIG_DATA["ui"]["prompt_suggestions"]
if "ui" in CONFIG_DATA
and "prompt_suggestions" in CONFIG_DATA["ui"]
and type(CONFIG_DATA["ui"]["prompt_suggestions"]) is list
else [
DEFAULT_PROMPT_SUGGESTIONS = PersistentConfig(
"DEFAULT_PROMPT_SUGGESTIONS",
"ui.prompt_suggestions",
[
{
"title": ["Help me study", "vocabulary for a college entrance exam"],
"content": "Help me study vocabulary: write a sentence for me to fill in the blank, and I'll try to pick the correct option.",
@@ -363,40 +531,74 @@ DEFAULT_PROMPT_SUGGESTIONS = (
"title": ["Show me a code snippet", "of a website's sticky header"],
"content": "Show me a code snippet of a website's sticky header in CSS and JavaScript.",
},
]
{
"title": [
"Explain options trading",
"if I'm familiar with buying and selling stocks",
],
"content": "Explain options trading in simple terms if I'm familiar with buying and selling stocks.",
},
{
"title": ["Overcome procrastination", "give me tips"],
"content": "Could you start by asking me about instances when I procrastinate the most and then give me some suggestions to overcome it?",
},
],
)
DEFAULT_USER_ROLE = os.getenv("DEFAULT_USER_ROLE", "pending")
DEFAULT_USER_ROLE = PersistentConfig(
"DEFAULT_USER_ROLE",
"ui.default_user_role",
os.getenv("DEFAULT_USER_ROLE", "pending"),
)
USER_PERMISSIONS_CHAT_DELETION = (
os.environ.get("USER_PERMISSIONS_CHAT_DELETION", "True").lower() == "true"
)
USER_PERMISSIONS = {"chat": {"deletion": USER_PERMISSIONS_CHAT_DELETION}}
USER_PERMISSIONS = PersistentConfig(
"USER_PERMISSIONS",
"ui.user_permissions",
{"chat": {"deletion": USER_PERMISSIONS_CHAT_DELETION}},
)
MODEL_FILTER_ENABLED = os.environ.get("MODEL_FILTER_ENABLED", "False").lower() == "true"
ENABLE_MODEL_FILTER = PersistentConfig(
"ENABLE_MODEL_FILTER",
"model_filter.enable",
os.environ.get("ENABLE_MODEL_FILTER", "False").lower() == "true",
)
MODEL_FILTER_LIST = os.environ.get("MODEL_FILTER_LIST", "")
MODEL_FILTER_LIST = [model.strip() for model in MODEL_FILTER_LIST.split(";")]
MODEL_FILTER_LIST = PersistentConfig(
"MODEL_FILTER_LIST",
"model_filter.list",
[model.strip() for model in MODEL_FILTER_LIST.split(";")],
)
WEBHOOK_URL = os.environ.get("WEBHOOK_URL", "")
WEBHOOK_URL = PersistentConfig(
"WEBHOOK_URL", "webhook_url", os.environ.get("WEBHOOK_URL", "")
)
ENABLE_ADMIN_EXPORT = os.environ.get("ENABLE_ADMIN_EXPORT", "True").lower() == "true"
####################################
# WEBUI_VERSION
####################################
ENABLE_COMMUNITY_SHARING = PersistentConfig(
"ENABLE_COMMUNITY_SHARING",
"ui.enable_community_sharing",
os.environ.get("ENABLE_COMMUNITY_SHARING", "True").lower() == "true",
)
WEBUI_VERSION = os.environ.get("WEBUI_VERSION", "v1.0.0-alpha.100")
####################################
# WEBUI_AUTH (Required for security)
####################################
class BannerModel(BaseModel):
id: str
type: str
title: Optional[str] = None
content: str
dismissible: bool
timestamp: int
WEBUI_AUTH = True
WEBUI_AUTH_TRUSTED_EMAIL_HEADER = os.environ.get(
"WEBUI_AUTH_TRUSTED_EMAIL_HEADER", None
WEBUI_BANNERS = PersistentConfig(
"WEBUI_BANNERS",
"ui.banners",
[BannerModel(**banner) for banner in json.loads("[]")],
)
####################################
@@ -418,19 +620,105 @@ if WEBUI_AUTH and WEBUI_SECRET_KEY == "":
####################################
CHROMA_DATA_PATH = f"{DATA_DIR}/vector_db"
CHROMA_TENANT = os.environ.get("CHROMA_TENANT", chromadb.DEFAULT_TENANT)
CHROMA_DATABASE = os.environ.get("CHROMA_DATABASE", chromadb.DEFAULT_DATABASE)
CHROMA_HTTP_HOST = os.environ.get("CHROMA_HTTP_HOST", "")
CHROMA_HTTP_PORT = int(os.environ.get("CHROMA_HTTP_PORT", "8000"))
# Comma-separated list of header=value pairs
CHROMA_HTTP_HEADERS = os.environ.get("CHROMA_HTTP_HEADERS", "")
if CHROMA_HTTP_HEADERS:
CHROMA_HTTP_HEADERS = dict(
[pair.split("=") for pair in CHROMA_HTTP_HEADERS.split(",")]
)
else:
CHROMA_HTTP_HEADERS = None
CHROMA_HTTP_SSL = os.environ.get("CHROMA_HTTP_SSL", "false").lower() == "true"
# 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)
RAG_EMBEDDING_ENGINE = os.environ.get("RAG_EMBEDDING_ENGINE", "")
RAG_EMBEDDING_MODEL = os.environ.get(
"RAG_EMBEDDING_MODEL", "sentence-transformers/all-MiniLM-L6-v2"
RAG_TOP_K = PersistentConfig(
"RAG_TOP_K", "rag.top_k", int(os.environ.get("RAG_TOP_K", "5"))
)
RAG_RELEVANCE_THRESHOLD = PersistentConfig(
"RAG_RELEVANCE_THRESHOLD",
"rag.relevance_threshold",
float(os.environ.get("RAG_RELEVANCE_THRESHOLD", "0.0")),
)
ENABLE_RAG_HYBRID_SEARCH = PersistentConfig(
"ENABLE_RAG_HYBRID_SEARCH",
"rag.enable_hybrid_search",
os.environ.get("ENABLE_RAG_HYBRID_SEARCH", "").lower() == "true",
)
ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION = PersistentConfig(
"ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION",
"rag.enable_web_loader_ssl_verification",
os.environ.get("ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION", "True").lower() == "true",
)
RAG_EMBEDDING_ENGINE = PersistentConfig(
"RAG_EMBEDDING_ENGINE",
"rag.embedding_engine",
os.environ.get("RAG_EMBEDDING_ENGINE", ""),
)
PDF_EXTRACT_IMAGES = PersistentConfig(
"PDF_EXTRACT_IMAGES",
"rag.pdf_extract_images",
os.environ.get("PDF_EXTRACT_IMAGES", "False").lower() == "true",
)
RAG_EMBEDDING_MODEL = PersistentConfig(
"RAG_EMBEDDING_MODEL",
"rag.embedding_model",
os.environ.get("RAG_EMBEDDING_MODEL", "sentence-transformers/all-MiniLM-L6-v2"),
)
log.info(f"Embedding model set: {RAG_EMBEDDING_MODEL.value}"),
RAG_EMBEDDING_MODEL_AUTO_UPDATE = (
os.environ.get("RAG_EMBEDDING_MODEL_AUTO_UPDATE", "").lower() == "true"
)
log.info(f"Embedding model set: {RAG_EMBEDDING_MODEL}"),
RAG_EMBEDDING_MODEL_TRUST_REMOTE_CODE = (
os.environ.get("RAG_EMBEDDING_MODEL_TRUST_REMOTE_CODE", "").lower() == "true"
)
RAG_RERANKING_MODEL = PersistentConfig(
"RAG_RERANKING_MODEL",
"rag.reranking_model",
os.environ.get("RAG_RERANKING_MODEL", ""),
)
if RAG_RERANKING_MODEL.value != "":
log.info(f"Reranking model set: {RAG_RERANKING_MODEL.value}"),
RAG_RERANKING_MODEL_AUTO_UPDATE = (
os.environ.get("RAG_RERANKING_MODEL_AUTO_UPDATE", "").lower() == "true"
)
RAG_RERANKING_MODEL_TRUST_REMOTE_CODE = (
os.environ.get("RAG_RERANKING_MODEL_TRUST_REMOTE_CODE", "").lower() == "true"
)
if CHROMA_HTTP_HOST != "":
CHROMA_CLIENT = chromadb.HttpClient(
host=CHROMA_HTTP_HOST,
port=CHROMA_HTTP_PORT,
headers=CHROMA_HTTP_HEADERS,
ssl=CHROMA_HTTP_SSL,
tenant=CHROMA_TENANT,
database=CHROMA_DATABASE,
settings=Settings(allow_reset=True, anonymized_telemetry=False),
)
else:
CHROMA_CLIENT = chromadb.PersistentClient(
path=CHROMA_DATA_PATH,
settings=Settings(allow_reset=True, anonymized_telemetry=False),
tenant=CHROMA_TENANT,
database=CHROMA_DATABASE,
)
# device type embedding models - "cpu" (default), "cuda" (nvidia gpu required) or "mps" (apple silicon) - choosing this right can lead to better performance
USE_CUDA = os.environ.get("USE_CUDA_DOCKER", "false")
@@ -439,16 +727,16 @@ if USE_CUDA.lower() == "true":
else:
DEVICE_TYPE = "cpu"
CHROMA_CLIENT = chromadb.PersistentClient(
path=CHROMA_DATA_PATH,
settings=Settings(allow_reset=True, anonymized_telemetry=False),
CHUNK_SIZE = PersistentConfig(
"CHUNK_SIZE", "rag.chunk_size", int(os.environ.get("CHUNK_SIZE", "1500"))
)
CHUNK_OVERLAP = PersistentConfig(
"CHUNK_OVERLAP",
"rag.chunk_overlap",
int(os.environ.get("CHUNK_OVERLAP", "100")),
)
CHUNK_SIZE = 1500
CHUNK_OVERLAP = 100
RAG_TEMPLATE = """Use the following context as your learned knowledge, inside <context></context> XML tags.
DEFAULT_RAG_TEMPLATE = """Use the following context as your learned knowledge, inside <context></context> XML tags.
<context>
[context]
</context>
@@ -458,12 +746,105 @@ When answer to user:
- If you don't know when you are not sure, ask for clarification.
Avoid mentioning that you obtained the information from the context.
And answer according to the language of the user's question.
Given the context information, answer the query.
Query: [query]"""
RAG_OPENAI_API_BASE_URL = os.getenv("RAG_OPENAI_API_BASE_URL", OPENAI_API_BASE_URL)
RAG_OPENAI_API_KEY = os.getenv("RAG_OPENAI_API_KEY", OPENAI_API_KEY)
RAG_TEMPLATE = PersistentConfig(
"RAG_TEMPLATE",
"rag.template",
os.environ.get("RAG_TEMPLATE", DEFAULT_RAG_TEMPLATE),
)
RAG_OPENAI_API_BASE_URL = PersistentConfig(
"RAG_OPENAI_API_BASE_URL",
"rag.openai_api_base_url",
os.getenv("RAG_OPENAI_API_BASE_URL", OPENAI_API_BASE_URL),
)
RAG_OPENAI_API_KEY = PersistentConfig(
"RAG_OPENAI_API_KEY",
"rag.openai_api_key",
os.getenv("RAG_OPENAI_API_KEY", OPENAI_API_KEY),
)
ENABLE_RAG_LOCAL_WEB_FETCH = (
os.getenv("ENABLE_RAG_LOCAL_WEB_FETCH", "False").lower() == "true"
)
YOUTUBE_LOADER_LANGUAGE = PersistentConfig(
"YOUTUBE_LOADER_LANGUAGE",
"rag.youtube_loader_language",
os.getenv("YOUTUBE_LOADER_LANGUAGE", "en").split(","),
)
ENABLE_RAG_WEB_SEARCH = PersistentConfig(
"ENABLE_RAG_WEB_SEARCH",
"rag.web.search.enable",
os.getenv("ENABLE_RAG_WEB_SEARCH", "False").lower() == "true",
)
RAG_WEB_SEARCH_ENGINE = PersistentConfig(
"RAG_WEB_SEARCH_ENGINE",
"rag.web.search.engine",
os.getenv("RAG_WEB_SEARCH_ENGINE", ""),
)
SEARXNG_QUERY_URL = PersistentConfig(
"SEARXNG_QUERY_URL",
"rag.web.search.searxng_query_url",
os.getenv("SEARXNG_QUERY_URL", ""),
)
GOOGLE_PSE_API_KEY = PersistentConfig(
"GOOGLE_PSE_API_KEY",
"rag.web.search.google_pse_api_key",
os.getenv("GOOGLE_PSE_API_KEY", ""),
)
GOOGLE_PSE_ENGINE_ID = PersistentConfig(
"GOOGLE_PSE_ENGINE_ID",
"rag.web.search.google_pse_engine_id",
os.getenv("GOOGLE_PSE_ENGINE_ID", ""),
)
BRAVE_SEARCH_API_KEY = PersistentConfig(
"BRAVE_SEARCH_API_KEY",
"rag.web.search.brave_search_api_key",
os.getenv("BRAVE_SEARCH_API_KEY", ""),
)
SERPSTACK_API_KEY = PersistentConfig(
"SERPSTACK_API_KEY",
"rag.web.search.serpstack_api_key",
os.getenv("SERPSTACK_API_KEY", ""),
)
SERPSTACK_HTTPS = PersistentConfig(
"SERPSTACK_HTTPS",
"rag.web.search.serpstack_https",
os.getenv("SERPSTACK_HTTPS", "True").lower() == "true",
)
SERPER_API_KEY = PersistentConfig(
"SERPER_API_KEY",
"rag.web.search.serper_api_key",
os.getenv("SERPER_API_KEY", ""),
)
RAG_WEB_SEARCH_RESULT_COUNT = PersistentConfig(
"RAG_WEB_SEARCH_RESULT_COUNT",
"rag.web.search.result_count",
int(os.getenv("RAG_WEB_SEARCH_RESULT_COUNT", "3")),
)
RAG_WEB_SEARCH_CONCURRENT_REQUESTS = PersistentConfig(
"RAG_WEB_SEARCH_CONCURRENT_REQUESTS",
"rag.web.search.concurrent_requests",
int(os.getenv("RAG_WEB_SEARCH_CONCURRENT_REQUESTS", "10")),
)
####################################
# Transcribe
@@ -480,31 +861,82 @@ WHISPER_MODEL_AUTO_UPDATE = (
# Images
####################################
ENABLE_IMAGE_GENERATION = (
os.environ.get("ENABLE_IMAGE_GENERATION", "").lower() == "true"
IMAGE_GENERATION_ENGINE = PersistentConfig(
"IMAGE_GENERATION_ENGINE",
"image_generation.engine",
os.getenv("IMAGE_GENERATION_ENGINE", ""),
)
AUTOMATIC1111_BASE_URL = os.getenv("AUTOMATIC1111_BASE_URL", "")
COMFYUI_BASE_URL = os.getenv("COMFYUI_BASE_URL", "")
IMAGES_OPENAI_API_BASE_URL = os.getenv(
"IMAGES_OPENAI_API_BASE_URL", OPENAI_API_BASE_URL
ENABLE_IMAGE_GENERATION = PersistentConfig(
"ENABLE_IMAGE_GENERATION",
"image_generation.enable",
os.environ.get("ENABLE_IMAGE_GENERATION", "").lower() == "true",
)
AUTOMATIC1111_BASE_URL = PersistentConfig(
"AUTOMATIC1111_BASE_URL",
"image_generation.automatic1111.base_url",
os.getenv("AUTOMATIC1111_BASE_URL", ""),
)
IMAGES_OPENAI_API_KEY = os.getenv("IMAGES_OPENAI_API_KEY", OPENAI_API_KEY)
COMFYUI_BASE_URL = PersistentConfig(
"COMFYUI_BASE_URL",
"image_generation.comfyui.base_url",
os.getenv("COMFYUI_BASE_URL", ""),
)
IMAGES_OPENAI_API_BASE_URL = PersistentConfig(
"IMAGES_OPENAI_API_BASE_URL",
"image_generation.openai.api_base_url",
os.getenv("IMAGES_OPENAI_API_BASE_URL", OPENAI_API_BASE_URL),
)
IMAGES_OPENAI_API_KEY = PersistentConfig(
"IMAGES_OPENAI_API_KEY",
"image_generation.openai.api_key",
os.getenv("IMAGES_OPENAI_API_KEY", OPENAI_API_KEY),
)
IMAGE_SIZE = PersistentConfig(
"IMAGE_SIZE", "image_generation.size", os.getenv("IMAGE_SIZE", "512x512")
)
IMAGE_STEPS = PersistentConfig(
"IMAGE_STEPS", "image_generation.steps", int(os.getenv("IMAGE_STEPS", 50))
)
IMAGE_GENERATION_MODEL = PersistentConfig(
"IMAGE_GENERATION_MODEL",
"image_generation.model",
os.getenv("IMAGE_GENERATION_MODEL", ""),
)
####################################
# Audio
####################################
AUDIO_OPENAI_API_BASE_URL = os.getenv("AUDIO_OPENAI_API_BASE_URL", OPENAI_API_BASE_URL)
AUDIO_OPENAI_API_KEY = os.getenv("AUDIO_OPENAI_API_KEY", OPENAI_API_KEY)
AUDIO_OPENAI_API_BASE_URL = PersistentConfig(
"AUDIO_OPENAI_API_BASE_URL",
"audio.openai.api_base_url",
os.getenv("AUDIO_OPENAI_API_BASE_URL", OPENAI_API_BASE_URL),
)
AUDIO_OPENAI_API_KEY = PersistentConfig(
"AUDIO_OPENAI_API_KEY",
"audio.openai.api_key",
os.getenv("AUDIO_OPENAI_API_KEY", OPENAI_API_KEY),
)
AUDIO_OPENAI_API_MODEL = PersistentConfig(
"AUDIO_OPENAI_API_MODEL",
"audio.openai.api_model",
os.getenv("AUDIO_OPENAI_API_MODEL", "tts-1"),
)
AUDIO_OPENAI_API_VOICE = PersistentConfig(
"AUDIO_OPENAI_API_VOICE",
"audio.openai.api_voice",
os.getenv("AUDIO_OPENAI_API_VOICE", "alloy"),
)
####################################
# LiteLLM
# Database
####################################
LITELLM_PROXY_PORT = int(os.getenv("LITELLM_PROXY_PORT", "14365"))
if LITELLM_PROXY_PORT < 0 or LITELLM_PROXY_PORT > 65535:
raise ValueError("Invalid port number for LITELLM_PROXY_PORT")
LITELLM_PROXY_HOST = os.getenv("LITELLM_PROXY_HOST", "127.0.0.1")
DATABASE_URL = os.environ.get("DATABASE_URL", f"sqlite:///{DATA_DIR}/webui.db")

View File

@@ -32,6 +32,8 @@ class ERROR_MESSAGES(str, Enum):
COMMAND_TAKEN = "Uh-oh! This command is already registered. Please choose another command string."
FILE_EXISTS = "Uh-oh! This file is already registered. Please choose another file."
MODEL_ID_TAKEN = "Uh-oh! This model id is already registered. Please choose another model id string."
NAME_TAG_TAKEN = "Uh-oh! This name tag is already registered. Please choose another name tag string."
INVALID_TOKEN = (
"Your session has expired or the token is invalid. Please sign in again."
@@ -42,6 +44,9 @@ class ERROR_MESSAGES(str, Enum):
"The password provided is incorrect. Please check for typos and try again."
)
INVALID_TRUSTED_HEADER = "Your provider has not provided a trusted header. Please contact your administrator for assistance."
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."
UNAUTHORIZED = "401 Unauthorized"
ACCESS_PROHIBITED = "You do not have permission to access this resource. Please contact your administrator for assistance."
ACTION_PROHIBITED = (
@@ -69,3 +74,13 @@ class ERROR_MESSAGES(str, Enum):
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."
EMPTY_CONTENT = "The content provided is empty. Please ensure that there is text or data present before proceeding."
DB_NOT_SQLITE = "This feature is only available when running with SQLite databases."
INVALID_URL = (
"Oops! The URL you provided is invalid. Please double-check and try again."
)
WEB_SEARCH_ERROR = (
lambda err="": f"{err if err else 'Oops! Something went wrong while searching the web.'}"
)

View File

@@ -18,6 +18,18 @@
{
"title": ["Show me a code snippet", "of a website's sticky header"],
"content": "Show me a code snippet of a website's sticky header in CSS and JavaScript."
},
{
"title": ["Explain options trading", "if I'm familiar with buying and selling stocks"],
"content": "Explain options trading in simple terms if I'm familiar with buying and selling stocks."
},
{
"title": ["Overcome procrastination", "give me tips"],
"content": "Could you start by asking me about instances when I procrastinate the most and then give me some suggestions to overcome it?"
},
{
"title": ["Grammar check", "rewrite it for better readability "],
"content": "Check the following sentence for grammar and clarity: \"[sentence]\". Rewrite it for better readability while maintaining its original meaning."
}
]
}

0
backend/dev.sh Normal file → Executable file
View File

View File

@@ -1,3 +1,4 @@
from contextlib import asynccontextmanager
from bs4 import BeautifulSoup
import json
import markdown
@@ -7,52 +8,60 @@ import sys
import logging
import aiohttp
import requests
import mimetypes
from fastapi import FastAPI, Request, Depends, status
from fastapi.staticfiles import StaticFiles
from fastapi.responses import JSONResponse
from fastapi import HTTPException
from fastapi.middleware.wsgi import WSGIMiddleware
from fastapi.middleware.cors import CORSMiddleware
from starlette.exceptions import HTTPException as StarletteHTTPException
from starlette.middleware.base import BaseHTTPMiddleware
from starlette.responses import StreamingResponse, Response
from apps.ollama.main import app as ollama_app, get_all_models as get_ollama_models
from apps.openai.main import app as openai_app, get_all_models as get_openai_models
from apps.ollama.main import app as ollama_app
from apps.openai.main import app as openai_app
from apps.litellm.main import (
app as litellm_app,
start_litellm_background,
shutdown_litellm_background,
)
from apps.audio.main import app as audio_app
from apps.images.main import app as images_app
from apps.rag.main import app as rag_app
from apps.web.main import app as webui_app
from apps.webui.main import app as webui_app
import asyncio
from pydantic import BaseModel
from typing import List
from typing import List, Optional
from utils.utils import get_admin_user
from apps.webui.models.models import Models, ModelModel
from utils.utils import (
get_admin_user,
get_verified_user,
get_current_user,
get_http_authorization_cred,
)
from apps.rag.utils import rag_messages
from config import (
CONFIG_DATA,
WEBUI_NAME,
WEBUI_URL,
WEBUI_AUTH,
ENV,
VERSION,
CHANGELOG,
FRONTEND_BUILD_DIR,
CACHE_DIR,
STATIC_DIR,
MODEL_FILTER_ENABLED,
ENABLE_OPENAI_API,
ENABLE_OLLAMA_API,
ENABLE_MODEL_FILTER,
MODEL_FILTER_LIST,
GLOBAL_LOG_LEVEL,
SRC_LOG_LEVELS,
WEBHOOK_URL,
ENABLE_ADMIN_EXPORT,
AppConfig,
WEBUI_BUILD_HASH,
)
from constants import ERROR_MESSAGES
@@ -73,7 +82,7 @@ class SPAStaticFiles(StaticFiles):
print(
f"""
rf"""
___ __ __ _ _ _ ___
/ _ \ _ __ ___ _ __ \ \ / /__| |__ | | | |_ _|
| | | | '_ \ / _ \ '_ \ \ \ /\ / / _ \ '_ \| | | || |
@@ -82,25 +91,57 @@ print(
|_|
v{VERSION} - building the best open-source AI user interface.
v{VERSION} - building the best open-source AI user interface.
{f"Commit: {WEBUI_BUILD_HASH}" if WEBUI_BUILD_HASH != "dev-build" else ""}
https://github.com/open-webui/open-webui
"""
)
app = FastAPI(docs_url="/docs" if ENV == "dev" else None, redoc_url=None)
app.state.MODEL_FILTER_ENABLED = MODEL_FILTER_ENABLED
app.state.MODEL_FILTER_LIST = MODEL_FILTER_LIST
@asynccontextmanager
async def lifespan(app: FastAPI):
yield
app.state.WEBHOOK_URL = WEBHOOK_URL
app = FastAPI(
docs_url="/docs" if ENV == "dev" else None, redoc_url=None, lifespan=lifespan
)
app.state.config = AppConfig()
app.state.config.ENABLE_OPENAI_API = ENABLE_OPENAI_API
app.state.config.ENABLE_OLLAMA_API = ENABLE_OLLAMA_API
app.state.config.ENABLE_MODEL_FILTER = ENABLE_MODEL_FILTER
app.state.config.MODEL_FILTER_LIST = MODEL_FILTER_LIST
app.state.config.WEBHOOK_URL = WEBHOOK_URL
app.state.MODELS = {}
origins = ["*"]
# Custom middleware to add security headers
# class SecurityHeadersMiddleware(BaseHTTPMiddleware):
# async def dispatch(self, request: Request, call_next):
# response: Response = await call_next(request)
# response.headers["Cross-Origin-Opener-Policy"] = "same-origin"
# response.headers["Cross-Origin-Embedder-Policy"] = "require-corp"
# return response
# app.add_middleware(SecurityHeadersMiddleware)
class RAGMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request: Request, call_next):
return_citations = False
if request.method == "POST" and (
"/api/chat" in request.url.path or "/chat/completions" in request.url.path
"/ollama/api/chat" in request.url.path
or "/chat/completions" in request.url.path
):
log.debug(f"request.url.path: {request.url.path}")
@@ -111,24 +152,29 @@ class RAGMiddleware(BaseHTTPMiddleware):
# Parse string to JSON
data = json.loads(body_str) if body_str else {}
return_citations = data.get("citations", False)
if "citations" in data:
del data["citations"]
# Example: Add a new key-value pair or modify existing ones
# data["modified"] = True # Example modification
if "docs" in data:
data = {**data}
data["messages"] = rag_messages(
data["docs"],
data["messages"],
rag_app.state.RAG_TEMPLATE,
rag_app.state.TOP_K,
rag_app.state.RAG_EMBEDDING_ENGINE,
rag_app.state.RAG_EMBEDDING_MODEL,
rag_app.state.sentence_transformer_ef,
rag_app.state.OPENAI_API_KEY,
rag_app.state.OPENAI_API_BASE_URL,
data["messages"], citations = rag_messages(
docs=data["docs"],
messages=data["messages"],
template=rag_app.state.config.RAG_TEMPLATE,
embedding_function=rag_app.state.EMBEDDING_FUNCTION,
k=rag_app.state.config.TOP_K,
reranking_function=rag_app.state.sentence_transformer_rf,
r=rag_app.state.config.RELEVANCE_THRESHOLD,
hybrid_search=rag_app.state.config.ENABLE_RAG_HYBRID_SEARCH,
)
del data["docs"]
log.debug(f"data['messages']: {data['messages']}")
log.debug(
f"data['messages']: {data['messages']}, citations: {citations}"
)
modified_body_bytes = json.dumps(data).encode("utf-8")
@@ -145,6 +191,149 @@ class RAGMiddleware(BaseHTTPMiddleware):
],
]
response = await call_next(request)
if return_citations:
# Inject the citations into the response
if isinstance(response, StreamingResponse):
# If it's a streaming response, inject it as SSE event or NDJSON line
content_type = response.headers.get("Content-Type")
if "text/event-stream" in content_type:
return StreamingResponse(
self.openai_stream_wrapper(response.body_iterator, citations),
)
if "application/x-ndjson" in content_type:
return StreamingResponse(
self.ollama_stream_wrapper(response.body_iterator, citations),
)
return response
async def _receive(self, body: bytes):
return {"type": "http.request", "body": body, "more_body": False}
async def openai_stream_wrapper(self, original_generator, citations):
yield f"data: {json.dumps({'citations': citations})}\n\n"
async for data in original_generator:
yield data
async def ollama_stream_wrapper(self, original_generator, citations):
yield f"{json.dumps({'citations': citations})}\n"
async for data in original_generator:
yield data
app.add_middleware(RAGMiddleware)
class PipelineMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request: Request, call_next):
if request.method == "POST" and (
"/ollama/api/chat" in request.url.path
or "/chat/completions" in request.url.path
):
log.debug(f"request.url.path: {request.url.path}")
# Read the original request body
body = await request.body()
# Decode body to string
body_str = body.decode("utf-8")
# Parse string to JSON
data = json.loads(body_str) if body_str else {}
model_id = data["model"]
filters = [
model
for model in app.state.MODELS.values()
if "pipeline" in model
and "type" in model["pipeline"]
and model["pipeline"]["type"] == "filter"
and (
model["pipeline"]["pipelines"] == ["*"]
or any(
model_id == target_model_id
for target_model_id in model["pipeline"]["pipelines"]
)
)
]
sorted_filters = sorted(filters, key=lambda x: x["pipeline"]["priority"])
user = None
if len(sorted_filters) > 0:
try:
user = get_current_user(
get_http_authorization_cred(
request.headers.get("Authorization")
)
)
user = {"id": user.id, "name": user.name, "role": user.role}
except:
pass
model = app.state.MODELS[model_id]
if "pipeline" in model:
sorted_filters.append(model)
for filter in sorted_filters:
r = None
try:
urlIdx = filter["urlIdx"]
url = openai_app.state.config.OPENAI_API_BASE_URLS[urlIdx]
key = openai_app.state.config.OPENAI_API_KEYS[urlIdx]
if key != "":
headers = {"Authorization": f"Bearer {key}"}
r = requests.post(
f"{url}/{filter['id']}/filter/inlet",
headers=headers,
json={
"user": user,
"body": data,
},
)
r.raise_for_status()
data = r.json()
except Exception as e:
# Handle connection error here
print(f"Connection error: {e}")
if r is not None:
try:
res = r.json()
if "detail" in res:
return JSONResponse(
status_code=r.status_code,
content=res,
)
except:
pass
else:
pass
if "pipeline" not in app.state.MODELS[model_id]:
if "chat_id" in data:
del data["chat_id"]
if "title" in data:
del data["title"]
modified_body_bytes = json.dumps(data).encode("utf-8")
# Replace the request body with the modified one
request._body = modified_body_bytes
# Set custom header to ensure content-length matches new body length
request.headers.__dict__["_list"] = [
(b"content-length", str(len(modified_body_bytes)).encode("utf-8")),
*[
(k, v)
for k, v in request.headers.raw
if k.lower() != b"content-length"
],
]
response = await call_next(request)
return response
@@ -152,7 +341,7 @@ class RAGMiddleware(BaseHTTPMiddleware):
return {"type": "http.request", "body": body, "more_body": False}
app.add_middleware(RAGMiddleware)
app.add_middleware(PipelineMiddleware)
app.add_middleware(
@@ -166,6 +355,11 @@ app.add_middleware(
@app.middleware("http")
async def check_url(request: Request, call_next):
if len(app.state.MODELS) == 0:
await get_all_models()
else:
pass
start_time = int(time.time())
response = await call_next(request)
process_time = int(time.time()) - start_time
@@ -174,21 +368,447 @@ async def check_url(request: Request, call_next):
return response
@app.on_event("startup")
async def on_startup():
asyncio.create_task(start_litellm_background())
@app.middleware("http")
async def update_embedding_function(request: Request, call_next):
response = await call_next(request)
if "/embedding/update" in request.url.path:
webui_app.state.EMBEDDING_FUNCTION = rag_app.state.EMBEDDING_FUNCTION
return response
app.mount("/api/v1", webui_app)
app.mount("/litellm/api", litellm_app)
app.mount("/ollama", ollama_app)
app.mount("/openai/api", openai_app)
app.mount("/openai", openai_app)
app.mount("/images/api/v1", images_app)
app.mount("/audio/api/v1", audio_app)
app.mount("/rag/api/v1", rag_app)
app.mount("/api/v1", webui_app)
webui_app.state.EMBEDDING_FUNCTION = rag_app.state.EMBEDDING_FUNCTION
async def get_all_models():
openai_models = []
ollama_models = []
if app.state.config.ENABLE_OPENAI_API:
openai_models = await get_openai_models()
openai_models = openai_models["data"]
if app.state.config.ENABLE_OLLAMA_API:
ollama_models = await get_ollama_models()
ollama_models = [
{
"id": model["model"],
"name": model["name"],
"object": "model",
"created": int(time.time()),
"owned_by": "ollama",
"ollama": model,
}
for model in ollama_models["models"]
]
models = openai_models + ollama_models
custom_models = Models.get_all_models()
for custom_model in custom_models:
if custom_model.base_model_id == None:
for model in models:
if (
custom_model.id == model["id"]
or custom_model.id == model["id"].split(":")[0]
):
model["name"] = custom_model.name
model["info"] = custom_model.model_dump()
else:
owned_by = "openai"
for model in models:
if (
custom_model.base_model_id == model["id"]
or custom_model.base_model_id == model["id"].split(":")[0]
):
owned_by = model["owned_by"]
break
models.append(
{
"id": custom_model.id,
"name": custom_model.name,
"object": "model",
"created": custom_model.created_at,
"owned_by": owned_by,
"info": custom_model.model_dump(),
"preset": True,
}
)
app.state.MODELS = {model["id"]: model for model in models}
webui_app.state.MODELS = app.state.MODELS
return models
@app.get("/api/models")
async def get_models(user=Depends(get_verified_user)):
models = await get_all_models()
# Filter out filter pipelines
models = [
model
for model in models
if "pipeline" not in model or model["pipeline"].get("type", None) != "filter"
]
if app.state.config.ENABLE_MODEL_FILTER:
if user.role == "user":
models = list(
filter(
lambda model: model["id"] in app.state.config.MODEL_FILTER_LIST,
models,
)
)
return {"data": models}
return {"data": models}
@app.post("/api/chat/completed")
async def chat_completed(form_data: dict, user=Depends(get_verified_user)):
data = form_data
model_id = data["model"]
filters = [
model
for model in app.state.MODELS.values()
if "pipeline" in model
and "type" in model["pipeline"]
and model["pipeline"]["type"] == "filter"
and (
model["pipeline"]["pipelines"] == ["*"]
or any(
model_id == target_model_id
for target_model_id in model["pipeline"]["pipelines"]
)
)
]
sorted_filters = sorted(filters, key=lambda x: x["pipeline"]["priority"])
print(model_id)
if model_id in app.state.MODELS:
model = app.state.MODELS[model_id]
if "pipeline" in model:
sorted_filters = [model] + sorted_filters
for filter in sorted_filters:
r = None
try:
urlIdx = filter["urlIdx"]
url = openai_app.state.config.OPENAI_API_BASE_URLS[urlIdx]
key = openai_app.state.config.OPENAI_API_KEYS[urlIdx]
if key != "":
headers = {"Authorization": f"Bearer {key}"}
r = requests.post(
f"{url}/{filter['id']}/filter/outlet",
headers=headers,
json={
"user": {"id": user.id, "name": user.name, "role": user.role},
"body": data,
},
)
r.raise_for_status()
data = r.json()
except Exception as e:
# Handle connection error here
print(f"Connection error: {e}")
if r is not None:
try:
res = r.json()
if "detail" in res:
return JSONResponse(
status_code=r.status_code,
content=res,
)
except:
pass
else:
pass
return data
@app.get("/api/pipelines/list")
async def get_pipelines_list(user=Depends(get_admin_user)):
responses = await get_openai_models(raw=True)
print(responses)
urlIdxs = [
idx
for idx, response in enumerate(responses)
if response != None and "pipelines" in response
]
return {
"data": [
{
"url": openai_app.state.config.OPENAI_API_BASE_URLS[urlIdx],
"idx": urlIdx,
}
for urlIdx in urlIdxs
]
}
class AddPipelineForm(BaseModel):
url: str
urlIdx: int
@app.post("/api/pipelines/add")
async def add_pipeline(form_data: AddPipelineForm, user=Depends(get_admin_user)):
r = None
try:
urlIdx = form_data.urlIdx
url = openai_app.state.config.OPENAI_API_BASE_URLS[urlIdx]
key = openai_app.state.config.OPENAI_API_KEYS[urlIdx]
headers = {"Authorization": f"Bearer {key}"}
r = requests.post(
f"{url}/pipelines/add", headers=headers, json={"url": form_data.url}
)
r.raise_for_status()
data = r.json()
return {**data}
except Exception as e:
# Handle connection error here
print(f"Connection error: {e}")
detail = "Pipeline not found"
if r is not None:
try:
res = r.json()
if "detail" in res:
detail = res["detail"]
except:
pass
raise HTTPException(
status_code=(r.status_code if r is not None else status.HTTP_404_NOT_FOUND),
detail=detail,
)
class DeletePipelineForm(BaseModel):
id: str
urlIdx: int
@app.delete("/api/pipelines/delete")
async def delete_pipeline(form_data: DeletePipelineForm, user=Depends(get_admin_user)):
r = None
try:
urlIdx = form_data.urlIdx
url = openai_app.state.config.OPENAI_API_BASE_URLS[urlIdx]
key = openai_app.state.config.OPENAI_API_KEYS[urlIdx]
headers = {"Authorization": f"Bearer {key}"}
r = requests.delete(
f"{url}/pipelines/delete", headers=headers, json={"id": form_data.id}
)
r.raise_for_status()
data = r.json()
return {**data}
except Exception as e:
# Handle connection error here
print(f"Connection error: {e}")
detail = "Pipeline not found"
if r is not None:
try:
res = r.json()
if "detail" in res:
detail = res["detail"]
except:
pass
raise HTTPException(
status_code=(r.status_code if r is not None else status.HTTP_404_NOT_FOUND),
detail=detail,
)
@app.get("/api/pipelines")
async def get_pipelines(urlIdx: Optional[int] = None, user=Depends(get_admin_user)):
r = None
try:
urlIdx
url = openai_app.state.config.OPENAI_API_BASE_URLS[urlIdx]
key = openai_app.state.config.OPENAI_API_KEYS[urlIdx]
headers = {"Authorization": f"Bearer {key}"}
r = requests.get(f"{url}/pipelines", headers=headers)
r.raise_for_status()
data = r.json()
return {**data}
except Exception as e:
# Handle connection error here
print(f"Connection error: {e}")
detail = "Pipeline not found"
if r is not None:
try:
res = r.json()
if "detail" in res:
detail = res["detail"]
except:
pass
raise HTTPException(
status_code=(r.status_code if r is not None else status.HTTP_404_NOT_FOUND),
detail=detail,
)
@app.get("/api/pipelines/{pipeline_id}/valves")
async def get_pipeline_valves(
urlIdx: Optional[int], pipeline_id: str, user=Depends(get_admin_user)
):
models = await get_all_models()
r = None
try:
url = openai_app.state.config.OPENAI_API_BASE_URLS[urlIdx]
key = openai_app.state.config.OPENAI_API_KEYS[urlIdx]
headers = {"Authorization": f"Bearer {key}"}
r = requests.get(f"{url}/{pipeline_id}/valves", headers=headers)
r.raise_for_status()
data = r.json()
return {**data}
except Exception as e:
# Handle connection error here
print(f"Connection error: {e}")
detail = "Pipeline not found"
if r is not None:
try:
res = r.json()
if "detail" in res:
detail = res["detail"]
except:
pass
raise HTTPException(
status_code=(r.status_code if r is not None else status.HTTP_404_NOT_FOUND),
detail=detail,
)
@app.get("/api/pipelines/{pipeline_id}/valves/spec")
async def get_pipeline_valves_spec(
urlIdx: Optional[int], pipeline_id: str, user=Depends(get_admin_user)
):
models = await get_all_models()
r = None
try:
url = openai_app.state.config.OPENAI_API_BASE_URLS[urlIdx]
key = openai_app.state.config.OPENAI_API_KEYS[urlIdx]
headers = {"Authorization": f"Bearer {key}"}
r = requests.get(f"{url}/{pipeline_id}/valves/spec", headers=headers)
r.raise_for_status()
data = r.json()
return {**data}
except Exception as e:
# Handle connection error here
print(f"Connection error: {e}")
detail = "Pipeline not found"
if r is not None:
try:
res = r.json()
if "detail" in res:
detail = res["detail"]
except:
pass
raise HTTPException(
status_code=(r.status_code if r is not None else status.HTTP_404_NOT_FOUND),
detail=detail,
)
@app.post("/api/pipelines/{pipeline_id}/valves/update")
async def update_pipeline_valves(
urlIdx: Optional[int],
pipeline_id: str,
form_data: dict,
user=Depends(get_admin_user),
):
models = await get_all_models()
r = None
try:
url = openai_app.state.config.OPENAI_API_BASE_URLS[urlIdx]
key = openai_app.state.config.OPENAI_API_KEYS[urlIdx]
headers = {"Authorization": f"Bearer {key}"}
r = requests.post(
f"{url}/{pipeline_id}/valves/update",
headers=headers,
json={**form_data},
)
r.raise_for_status()
data = r.json()
return {**data}
except Exception as e:
# Handle connection error here
print(f"Connection error: {e}")
detail = "Pipeline not found"
if r is not None:
try:
res = r.json()
if "detail" in res:
detail = res["detail"]
except:
pass
raise HTTPException(
status_code=(r.status_code if r is not None else status.HTTP_404_NOT_FOUND),
detail=detail,
)
@app.get("/api/config")
async def get_app_config():
@@ -204,19 +824,25 @@ async def get_app_config():
"name": WEBUI_NAME,
"version": VERSION,
"default_locale": default_locale,
"images": images_app.state.ENABLED,
"default_models": webui_app.state.DEFAULT_MODELS,
"default_prompt_suggestions": webui_app.state.DEFAULT_PROMPT_SUGGESTIONS,
"trusted_header_auth": bool(webui_app.state.AUTH_TRUSTED_EMAIL_HEADER),
"admin_export_enabled": ENABLE_ADMIN_EXPORT,
"default_models": webui_app.state.config.DEFAULT_MODELS,
"default_prompt_suggestions": webui_app.state.config.DEFAULT_PROMPT_SUGGESTIONS,
"features": {
"auth": WEBUI_AUTH,
"auth_trusted_header": bool(webui_app.state.AUTH_TRUSTED_EMAIL_HEADER),
"enable_signup": webui_app.state.config.ENABLE_SIGNUP,
"enable_web_search": rag_app.state.config.ENABLE_RAG_WEB_SEARCH,
"enable_image_generation": images_app.state.config.ENABLED,
"enable_community_sharing": webui_app.state.config.ENABLE_COMMUNITY_SHARING,
"enable_admin_export": ENABLE_ADMIN_EXPORT,
},
}
@app.get("/api/config/model/filter")
async def get_model_filter_config(user=Depends(get_admin_user)):
return {
"enabled": app.state.MODEL_FILTER_ENABLED,
"models": app.state.MODEL_FILTER_LIST,
"enabled": app.state.config.ENABLE_MODEL_FILTER,
"models": app.state.config.MODEL_FILTER_LIST,
}
@@ -229,28 +855,19 @@ class ModelFilterConfigForm(BaseModel):
async def update_model_filter_config(
form_data: ModelFilterConfigForm, user=Depends(get_admin_user)
):
app.state.MODEL_FILTER_ENABLED = form_data.enabled
app.state.MODEL_FILTER_LIST = form_data.models
ollama_app.state.MODEL_FILTER_ENABLED = app.state.MODEL_FILTER_ENABLED
ollama_app.state.MODEL_FILTER_LIST = app.state.MODEL_FILTER_LIST
openai_app.state.MODEL_FILTER_ENABLED = app.state.MODEL_FILTER_ENABLED
openai_app.state.MODEL_FILTER_LIST = app.state.MODEL_FILTER_LIST
litellm_app.state.MODEL_FILTER_ENABLED = app.state.MODEL_FILTER_ENABLED
litellm_app.state.MODEL_FILTER_LIST = app.state.MODEL_FILTER_LIST
app.state.config.ENABLE_MODEL_FILTER = form_data.enabled
app.state.config.MODEL_FILTER_LIST = form_data.models
return {
"enabled": app.state.MODEL_FILTER_ENABLED,
"models": app.state.MODEL_FILTER_LIST,
"enabled": app.state.config.ENABLE_MODEL_FILTER,
"models": app.state.config.MODEL_FILTER_LIST,
}
@app.get("/api/webhook")
async def get_webhook_url(user=Depends(get_admin_user)):
return {
"url": app.state.WEBHOOK_URL,
"url": app.state.config.WEBHOOK_URL,
}
@@ -260,15 +877,27 @@ class UrlForm(BaseModel):
@app.post("/api/webhook")
async def update_webhook_url(form_data: UrlForm, user=Depends(get_admin_user)):
app.state.WEBHOOK_URL = form_data.url
webui_app.state.WEBHOOK_URL = app.state.WEBHOOK_URL
app.state.config.WEBHOOK_URL = form_data.url
webui_app.state.WEBHOOK_URL = app.state.config.WEBHOOK_URL
return {
"url": app.state.WEBHOOK_URL,
"url": app.state.config.WEBHOOK_URL,
}
@app.get("/api/community_sharing", response_model=bool)
async def get_community_sharing_status(request: Request, user=Depends(get_admin_user)):
return webui_app.state.config.ENABLE_COMMUNITY_SHARING
@app.get("/api/community_sharing/toggle", response_model=bool)
async def toggle_community_sharing(request: Request, user=Depends(get_admin_user)):
webui_app.state.config.ENABLE_COMMUNITY_SHARING = (
not webui_app.state.config.ENABLE_COMMUNITY_SHARING
)
return webui_app.state.config.ENABLE_COMMUNITY_SHARING
@app.get("/api/version")
async def get_app_config():
return {
@@ -310,20 +939,41 @@ async def get_manifest_json():
"background_color": "#343541",
"theme_color": "#343541",
"orientation": "portrait-primary",
"icons": [{"src": "/favicon.png", "type": "image/png", "sizes": "844x884"}],
"icons": [{"src": "/static/logo.png", "type": "image/png", "sizes": "500x500"}],
}
@app.get("/opensearch.xml")
async def get_opensearch_xml():
xml_content = rf"""
<OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.1/" xmlns:moz="http://www.mozilla.org/2006/browser/search/">
<ShortName>{WEBUI_NAME}</ShortName>
<Description>Search {WEBUI_NAME}</Description>
<InputEncoding>UTF-8</InputEncoding>
<Image width="16" height="16" type="image/x-icon">{WEBUI_URL}/favicon.png</Image>
<Url type="text/html" method="get" template="{WEBUI_URL}/?q={"{searchTerms}"}"/>
<moz:SearchForm>{WEBUI_URL}</moz:SearchForm>
</OpenSearchDescription>
"""
return Response(content=xml_content, media_type="application/xml")
@app.get("/health")
async def healthcheck():
return {"status": True}
app.mount("/static", StaticFiles(directory=STATIC_DIR), name="static")
app.mount("/cache", StaticFiles(directory=CACHE_DIR), name="cache")
app.mount(
"/",
SPAStaticFiles(directory=FRONTEND_BUILD_DIR, html=True),
name="spa-static-files",
)
@app.on_event("shutdown")
async def shutdown_event():
await shutdown_litellm_background()
if os.path.exists(FRONTEND_BUILD_DIR):
mimetypes.add_type("text/javascript", ".js")
app.mount(
"/",
SPAStaticFiles(directory=FRONTEND_BUILD_DIR, html=True),
name="spa-static-files",
)
else:
log.warning(
f"Frontend build directory not found at '{FRONTEND_BUILD_DIR}'. Serving API only."
)

View File

@@ -0,0 +1,60 @@
import base64
import os
import random
from pathlib import Path
import typer
import uvicorn
app = typer.Typer()
KEY_FILE = Path.cwd() / ".webui_secret_key"
if (frontend_build_dir := Path(__file__).parent / "frontend").exists():
os.environ["FRONTEND_BUILD_DIR"] = str(frontend_build_dir)
@app.command()
def serve(
host: str = "0.0.0.0",
port: int = 8080,
):
if os.getenv("WEBUI_SECRET_KEY") is None:
typer.echo(
"Loading WEBUI_SECRET_KEY from file, not provided as an environment variable."
)
if not KEY_FILE.exists():
typer.echo(f"Generating a new secret key and saving it to {KEY_FILE}")
KEY_FILE.write_bytes(base64.b64encode(random.randbytes(12)))
typer.echo(f"Loading WEBUI_SECRET_KEY from {KEY_FILE}")
os.environ["WEBUI_SECRET_KEY"] = KEY_FILE.read_text()
if os.getenv("USE_CUDA_DOCKER", "false") == "true":
typer.echo(
"CUDA is enabled, appending LD_LIBRARY_PATH to include torch/cudnn & cublas libraries."
)
LD_LIBRARY_PATH = os.getenv("LD_LIBRARY_PATH", "").split(":")
os.environ["LD_LIBRARY_PATH"] = ":".join(
LD_LIBRARY_PATH
+ [
"/usr/local/lib/python3.11/site-packages/torch/lib",
"/usr/local/lib/python3.11/site-packages/nvidia/cudnn/lib",
]
)
import main # we need set environment variables before importing main
uvicorn.run(main.app, host=host, port=port, forwarded_allow_ips="*")
@app.command()
def dev(
host: str = "0.0.0.0",
port: int = 8080,
reload: bool = True,
):
uvicorn.run(
"main:app", host=host, port=port, reload=reload, forwarded_allow_ips="*"
)
if __name__ == "__main__":
app()

View File

@@ -1,57 +1,59 @@
fastapi
uvicorn[standard]
pydantic
python-multipart
fastapi==0.111.0
uvicorn[standard]==0.22.0
pydantic==2.7.1
python-multipart==0.0.9
flask
flask_cors
Flask==3.0.3
Flask-Cors==4.0.1
python-socketio
python-jose
passlib[bcrypt]
uuid
python-socketio==5.11.2
python-jose==3.3.0
passlib[bcrypt]==1.7.4
requests
aiohttp
peewee
peewee-migrate
bcrypt
requests==2.32.2
aiohttp==3.9.5
peewee==3.17.5
peewee-migrate==1.12.2
psycopg2-binary==2.9.9
PyMySQL==1.1.1
bcrypt==4.1.3
litellm==1.35.17
litellm[proxy]==1.35.17
boto3==1.34.110
boto3
argon2-cffi==23.1.0
APScheduler==3.10.4
google-generativeai==0.5.4
argon2-cffi
apscheduler
google-generativeai
langchain==0.2.0
langchain-community==0.2.0
langchain-chroma==0.1.1
langchain
langchain-chroma
langchain-community
fake_useragent
chromadb
sentence_transformers
pypdf
docx2txt
unstructured
markdown
pypandoc
pandas
openpyxl
pyxlsb
xlrd
fake-useragent==1.5.1
chromadb==0.5.0
sentence-transformers==2.7.0
pypdf==4.2.0
docx2txt==0.8
python-pptx==0.6.23
unstructured==0.14.0
Markdown==3.6
pypandoc==1.13
pandas==2.2.2
openpyxl==3.1.2
pyxlsb==1.0.10
xlrd==2.0.1
validators==0.28.1
opencv-python-headless
rapidocr-onnxruntime
opencv-python-headless==4.9.0.80
rapidocr-onnxruntime==1.3.22
fpdf2
rank_bm25
fpdf2==2.7.9
rank-bm25==0.2.2
faster-whisper
faster-whisper==1.0.2
PyJWT
pyjwt[crypto]
PyJWT[crypto]==2.8.0
black
langfuse
black==24.4.2
langfuse==2.33.0
youtube-transcript-api==0.6.2
pytube==15.0.0

View File

@@ -6,8 +6,9 @@ cd "$SCRIPT_DIR" || exit
KEY_FILE=.webui_secret_key
PORT="${PORT:-8080}"
HOST="${HOST:-0.0.0.0}"
if test "$WEBUI_SECRET_KEY $WEBUI_JWT_SECRET_KEY" = " "; then
echo "No WEBUI_SECRET_KEY provided"
echo "Loading WEBUI_SECRET_KEY from file, not provided as an environment variable."
if ! [ -e "$KEY_FILE" ]; then
echo "Generating WEBUI_SECRET_KEY"
@@ -29,4 +30,29 @@ if [ "$USE_CUDA_DOCKER" = "true" ]; then
export LD_LIBRARY_PATH="$LD_LIBRARY_PATH:/usr/local/lib/python3.11/site-packages/torch/lib:/usr/local/lib/python3.11/site-packages/nvidia/cudnn/lib"
fi
WEBUI_SECRET_KEY="$WEBUI_SECRET_KEY" exec uvicorn main:app --host 0.0.0.0 --port "$PORT" --forwarded-allow-ips '*'
# Check if SPACE_ID is set, if so, configure for space
if [ -n "$SPACE_ID" ]; then
echo "Configuring for HuggingFace Space deployment"
if [ -n "$ADMIN_USER_EMAIL" ] && [ -n "$ADMIN_USER_PASSWORD" ]; then
echo "Admin user configured, creating"
WEBUI_SECRET_KEY="$WEBUI_SECRET_KEY" uvicorn main:app --host "$HOST" --port "$PORT" --forwarded-allow-ips '*' &
webui_pid=$!
echo "Waiting for webui to start..."
while ! curl -s http://localhost:8080/health > /dev/null; do
sleep 1
done
echo "Creating admin user..."
curl \
-X POST "http://localhost:8080/api/v1/auths/signup" \
-H "accept: application/json" \
-H "Content-Type: application/json" \
-d "{ \"email\": \"${ADMIN_USER_EMAIL}\", \"password\": \"${ADMIN_USER_PASSWORD}\", \"name\": \"Admin\" }"
echo "Shutting down webui..."
kill $webui_pid
fi
export WEBUI_URL=${SPACE_HOST}
fi
WEBUI_SECRET_KEY="$WEBUI_SECRET_KEY" exec uvicorn main:app --host "$HOST" --port "$PORT" --forwarded-allow-ips '*'

View File

@@ -7,13 +7,13 @@ SET "SCRIPT_DIR=%~dp0"
cd /d "%SCRIPT_DIR%" || exit /b
SET "KEY_FILE=.webui_secret_key"
SET "PORT=%PORT:8080%"
IF "%PORT%"=="" SET PORT=8080
SET "WEBUI_SECRET_KEY=%WEBUI_SECRET_KEY%"
SET "WEBUI_JWT_SECRET_KEY=%WEBUI_JWT_SECRET_KEY%"
:: Check if WEBUI_SECRET_KEY and WEBUI_JWT_SECRET_KEY are not set
IF "%WEBUI_SECRET_KEY%%WEBUI_JWT_SECRET_KEY%" == " " (
echo No WEBUI_SECRET_KEY provided
echo Loading WEBUI_SECRET_KEY from file, not provided as an environment variable.
IF NOT EXIST "%KEY_FILE%" (
echo Generating WEBUI_SECRET_KEY

BIN
backend/static/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

View File

@@ -0,0 +1 @@
Name,Email,Password,Role
1 Name Email Password Role

BIN
backend/utils/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

View File

@@ -1,5 +1,6 @@
from pathlib import Path
import hashlib
import json
import re
from datetime import timedelta
from typing import Optional
@@ -38,9 +39,10 @@ def calculate_sha256_string(string):
def validate_email_format(email: str) -> bool:
if not re.match(r"[^@]+@[^@]+\.[^@]+", email):
return False
return True
if email.endswith("@localhost"):
return True
return bool(re.match(r"[^@]+@[^@]+\.[^@]+", email))
def sanitize_filename(file_name):
@@ -109,3 +111,76 @@ def parse_duration(duration: str) -> Optional[timedelta]:
total_duration += timedelta(weeks=number)
return total_duration
def parse_ollama_modelfile(model_text):
parameters_meta = {
"mirostat": int,
"mirostat_eta": float,
"mirostat_tau": float,
"num_ctx": int,
"repeat_last_n": int,
"repeat_penalty": float,
"temperature": float,
"seed": int,
"stop": str,
"tfs_z": float,
"num_predict": int,
"top_k": int,
"top_p": float,
}
data = {"base_model_id": None, "params": {}}
# Parse base model
base_model_match = re.search(
r"^FROM\s+(\w+)", model_text, re.MULTILINE | re.IGNORECASE
)
if base_model_match:
data["base_model_id"] = base_model_match.group(1)
# Parse template
template_match = re.search(
r'TEMPLATE\s+"""(.+?)"""', model_text, re.DOTALL | re.IGNORECASE
)
if template_match:
data["params"] = {"template": template_match.group(1).strip()}
# Parse stops
stops = re.findall(r'PARAMETER stop "(.*?)"', model_text, re.IGNORECASE)
if stops:
data["params"]["stop"] = stops
# Parse other parameters from the provided list
for param, param_type in parameters_meta.items():
param_match = re.search(rf"PARAMETER {param} (.+)", model_text, re.IGNORECASE)
if param_match:
value = param_match.group(1)
if param_type == int:
value = int(value)
elif param_type == float:
value = float(value)
data["params"][param] = value
# Parse adapter
adapter_match = re.search(r"ADAPTER (.+)", model_text, re.IGNORECASE)
if adapter_match:
data["params"]["adapter"] = adapter_match.group(1)
# Parse system description
system_desc_match = re.search(
r'SYSTEM\s+"""(.+?)"""', model_text, re.DOTALL | re.IGNORECASE
)
if system_desc_match:
data["params"]["system"] = system_desc_match.group(1).strip()
# Parse messages
messages = []
message_matches = re.findall(r"MESSAGE (\w+) (.+)", model_text, re.IGNORECASE)
for role, content in message_matches:
messages.append({"role": role, "content": content})
if messages:
data["params"]["messages"] = messages
return data

10
backend/utils/models.py Normal file
View File

@@ -0,0 +1,10 @@
from apps.webui.models.models import Models, ModelModel, ModelForm, ModelResponse
def get_model_id_from_custom_model_id(id: str):
model = Models.get_model_by_id(id)
if model:
return model.id
else:
return id

View File

@@ -1,7 +1,7 @@
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from fastapi import HTTPException, status, Depends
from apps.web.models.users import Users
from apps.webui.models.users import Users
from pydantic import BaseModel
from typing import Union, Optional
@@ -89,6 +89,8 @@ def get_current_user(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=ERROR_MESSAGES.INVALID_TOKEN,
)
else:
Users.update_user_last_active_by_id(user.id)
return user
else:
raise HTTPException(
@@ -99,11 +101,15 @@ def get_current_user(
def get_current_user_by_api_key(api_key: str):
user = Users.get_user_by_api_key(api_key)
if user is None:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=ERROR_MESSAGES.INVALID_TOKEN,
)
else:
Users.update_user_last_active_by_id(user.id)
return user

View File

@@ -2,7 +2,12 @@
echo "Warning: This will remove all containers and volumes, including persistent data. Do you want to continue? [Y/N]"
read ans
if [ "$ans" == "Y" ] || [ "$ans" == "y" ]; then
docker-compose down -v
command docker-compose 2>/dev/null
if [ "$?" == "0" ]; then
docker-compose down -v
else
docker compose down -v
fi
else
echo "Operation cancelled."
fi

8
cypress.config.ts Normal file
View File

@@ -0,0 +1,8 @@
import { defineConfig } from 'cypress';
export default defineConfig({
e2e: {
baseUrl: 'http://localhost:8080'
},
video: true
});

101
cypress/e2e/chat.cy.ts Normal file
View File

@@ -0,0 +1,101 @@
// eslint-disable-next-line @typescript-eslint/triple-slash-reference
/// <reference path="../support/index.d.ts" />
// These tests run through the chat flow.
describe('Settings', () => {
// Wait for 2 seconds after all tests to fix an issue with Cypress's video recording missing the last few frames
after(() => {
// eslint-disable-next-line cypress/no-unnecessary-waiting
cy.wait(2000);
});
beforeEach(() => {
// Login as the admin user
cy.loginAdmin();
// Visit the home page
cy.visit('/');
});
context('Ollama', () => {
it('user can select a model', () => {
// Click on the model selector
cy.get('button[aria-label="Select a model"]').click();
// Select the first model
cy.get('button[aria-label="model-item"]').first().click();
});
it('user can perform text chat', () => {
// Click on the model selector
cy.get('button[aria-label="Select a model"]').click();
// Select the first model
cy.get('button[aria-label="model-item"]').first().click();
// Type a message
cy.get('#chat-textarea').type('Hi, what can you do? A single sentence only please.', {
force: true
});
// Send the message
cy.get('button[type="submit"]').click();
// User's message should be visible
cy.get('.chat-user').should('exist');
// Wait for the response
cy.get('.chat-assistant', { timeout: 120_000 }) // .chat-assistant is created after the first token is received
.find('div[aria-label="Generation Info"]', { timeout: 120_000 }) // Generation Info is created after the stop token is received
.should('exist');
});
it('user can share chat', () => {
// Click on the model selector
cy.get('button[aria-label="Select a model"]').click();
// Select the first model
cy.get('button[aria-label="model-item"]').first().click();
// Type a message
cy.get('#chat-textarea').type('Hi, what can you do? A single sentence only please.', {
force: true
});
// Send the message
cy.get('button[type="submit"]').click();
// User's message should be visible
cy.get('.chat-user').should('exist');
// Wait for the response
cy.get('.chat-assistant', { timeout: 120_000 }) // .chat-assistant is created after the first token is received
.find('div[aria-label="Generation Info"]', { timeout: 120_000 }) // Generation Info is created after the stop token is received
.should('exist');
// spy on requests
const spy = cy.spy();
cy.intercept('GET', '/api/v1/chats/*', spy);
// Open context menu
cy.get('#chat-context-menu-button').click();
// Click share button
cy.get('#chat-share-button').click();
// Check if the share dialog is visible
cy.get('#copy-and-share-chat-button').should('exist');
cy.wrap({}, { timeout: 5000 }).should(() => {
// Check if the request was made twice (once for to replace chat object and once more due to change event)
expect(spy).to.be.callCount(2);
});
});
it('user can generate image', () => {
// Click on the model selector
cy.get('button[aria-label="Select a model"]').click();
// Select the first model
cy.get('button[aria-label="model-item"]').first().click();
// Type a message
cy.get('#chat-textarea').type('Hi, what can you do? A single sentence only please.', {
force: true
});
// Send the message
cy.get('button[type="submit"]').click();
// User's message should be visible
cy.get('.chat-user').should('exist');
// Wait for the response
cy.get('.chat-assistant', { timeout: 120_000 }) // .chat-assistant is created after the first token is received
.find('div[aria-label="Generation Info"]', { timeout: 120_000 }) // Generation Info is created after the stop token is received
.should('exist');
// Click on the generate image button
cy.get('[aria-label="Generate Image"]').click();
// Wait for image to be visible
cy.get('img[data-cy="image"]', { timeout: 60_000 }).should('be.visible');
});
});
});

View File

@@ -0,0 +1,52 @@
// eslint-disable-next-line @typescript-eslint/triple-slash-reference
/// <reference path="../support/index.d.ts" />
import { adminUser } from '../support/e2e';
// These tests assume the following defaults:
// 1. No users exist in the database or that the test admin user is an admin
// 2. Language is set to English
// 3. The default role for new users is 'pending'
describe('Registration and Login', () => {
// Wait for 2 seconds after all tests to fix an issue with Cypress's video recording missing the last few frames
after(() => {
// eslint-disable-next-line cypress/no-unnecessary-waiting
cy.wait(2000);
});
beforeEach(() => {
cy.visit('/');
});
it('should register a new user as pending', () => {
const userName = `Test User - ${Date.now()}`;
const userEmail = `cypress-${Date.now()}@example.com`;
// Toggle from sign in to sign up
cy.contains('Sign up').click();
// Fill out the form
cy.get('input[autocomplete="name"]').type(userName);
cy.get('input[autocomplete="email"]').type(userEmail);
cy.get('input[type="password"]').type('password');
// Submit the form
cy.get('button[type="submit"]').click();
// Wait until the user is redirected to the home page
cy.contains(userName);
// Expect the user to be pending
cy.contains('Check Again');
});
it('can login with the admin user', () => {
// Fill out the form
cy.get('input[autocomplete="email"]').type(adminUser.email);
cy.get('input[type="password"]').type(adminUser.password);
// Submit the form
cy.get('button[type="submit"]').click();
// Wait until the user is redirected to the home page
cy.contains(adminUser.name);
// Dismiss the changelog dialog if it is visible
cy.getAllLocalStorage().then((ls) => {
if (!ls['version']) {
cy.get('button').contains("Okay, Let's Go!").click();
}
});
});
});

View File

@@ -0,0 +1,84 @@
// eslint-disable-next-line @typescript-eslint/triple-slash-reference
/// <reference path="../support/index.d.ts" />
import { adminUser } from '../support/e2e';
// These tests run through the various settings pages, ensuring that the user can interact with them as expected
describe('Settings', () => {
// Wait for 2 seconds after all tests to fix an issue with Cypress's video recording missing the last few frames
after(() => {
// eslint-disable-next-line cypress/no-unnecessary-waiting
cy.wait(2000);
});
beforeEach(() => {
// Login as the admin user
cy.loginAdmin();
// Visit the home page
cy.visit('/');
// Click on the user menu
cy.get('button[aria-label="User Menu"]').click();
// Click on the settings link
cy.get('button').contains('Settings').click();
});
context('General', () => {
it('user can open the General modal and hit save', () => {
cy.get('button').contains('General').click();
cy.get('button').contains('Save').click();
});
});
context('Connections', () => {
it('user can open the Connections modal and hit save', () => {
cy.get('button').contains('Connections').click();
cy.get('button').contains('Save').click();
});
});
context('Models', () => {
it('user can open the Models modal', () => {
cy.get('button').contains('Models').click();
});
});
context('Interface', () => {
it('user can open the Interface modal and hit save', () => {
cy.get('button').contains('Interface').click();
cy.get('button').contains('Save').click();
});
});
context('Audio', () => {
it('user can open the Audio modal and hit save', () => {
cy.get('button').contains('Audio').click();
cy.get('button').contains('Save').click();
});
});
context('Images', () => {
it('user can open the Images modal and hit save', () => {
cy.get('button').contains('Images').click();
// Currently fails because the backend requires a valid URL
// cy.get('button').contains('Save').click();
});
});
context('Chats', () => {
it('user can open the Chats modal', () => {
cy.get('button').contains('Chats').click();
});
});
context('Account', () => {
it('user can open the Account modal and hit save', () => {
cy.get('button').contains('Account').click();
cy.get('button').contains('Save').click();
});
});
context('About', () => {
it('user can open the About modal', () => {
cy.get('button').contains('About').click();
});
});
});

73
cypress/support/e2e.ts Normal file
View File

@@ -0,0 +1,73 @@
/// <reference types="cypress" />
export const adminUser = {
name: 'Admin User',
email: 'admin@example.com',
password: 'password'
};
const login = (email: string, password: string) => {
return cy.session(
email,
() => {
// Visit auth page
cy.visit('/auth');
// Fill out the form
cy.get('input[autocomplete="email"]').type(email);
cy.get('input[type="password"]').type(password);
// Submit the form
cy.get('button[type="submit"]').click();
// Wait until the user is redirected to the home page
cy.get('#chat-search').should('exist');
// Get the current version to skip the changelog dialog
if (localStorage.getItem('version') === null) {
cy.get('button').contains("Okay, Let's Go!").click();
}
},
{
validate: () => {
cy.request({
method: 'GET',
url: '/api/v1/auths/',
headers: {
Authorization: 'Bearer ' + localStorage.getItem('token')
}
});
}
}
);
};
const register = (name: string, email: string, password: string) => {
return cy
.request({
method: 'POST',
url: '/api/v1/auths/signup',
body: {
name: name,
email: email,
password: password
},
failOnStatusCode: false
})
.then((response) => {
expect(response.status).to.be.oneOf([200, 400]);
});
};
const registerAdmin = () => {
return register(adminUser.name, adminUser.email, adminUser.password);
};
const loginAdmin = () => {
return login(adminUser.email, adminUser.password);
};
Cypress.Commands.add('login', (email, password) => login(email, password));
Cypress.Commands.add('register', (name, email, password) => register(name, email, password));
Cypress.Commands.add('registerAdmin', () => registerAdmin());
Cypress.Commands.add('loginAdmin', () => loginAdmin());
before(() => {
cy.registerAdmin();
});

11
cypress/support/index.d.ts vendored Normal file
View File

@@ -0,0 +1,11 @@
// load the global Cypress types
/// <reference types="cypress" />
declare namespace Cypress {
interface Chainable {
login(email: string, password: string): Chainable<Element>;
register(name: string, email: string, password: string): Chainable<Element>;
registerAdmin(): Chainable<Element>;
loginAdmin(): Chainable<Element>;
}
}

7
cypress/tsconfig.json Normal file
View File

@@ -0,0 +1,7 @@
{
"extends": "../tsconfig.json",
"compilerOptions": {
"inlineSourceMap": true,
"sourceMap": false
}
}

View File

@@ -0,0 +1,31 @@
# This is an overlay that spins up stable-diffusion-webui for integration testing
# This is not designed to be used in production
services:
stable-diffusion-webui:
# Not built for ARM64
platform: linux/amd64
image: ghcr.io/neggles/sd-webui-docker:latest
restart: unless-stopped
environment:
CLI_ARGS: "--api --use-cpu all --precision full --no-half --skip-torch-cuda-test --ckpt /empty.pt --do-not-download-clip --disable-nan-check --disable-opt-split-attention"
PYTHONUNBUFFERED: "1"
TERM: "vt100"
SD_WEBUI_VARIANT: "default"
# Hack to get container working on Apple Silicon
# Rosetta creates a conflict ${HOME}/.cache folder
entrypoint: /bin/bash
command:
- -c
- |
export HOME=/root-home
rm -rf $${HOME}/.cache
/docker/entrypoint.sh python -u webui.py --listen --port $${WEBUI_PORT} --skip-version-check $${CLI_ARGS}
volumes:
- ./test/test_files/image_gen/sd-empty.pt:/empty.pt
open-webui:
environment:
ENABLE_IMAGE_GENERATION: "true"
AUTOMATIC1111_BASE_URL: http://stable-diffusion-webui:7860
IMAGE_SIZE: "64x64"
IMAGE_STEPS: "3"

View File

@@ -1,5 +1,3 @@
version: '3.8'
services:
ollama:
# Expose Ollama API outside the container stack

View File

@@ -1,5 +1,3 @@
version: '3.8'
services:
ollama:
volumes:

View File

@@ -1,5 +1,3 @@
version: '3.8'
services:
ollama:
# GPU support

View File

@@ -1,5 +1,3 @@
version: '3.8'
services:
ollama:
volumes:

View File

@@ -17,7 +17,7 @@ If your issue or contribution pertains directly to the core Ollama technology, p
### 🚨 Reporting Issues
Noticed something off? Have an idea? Check our [Issues tab](https://github.com/open-webui/oopen-webui/issues) to see if it's already been reported or suggested. If not, feel free to open a new issue. When reporting an issue, please follow our issue templates. These templates are designed to ensure that all necessary details are provided from the start, enabling us to address your concerns more efficiently.
Noticed something off? Have an idea? Check our [Issues tab](https://github.com/open-webui/open-webui/issues) to see if it's already been reported or suggested. If not, feel free to open a new issue. When reporting an issue, please follow our issue templates. These templates are designed to ensure that all necessary details are provided from the start, enabling us to address your concerns more efficiently.
> [!IMPORTANT]
>
@@ -54,7 +54,7 @@ Help us make Open WebUI more accessible by improving documentation, writing tuto
Help us make Open WebUI available to a wider audience. In this section, we'll guide you through the process of adding new translations to the project.
We use JSON files to store translations. You can find the existing translation files in the `src/lib/i18n/locales` directory. Each directory corresponds to a specific language, for example, `en-US` for English (US), `fr-FR` for French (France) and so on. You can refer to [ISO 639 Language Codes][http://www.lingoes.net/en/translator/langcode.htm] to find the appropriate code for a specific language.
We use JSON files to store translations. You can find the existing translation files in the `src/lib/i18n/locales` directory. Each directory corresponds to a specific language, for example, `en-US` for English (US), `fr-FR` for French (France) and so on. You can refer to [ISO 639 Language Codes](http://www.lingoes.net/en/translator/langcode.htm) to find the appropriate code for a specific language.
To add a new language:

23
hatch_build.py Normal file
View File

@@ -0,0 +1,23 @@
# noqa: INP001
import os
import shutil
import subprocess
from sys import stderr
from hatchling.builders.hooks.plugin.interface import BuildHookInterface
class CustomBuildHook(BuildHookInterface):
def initialize(self, version, build_data):
super().initialize(version, build_data)
stderr.write(">>> Building Open Webui frontend\n")
npm = shutil.which("npm")
if npm is None:
raise RuntimeError(
"NodeJS `npm` is required for building Open Webui but it was not found"
)
stderr.write("### npm install\n")
subprocess.run([npm, "install"], check=True) # noqa: S603
stderr.write("\n### npm run build\n")
os.environ["APP_BUILD_HASH"] = version
subprocess.run([npm, "run", "build"], check=True) # noqa: S603

Some files were not shown because too many files have changed in this diff Show More