Compare commits

..
763 Commits
Author SHA1 Message Date
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
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
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
Timothy Jaeryang Baek 748cb7d446 Merge pull request #1654 from open-webui/dev
0.1.121
2024-04-24 12:31:01 -07:00
Timothy J. Baek 348186c405 Update CHANGELOG.md 2024-04-24 15:28:50 -04:00
Timothy J. Baek 08f7c2fd63 chore: format 2024-04-24 15:24:21 -04:00
Timothy J. Baek ed326f02c0 doc: changelog 2024-04-24 15:23:09 -04:00
Timothy Jaeryang Baek 1ec668f697 Merge pull request #1718 from velaton618/patch-1 2024-04-24 10:10:00 -07:00
Timothy Jaeryang Baek b591891464 Merge pull request #1704 from cheahjs/feat/litellm-config 2024-04-24 10:09:34 -07:00
Steven Kreitzer c0259aad67 feat: hybrid search and reranking support 2024-04-24 07:55:10 -05:00
velaton e318f92177 Update translation.json
Fixed some grammar mistakes
2024-04-24 17:12:02 +08:00
Silentoplayz 53b277e575 Update pull_request_template.md
fix
2024-04-24 06:02:25 +00:00
Timothy J. Baek 589de36af7 fix: #1705 2024-04-23 15:56:09 -04:00
Jun Siang Cheah 5245d037ac fix: harden litellm exec command to prevent unintended commands
logic was previously to split on space for arguments, but if any of the user controlled variables LITELLM_PROXY_HOST or DATA_DIR had spaces in them, this would not behave correctly.
2024-04-23 19:25:43 +01:00
Jun Siang Cheah 58bead0398 fix: DATA_DIR was not respected when loading litellm configs 2024-04-23 19:22:41 +01:00
Jun Siang Cheah 9e9306fd2b feat: add LITELLM_PROXY_HOST to configure address litellm listens on 2024-04-23 19:19:16 +01:00
Jun Siang Cheah 0ea9e19d79 feat: add LITELLM_PROXY_PORT to configure internal proxy port 2024-04-23 19:14:01 +01:00
Timothy Jaeryang Baek 86bc0c8c73 Merge pull request #1703 from Axodouble/patch-2
Fixed a single translation key for nl-NL
2024-04-23 11:02:40 -07:00
Axodouble 858f5ae1fe Fixed a single translation key for nl-NL
Fixed a single translation key.
2024-04-23 17:46:17 +02:00
Timothy J. Baek cc3312157b refac: model download 2024-04-23 07:36:46 -04:00
Timothy J. Baek 4809d363b3 Update manifest.json 2024-04-23 07:21:20 -04:00
Timothy J. Baek b1d204fdd4 feat: allow custom model name 2024-04-23 07:20:24 -04: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
Timothy J. Baek 25d09363df feat: editable openai url for images 2024-04-23 07:14:31 -04:00
Timothy J. Baek aa489be53b Update config.py 2024-04-23 06:58:57 -04:00
Timothy J. Baek e3d253b040 feat: image env var 2024-04-23 06:53:04 -04: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
Timothy Jaeryang Baek 2d7d6cfffc Merge pull request #1630 from cheahjs/feat/split-large-chunks
feat: split large openai responses into smaller chunks
2024-04-22 13:56:26 -07:00
Timothy Jaeryang Baek ef5af1e273 Merge pull request #1686 from cheahjs/feat/add-store-types
feat: add types to some frontend stores
2024-04-22 13:55:57 -07:00
Timothy Jaeryang Baek 0546ad58be Merge pull request #1687 from buroa/buroa/huggingface-embeddings
feat: move to native `sentence_transformers`
2024-04-22 13:55:34 -07:00
Steven Kreitzer f3e5700d49 feat: move to native sentence_transformer 2024-04-22 14:20:41 -05:00
Jun Siang Cheah ed13da8aba feat: add types to some frontend stores 2024-04-22 20:08:32 +01:00
Timothy Jaeryang Baek 48e37973a3 Merge pull request #1688 from cheahjs/feat/disable-all-users-export
feat: add ALLOW_ADMIN_EXPORT to disable exporting of chats and the db
2024-04-22 11:57:44 -07:00
Jun Siang Cheah e2a8ad5fca address comments, rename to ENABLE_ADMIN_EXPORT 2024-04-22 19:55:46 +01:00
Jun Siang Cheah 190b934ab5 feat: add ALLOW_ADMIN_EXPORT to disable exporting of chats and the db 2024-04-22 19:44:24 +01:00
Timothy Jaeryang Baek 1e76dbc9a0 Merge pull request #1665 from dannyl1u/fix/html-br-tag-escaped
fix: <br> is not escaped in output text
2024-04-22 07:55:52 -07:00
Timothy J. Baek b3da09f52c chore: pl-PL renamed 2024-04-22 09:53:01 -05:00
Timothy J. Baek 4ab5050379 chore: pl-pl rm 2024-04-22 09:52:34 -05:00
Silentoplayz a615308111 Update pull_request_template.md
Fix
2024-04-22 07:19:09 +00:00
Danny Liu 40c1b49e6d chore: run format 2024-04-22 00:17:43 -07:00
Danny Liu 8e94618c51 fix: <br> is not escaped in output text 2024-04-22 00:16:05 -07:00
Silentoplayz dcb32f78fe Update pull_request_template.md
Overhaul
2024-04-22 07:11:59 +00:00
Timothy Jaeryang Baek 83efebe06b Merge pull request #1657 from open-webui/main
dev
2024-04-21 17:28:51 -07:00
Timothy J. Baek e6fad5ccb0 fix: safari copy share link issue 2024-04-21 19:28:16 -05:00
Timothy J. Baek 424141d1da fix: copy share link 2024-04-21 19:09:59 -05:00
Timothy J. Baek 4651db8c09 refac: litellm model name validation 2024-04-21 18:25:53 -05:00
Timothy Jaeryang Baek 5997774ab8 Merge pull request #1653 from open-webui/litellm-as-subprocess
fix: litellm as subprocess
2024-04-21 15:40:59 -07:00
Timothy J. Baek 760c62739a refac: improved error handling 2024-04-21 17:37:59 -05:00
Timothy J. Baek e627b8bf21 feat: litellm model add/delete 2024-04-21 17:26:22 -05:00
Timothy J. Baek 31124d9deb feat: litellm config update 2024-04-21 16:10:01 -05:00
Timothy Jaeryang Baek 56c93bc2ac Merge branch 'dev' into litellm-as-subprocess 2024-04-21 12:58:19 -07:00
Timothy J. Baek f83eb7326f Update requirements.txt 2024-04-21 14:44:28 -05:00
Timothy J. Baek 8422d3ea79 Update requirements.txt 2024-04-21 14:43:51 -05:00
Timothy J. Baek 77426266d2 refac: port number update 2024-04-21 14:32:45 -05:00
Timothy J. Baek 7d4f9134bc refac: styling 2024-04-21 13:24:46 -05:00
Timothy Jaeryang Baek 4d8ba5c7f0 Merge pull request #1651 from open-webui/dev
fix
2024-04-21 11:20:19 -07:00
Timothy J. Baek 4148d70ec0 fix 2024-04-21 13:19:48 -05:00
Timothy J. Baek 6f6be2c03f fix: styling 2024-04-21 13:16:45 -05:00
Timothy J. Baek bfdefbf6e7 fix: archived chats modal styling 2024-04-21 13:02:26 -05:00
Timothy Jaeryang Baek 063dabbf4a Merge pull request #1650 from open-webui/dev
fix
2024-04-21 10:55:29 -07:00
Timothy J. Baek 302c5074e9 revert: litellm bump 2024-04-21 12:50:14 -05:00
Timothy Jaeryang Baek f202a95661 Merge pull request #1647 from dyamagishi/comfyui_ws_schema
fix: Websocket Connection failed with ComfyUI server over HTTPS
2024-04-21 10:49:29 -07:00
Timothy Jaeryang Baek e82d9c873b Merge pull request #1644 from Entaigner/patch-6
Bugfix: FileReader can't be reused so init one per image
2024-04-21 10:47:24 -07:00
dyamagishi 489c45ffdf fix: Update websocket protocol based on the original schema. 2024-04-22 01:19:34 +09:00
Jun Siang Cheah 81b7cdfed7 fix: add typescript types for models 2024-04-21 11:41:18 +01:00
Jun Siang Cheah 67df928c7a feat: make chunk splitting a configurable option 2024-04-21 11:00:33 +01:00
Entaigner 743bbae5d1 Bugfix: FileReader can't be resused so init one per image 2024-04-21 11:54:30 +02:00
Timothy J. Baek 2717fe7c20 fix 2024-04-21 02:00:03 -05:00
Timothy J. Baek 51191168bc feat: restart subprocess route 2024-04-21 01:51:38 -05:00
Timothy J. Baek a59fb6b9eb fix 2024-04-21 01:47:35 -05:00
Timothy J. Baek 3c382d4c6c refac: close subprocess gracefully 2024-04-21 01:46:09 -05:00
Timothy J. Baek 8651bec915 pwned :) 2024-04-21 01:22:02 -05:00
Timothy J. Baek a41b195f46 DO NOT TRACK ME >:( 2024-04-21 01:13:24 -05:00
Timothy J. Baek 5e458d490a fix: run litellm as subprocess 2024-04-21 00:52:27 -05:00
Timothy J. Baek 948f2e913e chore: litellm bump 2024-04-20 23:53:08 -05:00
Timothy Jaeryang Baek aa4b2cc36f Merge pull request #1638 from Silentoplayz/patch-1
Update README.md
2024-04-20 20:56:05 -07:00
Timothy Jaeryang Baek df7517f9c4 Merge pull request #1639 from open-webui/dev
fix
2024-04-20 20:53:29 -07:00
Timothy J. Baek 98369fba22 fix 2024-04-20 22:53:00 -05:00
Silentoplayz 7a1f1d36a1 Update README.md
Updated features list
2024-04-21 03:12:48 +00:00
Timothy Jaeryang Baek 040ea70585 Merge pull request #1637 from open-webui/dev
fix
2024-04-20 19:15:58 -07:00
Timothy J. Baek 1cf4fa96c1 fix 2024-04-20 21:15:39 -05:00
Timothy Jaeryang Baek d19143dd2b Merge pull request #1636 from open-webui/dev
fix: multiuser duplicate tag issue
2024-04-20 19:13:20 -07:00
Timothy J. Baek fe3291acb5 fix: multiuser duplicate tag issue 2024-04-20 21:12:59 -05:00
Timothy Jaeryang Baek 9873cad3d6 Merge pull request #1635 from open-webui/dev
fix: settings getModels issue
2024-04-20 18:50:14 -07:00
Timothy J. Baek 1e919abda3 fix: settings getModels issue 2024-04-20 20:49:16 -05:00
Timothy Jaeryang Baek c533ed91f5 Merge pull request #1634 from open-webui/dev
fix
2024-04-20 18:37:36 -07:00
Timothy J. Baek 38321355d3 fix 2024-04-20 20:37:18 -05:00
Timothy Jaeryang Baek 22c50f62cb Merge pull request #1631 from open-webui/dev
0.1.120
2024-04-20 17:41:00 -07:00
Timothy J. Baek eefe01454f refac: include archived flag in exports 2024-04-20 19:32:32 -05:00
Timothy J. Baek 74dd9b561a revert: export chats to include archived chats 2024-04-20 19:24:58 -05:00
Timothy J. Baek 2fd2f792d6 refac 2024-04-20 19:20:46 -05:00
Timothy J. Baek 93bd20b854 doc: changelog 2024-04-20 19:15:11 -05:00
Timothy J. Baek 291c7595c4 refac 2024-04-20 19:15:04 -05:00
Timothy J. Baek 68de49e533 fix: feedback area scroll into view 2024-04-20 19:10:05 -05:00
Timothy J. Baek d6a0805966 refac: audio settings 2024-04-20 19:01:46 -05:00
Timothy J. Baek a5f8e87f3f feat: archived chat link 2024-04-20 18:50:41 -05:00
Timothy J. Baek 45ecaaf392 feat: unarchive 2024-04-20 18:47:20 -05:00
Timothy J. Baek 4c221eabef refac 2024-04-20 18:29:14 -05:00
Timothy J. Baek b12edb4a7a refac: replace timestamp field 2024-04-20 18:24:18 -05:00
Timothy J. Baek 50e8979c00 Update README.md 2024-04-20 17:40:55 -05:00
Timothy J. Baek 459a41774e Update bug_report.md 2024-04-20 17:11:03 -05:00
Timothy J. Baek fbd520bf07 feat: archive chat 2024-04-20 17:03:39 -05:00
Timothy J. Baek 00b01c973e feat: archive button 2024-04-20 16:13:16 -05:00
Timothy Jaeryang Baek f04164378a Merge pull request #1632 from cheahjs/fix/error-object-object
fix: use model name when outputting error message
2024-04-20 14:05:23 -07:00
Timothy J. Baek c468df2f71 feat: env var for audio 2024-04-20 16:04:16 -05:00
Timothy J. Baek fa9593b4e8 refac: openai error message 2024-04-20 16:01:25 -05:00
Timothy J. Baek cbd18ec63c feat: external openai tts support 2024-04-20 16:00:24 -05:00
Jun Siang Cheah 7e5bda6016 fix: use model name when outputting error message 2024-04-20 21:24:02 +01:00
Timothy J. Baek 713934edb6 refac 2024-04-20 15:21:52 -05:00
Timothy J. Baek 710850e442 refac: audio 2024-04-20 15:15:59 -05:00
Timothy J. Baek 2a10438b4d feat: share from chat menu 2024-04-20 14:40:06 -05:00
Jun Siang Cheah efa258c695 feat: split large openai responses into smaller chunkers 2024-04-20 20:34:23 +01:00
Timothy J. Baek 7eb14437ff feat: shortcut 2024-04-20 14:26:27 -05:00
Timothy J. Baek 9451726ee6 feat: save edited message shortcut 2024-04-20 14:24:19 -05:00
Timothy J. BaekandEntaigner 97d68a6a05 feat: multiple files input
Co-Authored-By: Entaigner <61445450+entaigner@users.noreply.github.com>
2024-04-20 13:41:16 -05:00
Timothy Jaeryang Baek cd79afb425 Merge pull request #1624 from que-nguyen/dev
Add i18n translation for feedback reasons
2024-04-20 11:35:19 -07:00
Timothy J. Baek b79596332a chore: format 2024-04-20 13:35:01 -05:00
Timothy Jaeryang Baek 21cc6ebcb7 Merge pull request #1623 from que-nguyen/main
Fix hover and selected state issues for reason buttons in light theme.
2024-04-20 11:33:43 -07:00
Timothy Jaeryang Baek 80ecea980e Merge pull request #1622 from giga-t/main
Added Georgian Language (ka-GE)
2024-04-20 11:33:07 -07:00
Timothy Jaeryang Baek 4c8932d71c Merge pull request #1621 from Entaigner/patch-4
Tagged chat suggestion placeholders for translation (settings>interface)
2024-04-20 11:32:41 -07:00
Que Nguyen 7a5a3c45e0 Merge branch 'open-webui:dev' into dev 2024-04-20 13:26:49 +07:00
Que Nguyen 3df03fa3fe Add i18n translation for feedback reasons 2024-04-20 11:51:11 +07:00
Que Nguyen 77ec9dd1f2 Fix hover and selected state issues for reason buttons in light theme. 2024-04-20 11:03:47 +07:00
Giga 22b694e0fd Update translation.json
fixed one word.
2024-04-19 23:26:23 +02:00
Giga 5e9ace1c6e Add files via upload
Added Georgian translation (ka-GE).
2024-04-19 23:24:19 +02:00
Entaigner 76bd77bc56 Enable translation for chat suggestion placeholders (settings>interface) 2024-04-19 23:13:17 +02: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
Timothy J. Baek a4083f43cb fix: safari copy link issue 2024-04-19 06:34:55 -05:00
Timothy Jaeryang Baek b67f80f7a4 Merge pull request #1600 from Fusseldieb/patch-1 2024-04-18 06:30:33 -07:00
Valentino Stillhardt a53c1cfc77 Fixed translated author name 2024-04-18 09:40:38 -03:00
Valentino Stillhardt b788514a10 Fixed translated author name 2024-04-18 09:38:26 -03:00
Valentino Stillhardt 5f16ec077a Fixed malformed time format 2024-04-18 09:34:03 -03:00
Valentino Stillhardt 941dc41c3d Fixed translated variable names and malformed date string 2024-04-18 09:28:10 -03:00
Valentino Stillhardt 44f9e930d2 Fixed malformed date string 2024-04-18 09:26:54 -03:00
Valentino Stillhardt c235a3d539 Fixed translated variable names 2024-04-18 09:25:39 -03:00
Valentino Stillhardt e720afacfa Fixed translated variable names 2024-04-18 09:24:29 -03:00
Valentino Stillhardt b352732b43 Fixed translated variable names 2024-04-18 09:19:55 -03:00
Valentino Stillhardt 0caf04617e Fixed translated variable name
"w" as in "week" was translated into "s" (as in "semana"), which is obviously wrong.
2024-04-18 09:17:35 -03:00
Timothy Jaeryang Baek e0ebd7aeaf Merge pull request #1590 from open-webui/dev
dev
2024-04-17 13:23:58 -07:00
Timothy Jaeryang Baek 2ffe55f128 Merge pull request #1584 from pkrolkgp/patch-2
Update translation.json
2024-04-17 13:22:59 -07:00
Timothy Jaeryang Baek 9d73f22aff Merge pull request #1586 from Fusseldieb/patch-1
Fixed malformed date string (again)
2024-04-17 13:22:38 -07:00
Timothy J. Baek 9dad7e7c9a fix 2024-04-17 15:22:21 -05:00
Valentino Stillhardt b5372bf715 Fixed malformed date string 2024-04-17 11:08:02 -03:00
pkrolkgp 20a3db975d Update translation.json
fix translated variable names to original
2024-04-17 12:28:32 +00:00
Timothy Jaeryang Baek 851754700a Merge pull request #1555 from open-webui/dev
0.1.119
2024-04-16 15:12:52 -07:00
Timothy J. Baek 375056f8dc refac: wording 2024-04-16 17:11:05 -05:00
Timothy J. Baek 236e2c040c refac 2024-04-16 17:08:50 -05:00
Timothy J. Baek cf811edefb doc: changelog 2024-04-16 17:06:16 -05:00
Timothy J. Baek f6e839611b refac 2024-04-16 17:03:12 -05:00
Timothy J. Baek 2a79c30657 refac 2024-04-16 16:51:01 -05:00
Timothy J. Baek d882cb41ac chore: unused var 2024-04-16 16:43:19 -05:00
Timothy J. Baek 323b9adc63 feat: chat menu tag 2024-04-16 16:31:06 -05:00
Timothy J. Baek 3488b7f006 refac: changelog modal close behaviour 2024-04-16 16:01:12 -05:00
Timothy J. Baek daed66f7c6 feat: sidebar swipe support 2024-04-16 15:57:14 -05:00
Timothy Jaeryang Baek b219906fa3 Merge pull request #1580 from djismgaming/spanish-translation-update
Small fixes to the Spanish translation
2024-04-16 13:42:41 -07:00
Ismael a556e39651 little word fix 2024-04-16 16:30:13 -04:00
Ismael b35316177b translation, into Spanish, of imported strings from English 2024-04-16 16:24:27 -04:00
Ismael 3ed509c5d3 added new strings from English to be translated to Spanish 2024-04-16 16:15:23 -04:00
Ismael 6699f0c339 small fixes to the spanish translation 2024-04-16 15:59:33 -04:00
Timothy J. Baek 9dbd223b4e fix: formatting 2024-04-16 14:32:27 -05:00
Timothy J. Baek 11e0aaa32f revert 2024-04-16 14:32:02 -05:00
Timothy J. Baek 9aea0fb03d Update translation.json 2024-04-16 14:25:45 -05:00
Timothy J. Baek b51922809b Update languages.json 2024-04-16 14:24:15 -05:00
Timothy J. Baek 314b5b5f6c fix 2024-04-16 14:23:57 -05:00
Timothy Jaeryang Baek 7c0c11b776 Merge pull request #1573 from pkrolkgp/patch-1
Create translation.json for polish language
2024-04-16 12:18:14 -07:00
Timothy Jaeryang Baek 44ad7c4132 Merge pull request #1577 from justinh-rahb/pip2uv2
Switch `pip3` to `uv` in Dockerfile
2024-04-16 12:17:47 -07:00
Justin Hayes 748a930a5f Replace pip3 with uv 2024-04-16 09:57:32 -04:00
pkrolkgp dba45acbda Create translation.json for polish language
Created polish language translation
2024-04-16 12:09:57 +00:00
Timothy J. Baek ac7bb03cb3 chore: formatting 2024-04-14 20:19:16 -04:00
Timothy J. Baek 1be60c9729 chore: version bump 2024-04-14 20:13:00 -04:00
Timothy Jaeryang Baek 54a4b7db14 Merge pull request #1554 from open-webui/external-embeddings
feat: external embeddings
2024-04-14 16:57:57 -07:00
Timothy J. Baek 741ed5dc4c fix 2024-04-14 19:56:33 -04:00
Timothy J. Baek b1b72441bb feat: openai embeddings integration 2024-04-14 19:48:15 -04:00
Timothy J. Baek b48e73fa43 feat: openai embeddings support 2024-04-14 19:15:39 -04:00
Timothy Jaeryang Baek 2e0def73eb Merge pull request #1553 from open-webui/external-embeddings
feat: external embeddings support
2024-04-14 15:48:15 -07:00
Timothy J. Baek 36ce157907 fix: integration 2024-04-14 18:47:45 -04:00
Timothy J. Baek 9cdb5bf9fe feat: frontend integration 2024-04-14 18:31:40 -04:00
Timothy J. Baek 2952e61167 feat: external embeddings support 2024-04-14 17:55:00 -04:00
Timothy Jaeryang Baek 8b10b058e5 Merge pull request #1539 from cheahjs/feat/customizable-title-prompt-length
feat: add {{prompt:start:length}} and {{prompt:end:length}} to title gen
2024-04-14 14:04:39 -07:00
Timothy J. Baek 0c441b588c refac: naming convention 2024-04-14 17:04:24 -04:00
Timothy Jaeryang Baek a938ffb586 Merge branch 'dev' into feat/customizable-title-prompt-length 2024-04-14 14:02:53 -07:00
Timothy Jaeryang Baek b5d882606a Merge pull request #1499 from lainedfles/whisper_auto_update
feat: introduce Whisper model auto-update control.
2024-04-14 13:58:17 -07:00
Timothy Jaeryang Baek d9ce1d3ea3 Merge pull request #1544 from lainedfles/generation_info_approximate_total
feat: human readable Generation Info total
2024-04-14 13:57:23 -07:00
Timothy J. Baek 9091513c39 fix 2024-04-14 16:52:59 -04:00
Timothy J. Baek 0f5ecafc57 fix: api usage issue 2024-04-14 16:51:13 -04:00
Timothy J. Baek 98c16776f8 refac: styling 2024-04-14 16:41:58 -04:00
Timothy J. Baek 91dda2401d Update Selector.svelte 2024-04-14 16:29:27 -04:00
Timothy Jaeryang Baek 6238495d61 Merge pull request #1395 from 7a6ac0/admin_pagination
feat: admin panel user list pagination
2024-04-14 13:23:36 -07:00
Timothy J. Baek 0708a3d75e fix: styling 2024-04-14 16:23:13 -04:00
Timothy J. Baek b25e3ed364 refac 2024-04-14 16:19:46 -04:00
Timothy J. Baek 1fa3bf6793 fix: semi-lazy load 2024-04-14 15:44:19 -04:00
Self Denial b93337e62f Format fix 2024-04-13 22:41:22 -06:00
Self Denial d2d255228c Format fix 2024-04-13 22:39:10 -06:00
Self Denial 23b674ddda feat: human readable Generation Info total
Add new function to convert nanoseconds into `approximate_total:` for *Generation Info* tooltip.
2024-04-13 22:27:00 -06:00
Timothy Jaeryang Baek 0d3ce18a61 Merge pull request #1543 from lainedfles/fix-pr-1500 2024-04-13 19:04:31 -07:00
Self Denial 067c94810a Fix open-webui/open-webui#1500
Looks like the convention update per commit 0981fae161 missed `config.py`.
2024-04-13 19:55:43 -06:00
Timothy Jaeryang Baek 916ce9bc4f Merge pull request #1500 from lainedfles/config-image-generation-enabled
feat: add IMAGE_GENERATION_ENABLED env var
2024-04-13 14:51:01 -07:00
Timothy J. Baek 0981fae161 refac: convention 2024-04-13 14:50:45 -07:00
Timothy J. Baek 2649d29a3e fix: manifest.json issue 2024-04-13 14:42:38 -07:00
Timothy J. Baek 838778aa3e fix: typo 2024-04-13 14:40:54 -07:00
Timothy Jaeryang Baek 8c8388ea9f Merge pull request #1516 from que-nguyen/main
Update locales/vi-VN/translation.json
2024-04-13 14:39:27 -07:00
Timothy Jaeryang Baek b40054c54d Merge pull request #1534 from lainedfles/ollama-pull-qol
feat: small change to support ollama pull QOL
2024-04-13 14:39:13 -07:00
Jun Siang Cheah fffd42e4d7 fix: replace all instances of prompt:start and prompt:end 2024-04-13 18:44:49 +01:00
Jun Siang Cheah db817fcf29 feat: add {{prompt:start:length}} and {{prompt:end:length}} to title gen 2024-04-13 18:26:50 +01:00
Self Denial faa5884150 Use log.warning instead of log.debug 2024-04-13 03:18:13 -06:00
Self Denial 922628c1ee feat: small change to support ollama pull QOL
Use regex replace during trim "sanitize" to support `ollama run ` or `ollama pull ` syntax.
2024-04-13 03:04:11 -06:00
Timothy Jaeryang Baek 8e30948de9 Merge pull request #1532 from dariothornhill/1478-remove-trailing-slash
fix(helm): remove trailing slash
2024-04-13 00:06:29 -07:00
Dario Thornhill e08c144f0b fix(helm): remove trailing slash
The trailing `/` causes requests to be written with `//` and results in 404 responses from the ollama service.

This results in ollama models being unusable. Removing the training slash here resolves the issue.
2024-04-13 07:32:28 +02:00
Timothy Jaeryang Baek 51afb1e378 Merge pull request #1523 from 7a6ac0/call_before_defining
fix: Invoke the function before it is defined
2024-04-12 13:31:40 -07:00
tabacoWang c49cc3fa86 fix: Invoke the function before it is defined 2024-04-12 17:27:40 +08:00
Que Nguyen e17d66eb61 Update locales/vi-VN/translation.json
Fixed translation errors in Vietnamese locale file `locales/vi-VN/translation.json`
2024-04-12 09:44:40 +07:00
Timothy Jaeryang Baek d6260fd20c Merge pull request #1514 from JanSolo1/EMBBEDING-MODELS-COMMENT-FIX
Comment fix config.py ln 412
2024-04-11 18:53:12 -07:00
JanSolo1 79a4abc3ec Comment spelling mistake fix 2024-04-12 00:44:44 +02:00
JanSolo1 6623ecb302 Comment fix config.py ln 412 2024-04-12 00:36:48 +02:00
Timothy Jaeryang Baek 18a3f06a62 Merge pull request #1505 from Fusseldieb/patch-1
Fixed malformed date format
2024-04-11 09:55:28 -07:00
Valentino Stillhardt 1d8496eabb Fixed malformed date format 2024-04-11 10:58:46 -03:00
tabacoWang 8a9cf44dbc feat: combine with search result 2024-04-11 14:00:28 +08:00
tabacoWang 872ea83c50 feat: admin panel user list pagination 2024-04-11 14:00:28 +08:00
Self Denial ff01398812 Format fix 2024-04-10 23:39:11 -06:00
Self Denial 3b8ea55bc8 Add IMAGE_GENERATION_ENABLED env var
With default of `False`.
2024-04-10 23:21:12 -06:00
Self Denial 81c8717d75 Format fix 2024-04-10 20:44:44 -06:00
Self Denial 429242b4d3 Introduce Whisper model auto-update control.
* Introduce WHISPER_MODEL_AUTO_UPDATE env var
* Pass local_files_only to WhisperModel()
* Handle cases where auto-update is disabled but model is non-existent
2024-04-10 20:30:00 -06:00
Timothy Jaeryang Baek 0399a69b73 Merge pull request #1498 from open-webui/dev
Update CHANGELOG.md
2024-04-10 15:41:12 -07:00
Timothy J. Baek 9e726a32e6 Update CHANGELOG.md 2024-04-10 15:40:47 -07:00
Timothy Jaeryang Baek 78284e49d7 Merge pull request #1488 from open-webui/dev
0.1.118
2024-04-10 15:38:47 -07:00
Timothy J. Baek 64a6db4b55 Update package-lock.json 2024-04-10 15:38:15 -07:00
Timothy J. Baek 16c9042318 chore: formatting 2024-04-10 15:36:43 -07:00
Timothy J. Baek 7a47ba14f7 doc: changelog 2024-04-10 15:35:32 -07:00
Timothy Jaeryang Baek 988123a2c8 Merge pull request #1496 from cheahjs/fix/missing-ollama-cuda-tags
fix: missing ollama cuda tags
2024-04-10 13:01:45 -07:00
Jun Siang Cheah 3a661fda1a fix: missing ollama/cuda tags from #1495 2024-04-10 20:58:26 +01:00
Timothy Jaeryang Baek a85e93f1f9 Merge pull request #1495 from cheahjs/feat/parallelize-docker-build
feat: parallelize docker build
2024-04-10 12:53:37 -07:00
Jun Siang Cheah 7050d53718 build ollama and cuda tags 2024-04-10 20:41:36 +01:00
Jun Siang Cheah cd91d8a987 run apt install first for better potential layer caching 2024-04-10 20:25:52 +01:00
Jun Siang Cheah eddaa7fa89 cross compile node 2024-04-10 20:25:52 +01:00
Jun Siang Cheah 550fc63ebd feat: parallelize x86/arm docker builds 2024-04-10 20:25:52 +01:00
Timothy J. Baek 57a81d70c1 Update README.md 2024-04-10 01:34:25 -07:00
Timothy J. Baek 295472fca1 chore: formatting 2024-04-10 01:27:19 -07:00
Timothy J. Baek a3e41db8d7 chore: formatting 2024-04-10 01:26:23 -07:00
Timothy Jaeryang Baek 546efe0d7b Merge pull request #1489 from jannikstdl/patch-1
README.md Dockersection formatting and wording fix
2024-04-10 01:23:16 -07:00
Jannik S 27f01b0bc8 Update README.md 2024-04-10 10:20:52 +02:00
Jannik S 62ec2651ba README.md Dockersection formatting and wording fix 2024-04-10 10:14:54 +02:00
Timothy Jaeryang Baek b9cadff16b Merge pull request #1419 from lainedfles/embedding-model-fix-and-manual-update
feat: improve embedding model update & resolve network dependency
2024-04-10 01:10:07 -07:00
Timothy J. Baek c0d273f162 refac 2024-04-10 01:06:57 -07:00
Timothy J. Baek 098ba6ea4e refac: frontend 2024-04-10 01:01:42 -07:00
Timothy J. Baek 582d11f191 refac: RAG_EMBEDDING_MODEL_PATH removed 2024-04-10 00:59:05 -07:00
Timothy J. Baek cb2158a794 fix 2024-04-10 00:51:16 -07:00
Timothy J. Baek abfcceecef refac 2024-04-10 00:46:09 -07:00
Timothy J. Baek f4b87ecb23 refac 2024-04-10 00:33:45 -07:00
Timothy J. Baek 48aad65514 refac 2024-04-09 23:54:20 -07:00
Timothy J. Baek cbb21a148d fix: async version check 2024-04-09 23:03:05 -07:00
Timothy Jaeryang Baek 2f6e683163 Merge pull request #1476 from buroa/dev
fix: support batching chromadb
2024-04-09 22:53:51 -07:00
Timothy Jaeryang Baek 1448c32fdf Merge pull request #1472 from shivaraj-bh/static-dir
feat: configurable `STATIC_DIR`; fix: mount `CACHE_DIR` to the `/cache` endpoint
2024-04-09 22:53:21 -07:00
Timothy J. Baek 0a3b99b94a chore: docker build workflow 2024-04-09 20:46:42 -07:00
Steven Kreitzer 0bae789d39 fix: support batching chromadb 2024-04-09 10:13:29 -05:00
shivaraj-bh 304bf9d9b1 feat: configurable STATIC_DIR; fix: mount CACHE_DIR to the /cache endpoint 2024-04-09 16:04:55 +05:30
Timothy Jaeryang Baek 839efa4443 Merge pull request #1464 from jmferrer/allow-using-external-ollama
Allow using external ollama service.
2024-04-08 22:19:41 -07:00
Timothy Jaeryang Baek 0a488c783f Merge pull request #1466 from jmferrer/make-chart-idempotent
Make chart idempotent.
2024-04-08 22:19:03 -07:00
jmferrerm d6275ee941 Make chart idempotent. 2024-04-09 07:02:45 +02:00
jmferrerm 2da7dd67ea Allow using external ollama service. 2024-04-09 06:43:08 +02:00
lainedfles 506a061387 Merge branch 'dev' into embedding-model-fix-and-manual-update 2024-04-08 14:57:54 -06:00
Timothy J. Baek 1a2971ae7b Update docker-build.yaml 2024-04-08 13:36:50 -07:00
Timothy J. Baek da9a54288e fix: styling 2024-04-08 03:11:00 -07:00
Timothy J. Baek 28c8b5841c fix: styling 2024-04-08 03:10:31 -07:00
Timothy J. Baek e48cdf63d9 feat: better annotation 2024-04-08 03:08:30 -07:00
Timothy J. Baek a649dc80c0 Update Dockerfile 2024-04-08 01:40:03 -07:00
Timothy Jaeryang Baek 733d69425e Merge pull request #1457 from aguvener/patch-1
fix: wording translation
2024-04-08 00:43:49 -07:00
Timothy Jaeryang Baek e844e7f708 Merge pull request #1165 from jannikstdl/dockerfile-optimisation
refac: Dockerfile
2024-04-08 00:43:08 -07:00
Jannik S 3b3d0cce1e Merge branch 'dev' into dockerfile-optimisation 2024-04-08 09:15:00 +02:00
aguvener b07641e58a fix: wording translation 2024-04-08 10:01:39 +03:00
Timothy J. Baek 72110c293a refac: admin panel styling 2024-04-07 01:52:58 -07:00
Timothy J. Baek f64ac3269f fix: share chat permission issue 2024-04-07 01:21:12 -07:00
Timothy J. Baek eb5ed905eb feat: close dragged overlay with esc 2024-04-07 01:03:16 -07:00
Timothy J. Baek 4207f80ce9 feat: close model on esc 2024-04-07 00:57:23 -07:00
Timothy J. Baek 073aecd427 refac: styling 2024-04-07 00:31:59 -07:00
Timothy Jaeryang Baek 3e1c679f2d Merge pull request #1435 from cheahjs/fix/build-docker-tag
feat: trigger docker build after release
2024-04-07 00:29:49 -07:00
Timothy J. Baek 47aeab81d8 refac: styling 2024-04-07 00:05:13 -07:00
Timothy J. Baek f4e165d028 chore: comments removed 2024-04-07 00:00:50 -07:00
Timothy Jaeryang Baek 02d3fb427b Merge pull request #1386 from dannyl1u/feat/profile-image-initials
feat: default profile image with user initials
2024-04-06 23:59:20 -07:00
Timothy J. Baek 117e07b0d4 Update languages.json 2024-04-06 23:57:55 -07:00
Timothy J. Baek efa6aa4b4c Update languages.json 2024-04-06 23:53:01 -07:00
Timothy J. Baek c73e8916ed chore: formatting 2024-04-06 23:52:02 -07:00
Timothy J. Baek df2cb16086 refac: styling 2024-04-06 23:49:25 -07:00
Timothy J. Baek f267f3f7b0 refac 2024-04-06 23:16:29 -07:00
Timothy Jaeryang Baek 8d8075e81f Merge pull request #1447 from sammcj/en-international
feat(i18n): add international English (en-GB) localisation
2024-04-06 22:44:06 -07:00
Timothy J. Baek 0a403eb129 Update languages.json 2024-04-06 22:43:48 -07:00
Sam McLeod f74642df1b feat(i18n): add international English (en-GB) localisation 2024-04-07 13:57:04 +10:00
Timothy Jaeryang Baek 782cce7c44 Merge pull request #1340 from joequant/main
allow version to work with -rc tag
2024-04-06 18:57:41 -07:00
Timothy J. Baek 395ca82175 refac: '-rc' tag handling 2024-04-06 18:55:51 -07:00
Danny Liu 663b5adaf2 Merge pull request #3 from lainedfles/feat/profile-image-initials
Fix: Restore Gravatar functionality, add button initials, toast duration
2024-04-06 16:37:09 -07:00
Self Denial 924ebf035b Fix: Restore Gravatar functionality, add button initials, toast duration
- Restore Gravatar functionality
- Add new button for "Use Initials"
- Set both buttons to use text-left class
- Update toast property autoClose to duration (wrong library, my bad!)
- Update toast messages to clarify that this isn't "Gravatar" but "avatar"
- Add i18n text to en-US/translation.json
2024-04-06 16:04:05 -06:00
Timothy Jaeryang Baek fafb8cd263 Merge pull request #1439 from justinh-rahb/rocm-compose
Add AMD Docker Compose config file
2024-04-06 13:33:21 -07:00
Timothy J. Baek 8110a872d5 fix: wording 2024-04-06 13:29:39 -07:00
Timothy Jaeryang Baek 331fe04df7 Merge pull request #1445 from open-webui/dev
dev
2024-04-06 13:21:15 -07:00
Timothy Jaeryang Baek aed4763bfe Merge pull request #1443 from aguvener/main
feat: Added tr-TR translation
2024-04-06 13:15:44 -07:00
aguvener 8d0f3fbde9 added tr-TR to languages.json 2024-04-06 19:03:59 +03:00
aguvener 318d4d1ce1 Create translation.json
Created Turkish translation file
2024-04-06 19:00:23 +03:00
Self Denial ec530ac9f8 format fix 2024-04-06 04:38:34 -06:00
Self Denial 11741ea7f0 Tooltip info & warn. Detect file path during update. Add translation.
* Tooltips added to show full model name and warning regarding vector storage
* Send toast error and return early if more than a single front slash is detected
* Extend toast duration for model update action
* Add i18n en_US translation
* Remove commented code copied from existing function
2024-04-06 04:22:48 -06:00
Danny Liu bee0338763 captalize Initial for improved clarity 2024-04-06 02:23:32 -07:00
Timothy Jaeryang Baek 073f06d449 refac: cleaner tag 2024-04-06 04:06:47 -05:00
Timothy J. Baek 5a787ab2c5 refac: pdf styling 2024-04-06 02:04:28 -07:00
Timothy J. Baek 81dbc65853 refac: pdf generation 2024-04-06 01:54:59 -07:00
Danny Liu 8a7075c3bf style: npm run format 2024-04-06 01:13:22 -07:00
Danny Liu 78565e554b notify user with toast.info() + update toast message 2024-04-06 01:12:51 -07:00
Danny Liu 7081755e19 Merge pull request #2 from lainedfles/feat/profile-image-initials-fixes
Feat/profile image initials fixes
2024-04-06 01:12:11 -07:00
Self Denial cf54adf5c4 Toast error consistency 2024-04-05 23:01:48 -06:00
Self Denial ae9922a2cd Re-word account creation error to be more descriptive 2024-04-05 22:58:40 -06:00
Self Denial bad7dca51e Move login page toast error to the bottom of page and enable i18n for translation 2024-04-05 22:43:42 -06:00
Self Denial 69716a5cec Revert "omit canvas spoofing toast.error for signups"
This reverts commit 1af62a70f0.
2024-04-05 22:31:40 -06:00
Danny Liu 1af62a70f0 omit canvas spoofing toast.error for signups 2024-04-05 21:16:48 -07:00
Danny Liu 5694f16624 style: run npm run format 2024-04-05 21:02:02 -07:00
Danny Liu a68b95c95f Merge pull request #1 from lainedfles/feat/profile-image-initials-rfp
Feat/profile image initials with fix for rfp & canvas spoofing
2024-04-05 21:00:48 -07:00
Justin Hayes f34b9733e3 Add Docker tag variables 2024-04-05 23:48:16 -04:00
Justin Hayes 8db03f3ab2 Add variables 2024-04-05 23:46:20 -04:00
Timothy Jaeryang Baek d001d7afb1 Merge pull request #1436 from lainedfles/feat/litellm-local-cost-map
Use locally bundled version of the LiteLLM cost map json
2024-04-05 17:26:20 -07:00
Timothy Jaeryang Baek 183b84beae Merge pull request #1438 from MurielLima/main
feat: translate portuguese brazil
2024-04-05 17:24:14 -07:00
Justin Hayes 075dddf3d3 Add AMD docker compose file 2024-04-05 20:11:33 -04:00
Muriel.Lima 7d64ada43e feat: translate portuguese brazil 2024-04-05 19:45:10 -03:00
Self Denial 802eb3beec Use locally bundled version of the LiteLLM cost map json
Intended to avoid repetitive startup connections by setting `LITELLM_LOCAL_MODEL_COST_MAP="True"`
2024-04-05 16:27:31 -06:00
Self Denial ac9308dbed Introduce canvasPixelTest() intended to validate canvas functionality
Browsers and plugins that spoof canvas data produce corrupt images. In attempt to mitigate:

* Add canvasPixelTest() to test a single pixel and test the RGB values
* Test canvasPixelTest() inside generateInitialsImage() and use default `/user.png` if failure detected
* Call canvasPixelTest() directly within settings to avoid setting an invalid image
* Use toast.error() with 10 second autoClose
2024-04-05 16:04:00 -06:00
Jun Siang Cheah b7ced4e4f1 feat: trigger docker build after release 2024-04-05 20:29:11 +01:00
Jannik S 536fd347b9 Merge pull request #4 from lainedfles/dockerfile-optimisation-dev
Set cudnn LD_LIBRARY_PATH to fix whisper inference
2024-04-05 08:23:06 +02:00
Danny Liu c8f7bb990c code style: npm run format 2024-04-04 20:07:52 -07:00
Danny Liu b8790200e9 refac: code style 2024-04-04 20:05:39 -07:00
Danny Liu 4200ad111c handle names with trailing whitespace 2024-04-04 19:56:23 -07:00
Self Denial 0f90332e61 Set cudnn LD_LIBRARY_PATH to fix whisper inference 2024-04-04 20:08:14 -06:00
Timothy Jaeryang Baek f87805b46f Merge pull request #1424 from open-webui/dev
fix
2024-04-04 17:40:53 -07:00
Timothy Jaeryang Baek 7a7d1931d9 Merge pull request #1423 from slash-proc/fix_helm_ollama_base_url
remove deprecated /api from ollama base url
2024-04-04 17:39:21 -07:00
Timothy J. Baek e61e1b079f fix: file upload issue 2024-04-04 17:38:59 -07:00
Doug Danat 7813c3316f remove deprecated /api from ollama base url 2024-04-04 23:57:32 +02:00
Danny Liu ac470e64e0 npm run format 2024-04-04 13:26:00 -07:00
Danny Liu c52cc46d2c feat: initial avatar set to first letter of firstname, first letter of lastname 2024-04-04 13:23:59 -07:00
Danny Liu a2bd9b8639 style: npm run format 2024-04-04 12:10:45 -07:00
Danny Liu 0c43897f3d refac: move generateInitialsImage function to utils 2024-04-04 12:09:07 -07:00
Danny Liu 4195af4942 pass generated profile image in signup api call 2024-04-04 12:08:20 -07:00
Self Denial 9f82f5abba Formatting... 2024-04-04 12:09:48 -06:00
Self Denial 075fbedb02 More format fixes 2024-04-04 12:07:42 -06:00
Self Denial bcf79c8366 Format fixes 2024-04-04 12:02:48 -06:00
Self Denial 3b66aa55c0 Improve embedding model update & resolve network dependency
* Add config variable RAG_EMBEDDING_MODEL_AUTO_UPDATE to control update behavior
* Add RAG utils embedding_model_get_path() function to output the filesystem path in addition to update of the model using huggingface_hub
* Update and utilize existing RAG functions in main: get_embedding_model() & update_embedding_model()
* Add GUI setting to execute manual update process
2024-04-04 11:01:23 -06:00
Danny Liu 3b06096c52 run npm run lint:backend 2024-04-04 01:10:51 -07:00
Jannik Streidl a602089b64 Added preload if embedding model in CUDA mode 2024-04-04 10:06:29 +02:00
Danny Liu 8d1db9a1c0 feat: api endpoint to receive profile_image_uirl on signup 2024-04-03 22:36:27 -07:00
Timothy Jaeryang Baek 46774aa5cd Merge pull request #1416 from open-webui/dev
0.1.117
2024-04-03 21:43:17 -07:00
Timothy J. Baek 62392aa88a Update CHANGELOG.md 2024-04-03 21:37:15 -07:00
Timothy J. Baek c32d41b47f refac: about 2024-04-03 21:24:57 -07:00
Timothy Jaeryang Baek 9404b6135a Merge pull request #1401 from theasp/add-dynamic-manifest
feat: Allow overriding application name in manifest.json
2024-04-03 20:45:51 -07:00
Timothy J. Baek 8caad59cb4 feat: allow webui name customisation 2024-04-03 20:45:23 -07:00
Timothy J. Baek 21132420d8 Update Dockerfile 2024-04-03 20:44:18 -07:00
Timothy J. Baek c89070797c refac 2024-04-03 20:43:55 -07:00
Timothy J. Baek 1f85354856 feat: stylised pdf 2024-04-03 20:35:32 -07:00
Timothy J. Baek bfd066cc2f fix: share chat modal 2024-04-03 19:45:08 -07:00
Timothy J. Baek 000bea84ae feat: chat pdf download 2024-04-03 19:30:25 -07:00
Timothy J. Baek 944efd2cd8 feat: sanitise response content 2024-04-03 10:57:58 -07:00
Timothy J. Baek 4a09342a71 chore: npm audit fix 2024-04-03 10:26:24 -07:00
Timothy Jaeryang Baek aad13607c0 Merge pull request #1403 from helmanofer/main
Added boto3 to  requirements.txt
2024-04-03 10:21:02 -07:00
Timothy Jaeryang Baek 1509c2c2ee Merge pull request #1406 from jannikstdl/fix-whitemode-onhover
Fix hover in whitemode
2024-04-03 10:20:04 -07:00
Timothy Jaeryang Baek d94bc21ac8 Merge pull request #1408 from Mmx233/main
fix: manually check the docs' filename
2024-04-03 10:19:10 -07:00
Mmx233 947c392f72 fix: manually check the docs' filename 2024-04-03 23:37:13 +08:00
Jannik Streidl 15d24a6a11 Fix hover in whitemode 2024-04-03 16:00:06 +02:00
Ofer Helman a3d6343dbe Update requirements.txt 2024-04-03 16:27:49 +03:00
Jannik Streidl ca8fd8af1d Merge branch 'dockerfile-optimisation' of https://github.com/jannikstdl/open-webui into dockerfile-optimisation 2024-04-03 11:44:18 +02:00
Jannik Streidl d0d01c95f9 possible fix for format-backend check 2024-04-03 11:43:13 +02:00
Jannik S f669c0e78e Merge branch 'dev' into dockerfile-optimisation 2024-04-03 11:37:46 +02:00
Jannik Streidl 33ad2381aa README instructions and build fixes 2024-04-03 11:34:25 +02:00
Ofer Helman 14fcf4d267 Update requirements.txt
In case of using bedrock with litellm import boto3 exception is raised
2024-04-03 11:44:01 +03:00
Andrew Phillips 83ad488e31 Do not use hardcoded manifest.json
Generate manifest.json dynamically, using MANIFEST_NAME and
MANIFEST_SHORT_NAME from the environment.
2024-04-02 16:08:10 -03:00
Timothy Jaeryang Baek fa61e738c3 Merge pull request #1306 from vaayne/feature/support_auth_by_api_key
Feature/support auth by api key
2024-04-02 10:09:27 -07:00
Timothy J. Baek 0aa4d305c7 chore: print statement removed 2024-04-02 10:07:44 -07:00
Timothy J. Baek 1066096b33 fix 2024-04-02 10:06:58 -07:00
Timothy J. Baek 398b2076cd refac 2024-04-02 10:05:53 -07:00
Timothy J. Baek fb516ea694 refac: styling 2024-04-02 09:58:34 -07:00
Timothy J. Baek b35cc36e19 chore: formatting 2024-04-02 09:42:45 -07:00
Timothy J. Baek 6d1ce99537 revert: auth page 2024-04-02 09:40:36 -07:00
Timothy J. Baek 7c5f6d71b3 refac 2024-04-02 09:39:55 -07:00
Timothy J. Baek e49e04c56a chore: formatting 2024-04-02 09:33:27 -07:00
Timothy J. Baek da8646cae9 refac 2024-04-02 09:27:35 -07:00
Timothy J. Baek ba0523cd69 refac: api_key field moved to user 2024-04-02 09:23:55 -07:00
Timothy J. Baek 38c34b4444 fix: import 2024-04-02 09:18:36 -07:00
Timothy J. Baek b11af9ea0e fix 2024-04-02 09:18:15 -07:00
Timothy Jaeryang Baek 56369fea3a Merge branch 'dev' into feature/support_auth_by_api_key 2024-04-02 09:12:19 -07:00
Timothy Jaeryang Baek 24fb77759e Merge pull request #1347 from cheahjs/feat/trusted-email-header
feat: allow authenticating with a trusted email header
2024-04-02 09:11:25 -07:00
Timothy Jaeryang Baek f9106cf394 Merge pull request #1364 from cheahjs/feat/local-sharing
feat: add local sharing of chats
2024-04-02 09:10:15 -07:00
Timothy J. Baek 807a60cccf fix: styling 2024-04-02 09:01:15 -07:00
Timothy J. Baek ac4675900e Merge branch 'feat/local-sharing' of https://github.com/cheahjs/open-webui-fork into pr/1364 2024-04-02 08:58:38 -07:00
Timothy J. Baek f7a1fc207a refac: wording 2024-04-02 08:58:03 -07:00
Timothy J. Baek b9b1d64bd5 refac: styling 2024-04-02 08:58:03 -07:00
Timothy J. Baek 7e6a00b009 refac: share chat page 2024-04-02 08:58:03 -07:00
Timothy J. Baek 5aad328040 refac: toast message wording 2024-04-02 08:58:03 -07:00
Timothy J. Baek 03a7f88dca chore: remove print statement 2024-04-02 08:58:03 -07:00
Timothy J. Baek f61869d90d fix: cascade shared chat delete 2024-04-02 08:58:03 -07:00
Timothy J. Baek 95505bc4c1 feat: chat cascade delete 2024-04-02 08:58:03 -07:00
Timothy J. Baek eca59f759d fix: share chat modal 2024-04-02 08:58:03 -07:00
Timothy J. Baek 9975cb17a9 feat: update shared chat 2024-04-02 08:58:03 -07:00
Timothy J. Baek 0b823f90e6 feat: delete shared chat link 2024-04-02 08:58:03 -07:00
Timothy J. Baek c0fff4c69f fix: typo 2024-04-02 08:58:03 -07:00
Timothy J. Baek 180dd3966b refac: share chat routes 2024-04-02 08:58:03 -07:00
Timothy J. Baek e4d101e550 fix: delete shared chat by id 2024-04-02 08:58:03 -07:00
Timothy J. Baek 88c5b7f495 refac: share chat model 2024-04-02 08:58:03 -07:00
Timothy J. Baek 4e9ca31146 Update chats.py 2024-04-02 08:58:03 -07:00
Timothy J. Baek 48e7376336 refac: shared chat user id 2024-04-02 08:58:03 -07:00
Timothy J. Baek 488301f150 Update 001_initial_schema.py 2024-04-02 08:58:03 -07:00
Timothy J. Baek 287c95e72d refac: dedicated shared chat page 2024-04-02 08:58:03 -07:00
Timothy J. Baek af5a7a35c0 refac: copy to clipboard message 2024-04-02 08:58:03 -07:00
Timothy J. Baek 5faa05a117 refac: share chat modal styling update 2024-04-02 08:58:03 -07:00
Jun Siang Cheah e273a99153 feat: migrate db schema for local sharing 2024-04-02 08:58:03 -07:00
Jun Siang Cheah 151055590d feat: add ability to run db migrations 2024-04-02 08:58:03 -07:00
Jun Siang Cheah 7978adbf45 feat: add frontend support for locally sharing chats 2024-04-02 08:58:03 -07:00
Jun Siang Cheah 94976e5ed3 feat: add backend functions for sharing chats 2024-04-02 08:58:03 -07:00
Timothy J. Baek 3a1d747677 refac: wording 2024-04-02 08:18:47 -07:00
Timothy J. Baek 7121465eda refac: styling 2024-04-02 08:12:55 -07:00
Timothy J. Baek 52e83891b4 refac: share chat page 2024-04-02 08:11:48 -07:00
Timothy J. Baek 456a78ada9 refac: toast message wording 2024-04-02 08:01:45 -07:00
Timothy J. Baek cd60dbe08c chore: remove print statement 2024-04-02 08:00:47 -07:00
Timothy J. Baek a0d932b986 fix: cascade shared chat delete 2024-04-02 08:00:26 -07:00
Timothy J. Baek 10cad60566 feat: chat cascade delete 2024-04-02 07:55:56 -07:00
Timothy J. Baek c6296e5108 fix: share chat modal 2024-04-02 07:45:33 -07:00
Timothy J. Baek dd9cbd0ab9 feat: update shared chat 2024-04-02 07:42:37 -07:00
Timothy J. Baek 865c268312 feat: delete shared chat link 2024-04-02 07:16:25 -07:00
Timothy J. Baek dcac8a773b fix: typo 2024-04-02 07:06:07 -07:00
Timothy J. Baek 96d75dfc1b refac: share chat routes 2024-04-02 07:04:29 -07:00
Timothy J. Baek 49428eb7ef fix: delete shared chat by id 2024-04-02 07:04:04 -07:00
Timothy J. Baek 6c0e3972dc refac: share chat model 2024-04-02 06:42:57 -07:00
Timothy J. Baek 458c317736 Update chats.py 2024-04-02 06:36:02 -07:00
Timothy J. Baek 98f68b8b08 refac: shared chat user id 2024-04-02 06:33:59 -07:00
Timothy J. Baek b76e934140 Update 001_initial_schema.py 2024-04-02 06:27:11 -07:00
Timothy J. Baek 0cdcc2deb6 refac: dedicated shared chat page 2024-04-02 06:17:51 -07:00
Timothy J. Baek 58992d592c refac: copy to clipboard message 2024-04-02 06:14:54 -07:00
Timothy J. Baek 04e172deac refac: share chat modal styling update 2024-04-02 06:12:45 -07:00
Jannik Streidl 9bcb37ea10 fixes and updates 2024-04-02 14:47:52 +02:00
Timothy J. Baek a363c1f2f1 fix: styling 2024-04-02 04:37:35 -07:00
Timothy Jaeryang Baek 6425e794f6 Merge pull request #1394 from Axodouble/patch-1
Update Dutch nl-NL translations again.
2024-04-02 03:40:26 -07:00
Timothy Jaeryang Baek f340178bb7 Merge pull request #1396 from open-webui/main
dev
2024-04-02 03:39:09 -07:00
Timothy J. Baek aa6c8b1bfd feat: ascii art on startup 2024-04-02 03:03:55 -07:00
Axodouble d1ca6b50d9 Update Dutch nl-NL translations again.
Updated all translations to have a Dutch translation.
2024-04-02 11:51:58 +02:00
Jannik S 099b1d066b Revert "Merge Updates & Dockerfile improvements" (#3)
This reverts commit 9763d885be.
2024-04-02 11:28:04 +02:00
lainedfles 9763d885be Merge Updates & Dockerfile improvements 2024-04-02 11:25:20 +02:00
Timothy J. Baek 57530d87f6 feat: /ollama head route 2024-04-02 02:03:13 -07:00
Timothy J. Baek 2c030218f8 fix: latex dollar sign issue 2024-04-01 19:25:47 -07:00
Danny Liu 6bb299ae25 run npm run format 2024-04-01 16:26:05 -07:00
Danny Liu a0a064f4c8 update initals avatar if user changes name 2024-04-01 16:23:54 -07:00
Danny Liu 40e1e212d4 feat: default profile image with user initials 2024-04-01 16:11:28 -07:00
Timothy J. Baek 5558514ff1 fix 2024-04-01 15:23:12 -07:00
Timothy Jaeryang Baek 83c7633acb Merge pull request from GHSA-9pgh-j74g-qj6m
Suggested mitigation for KL-CAN-2024-002.
2024-04-01 15:21:56 -07:00
Timothy Jaeryang Baek 6c3f2f8d38 Merge pull request #1382 from open-webui/main
dev
2024-04-01 14:16:35 -07:00
Timothy J. Baek d72653cdea fix: download allowed hosts 2024-04-01 14:01:05 -07:00
KoreLogic Disclosures 6c96361402 Suggested mitigation for KL-CAN-2024-002. 2024-04-01 15:55:14 -05:00
Timothy Jaeryang Baek 554e56687c Merge pull request from GHSA-39wr-r5vm-3jxj
fix: allowed hosts
2024-04-01 13:27:01 -07:00
Timothy J. Baek 77b1edcd0f fix: allowed hosts 2024-04-01 13:24:48 -07:00
Jun Siang Cheah d667faea33 feat: migrate db schema for local sharing 2024-04-01 11:13:02 +01:00
Jun Siang Cheah 4cb2cc62d2 feat: add ability to run db migrations 2024-04-01 11:12:46 +01:00
Timothy J. Baek 698bfcf87f fix: navbar styling 2024-04-01 01:48:44 -07:00
Timothy Jaeryang Baek edeff20e1d Merge pull request #1374 from open-webui/dev
fix
2024-03-31 21:04:43 -07:00
Timothy J. Baek eda157e303 fix: openai issue 2024-03-31 21:02:29 -07:00
Timothy J. Baek 653a0ff02f fix: katex overflow issue 2024-03-31 19:50:46 -07:00
Timothy Jaeryang Baek 8175a323e9 Merge pull request #1372 from cheahjs/chore/update-npm-format-scripts
chore: add format:backend to npm scripts
2024-03-31 15:34:51 -07:00
Jun Siang Cheah 11850b75c8 chore: add format:backend npm script to match ci 2024-03-31 22:13:39 +01:00
Jun Siang Cheah 562e40a7bd Merge branch 'dev' into feat/trusted-email-header 2024-03-31 22:08:58 +01:00
Jun Siang Cheah 0e3b7a11e3 chore: python formatting 2024-03-31 22:07:43 +01:00
Jun Siang Cheah 196f91d68c feat: add frontend support for locally sharing chats 2024-03-31 22:03:28 +01:00
Jun Siang Cheah bfbfdae1c5 feat: add backend functions for sharing chats 2024-03-31 22:02:40 +01:00
Timothy J. Baek a6c154d839 feat: rag context logging 2024-03-31 14:02:31 -07:00
Timothy Jaeryang Baek 7563619e26 Merge pull request #1368 from Patrice-Gaudicheau/fix/data-dir-handling
Update of main.py
2024-03-31 14:00:39 -07:00
Timothy J. Baek 1b5c92d66c revert: prompt suggestion change 2024-03-31 13:59:39 -07:00
Timothy Jaeryang Baek 2f23325c90 Merge branch 'dev' into fix/data-dir-handling 2024-03-31 13:57:34 -07:00
Patrice-Gaudicheau cef676429f Spellchecking 2024-03-31 22:50:17 +02:00
Timothy Jaeryang Baek a0935dec60 Merge branch 'dev' into feature/support_auth_by_api_key 2024-03-31 13:48:36 -07:00
Timothy Jaeryang Baek 587a8c5992 Merge pull request #1370 from lainedfles/logging-improvements
Improve logging.
2024-03-31 13:46:37 -07:00
Timothy J. Baek 4ddb7e124e fix: error message 2024-03-31 13:40:57 -07:00
Patrice-Gaudicheau 68cfdd89c4 Small simplification 2024-03-31 22:35:44 +02:00
Self Denial 144c9059a3 Improve logging. Move print() statements to appropiate log().
Add COMFYUI and WEBHOOK logging and associated environment variable
control. Add WEBHOOK payload & request debug logs.
2024-03-31 13:17:29 -06:00
Patrice-Gaudicheau ee38b3c84d Enhance DATA_DIR handling in main.py and docker-compose configuration 2024-03-31 20:43:09 +02:00
Timothy Jaeryang Baek 86aa2ca6cb Merge pull request #1363 from open-webui/dev
fix
2024-03-31 02:28:20 -07:00
Timothy J. Baek 1d7ca44017 fix: config data issue 2024-03-31 02:27:03 -07:00
Timothy J. Baek 174c082c69 chore: frontend formatting 2024-03-31 02:04:15 -07:00
Timothy J. Baek bbb63104ea chore: frontend format 2024-03-31 02:01:42 -07:00
Timothy J. Baek 43832d165f chore: frontend formatting 2024-03-31 01:59:10 -07:00
Timothy J. Baek d630485051 chore: delete test.json 2024-03-31 01:57:21 -07:00
Timothy J. Baek 784572590f chore: frontend i18n formatting 2024-03-31 01:54:54 -07:00
Timothy J. Baek 90a03c331d refac: ci 2024-03-31 01:51:13 -07:00
Timothy J. Baek b391ad12ae refac: format backend ci 2024-03-31 01:47:53 -07:00
Timothy J. Baek 6c2eb814ed Update format-backend.yaml 2024-03-31 01:43:52 -07:00
Jun Siang Cheah 150152ddbd fix: accidental indent during format changed logic 2024-03-29 21:04:30 +00:00
Timothy J. Baek 12287f8680 chore: code formatting 2024-03-29 13:06:18 -07:00
Timothy J. Baek 047c9fe82c fix: styling 2024-03-29 13:02:38 -07:00
Joseph C Wang c9f17ecb3a allow version to work with -rc tag 2024-03-29 01:44:37 +08:00
Jun Siang Cheah 50f6addd6f feat: auto signup/login with WEBUI_AUTH_TRUSTED_EMAIL_HEADER 2024-03-28 10:34:57 +00:00
Jun Siang Cheah 29f13f34d3 feat: add WEBUI_AUTH_TRUSTED_EMAIL_HEADER for authenticating users by a trusted header
This is very yolo code, use at your own risk
2024-03-27 09:28:03 +00:00
liu.vaayne b4b56f9c85 frontend support api key 2024-03-26 18:24:14 +08:00
liu.vaayne 81e928030f backend support api key 2024-03-26 18:22:17 +08:00
Jannik Streidl fdef2abdfb cuda fix 2024-03-22 12:48:48 +01:00
Jannik Streidl fc4e762b05 ENV fix 2024-03-22 10:57:41 +01:00
Jannik Streidl c08631d6ff updates docker-build action 2024-03-22 10:05:20 +01:00
Jannik Streidl 3f973fe77f i need coffee 2024-03-22 09:57:17 +01:00
Jannik Streidl 953d05428e grammar 2024-03-22 09:55:46 +01:00
Jannik Streidl 6dc4b748fb added comments 2024-03-22 09:50:01 +01:00
Jannik Streidl fde0139bf7 All in one Dockerfile for including Ollama 2024-03-22 09:31:35 +01:00
Timothy J. Baek afa591afb2 Create Dockerfile-ollama 2024-03-20 20:58:23 -07:00
Joseph Young 2588da0e27 Update PyTorch wheel source to CUDA 11.8
Modified the Dockerfile to install PyTorch, torchvision, and torchaudio from a CUDA 11.8 specific wheel URL. This ensures compatibility with the CUDA version in our environment and potentially improves performance and stability for GPU-accelerated operations.
2024-03-20 18:33:34 -04:00
Joseph Young 8ce48dc7d1 Fix typo in Dockerfile comment for model recommendation
Okay, this was driving my OCD crazy.

Corrected a spelling error in the Dockerfile's comment section to enhance documentation clarity. The typo 'persormance' was updated to 'performance,' ensuring accurate guidance on using multilingual sentence transformer models for better performance and language support.
2024-03-20 18:28:57 -04:00
Joseph Young 9bea40bd40 Adding the missing env variable
ENV RAG_EMBEDDING_MODEL_DEVICE_TYPE="cuda"
2024-03-20 18:16:11 -04:00
Jannik Streidl 9a8a48b879 gh build action for the different build args 2024-03-20 09:05:38 +01:00
Jannik Streidl 1f6739337b docker improvements & changed universal device type env for different models used 2024-03-20 08:44:09 +01:00
Jannik Streidl 132d741c55 set default to cpu 2024-03-18 17:09:43 +01:00
Jannik Streidl 5abe0089cb cuda support 2024-03-18 17:08:34 +01:00
Joseph Young c5948d3e2c Updated Dockerfile for CUDA backend
Enabled NVIDIA CUDA backend build stage in the Dockerfile for enhanced performance with GPU support. Moved the environment variable defining the device type for the embedding and TTS models to be shared between CPU and GPU configurations. The default device type for CPU build is now explicitly set to "cpu", while the CUDA build retains "cuda", ensuring clarity and performance optimization across different hardware setups.
2024-03-17 22:50:52 -04:00
Joseph Young c004ecdccc Refactor Dockerfile for CPU and CUDA builds
Switched to Chainguard images as base for both CPU and CUDA backend builds for improved security and compatibility. Replaced Ubuntu base with Chainguard's Python image for the CPU builds and PyTorch CUDA image for GPU acceleration, resolving python requirements conflicts. Updated package installation commands to align with the new Redhat-compatible base images. The Dockerfile now installs only the necessary dependencies, as Python is provided by the base image.

These changes will facilitate a more secure and streamlined build process with better dependency management across different platforms.
2024-03-17 17:03:43 -04:00
Joseph Young e3b1cbbb86 Parametrize CUDA_VERSION in Dockerfile
Standardized CUDA_VERSION as a global ARG to ensure consistency and facilitate version updates across the Dockerfile. This change allows the CUDA version to be defined once at the beginning and reused, reducing the chance of mismatched versions and easing maintenance when changing CUDA versions. It further streamlines the build process for potential multi-stage builds with varying CUDA dependencies.

Refs #nvidia-update
2024-03-17 02:27:06 -04:00
Joseph Young f6cef312f2 Optimize Dockerfile for CUDA support
Refactored the Dockerfile to better organize and streamline environment variable settings, emphasizing support for a CUDA-based WebUI backend while retaining the ability to build a CPU-only image. Consolidated ENV commands to reduce layers, improving build efficiency, and set a default PORT environment to enhance container usability. Enabled exposure of the backend service on port 8080 and leveraged combined RUN directives to minimize the image footprint. These changes facilitate a more robust deployment process, catering to both CPU and CUDA environments.
2024-03-17 01:55:37 -04:00
Joseph Young 75a40dead6 Create Dockerfile-cuda
+Dockerfile-cuda

I created this file to help add CUDA support to open-webui for access to a GPU during embedding operations.
2024-03-16 19:26:21 -04:00
Jannik S 29e48b1c1f Exposed port 8080 2024-03-16 20:11:09 +01:00
Jannik S 62ab163316 Update Dockerfile 2024-03-16 12:43:48 +01:00
Jannik Streidl 384b7e8462 changed from bullseye to bookworm + removed unused steps 2024-03-14 11:33:54 +01:00
Jannik Streidl 50bec32153 Dockerfile optimisation 2024-03-14 11:18:27 +01:00
196 changed files with 19111 additions and 4919 deletions
+6 -1
View File
@@ -9,4 +9,9 @@ OPENAI_API_KEY=''
# DO NOT TRACK
SCARF_NO_ANALYTICS=true
DO_NOT_TRACK=true
DO_NOT_TRACK=true
ANONYMIZED_TELEMETRY=false
# Use locally bundled version of the LiteLLM cost map json
# to avoid repetitive startup connections
LITELLM_LOCAL_MODEL_COST_MAP="True"
+1
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',
+3
View File
@@ -24,6 +24,9 @@ assignees: ''
## Environment
- **Open WebUI Version:** [e.g., 0.1.120]
- **Ollama (if applicable):** [e.g., 0.1.30, 0.1.32-rc1]
- **Operating System:** [e.g., Windows 10, macOS Big Sur, Ubuntu 20.04]
- **Browser (if applicable):** [e.g., Chrome 100.0, Firefox 98.0]
+11
View File
@@ -0,0 +1,11 @@
version: 2
updates:
- package-ecosystem: pip
directory: "/backend"
schedule:
interval: daily
time: "13:00"
groups:
python-packages:
patterns:
- "*"
+24 -6
View File
@@ -2,14 +2,16 @@
- [ ] **Description:** Briefly describe the changes 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 the changes?
- [ ] **Code Review:** Have you self-reviewed your code and addressed any coding standard issues?
---
## Description
[Insert a brief description of the changes made in this pull request]
[Insert a brief description of the changes made in this pull request, including any relevant motivation and impact.]
---
@@ -17,16 +19,32 @@
### Added
- [List any new features or additions]
- [List any new features, functionalities, or additions]
### Fixed
- [List any fixes or corrections]
- [List any fixes, corrections, or bug fixes]
### Changed
- [List any changes or updates]
- [List any changes, updates, refactorings, or optimizations]
### Removed
- [List any removed features or files]
- [List any removed features, files, or deprecated functionalities]
### Security
- [List any new or updated security-related changes, including vulnerability fixes]
### Breaking Changes
- [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]
+11
View File
@@ -57,3 +57,14 @@ jobs:
path: .
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Trigger Docker build workflow
uses: actions/github-script@v7
with:
script: |
github.rest.actions.createWorkflowDispatch({
owner: context.repo.owner,
repo: context.repo.repo,
workflow_id: 'docker-build.yaml',
ref: 'v${{ steps.get_version.outputs.version }}',
})
+335 -17
View File
@@ -1,8 +1,7 @@
#
name: Create and publish a Docker image
name: Create and publish Docker images with specific build args
# Configures this workflow to run every time a change is pushed to the branch called `release`.
on:
workflow_dispatch:
push:
branches:
- main
@@ -10,30 +9,39 @@ on:
tags:
- v*
# Defines two custom environment variables for the workflow. These are used for the Container registry domain, and a name for the Docker image that this workflow builds.
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
FULL_IMAGE_NAME: ghcr.io/${{ github.repository }}
# There is a single job in this workflow. It's configured to run on the latest available version of Ubuntu.
jobs:
build-and-push-image:
build-main-image:
runs-on: ubuntu-latest
# Sets the permissions granted to the `GITHUB_TOKEN` for the actions in this job.
permissions:
contents: read
packages: write
#
strategy:
fail-fast: false
matrix:
platform:
- linux/amd64
- linux/arm64
steps:
- name: Prepare
run: |
platform=${{ matrix.platform }}
echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV
- name: Checkout repository
uses: actions/checkout@v4
# Required for multi architecture build
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
# Required for multi architecture build
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
# Uses the `docker/login-action` action to log in to the Container registry registry using the account and password that will publish the packages. Once published, the packages are scoped to the account defined here.
- name: Log in to the Container registry
uses: docker/login-action@v3
with:
@@ -41,12 +49,11 @@ jobs:
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata for Docker images
- name: Extract metadata for Docker images (default latest tag)
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
# This configuration dynamically generates tags based on the branch, tag, commit, and custom suffix for lite version.
images: ${{ env.FULL_IMAGE_NAME }}
tags: |
type=ref,event=branch
type=ref,event=tag
@@ -56,11 +63,322 @@ jobs:
flavor: |
latest=${{ github.ref == 'refs/heads/main' }}
- name: Build and push Docker image
- name: Build Docker image (latest)
uses: docker/build-push-action@v5
id: build
with:
context: .
push: true
platforms: linux/amd64,linux/arm64
tags: ${{ steps.meta.outputs.tags }}
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
- name: Export digest
run: |
mkdir -p /tmp/digests
digest="${{ steps.build.outputs.digest }}"
touch "/tmp/digests/${digest#sha256:}"
- name: Upload digest
uses: actions/upload-artifact@v4
with:
name: digests-main-${{ env.PLATFORM_PAIR }}
path: /tmp/digests/*
if-no-files-found: error
retention-days: 1
build-cuda-image:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
strategy:
fail-fast: false
matrix:
platform:
- linux/amd64
- linux/arm64
steps:
- name: Prepare
run: |
platform=${{ matrix.platform }}
echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to the Container registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata for Docker images (default latest tag)
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.FULL_IMAGE_NAME }}
tags: |
type=ref,event=branch
type=ref,event=tag
type=sha,prefix=git-
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=raw,enable=${{ github.ref == 'refs/heads/main' }},prefix=,suffix=,value=cuda
flavor: |
latest=${{ github.ref == 'refs/heads/main' }}
suffix=-cuda,onlatest=true
- name: Build Docker image (cuda)
uses: docker/build-push-action@v5
id: build
with:
context: .
push: true
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
- name: Export digest
run: |
mkdir -p /tmp/digests
digest="${{ steps.build.outputs.digest }}"
touch "/tmp/digests/${digest#sha256:}"
- name: Upload digest
uses: actions/upload-artifact@v4
with:
name: digests-cuda-${{ env.PLATFORM_PAIR }}
path: /tmp/digests/*
if-no-files-found: error
retention-days: 1
build-ollama-image:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
strategy:
fail-fast: false
matrix:
platform:
- linux/amd64
- linux/arm64
steps:
- name: Prepare
run: |
platform=${{ matrix.platform }}
echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to the Container registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata for Docker images (ollama tag)
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.FULL_IMAGE_NAME }}
tags: |
type=ref,event=branch
type=ref,event=tag
type=sha,prefix=git-
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=raw,enable=${{ github.ref == 'refs/heads/main' }},prefix=,suffix=,value=ollama
flavor: |
latest=${{ github.ref == 'refs/heads/main' }}
suffix=-ollama,onlatest=true
- name: Build Docker image (ollama)
uses: docker/build-push-action@v5
id: build
with:
context: .
push: true
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
- name: Export digest
run: |
mkdir -p /tmp/digests
digest="${{ steps.build.outputs.digest }}"
touch "/tmp/digests/${digest#sha256:}"
- name: Upload digest
uses: actions/upload-artifact@v4
with:
name: digests-ollama-${{ env.PLATFORM_PAIR }}
path: /tmp/digests/*
if-no-files-found: error
retention-days: 1
merge-main-images:
runs-on: ubuntu-latest
needs: [ build-main-image ]
steps:
- name: Download digests
uses: actions/download-artifact@v4
with:
pattern: digests-main-*
path: /tmp/digests
merge-multiple: true
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to the Container registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata for Docker images (default latest tag)
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.FULL_IMAGE_NAME }}
tags: |
type=ref,event=branch
type=ref,event=tag
type=sha,prefix=git-
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
flavor: |
latest=${{ github.ref == 'refs/heads/main' }}
- name: Create manifest list and push
working-directory: /tmp/digests
run: |
docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
$(printf '${{ env.FULL_IMAGE_NAME }}@sha256:%s ' *)
- name: Inspect image
run: |
docker buildx imagetools inspect ${{ env.FULL_IMAGE_NAME }}:${{ steps.meta.outputs.version }}
merge-cuda-images:
runs-on: ubuntu-latest
needs: [ build-cuda-image ]
steps:
- name: Download digests
uses: actions/download-artifact@v4
with:
pattern: digests-cuda-*
path: /tmp/digests
merge-multiple: true
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to the Container registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata for Docker images (default latest tag)
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.FULL_IMAGE_NAME }}
tags: |
type=ref,event=branch
type=ref,event=tag
type=sha,prefix=git-
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=raw,enable=${{ github.ref == 'refs/heads/main' }},prefix=,suffix=,value=cuda
flavor: |
latest=${{ github.ref == 'refs/heads/main' }}
suffix=-cuda,onlatest=true
- name: Create manifest list and push
working-directory: /tmp/digests
run: |
docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
$(printf '${{ env.FULL_IMAGE_NAME }}@sha256:%s ' *)
- name: Inspect image
run: |
docker buildx imagetools inspect ${{ env.FULL_IMAGE_NAME }}:${{ steps.meta.outputs.version }}
merge-ollama-images:
runs-on: ubuntu-latest
needs: [ build-ollama-image ]
steps:
- name: Download digests
uses: actions/download-artifact@v4
with:
pattern: digests-ollama-*
path: /tmp/digests
merge-multiple: true
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to the Container registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata for Docker images (default ollama tag)
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.FULL_IMAGE_NAME }}
tags: |
type=ref,event=branch
type=ref,event=tag
type=sha,prefix=git-
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=raw,enable=${{ github.ref == 'refs/heads/main' }},prefix=,suffix=,value=ollama
flavor: |
latest=${{ github.ref == 'refs/heads/main' }}
suffix=-ollama,onlatest=true
- name: Create manifest list and push
working-directory: /tmp/digests
run: |
docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
$(printf '${{ env.FULL_IMAGE_NAME }}@sha256:%s ' *)
- name: Inspect image
run: |
docker buildx imagetools inspect ${{ env.FULL_IMAGE_NAME }}:${{ steps.meta.outputs.version }}
+21 -11
View File
@@ -1,29 +1,39 @@
name: Python CI
on:
push:
branches: ['main']
branches:
- main
- dev
pull_request:
branches:
- main
- dev
jobs:
build:
name: 'Format Backend'
env:
PUBLIC_API_BASE_URL: ''
runs-on: ubuntu-latest
strategy:
matrix:
node-version:
- latest
python-version: [3.11]
steps:
- uses: actions/checkout@v4
- name: Use Python
uses: actions/setup-python@v4
- name: Use Bun
uses: oven-sh/setup-bun@v1
- name: Set up Python
uses: actions/setup-python@v2
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install yapf
pip install black
- name: Format backend
run: bun run format:backend
run: npm run format:backend
- name: Check for changes after format
run: git diff --exit-code
+30 -16
View File
@@ -1,25 +1,39 @@
name: Bun CI
name: Frontend Build
on:
push:
branches: ['main']
branches:
- main
- dev
pull_request:
branches:
- main
- dev
jobs:
build:
name: 'Format & Build Frontend'
env:
PUBLIC_API_BASE_URL: ''
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Use Bun
uses: oven-sh/setup-bun@v1
- run: bun --version
- name: Install frontend dependencies
run: bun install
- name: Format frontend
run: bun run format
- name: Check for changes after format
- name: Checkout Repository
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '20' # Or specify any other version you want to use
- name: Install Dependencies
run: npm install
- 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
if: always()
run: bun run build
- name: Build Frontend
run: npm run build
+186
View File
@@ -0,0 +1,186 @@
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 up --detach --build
- 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@v2
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
+5 -1
View File
@@ -297,4 +297,8 @@ dist
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*
.pnp.*
# cypress artifacts
cypress/videos
cypress/screenshots
+136
View File
@@ -5,6 +5,142 @@ 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.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
- **🔧 Translation Issues**: Addressed various translation discrepancies.
- **🔒 LiteLLM Security Fix**: Updated LiteLLM version to resolve a security vulnerability.
- **🖥️ HTML Tag Display**: Rectified the issue where the '< br >' tag wasn't displaying correctly.
- **🔗 WebSocket Connection**: Resolved the failure of WebSocket connection under HTTPS security for ComfyUI server.
- **📜 FileReader Optimization**: Implemented FileReader initialization per image in multi-file drag & drop to ensure reusability.
- **🏷️ Tag Display**: Corrected tag display inconsistencies.
- **📦 Archived Chat Styling**: Fixed styling issues in archived chat.
- **🔖 Safari Copy Button Bug**: Addressed the bug where the copy button failed to copy links in Safari.
## [0.1.120] - 2024-04-20
### Added
- **📦 Archive Chat Feature**: Easily archive chats with a new sidebar button, and access archived chats via the profile button > archived chats.
- **🔊 Configurable Text-to-Speech Endpoint**: Customize your Text-to-Speech experience with configurable OpenAI endpoints.
- **🛠️ Improved Error Handling**: Enhanced error message handling for connection failures.
- **⌨️ Enhanced Shortcut**: When editing messages, use ctrl/cmd+enter to save and submit, and esc to close.
- **🌐 Language Support**: Added support for Georgian and enhanced translations for Portuguese and Vietnamese.
### Fixed
- **🔧 Model Selector**: Resolved issue where default model selection was not saving.
- **🔗 Share Link Copy Button**: Fixed bug where the copy button wasn't copying links in Safari.
- **🎨 Light Theme Styling**: Addressed styling issue with the light theme.
## [0.1.119] - 2024-04-16
### Added
- **🌟 Enhanced RAG Embedding Support**: Ollama, and OpenAI models can now be used for RAG embedding model.
- **🔄 Seamless Integration**: Copy 'ollama run <model name>' directly from Ollama page to easily select and pull models.
- **🏷️ Tagging Feature**: Add tags to chats directly via the sidebar chat menu.
- **📱 Mobile Accessibility**: Swipe left and right on mobile to effortlessly open and close the sidebar.
- **🔍 Improved Navigation**: Admin panel now supports pagination for user list.
- **🌍 Additional Language Support**: Added Polish language support.
### Fixed
- **🌍 Language Enhancements**: Vietnamese and Spanish translations have been improved.
- **🔧 Helm Fixes**: Resolved issues with Helm trailing slash and manifest.json.
### Changed
- **🐳 Docker Optimization**: Updated docker image build process to utilize 'uv' for significantly faster builds compared to 'pip3'.
## [0.1.118] - 2024-04-10
### Added
- **🦙 Ollama and CUDA Images**: Added support for ':ollama' and ':cuda' tagged images.
- **👍 Enhanced Response Rating**: Now you can annotate your ratings for better feedback.
- **👤 User Initials Profile Photo**: User initials are now the default profile photo.
- **🔍 Update RAG Embedding Model**: Customize RAG embedding model directly in document settings.
- **🌍 Additional Language Support**: Added Turkish language support.
### Fixed
- **🔒 Share Chat Permission**: Resolved issue with chat sharing permissions.
- **🛠 Modal Close**: Modals can now be closed using the Esc key.
### Changed
- **🎨 Admin Panel Styling**: Refreshed styling for the admin panel.
- **🐳 Docker Image Build**: Updated docker image build process for improved efficiency.
## [0.1.117] - 2024-04-03
### Added
- 🗨️ **Local Chat Sharing**: Share chat links seamlessly between users.
- 🔑 **API Key Generation Support**: Generate secret keys to leverage Open WebUI with OpenAI libraries.
- 📄 **Chat Download as PDF**: Easily download chats in PDF format.
- 📝 **Improved Logging**: Enhancements to logging functionality.
- 📧 **Trusted Email Authentication**: Authenticate using a trusted email header.
### Fixed
- 🌷 **Enhanced Dutch Translation**: Improved translation for Dutch users.
- ⚪ **White Theme Styling**: Resolved styling issue with the white theme.
- 📜 **LaTeX Chat Screen Overflow**: Fixed screen overflow issue with LaTeX rendering.
- 🔒 **Security Patches**: Applied necessary security patches.
## [0.1.116] - 2024-03-31
### Added
+97 -44
View File
@@ -1,77 +1,128 @@
# syntax=docker/dockerfile:1
# Initialize device type args
# use build args in the docker build commmand with --build-arg="BUILDARG=true"
ARG USE_CUDA=false
ARG USE_OLLAMA=false
# Tested with cu117 for CUDA 11 and cu121 for CUDA 12 (default)
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 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=""
FROM node:alpine as build
######## WebUI frontend ########
FROM --platform=$BUILDPLATFORM node:21-alpine3.19 as build
WORKDIR /app
# wget embedding model weight from alpine (does not exist from slim-buster)
RUN wget "https://chroma-onnx-models.s3.amazonaws.com/all-MiniLM-L6-v2/onnx.tar.gz" -O - | \
tar -xzf - -C /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build
######## WebUI backend ########
FROM python:3.11-slim-bookworm as base
ENV ENV=prod
ENV PORT ""
# Use args
ARG USE_CUDA
ARG USE_OLLAMA
ARG USE_CUDA_VER
ARG USE_EMBEDDING_MODEL
ARG USE_RERANKING_MODEL
ENV OLLAMA_BASE_URL "/ollama"
## Basis ##
ENV ENV=prod \
PORT=8080 \
# pass build args to the build
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_RERANKING_MODEL_DOCKER=${USE_RERANKING_MODEL}
ENV OPENAI_API_BASE_URL ""
ENV OPENAI_API_KEY ""
## Basis URL Config ##
ENV OLLAMA_BASE_URL="/ollama" \
OPENAI_API_BASE_URL=""
ENV WEBUI_SECRET_KEY ""
## API Key and Security Config ##
ENV OPENAI_API_KEY="" \
WEBUI_SECRET_KEY="" \
SCARF_NO_ANALYTICS=true \
DO_NOT_TRACK=true \
ANONYMIZED_TELEMETRY=false
ENV SCARF_NO_ANALYTICS true
ENV 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"
######## Preloaded models ########
# whisper TTS Settings
ENV WHISPER_MODEL="base"
ENV WHISPER_MODEL_DIR="/app/backend/data/cache/whisper/models"
# RAG Embedding Model Settings
# 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 persormance and multilangauge support use "intfloat/multilingual-e5-large" (~2.5GB) or "intfloat/multilingual-e5-base" (~1.5GB)
# IMPORTANT: If you change the default model (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.
ENV RAG_EMBEDDING_MODEL="all-MiniLM-L6-v2"
# device type for whisper tts and embbeding models - "cpu" (default), "cuda" (nvidia gpu and CUDA required) or "mps" (apple silicon) - choosing this right can lead to better performance
ENV RAG_EMBEDDING_MODEL_DEVICE_TYPE="cpu"
ENV RAG_EMBEDDING_MODEL_DIR="/app/backend/data/cache/embedding/models"
ENV SENTENCE_TRANSFORMERS_HOME $RAG_EMBEDDING_MODEL_DIR
#### Other models #########################################################
## whisper TTS model settings ##
ENV WHISPER_MODEL="base" \
WHISPER_MODEL_DIR="/app/backend/data/cache/whisper/models"
######## Preloaded models ########
## RAG Embedding model settings ##
ENV RAG_EMBEDDING_MODEL="$USE_EMBEDDING_MODEL_DOCKER" \
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
RUN mkdir -p $HOME/.cache/chroma
RUN echo -n 00000000-0000-0000-0000-000000000000 > $HOME/.cache/chroma/telemetry_user_id
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/*; \
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/*; \
fi
# install python dependencies
COPY ./backend/requirements.txt ./requirements.txt
RUN apt-get update && apt-get install ffmpeg libsm6 libxext6 -y
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'])"; \
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
RUN pip3 install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cpu --no-cache-dir
RUN pip3 install -r requirements.txt --no-cache-dir
# Install pandoc and netcat
# RUN python -c "import pypandoc; pypandoc.download_pandoc()"
RUN apt-get update \
&& apt-get install -y pandoc netcat-openbsd \
&& rm -rf /var/lib/apt/lists/*
# preload embedding model
RUN python -c "import os; from chromadb.utils import embedding_functions; sentence_transformer_ef = embedding_functions.SentenceTransformerEmbeddingFunction(model_name=os.environ['RAG_EMBEDDING_MODEL'], device=os.environ['RAG_EMBEDDING_MODEL_DEVICE_TYPE'])"
# preload tts model
RUN python -c "import os; from faster_whisper import WhisperModel; WhisperModel(os.environ['WHISPER_MODEL'], device='auto', compute_type='int8', download_root=os.environ['WHISPER_MODEL_DIR'])"
# copy embedding weight from build
RUN mkdir -p /root/.cache/chroma/onnx_models/all-MiniLM-L6-v2
COPY --from=build /app/onnx /root/.cache/chroma/onnx_models/all-MiniLM-L6-v2/onnx
# RUN mkdir -p /root/.cache/chroma/onnx_models/all-MiniLM-L6-v2
# 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
@@ -81,4 +132,6 @@ COPY --from=build /app/package.json /app/package.json
# copy backend files
COPY ./backend .
EXPOSE 8080
CMD [ "bash", "start.sh"]
+15 -7
View File
@@ -1,25 +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
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
+37 -14
View File
@@ -25,22 +25,28 @@ Open WebUI is an extensible, feature-rich, and user-friendly self-hosted WebUI d
- 🚀 **Effortless Setup**: Install seamlessly using Docker or Kubernetes (kubectl, kustomize or helm) for a hassle-free experience.
- 🌈 **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.
- ✒️🔢 **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.
- 🔍 **RAG Embedding Support**: Change the RAG embedding model directly in document settings, enhancing document processing. This feature supports Ollama and OpenAI models.
- 🌐 **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.
- 📜 **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.
- 👍👎 **RLHF Annotation**: Empower your messages by rating them with thumbs up and thumbs down, 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.
- 👍👎 **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.
@@ -53,28 +59,42 @@ Open WebUI is an extensible, feature-rich, and user-friendly self-hosted WebUI d
- 💬 **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 AUTOMATIC1111 API (local) and DALL-E, enriching your chat experience with dynamic visual content.
- 🎨🤖 **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.
@@ -94,24 +114,27 @@ Don't forget to explore our sibling project, [Open WebUI Community](https://open
### Quick Start with Docker 🐳
> [!IMPORTANT]
> [!WARNING]
> When using Docker to install Open WebUI, make sure to include the `-v open-webui:/app/backend/data` in your Docker command. This step is crucial as it ensures your database is properly mounted and prevents any loss of data.
- **If Ollama is on your computer**, use this command:
> [!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.
```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:
- After installation, you can access Open WebUI at [http://localhost:3000](http://localhost:3000). Enjoy! 😄
```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
```
After installation, you can access Open WebUI at [http://localhost:3000](http://localhost:3000). Enjoy! 😄
#### Open WebUI: Server Connection Error
@@ -182,4 +205,4 @@ If you have any questions, suggestions, or need assistance, please open an issue
---
Created by [Timothy J. Baek](https://github.com/tjbck) - Let's make Open Web UI even more amazing together! 💪
Created by [Timothy J. Baek](https://github.com/tjbck) - Let's make Open WebUI even more amazing together! 💪
+130 -7
View File
@@ -10,8 +10,19 @@ from fastapi import (
File,
Form,
)
from fastapi.responses import StreamingResponse, JSONResponse, FileResponse
from fastapi.middleware.cors import CORSMiddleware
from faster_whisper import WhisperModel
from pydantic import BaseModel
import requests
import hashlib
from pathlib import Path
import json
from constants import ERROR_MESSAGES
from utils.utils import (
@@ -28,6 +39,10 @@ from config import (
UPLOAD_DIR,
WHISPER_MODEL,
WHISPER_MODEL_DIR,
WHISPER_MODEL_AUTO_UPDATE,
DEVICE_TYPE,
AUDIO_OPENAI_API_BASE_URL,
AUDIO_OPENAI_API_KEY,
)
log = logging.getLogger(__name__)
@@ -43,7 +58,103 @@ app.add_middleware(
)
@app.post("/transcribe")
app.state.OPENAI_API_BASE_URL = AUDIO_OPENAI_API_BASE_URL
app.state.OPENAI_API_KEY = AUDIO_OPENAI_API_KEY
# setting device type for whisper model
whisper_device_type = DEVICE_TYPE if DEVICE_TYPE and DEVICE_TYPE == "cuda" else "cpu"
log.info(f"whisper_device_type: {whisper_device_type}")
SPEECH_CACHE_DIR = Path(CACHE_DIR).joinpath("./audio/speech/")
SPEECH_CACHE_DIR.mkdir(parents=True, exist_ok=True)
class OpenAIConfigUpdateForm(BaseModel):
url: str
key: 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,
}
@app.post("/config/update")
async def update_openai_config(
form_data: OpenAIConfigUpdateForm, user=Depends(get_admin_user)
):
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
return {
"status": True,
"OPENAI_API_BASE_URL": app.state.OPENAI_API_BASE_URL,
"OPENAI_API_KEY": app.state.OPENAI_API_KEY,
}
@app.post("/speech")
async def speech(request: Request, user=Depends(get_verified_user)):
body = await request.body()
name = hashlib.sha256(body).hexdigest()
file_path = SPEECH_CACHE_DIR.joinpath(f"{name}.mp3")
file_body_path = SPEECH_CACHE_DIR.joinpath(f"{name}.json")
# Check if the file already exists in the cache
if file_path.is_file():
return FileResponse(file_path)
headers = {}
headers["Authorization"] = f"Bearer {app.state.OPENAI_API_KEY}"
headers["Content-Type"] = "application/json"
r = None
try:
r = requests.post(
url=f"{app.state.OPENAI_API_BASE_URL}/audio/speech",
data=body,
headers=headers,
stream=True,
)
r.raise_for_status()
# Save the streaming content to a file
with open(file_path, "wb") as f:
for chunk in r.iter_content(chunk_size=8192):
f.write(chunk)
with open(file_body_path, "w") as f:
json.dump(json.loads(body.decode("utf-8")), f)
# Return the saved file
return FileResponse(file_path)
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']}"
except:
error_detail = f"External: {e}"
raise HTTPException(
status_code=r.status_code if r != None else 500,
detail=error_detail,
)
@app.post("/transcriptions")
def transcribe(
file: UploadFile = File(...),
user=Depends(get_current_user),
@@ -64,12 +175,24 @@ def transcribe(
f.write(contents)
f.close()
model = WhisperModel(
WHISPER_MODEL,
device="auto",
compute_type="int8",
download_root=WHISPER_MODEL_DIR,
)
whisper_kwargs = {
"model_size_or_path": WHISPER_MODEL,
"device": whisper_device_type,
"compute_type": "int8",
"download_root": WHISPER_MODEL_DIR,
"local_files_only": not WHISPER_MODEL_AUTO_UPDATE,
}
log.debug(f"whisper_kwargs: {whisper_kwargs}")
try:
model = WhisperModel(**whisper_kwargs)
except:
log.warning(
"WhisperModel initialization failed, attempting download with local_files_only=False"
)
whisper_kwargs["local_files_only"] = False
model = WhisperModel(**whisper_kwargs)
segments, info = model.transcribe(file_path, beam_size=5)
log.info(
+76 -41
View File
@@ -24,12 +24,25 @@ 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
import logging
from config import SRC_LOG_LEVELS, CACHE_DIR, AUTOMATIC1111_BASE_URL, COMFYUI_BASE_URL
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,
)
log = logging.getLogger(__name__)
@@ -47,19 +60,21 @@ app.add_middleware(
allow_headers=["*"],
)
app.state.ENGINE = ""
app.state.ENABLED = False
app.state.ENGINE = IMAGE_GENERATION_ENGINE
app.state.ENABLED = ENABLE_IMAGE_GENERATION
app.state.OPENAI_API_KEY = ""
app.state.MODEL = ""
app.state.OPENAI_API_BASE_URL = IMAGES_OPENAI_API_BASE_URL
app.state.OPENAI_API_KEY = IMAGES_OPENAI_API_KEY
app.state.MODEL = IMAGE_GENERATION_MODEL
app.state.AUTOMATIC1111_BASE_URL = AUTOMATIC1111_BASE_URL
app.state.COMFYUI_BASE_URL = COMFYUI_BASE_URL
app.state.IMAGE_SIZE = "512x512"
app.state.IMAGE_STEPS = 50
app.state.IMAGE_SIZE = IMAGE_SIZE
app.state.IMAGE_STEPS = IMAGE_STEPS
@app.get("/config")
@@ -125,27 +140,33 @@ async def update_engine_url(
}
class OpenAIKeyUpdateForm(BaseModel):
class OpenAIConfigUpdateForm(BaseModel):
url: str
key: str
@app.get("/key")
async def get_openai_key(user=Depends(get_admin_user)):
return {"OPENAI_API_KEY": app.state.OPENAI_API_KEY}
@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,
}
@app.post("/key/update")
async def update_openai_key(
form_data: OpenAIKeyUpdateForm, user=Depends(get_admin_user)
@app.post("/openai/config/update")
async def update_openai_config(
form_data: OpenAIConfigUpdateForm, user=Depends(get_admin_user)
):
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
return {
"OPENAI_API_KEY": app.state.OPENAI_API_KEY,
"status": True,
"OPENAI_API_BASE_URL": app.state.OPENAI_API_BASE_URL,
"OPENAI_API_KEY": app.state.OPENAI_API_KEY,
}
@@ -295,38 +316,50 @@ 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)
header, encoded = b64_str.split(",", 1)
mime_type = header.split(";")[0]
# Write the image data to a file
img_data = base64.b64decode(encoded)
image_id = str(uuid.uuid4())
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_id
return image_filename
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")
file_path = IMAGE_CACHE_DIR.joinpath(f"{image_id}{image_format}")
with open(file_path, "wb") as image_file:
for chunk in r.iter_content(chunk_size=8192):
image_file.write(chunk)
return image_id, image_format
else:
log.error(f"Url does not point to an image.")
return None, None
return image_id
except Exception as e:
print(f"Error saving image: {e}")
return None
log.exception(f"Error saving image: {e}")
return None, None
@app.post("/generations")
@@ -354,7 +387,7 @@ def generate_image(
}
r = requests.post(
url=f"https://api.openai.com/v1/images/generations",
url=f"{app.state.OPENAI_API_BASE_URL}/images/generations",
json=data,
headers=headers,
)
@@ -365,8 +398,8 @@ 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"})
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_id}.json")
with open(file_body_path, "w") as f:
@@ -397,19 +430,21 @@ def generate_image(
user.id,
app.state.COMFYUI_BASE_URL,
)
print(res)
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"})
image_id, image_format = save_url_image(image["url"])
images.append(
{"url": f"/cache/image/generations/{image_id}{image_format}"}
)
file_body_path = IMAGE_CACHE_DIR.joinpath(f"{image_id}.json")
with open(file_body_path, "w") as f:
json.dump(data.model_dump(exclude_none=True), f)
print(images)
log.debug(f"images: {images}")
return images
else:
if form_data.model:
@@ -440,8 +475,8 @@ 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"})
image_filename = save_b64_image(image)
images.append({"url": f"/cache/image/generations/{image_filename}"})
file_body_path = IMAGE_CACHE_DIR.joinpath(f"{image_id}.json")
with open(file_body_path, "w") as f:
+15 -9
View File
@@ -4,6 +4,12 @@ import json
import urllib.request
import urllib.parse
import random
import logging
from config import SRC_LOG_LEVELS
log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["COMFYUI"])
from pydantic import BaseModel
@@ -121,7 +127,7 @@ COMFYUI_DEFAULT_PROMPT = """
def queue_prompt(prompt, client_id, base_url):
print("queue_prompt")
log.info("queue_prompt")
p = {"prompt": prompt, "client_id": client_id}
data = json.dumps(p).encode("utf-8")
req = urllib.request.Request(f"{base_url}/prompt", data=data)
@@ -129,7 +135,7 @@ def queue_prompt(prompt, client_id, base_url):
def get_image(filename, subfolder, folder_type, base_url):
print("get_image")
log.info("get_image")
data = {"filename": filename, "subfolder": subfolder, "type": folder_type}
url_values = urllib.parse.urlencode(data)
with urllib.request.urlopen(f"{base_url}/view?{url_values}") as response:
@@ -137,14 +143,14 @@ def get_image(filename, subfolder, folder_type, base_url):
def get_image_url(filename, subfolder, folder_type, base_url):
print("get_image")
log.info("get_image")
data = {"filename": filename, "subfolder": subfolder, "type": folder_type}
url_values = urllib.parse.urlencode(data)
return f"{base_url}/view?{url_values}"
def get_history(prompt_id, base_url):
print("get_history")
log.info("get_history")
with urllib.request.urlopen(f"{base_url}/history/{prompt_id}") as response:
return json.loads(response.read())
@@ -189,7 +195,7 @@ class ImageGenerationPayload(BaseModel):
def comfyui_generate_image(
model: str, payload: ImageGenerationPayload, client_id, base_url
):
host = base_url.replace("http://", "").replace("https://", "")
ws_url = base_url.replace("http://", "ws://").replace("https://", "wss://")
comfyui_prompt = json.loads(COMFYUI_DEFAULT_PROMPT)
@@ -211,16 +217,16 @@ def comfyui_generate_image(
try:
ws = websocket.WebSocket()
ws.connect(f"ws://{host}/ws?clientId={client_id}")
print("WebSocket connection established.")
ws.connect(f"{ws_url}/ws?clientId={client_id}")
log.info("WebSocket connection established.")
except Exception as e:
print(f"Failed to connect to WebSocket server: {e}")
log.exception(f"Failed to connect to WebSocket server: {e}")
return None
try:
images = get_images(ws, comfyui_prompt, client_id, base_url)
except Exception as e:
print(f"Error while receiving images: {e}")
log.exception(f"Error while receiving images: {e}")
images = None
ws.close()
+336 -64
View File
@@ -1,100 +1,372 @@
import sys
from fastapi import FastAPI, Depends, HTTPException
from fastapi.routing import APIRoute
from fastapi.middleware.cors import CORSMiddleware
import logging
from litellm.proxy.proxy_server import ProxyConfig, initialize
from litellm.proxy.proxy_server import app
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 utils.utils import get_http_authorization_cred, get_current_user
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
import os
log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["LITELLM"])
from config import (
MODEL_FILTER_ENABLED,
ENABLE_LITELLM,
ENABLE_MODEL_FILTER,
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=["*"],
)
proxy_config = ProxyConfig()
LITELLM_CONFIG_DIR = f"{DATA_DIR}/litellm/config.yaml"
with open(LITELLM_CONFIG_DIR, "r") as file:
litellm_config = yaml.safe_load(file)
async def config():
router, model_list, general_settings = await proxy_config.load_config(
router=None, config_file_path="./data/litellm/config.yaml"
)
app.state.ENABLE = ENABLE_LITELLM
app.state.CONFIG = litellm_config
await initialize(config="./data/litellm/config.yaml", telemetry=False)
# Global variable to store the subprocess reference
background_process = None
CONFLICT_ENV_VARS = [
# Uvicorn uses PORT, so LiteLLM might use it as well
"PORT",
# LiteLLM uses DATABASE_URL for Prisma connections
"DATABASE_URL",
]
async def startup():
await config()
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}")
# Filter environment variables known to conflict with litellm
env = {k: v for k, v in os.environ.items() if k not in CONFLICT_ENV_VARS}
# Execute the command and create a subprocess
process = await asyncio.create_subprocess_exec(
*command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, env=env
)
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 on_startup():
await 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.ENABLE_MODEL_FILTER = ENABLE_MODEL_FILTER
app.state.MODEL_FILTER_LIST = MODEL_FILTER_LIST
@app.middleware("http")
async def auth_middleware(request: Request, call_next):
auth_header = request.headers.get("Authorization", "")
request.state.user = None
@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)):
if app.state.ENABLE:
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.ENABLE_MODEL_FILTER:
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",
}
else:
return {
"data": [],
"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:
user = get_current_user(get_http_authorization_cred(auth_header))
log.debug(f"user: {user}")
request.state.user = user
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:
return JSONResponse(status_code=400, content={"detail": str(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}"
response = await call_next(request)
return response
class ModifyModelsResponseMiddleware(BaseHTTPMiddleware):
async def dispatch(
self, request: Request, call_next: RequestResponseEndpoint
) -> Response:
response = await call_next(request)
user = request.state.user
if "/models" in request.url.path:
if isinstance(response, StreamingResponse):
# Read the content of the streaming response
body = b""
async for chunk in response.body_iterator:
body += chunk
data = json.loads(body.decode("utf-8"))
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"],
)
)
# Modified Flag
data["modified"] = True
return JSONResponse(content=data)
return response
app.add_middleware(ModifyModelsResponseMiddleware)
raise HTTPException(
status_code=r.status_code if r else 500, detail=error_detail
)
+107 -14
View File
@@ -16,6 +16,7 @@ from fastapi.concurrency import run_in_threadpool
from pydantic import BaseModel, ConfigDict
import os
import re
import copy
import random
import requests
@@ -36,7 +37,7 @@ from utils.utils import decode_token, get_current_user, get_admin_user
from config import (
SRC_LOG_LEVELS,
OLLAMA_BASE_URLS,
MODEL_FILTER_ENABLED,
ENABLE_MODEL_FILTER,
MODEL_FILTER_LIST,
UPLOAD_DIR,
)
@@ -55,7 +56,7 @@ app.add_middleware(
)
app.state.MODEL_FILTER_ENABLED = MODEL_FILTER_ENABLED
app.state.ENABLE_MODEL_FILTER = ENABLE_MODEL_FILTER
app.state.MODEL_FILTER_LIST = MODEL_FILTER_LIST
app.state.OLLAMA_BASE_URLS = OLLAMA_BASE_URLS
@@ -81,6 +82,12 @@ async def check_url(request: Request, call_next):
return response
@app.head("/")
@app.get("/")
async def get_status():
return {"status": True}
@app.get("/urls")
async def get_ollama_api_urls(user=Depends(get_admin_user)):
return {"OLLAMA_BASE_URLS": app.state.OLLAMA_BASE_URLS}
@@ -162,7 +169,7 @@ async def get_ollama_tags(
if url_idx == None:
models = await get_all_models()
if app.state.MODEL_FILTER_ENABLED:
if app.state.ENABLE_MODEL_FILTER:
if user.role == "user":
models["models"] = list(
filter(
@@ -209,7 +216,10 @@ async def get_ollama_versions(url_idx: Optional[int] = None):
if len(responses) > 0:
lowest_version = min(
responses, key=lambda x: tuple(map(int, x["version"].split(".")))
responses,
key=lambda x: tuple(
map(int, re.sub(r"^v|-.*", "", x["version"]).split("."))
),
)
return {"version": lowest_version["version"]}
@@ -272,7 +282,7 @@ async def pull_model(
if request_id in REQUEST_POOL:
yield chunk
else:
print("User: canceled request")
log.warning("User: canceled request")
break
finally:
if hasattr(r, "close"):
@@ -605,8 +615,13 @@ async def generate_embeddings(
user=Depends(get_current_user),
):
if url_idx == None:
if form_data.model in app.state.MODELS:
url_idx = random.choice(app.state.MODELS[form_data.model]["urls"])
model = form_data.model
if ":" not in model:
model = f"{model}:latest"
if model in app.state.MODELS:
url_idx = random.choice(app.state.MODELS[model]["urls"])
else:
raise HTTPException(
status_code=400,
@@ -642,6 +657,60 @@ async def generate_embeddings(
)
def generate_ollama_embeddings(
form_data: GenerateEmbeddingsForm,
url_idx: Optional[int] = None,
):
log.info(f"generate_ollama_embeddings {form_data}")
if url_idx == None:
model = form_data.model
if ":" not in model:
model = f"{model}:latest"
if model in app.state.MODELS:
url_idx = random.choice(app.state.MODELS[model]["urls"])
else:
raise HTTPException(
status_code=400,
detail=ERROR_MESSAGES.MODEL_NOT_FOUND(form_data.model),
)
url = app.state.OLLAMA_BASE_URLS[url_idx]
log.info(f"url: {url}")
try:
r = requests.request(
method="POST",
url=f"{url}/api/embeddings",
data=form_data.model_dump_json(exclude_none=True).encode(),
)
r.raise_for_status()
data = r.json()
log.info(f"generate_ollama_embeddings {data}")
if "embedding" in data:
return data["embedding"]
else:
raise "Something went wrong :/"
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"Ollama: {res['error']}"
except:
error_detail = f"Ollama: {e}"
raise error_detail
class GenerateCompletionForm(BaseModel):
model: str
prompt: str
@@ -665,12 +734,17 @@ async def generate_completion(
):
if url_idx == None:
if form_data.model in app.state.MODELS:
url_idx = random.choice(app.state.MODELS[form_data.model]["urls"])
model = form_data.model
if ":" not in model:
model = f"{model}:latest"
if model in app.state.MODELS:
url_idx = random.choice(app.state.MODELS[model]["urls"])
else:
raise HTTPException(
status_code=400,
detail="error_detail",
detail=ERROR_MESSAGES.MODEL_NOT_FOUND(form_data.model),
)
url = app.state.OLLAMA_BASE_URLS[url_idx]
@@ -763,8 +837,13 @@ async def generate_chat_completion(
):
if url_idx == None:
if form_data.model in app.state.MODELS:
url_idx = random.choice(app.state.MODELS[form_data.model]["urls"])
model = form_data.model
if ":" not in model:
model = f"{model}:latest"
if model in app.state.MODELS:
url_idx = random.choice(app.state.MODELS[model]["urls"])
else:
raise HTTPException(
status_code=400,
@@ -867,8 +946,13 @@ async def generate_openai_chat_completion(
):
if url_idx == None:
if form_data.model in app.state.MODELS:
url_idx = random.choice(app.state.MODELS[form_data.model]["urls"])
model = form_data.model
if ":" not in model:
model = f"{model}:latest"
if model in app.state.MODELS:
url_idx = random.choice(app.state.MODELS[model]["urls"])
else:
raise HTTPException(
status_code=400,
@@ -1029,6 +1113,14 @@ async def download_model(
url_idx: Optional[int] = None,
):
allowed_hosts = ["https://huggingface.co/", "https://github.com/"]
if not any(form_data.url.startswith(host) for host in allowed_hosts):
raise HTTPException(
status_code=400,
detail="Invalid file_url. Only URLs from allowed hosts are permitted.",
)
if url_idx == None:
url_idx = 0
url = app.state.OLLAMA_BASE_URLS[url_idx]
@@ -1037,6 +1129,7 @@ async def download_model(
if file_name:
file_path = f"{UPLOAD_DIR}/{file_name}"
return StreamingResponse(
download_file_stream(url, form_data.url, file_path, file_name),
)
+10 -6
View File
@@ -24,7 +24,7 @@ from config import (
OPENAI_API_BASE_URLS,
OPENAI_API_KEYS,
CACHE_DIR,
MODEL_FILTER_ENABLED,
ENABLE_MODEL_FILTER,
MODEL_FILTER_LIST,
)
from typing import List, Optional
@@ -45,7 +45,7 @@ app.add_middleware(
allow_headers=["*"],
)
app.state.MODEL_FILTER_ENABLED = MODEL_FILTER_ENABLED
app.state.ENABLE_MODEL_FILTER = ENABLE_MODEL_FILTER
app.state.MODEL_FILTER_LIST = MODEL_FILTER_LIST
app.state.OPENAI_API_BASE_URLS = OPENAI_API_BASE_URLS
@@ -80,6 +80,7 @@ async def get_openai_urls(user=Depends(get_admin_user)):
@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}
@@ -170,6 +171,7 @@ async def fetch_url(url, key):
def merge_models_lists(model_lists):
log.info(f"merge_models_lists {model_lists}")
merged_list = []
for idx, models in enumerate(model_lists):
@@ -198,14 +200,16 @@ async def get_all_models():
]
responses = await asyncio.gather(*tasks)
log.info(f"get_all_models:responses() {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,
)
@@ -224,7 +228,7 @@ 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.ENABLE_MODEL_FILTER:
if user.role == "user":
models["data"] = list(
filter(
@@ -341,7 +345,7 @@ async def proxy(path: str, request: Request, user=Depends(get_verified_user)):
try:
res = r.json()
if "error" in res:
error_detail = f"External: {res['error']}"
error_detail = f"External: {res['error']['message'] if 'message' in res['error'] else res['error']}"
except:
error_detail = f"External: {e}"
+329 -69
View File
@@ -8,13 +8,12 @@ from fastapi import (
Form,
)
from fastapi.middleware.cors import CORSMiddleware
import os, shutil, logging
import os, shutil, logging, re
from pathlib import Path
from typing import List
from sentence_transformers import SentenceTransformer
from chromadb.utils import embedding_functions
from chromadb.utils.batch_utils import create_batches
from langchain_community.document_loaders import (
WebBaseLoader,
@@ -29,15 +28,22 @@ from langchain_community.document_loaders import (
UnstructuredXMLLoader,
UnstructuredRSTLoader,
UnstructuredExcelLoader,
YoutubeLoader,
)
from langchain.text_splitter import RecursiveCharacterTextSplitter
import validators
import urllib.parse
import socket
from pydantic import BaseModel
from typing import Optional
import mimetypes
import uuid
import json
import sentence_transformers
from apps.web.models.documents import (
Documents,
@@ -45,7 +51,14 @@ from apps.web.models.documents import (
DocumentResponse,
)
from apps.rag.utils import query_doc, query_collection
from apps.rag.utils import (
get_model_path,
get_embedding_function,
query_doc,
query_doc_with_hybrid_search,
query_collection,
query_collection_with_hybrid_search,
)
from utils.misc import (
calculate_sha256,
@@ -54,16 +67,30 @@ from utils.misc import (
extract_folders_after_data_docs,
)
from utils.utils import get_current_user, get_admin_user
from config import (
SRC_LOG_LEVELS,
UPLOAD_DIR,
DOCS_DIR,
RAG_TOP_K,
RAG_RELEVANCE_THRESHOLD,
RAG_EMBEDDING_ENGINE,
RAG_EMBEDDING_MODEL,
RAG_EMBEDDING_MODEL_DEVICE_TYPE,
RAG_EMBEDDING_MODEL_AUTO_UPDATE,
RAG_EMBEDDING_MODEL_TRUST_REMOTE_CODE,
ENABLE_RAG_HYBRID_SEARCH,
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,
CHROMA_CLIENT,
CHUNK_SIZE,
CHUNK_OVERLAP,
RAG_TEMPLATE,
ENABLE_LOCAL_WEB_FETCH,
)
from constants import ERROR_MESSAGES
@@ -71,34 +98,77 @@ from constants import ERROR_MESSAGES
log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["RAG"])
#
# if RAG_EMBEDDING_MODEL:
# sentence_transformer_ef = SentenceTransformer(
# model_name_or_path=RAG_EMBEDDING_MODEL,
# cache_folder=RAG_EMBEDDING_MODEL_DIR,
# device=RAG_EMBEDDING_MODEL_DEVICE_TYPE,
# )
app = FastAPI()
app.state.PDF_EXTRACT_IMAGES = False
app.state.TOP_K = RAG_TOP_K
app.state.RELEVANCE_THRESHOLD = RAG_RELEVANCE_THRESHOLD
app.state.ENABLE_RAG_HYBRID_SEARCH = ENABLE_RAG_HYBRID_SEARCH
app.state.CHUNK_SIZE = CHUNK_SIZE
app.state.CHUNK_OVERLAP = CHUNK_OVERLAP
app.state.RAG_TEMPLATE = RAG_TEMPLATE
app.state.RAG_EMBEDDING_MODEL = RAG_EMBEDDING_MODEL
app.state.TOP_K = 4
app.state.sentence_transformer_ef = (
embedding_functions.SentenceTransformerEmbeddingFunction(
model_name=app.state.RAG_EMBEDDING_MODEL,
device=RAG_EMBEDDING_MODEL_DEVICE_TYPE,
)
app.state.RAG_EMBEDDING_ENGINE = RAG_EMBEDDING_ENGINE
app.state.RAG_EMBEDDING_MODEL = RAG_EMBEDDING_MODEL
app.state.RAG_RERANKING_MODEL = RAG_RERANKING_MODEL
app.state.RAG_TEMPLATE = RAG_TEMPLATE
app.state.OPENAI_API_BASE_URL = RAG_OPENAI_API_BASE_URL
app.state.OPENAI_API_KEY = RAG_OPENAI_API_KEY
app.state.PDF_EXTRACT_IMAGES = PDF_EXTRACT_IMAGES
def update_embedding_model(
embedding_model: str,
update_model: bool = False,
):
if embedding_model and app.state.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.RAG_EMBEDDING_MODEL,
RAG_EMBEDDING_MODEL_AUTO_UPDATE,
)
update_reranking_model(
app.state.RAG_RERANKING_MODEL,
RAG_RERANKING_MODEL_AUTO_UPDATE,
)
app.state.EMBEDDING_FUNCTION = get_embedding_function(
app.state.RAG_EMBEDDING_ENGINE,
app.state.RAG_EMBEDDING_MODEL,
app.state.sentence_transformer_ef,
app.state.OPENAI_API_KEY,
app.state.OPENAI_API_BASE_URL,
)
origins = ["*"]
app.add_middleware(
CORSMiddleware,
allow_origins=origins,
@@ -112,7 +182,7 @@ class CollectionNameForm(BaseModel):
collection_name: Optional[str] = "test"
class StoreWebForm(CollectionNameForm):
class UrlForm(CollectionNameForm):
url: str
@@ -123,38 +193,110 @@ async def get_status():
"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,
"reranking_model": app.state.RAG_RERANKING_MODEL,
}
@app.get("/embedding/model")
async def get_embedding_model(user=Depends(get_admin_user)):
@app.get("/embedding")
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,
"openai_config": {
"url": app.state.OPENAI_API_BASE_URL,
"key": app.state.OPENAI_API_KEY,
},
}
@app.get("/reranking")
async def get_reraanking_config(user=Depends(get_admin_user)):
return {"status": True, "reranking_model": app.state.RAG_RERANKING_MODEL}
class OpenAIConfigForm(BaseModel):
url: str
key: str
class EmbeddingModelUpdateForm(BaseModel):
openai_config: Optional[OpenAIConfigForm] = None
embedding_engine: str
embedding_model: str
@app.post("/embedding/model/update")
async def update_embedding_model(
@app.post("/embedding/update")
async def update_embedding_config(
form_data: EmbeddingModelUpdateForm, user=Depends(get_admin_user)
):
app.state.RAG_EMBEDDING_MODEL = form_data.embedding_model
app.state.sentence_transformer_ef = (
embedding_functions.SentenceTransformerEmbeddingFunction(
model_name=app.state.RAG_EMBEDDING_MODEL,
device=RAG_EMBEDDING_MODEL_DEVICE_TYPE,
)
log.info(
f"Updating embedding model: {app.state.RAG_EMBEDDING_MODEL} to {form_data.embedding_model}"
)
try:
app.state.RAG_EMBEDDING_ENGINE = form_data.embedding_engine
app.state.RAG_EMBEDDING_MODEL = form_data.embedding_model
return {
"status": True,
"embedding_model": app.state.RAG_EMBEDDING_MODEL,
}
if app.state.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
update_embedding_model(app.state.RAG_EMBEDDING_MODEL, True)
app.state.EMBEDDING_FUNCTION = get_embedding_function(
app.state.RAG_EMBEDDING_ENGINE,
app.state.RAG_EMBEDDING_MODEL,
app.state.sentence_transformer_ef,
app.state.OPENAI_API_KEY,
app.state.OPENAI_API_BASE_URL,
)
return {
"status": True,
"embedding_engine": app.state.RAG_EMBEDDING_ENGINE,
"embedding_model": app.state.RAG_EMBEDDING_MODEL,
"openai_config": {
"url": app.state.OPENAI_API_BASE_URL,
"key": app.state.OPENAI_API_KEY,
},
}
except Exception as e:
log.exception(f"Problem updating embedding model: {e}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=ERROR_MESSAGES.DEFAULT(e),
)
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.RAG_RERANKING_MODEL} to {form_data.reranking_model}"
)
try:
app.state.RAG_RERANKING_MODEL = form_data.reranking_model
update_reranking_model(app.state.RAG_RERANKING_MODEL, True)
return {
"status": True,
"reranking_model": app.state.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")
@@ -209,12 +351,16 @@ async def get_query_settings(user=Depends(get_admin_user)):
"status": True,
"template": app.state.RAG_TEMPLATE,
"k": app.state.TOP_K,
"r": app.state.RELEVANCE_THRESHOLD,
"hybrid": app.state.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")
@@ -223,13 +369,23 @@ async def update_query_settings(
):
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.RELEVANCE_THRESHOLD = form_data.r if form_data.r else 0.0
app.state.ENABLE_RAG_HYBRID_SEARCH = form_data.hybrid if form_data.hybrid else False
return {
"status": True,
"template": app.state.RAG_TEMPLATE,
"k": app.state.TOP_K,
"r": app.state.RELEVANCE_THRESHOLD,
"hybrid": app.state.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")
@@ -237,14 +393,23 @@ def query_doc_handler(
form_data: QueryDocForm,
user=Depends(get_current_user),
):
try:
return query_doc(
collection_name=form_data.collection_name,
query=form_data.query,
k=form_data.k if form_data.k else app.state.TOP_K,
embedding_function=app.state.sentence_transformer_ef,
)
if app.state.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.TOP_K,
reranking_function=app.state.sentence_transformer_rf,
r=form_data.r if form_data.r else app.state.RELEVANCE_THRESHOLD,
)
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.TOP_K,
)
except Exception as e:
log.exception(e)
raise HTTPException(
@@ -257,6 +422,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")
@@ -264,19 +431,36 @@ def query_collection_handler(
form_data: QueryCollectionsForm,
user=Depends(get_current_user),
):
return query_collection(
collection_names=form_data.collection_names,
query=form_data.query,
k=form_data.k if form_data.k else app.state.TOP_K,
embedding_function=app.state.sentence_transformer_ef,
)
@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"
try:
loader = WebBaseLoader(form_data.url)
if app.state.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.TOP_K,
reranking_function=app.state.sentence_transformer_rf,
r=form_data.r if form_data.r else app.state.RELEVANCE_THRESHOLD,
)
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.TOP_K,
)
except Exception as e:
log.exception(e)
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.DEFAULT(e),
)
@app.post("/youtube")
def store_youtube_video(form_data: UrlForm, user=Depends(get_current_user)):
try:
loader = YoutubeLoader.from_youtube_url(form_data.url, add_video_info=False)
data = loader.load()
collection_name = form_data.collection_name
@@ -297,6 +481,62 @@ 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)
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: str):
# Check if the URL is valid
if isinstance(validators.url(url), validators.ValidationError):
raise ValueError(ERROR_MESSAGES.INVALID_URL)
if not ENABLE_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 WebBaseLoader(url)
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 store_data_in_vector_db(data, collection_name, overwrite: bool = False) -> bool:
text_splitter = RecursiveCharacterTextSplitter(
@@ -304,9 +544,11 @@ def store_data_in_vector_db(data, collection_name, overwrite: bool = False) -> b
chunk_overlap=app.state.CHUNK_OVERLAP,
add_start_index=True,
)
docs = text_splitter.split_documents(data)
if len(docs) > 0:
log.info(f"store_data_in_vector_db {docs}")
return store_docs_in_vector_db(docs, collection_name, overwrite), None
else:
raise ValueError(ERROR_MESSAGES.EMPTY_CONTENT)
@@ -325,6 +567,7 @@ def store_text_in_vector_db(
def store_docs_in_vector_db(docs, collection_name, overwrite: bool = False) -> bool:
log.info(f"store_docs_in_vector_db {docs} {collection_name}")
texts = [doc.page_content for doc in docs]
metadatas = [doc.metadata for doc in docs]
@@ -333,20 +576,34 @@ def store_docs_in_vector_db(docs, collection_name, overwrite: bool = False) -> b
if overwrite:
for collection in CHROMA_CLIENT.list_collections():
if collection_name == collection.name:
print(f"deleting existing collection {collection_name}")
log.info(f"deleting existing collection {collection_name}")
CHROMA_CLIENT.delete_collection(name=collection_name)
collection = CHROMA_CLIENT.create_collection(
name=collection_name,
embedding_function=app.state.sentence_transformer_ef,
collection = CHROMA_CLIENT.create_collection(name=collection_name)
embedding_func = get_embedding_function(
app.state.RAG_EMBEDDING_ENGINE,
app.state.RAG_EMBEDDING_MODEL,
app.state.sentence_transformer_ef,
app.state.OPENAI_API_KEY,
app.state.OPENAI_API_BASE_URL,
)
collection.add(
documents=texts, metadatas=metadatas, ids=[str(uuid.uuid1()) for _ in texts]
)
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],
metadatas=metadatas,
embeddings=embeddings,
documents=texts,
):
collection.add(*batch)
return True
except Exception as e:
print(e)
log.exception(e)
if e.__class__.__name__ == "UniqueConstraintError":
return True
@@ -448,8 +705,11 @@ def store_doc(
log.info(f"file.content_type: {file.content_type}")
try:
filename = file.filename
unsanitized_filename = file.filename
filename = os.path.basename(unsanitized_filename)
file_path = f"{UPLOAD_DIR}/{filename}"
contents = file.file.read()
with open(file_path, "wb") as f:
f.write(contents)
@@ -460,7 +720,7 @@ def store_doc(
collection_name = calculate_sha256(f)[:63]
f.close()
loader, known_type = get_loader(file.filename, file.content_type, file_path)
loader, known_type = get_loader(filename, file.content_type, file_path)
data = loader.load()
try:
@@ -575,7 +835,7 @@ def scan_docs_dir(user=Depends(get_admin_user)):
),
)
except Exception as e:
print(e)
log.exception(e)
pass
except Exception as e:
+393 -64
View File
@@ -1,97 +1,189 @@
import re
import os
import logging
import requests
from typing import List
from apps.ollama.main import (
generate_ollama_embeddings,
GenerateEmbeddingsForm,
)
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
log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["RAG"])
def query_doc(collection_name: str, query: str, k: int, embedding_function):
def query_doc(
collection_name: str,
query: str,
embedding_function,
k: int,
):
try:
# if you use docker use the model from the environment variable
collection = CHROMA_CLIENT.get_collection(
name=collection_name,
embedding_function=embedding_function,
)
collection = CHROMA_CLIENT.get_collection(name=collection_name)
query_embeddings = embedding_function(query)
result = collection.query(
query_texts=[query],
query_embeddings=[query_embeddings],
n_results=k,
)
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_collection(
collection_names: List[str], query: str, k: int, embedding_function
collection_names: List[str],
query: str,
embedding_function,
k: int,
):
results = []
for collection_name in collection_names:
try:
# if you use docker use the model from the environment variable
collection = CHROMA_CLIENT.get_collection(
name=collection_name,
result = query_doc(
collection_name=collection_name,
query=query,
k=k,
embedding_function=embedding_function,
)
result = collection.query(
query_texts=[query],
n_results=k,
)
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):
@@ -100,8 +192,53 @@ def rag_template(template: str, context: str, query: str):
return template
def rag_messages(docs, messages, template, k, embedding_function):
log.debug(f"docs: {docs}")
def get_embedding_function(
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):
@@ -128,38 +265,69 @@ def rag_messages(docs, messages, template, k, embedding_function):
content_type = None
query = ""
extracted_collections = []
relevant_contexts = []
for doc in docs:
context = None
collection = doc.get("collection_name")
if collection:
collection = [collection]
else:
collection = doc.get("collection_names", [])
collection = set(collection).difference(extracted_collections)
if not collection:
log.debug(f"skipping {doc} as it has already been extracted")
continue
try:
if doc["type"] == "collection":
context = query_collection(
collection_names=doc["collection_names"],
query=query,
k=k,
embedding_function=embedding_function,
)
elif doc["type"] == "text":
if doc["type"] == "text":
context = doc["content"]
else:
context = query_doc(
collection_name=doc["collection_name"],
query=query,
k=k,
embedding_function=embedding_function,
)
if hybrid_search:
context = query_collection_with_hybrid_search(
collection_names=(
doc["collection_names"]
if doc["type"] == "collection"
else [doc["collection_name"]]
),
query=query,
embedding_function=embedding_function,
k=k,
reranking_function=reranking_function,
r=r,
)
else:
context = query_collection(
collection_names=(
doc["collection_names"]
if doc["type"] == "collection"
else [doc["collection_name"]]
),
query=query,
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)
extracted_collections.extend(collection)
context_string = ""
for context in relevant_contexts:
if context:
context_string += " ".join(context["documents"][0]) + "\n"
try:
if "documents" in context:
items = [item for item in context["documents"][0] if item is not None]
context_string += "\n\n".join(items)
except Exception as e:
log.exception(e)
context_string = context_string.strip()
ra_content = rag_template(
template=template,
@@ -167,6 +335,8 @@ def rag_messages(docs, messages, template, k, embedding_function):
query=query,
)
log.debug(f"ra_content: {ra_content}")
if content_type == "list":
new_content = []
for content_item in user_message["content"]:
@@ -186,3 +356,162 @@ def rag_messages(docs, messages, template, k, embedding_function):
messages[last_user_message_idx] = new_user_message
return messages
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(
model: str, text: str, key: str, url: str = "https://api.openai.com/v1"
):
try:
r = requests.post(
f"{url}/embeddings",
headers={
"Content-Type": "application/json",
"Authorization": f"Bearer {key}",
},
json={"input": text, "model": model},
)
r.raise_for_status()
data = r.json()
if "data" in data:
return data["data"][0]["embedding"]
else:
raise "Something went wrong :/"
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
+9 -5
View File
@@ -1,5 +1,7 @@
from peewee import *
from config import SRC_LOG_LEVELS, DATA_DIR
from peewee_migrate import Router
from playhouse.db_url import connect
from config import SRC_LOG_LEVELS, DATA_DIR, DATABASE_URL
import os
import logging
@@ -10,10 +12,12 @@ log.setLevel(SRC_LOG_LEVELS["DB"])
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.")
log.info("Database migrated from Ollama-WebUI successfully.")
else:
pass
DB = SqliteDatabase(f"{DATA_DIR}/webui.db")
DB.connect()
DB = connect(DATABASE_URL)
log.info(f"Connected to a {DB.__class__.__name__} database.")
router = Router(DB, migrate_dir="apps/web/internal/migrations", logger=log)
router.run()
DB.connect(reuse_if_open=True)
@@ -0,0 +1,254 @@
"""Peewee migrations -- 001_initial_schema.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."""
# 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)
email = pw.CharField(max_length=255)
password = pw.CharField(max_length=255)
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.CharField()
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.CharField()
filename = pw.CharField()
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.CharField()
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.CharField(max_length=255)
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"
def rollback(migrator: Migrator, database: pw.Database, *, fake=False):
"""Write your rollback migrations here."""
migrator.remove_model("user")
migrator.remove_model("tag")
migrator.remove_model("prompt")
migrator.remove_model("modelfile")
migrator.remove_model("document")
migrator.remove_model("chatidtag")
migrator.remove_model("chat")
migrator.remove_model("auth")
@@ -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."""
migrator.add_fields(
"chat", share_id=pw.CharField(max_length=255, null=True, unique=True)
)
def rollback(migrator: Migrator, database: pw.Database, *, fake=False):
"""Write your rollback migrations here."""
migrator.remove_fields("chat", "share_id")
@@ -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."""
migrator.add_fields(
"user", api_key=pw.CharField(max_length=255, null=True, unique=True)
)
def rollback(migrator: Migrator, database: pw.Database, *, fake=False):
"""Write your rollback migrations here."""
migrator.remove_fields("user", "api_key")
@@ -0,0 +1,46 @@
"""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."""
migrator.add_fields("chat", archived=pw.BooleanField(default=False))
def rollback(migrator: Migrator, database: pw.Database, *, fake=False):
"""Write your rollback migrations here."""
migrator.remove_fields("chat", "archived")
@@ -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))
@@ -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(),
)
@@ -0,0 +1,79 @@
"""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 created_at and updated_at to the 'user' table
migrator.add_fields(
"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 "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("user", "timestamp")
# Update the fields to be not null now that they are populated
migrator.change_fields(
"user",
created_at=pw.BigIntegerField(null=False),
updated_at=pw.BigIntegerField(null=False),
last_active_at=pw.BigIntegerField(null=False),
)
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("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 "user" SET timestamp = created_at')
# Remove the created_at and updated_at fields
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("user", timestamp=pw.BigIntegerField(null=False))
@@ -0,0 +1,21 @@
# Database Migrations
This directory contains all the database migrations for the web app.
Migrations are done using the [`peewee-migrate`](https://github.com/klen/peewee_migrate) library.
Migrations are automatically ran at app startup.
## Creating a migration
Have you made a change to the schema of an existing model?
You will need to create a migration file to ensure that existing databases are updated for backwards compatibility.
1. Have a database file (`webui.db`) that has the old schema prior to any of your changes.
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}
```
- `$SQLITE_DB` should be the path to the database file.
- `$MIGRATION_NAME` should be a descriptive name for the migration.
4. The migration file will be created in the `apps/web/internal/migrations` directory.
+2 -1
View File
@@ -20,6 +20,7 @@ from config import (
ENABLE_SIGNUP,
USER_PERMISSIONS,
WEBHOOK_URL,
WEBUI_AUTH_TRUSTED_EMAIL_HEADER,
)
app = FastAPI()
@@ -34,7 +35,7 @@ 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.AUTH_TRUSTED_EMAIL_HEADER = WEBUI_AUTH_TRUSTED_EMAIL_HEADER
app.add_middleware(
CORSMiddleware,
+39 -3
View File
@@ -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:
@@ -47,6 +47,10 @@ class Token(BaseModel):
token_type: str
class ApiKey(BaseModel):
api_key: Optional[str] = None
class UserResponse(BaseModel):
id: str
email: str
@@ -82,6 +86,11 @@ class SignupForm(BaseModel):
name: str
email: str
password: str
profile_image_url: Optional[str] = "/user.png"
class AddUserForm(SignupForm):
role: Optional[str] = "pending"
class AuthsTable:
@@ -90,7 +99,12 @@ class AuthsTable:
self.db.create_tables([Auth])
def insert_new_auth(
self, email: str, password: str, name: str, role: str = "pending"
self,
email: str,
password: str,
name: str,
profile_image_url: str = "/user.png",
role: str = "pending",
) -> Optional[UserModel]:
log.info("insert_new_auth")
@@ -101,7 +115,7 @@ class AuthsTable:
)
result = Auth.create(**auth.model_dump())
user = Users.insert_new_user(id, name, email, role)
user = Users.insert_new_user(id, name, email, profile_image_url, role)
if result and user:
return user
@@ -123,6 +137,28 @@ class AuthsTable:
except:
return None
def authenticate_user_by_api_key(self, api_key: str) -> Optional[UserModel]:
log.info(f"authenticate_user_by_api_key: {api_key}")
# if no api_key, return None
if not api_key:
return None
try:
user = Users.get_user_by_api_key(api_key)
return user if user else None
except:
return False
def authenticate_user_by_trusted_header(self, email: str) -> Optional[UserModel]:
log.info(f"authenticate_user_by_trusted_header: {email}")
try:
auth = Auth.get(Auth.email == email, Auth.active == True)
if auth:
user = Users.get_user_by_id(auth.id)
return user
except:
return None
def update_user_password_by_id(self, id: str, new_password: str) -> bool:
try:
query = Auth.update(password=new_password).where(Auth.id == id)
+176 -24
View File
@@ -17,9 +17,14 @@ 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
timestamp = DateField()
created_at = BigIntegerField()
updated_at = BigIntegerField()
share_id = CharField(null=True, unique=True)
archived = BooleanField(default=False)
class Meta:
database = DB
@@ -30,7 +35,12 @@ class ChatModel(BaseModel):
user_id: str
title: str
chat: str
timestamp: int # timestamp in epoch
created_at: int # timestamp in epoch
updated_at: int # timestamp in epoch
share_id: Optional[str] = None
archived: bool = False
####################
@@ -51,12 +61,17 @@ class ChatResponse(BaseModel):
user_id: str
title: str
chat: dict
timestamp: int # timestamp in epoch
updated_at: int # timestamp in epoch
created_at: int # timestamp in epoch
share_id: Optional[str] = None # id of the chat to be shared
archived: bool
class ChatTitleIdResponse(BaseModel):
id: str
title: str
updated_at: int
created_at: int
class ChatTable:
@@ -74,7 +89,8 @@ class ChatTable:
form_data.chat["title"] if "title" in form_data.chat else "New Chat"
),
"chat": json.dumps(form_data.chat),
"timestamp": int(time.time()),
"created_at": int(time.time()),
"updated_at": int(time.time()),
}
)
@@ -86,7 +102,7 @@ class ChatTable:
query = Chat.update(
chat=json.dumps(chat),
title=chat["title"] if "title" in chat else "New Chat",
timestamp=int(time.time()),
updated_at=int(time.time()),
).where(Chat.id == id)
query.execute()
@@ -95,41 +111,141 @@ class ChatTable:
except:
return None
def get_chat_lists_by_user_id(
def insert_shared_chat_by_chat_id(self, chat_id: str) -> Optional[ChatModel]:
# Get the existing chat to share
chat = Chat.get(Chat.id == chat_id)
# Check if the chat is already shared
if chat.share_id:
return self.get_chat_by_id_and_user_id(chat.share_id, "shared")
# Create a new chat with the same data, but with a new ID
shared_chat = ChatModel(
**{
"id": str(uuid.uuid4()),
"user_id": f"shared-{chat_id}",
"title": chat.title,
"chat": chat.chat,
"created_at": chat.created_at,
"updated_at": int(time.time()),
}
)
shared_result = Chat.create(**shared_chat.model_dump())
# Update the original chat with the share_id
result = (
Chat.update(share_id=shared_chat.id).where(Chat.id == chat_id).execute()
)
return shared_chat if (shared_result and result) else None
def update_shared_chat_by_chat_id(self, chat_id: str) -> Optional[ChatModel]:
try:
print("update_shared_chat_by_id")
chat = Chat.get(Chat.id == chat_id)
print(chat)
query = Chat.update(
title=chat.title,
chat=chat.chat,
).where(Chat.id == chat.share_id)
query.execute()
chat = Chat.get(Chat.id == chat.share_id)
return ChatModel(**model_to_dict(chat))
except:
return None
def delete_shared_chat_by_chat_id(self, chat_id: str) -> bool:
try:
query = Chat.delete().where(Chat.user_id == f"shared-{chat_id}")
query.execute() # Remove the rows, return number of rows removed.
return True
except:
return False
def update_chat_share_id_by_id(
self, id: str, share_id: Optional[str]
) -> Optional[ChatModel]:
try:
query = Chat.update(
share_id=share_id,
).where(Chat.id == id)
query.execute()
chat = Chat.get(Chat.id == id)
return ChatModel(**model_to_dict(chat))
except:
return None
def toggle_chat_archive_by_id(self, id: str) -> Optional[ChatModel]:
try:
chat = self.get_chat_by_id(id)
query = Chat.update(
archived=(not chat.archived),
).where(Chat.id == id)
query.execute()
chat = Chat.get(Chat.id == id)
return ChatModel(**model_to_dict(chat))
except:
return None
def get_archived_chat_list_by_user_id(
self, user_id: str, skip: int = 0, limit: int = 50
) -> List[ChatModel]:
return [
ChatModel(**model_to_dict(chat))
for chat in Chat.select()
.where(Chat.archived == True)
.where(Chat.user_id == user_id)
.order_by(Chat.timestamp.desc())
.order_by(Chat.updated_at.desc())
# .limit(limit)
# .offset(skip)
]
def get_chat_lists_by_chat_ids(
def get_chat_list_by_user_id(
self, user_id: str, 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)
]
def get_chat_list_by_chat_ids(
self, chat_ids: List[str], 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.id.in_(chat_ids))
.order_by(Chat.timestamp.desc())
.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.timestamp.desc())
]
def get_chat_by_id(self, id: str) -> Optional[ChatModel]:
try:
chat = Chat.get(Chat.id == id)
return ChatModel(**model_to_dict(chat))
except:
return None
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.timestamp.desc())
]
def get_chat_by_share_id(self, id: str) -> Optional[ChatModel]:
try:
chat = Chat.get(Chat.share_id == id)
if chat:
chat = Chat.get(Chat.id == id)
return ChatModel(**model_to_dict(chat))
else:
return None
except:
return None
def get_chat_by_id_and_user_id(self, id: str, user_id: str) -> Optional[ChatModel]:
try:
@@ -141,20 +257,42 @@ 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))
query.execute() # Remove the rows, return number of rows removed.
return True
return True and self.delete_shared_chat_by_chat_id(id)
except:
return False
def delete_chats_by_user_id(self, user_id: str) -> bool:
try:
self.delete_shared_chats_by_user_id(user_id)
query = Chat.delete().where(Chat.user_id == user_id)
query.execute() # Remove the rows, return number of rows removed.
@@ -162,5 +300,19 @@ class ChatTable:
except:
return False
def delete_shared_chats_by_user_id(self, user_id: str) -> bool:
try:
shared_chat_ids = [
f"shared-{chat.id}"
for chat in Chat.select().where(Chat.user_id == user_id)
]
query = Chat.delete().where(Chat.user_id << shared_chat_ids)
query.execute() # Remove the rows, return number of rows removed.
return True
except:
return False
Chats = ChatTable(DB)
+3 -3
View File
@@ -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
+1 -1
View File
@@ -20,7 +20,7 @@ class Modelfile(Model):
tag_name = CharField(unique=True)
user_id = CharField()
modelfile = TextField()
timestamp = DateField()
timestamp = BigIntegerField()
class Meta:
database = DB
+2 -2
View File
@@ -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
+7 -3
View File
@@ -35,7 +35,7 @@ class ChatIdTag(Model):
tag_name = CharField()
chat_id = CharField()
user_id = CharField()
timestamp = DateField()
timestamp = BigIntegerField()
class Meta:
database = DB
@@ -136,7 +136,9 @@ class TagTable:
return [
TagModel(**model_to_dict(tag))
for tag in Tag.select().where(Tag.name.in_(tag_names))
for tag in Tag.select()
.where(Tag.user_id == user_id)
.where(Tag.name.in_(tag_names))
]
def get_tags_by_chat_id_and_user_id(
@@ -151,7 +153,9 @@ class TagTable:
return [
TagModel(**model_to_dict(tag))
for tag in Tag.select().where(Tag.name.in_(tag_names))
for tag in Tag.select()
.where(Tag.user_id == user_id)
.where(Tag.name.in_(tag_names))
]
def get_chat_ids_by_tag_name_and_user_id(
+64 -7
View File
@@ -18,8 +18,13 @@ 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)
class Meta:
database = DB
@@ -30,8 +35,13 @@ class UserModel(BaseModel):
name: str
email: str
role: str = "pending"
profile_image_url: str = "/user.png"
timestamp: int # timestamp in epoch
profile_image_url: str
last_active_at: int # timestamp in epoch
updated_at: int # timestamp in epoch
created_at: int # timestamp in epoch
api_key: Optional[str] = None
####################
@@ -57,7 +67,12 @@ class UsersTable:
self.db.create_tables([User])
def insert_new_user(
self, id: str, name: str, email: str, role: str = "pending"
self,
id: str,
name: str,
email: str,
profile_image_url: str = "/user.png",
role: str = "pending",
) -> Optional[UserModel]:
user = UserModel(
**{
@@ -65,8 +80,10 @@ class UsersTable:
"name": name,
"email": email,
"role": role,
"profile_image_url": "/user.png",
"timestamp": int(time.time()),
"profile_image_url": profile_image_url,
"last_active_at": int(time.time()),
"created_at": int(time.time()),
"updated_at": int(time.time()),
}
)
result = User.create(**user.model_dump())
@@ -82,6 +99,13 @@ class UsersTable:
except:
return None
def get_user_by_api_key(self, api_key: str) -> Optional[UserModel]:
try:
user = User.get(User.api_key == api_key)
return UserModel(**model_to_dict(user))
except:
return None
def get_user_by_email(self, email: str) -> Optional[UserModel]:
try:
user = User.get(User.email == email)
@@ -99,6 +123,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)
@@ -123,6 +154,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)
@@ -149,5 +190,21 @@ class UsersTable:
except:
return False
def update_user_api_key_by_id(self, id: str, api_key: str) -> str:
try:
query = User.update(api_key=api_key).where(User.id == id)
result = query.execute()
return True if result == 1 else False
except:
return False
def get_user_api_key_by_id(self, id: str) -> Optional[str]:
try:
user = User.get(User.id == id)
return user.api_key
except:
return None
Users = UsersTable(DB)
+117 -9
View File
@@ -1,22 +1,25 @@
from fastapi import Response, Request
from fastapi import Depends, FastAPI, HTTPException, status
from datetime import datetime, timedelta
from typing import List, Union
import logging
from fastapi import APIRouter, status
from fastapi import Request, UploadFile, File
from fastapi import Depends, HTTPException, status
from fastapi import APIRouter
from pydantic import BaseModel
import time
import uuid
import re
import uuid
import csv
from apps.web.models.auths import (
SigninForm,
SignupForm,
AddUserForm,
UpdateProfileForm,
UpdatePasswordForm,
UserResponse,
SigninResponse,
Auths,
ApiKey,
)
from apps.web.models.users import Users
@@ -25,10 +28,12 @@ from utils.utils import (
get_current_user,
get_admin_user,
create_token,
create_api_key,
)
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
router = APIRouter()
@@ -79,6 +84,8 @@ async def update_profile(
async def update_password(
form_data: UpdatePasswordForm, session_user=Depends(get_current_user)
):
if WEBUI_AUTH_TRUSTED_EMAIL_HEADER:
raise HTTPException(400, detail=ERROR_MESSAGES.ACTION_PROHIBITED)
if session_user:
user = Auths.authenticate_user(session_user.email, form_data.password)
@@ -98,7 +105,22 @@ async def update_password(
@router.post("/signin", response_model=SigninResponse)
async def signin(request: Request, form_data: SigninForm):
user = Auths.authenticate_user(form_data.email.lower(), form_data.password)
if WEBUI_AUTH_TRUSTED_EMAIL_HEADER:
if WEBUI_AUTH_TRUSTED_EMAIL_HEADER not in request.headers:
raise HTTPException(400, detail=ERROR_MESSAGES.INVALID_TRUSTED_HEADER)
trusted_email = request.headers[WEBUI_AUTH_TRUSTED_EMAIL_HEADER].lower()
if not Users.get_user_by_email(trusted_email.lower()):
await signup(
request,
SignupForm(
email=trusted_email, password=str(uuid.uuid4()), name=trusted_email
),
)
user = Auths.authenticate_user_by_trusted_header(trusted_email)
else:
user = Auths.authenticate_user(form_data.email.lower(), form_data.password)
if user:
token = create_token(
data={"id": user.id},
@@ -146,7 +168,11 @@ async def signup(request: Request, form_data: SignupForm):
)
hashed = get_password_hash(form_data.password)
user = Auths.insert_new_auth(
form_data.email.lower(), hashed, form_data.name, role
form_data.email.lower(),
hashed,
form_data.name,
form_data.profile_image_url,
role,
)
if user:
@@ -182,6 +208,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
############################
@@ -249,3 +320,40 @@ async def update_token_expires_duration(
return request.app.state.JWT_EXPIRES_IN
else:
return request.app.state.JWT_EXPIRES_IN
############################
# API Key
############################
# create api key
@router.post("/api_key", response_model=ApiKey)
async def create_api_key_(user=Depends(get_current_user)):
api_key = create_api_key()
success = Users.update_user_api_key_by_id(user.id, api_key)
if success:
return {
"api_key": api_key,
}
else:
raise HTTPException(500, detail=ERROR_MESSAGES.CREATE_API_KEY_ERROR)
# delete api key
@router.delete("/api_key", response_model=bool)
async def delete_api_key(user=Depends(get_current_user)):
success = Users.update_user_api_key_by_id(user.id, None)
return success
# get api key
@router.get("/api_key", response_model=ApiKey)
async def get_api_key(user=Depends(get_current_user)):
api_key = Users.get_user_api_key_by_id(user.id)
if api_key:
return {
"api_key": api_key,
}
else:
raise HTTPException(404, detail=ERROR_MESSAGES.API_KEY_NOT_FOUND)
+202 -74
View File
@@ -28,7 +28,7 @@ from apps.web.models.tags import (
from constants import ERROR_MESSAGES
from config import SRC_LOG_LEVELS
from config import SRC_LOG_LEVELS, ENABLE_ADMIN_EXPORT
log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["MODELS"])
@@ -36,27 +36,73 @@ 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)
############################
# GetAllChats
# 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
############################
# 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, skip, limit)
############################
# 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)
############################
# GetChats
############################
@router.get("/all", response_model=List[ChatResponse])
async def get_all_user_chats(user=Depends(get_current_user)):
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_all_chats_by_user_id(user.id)
for chat in Chats.get_chats_by_user_id(user.id)
]
@@ -67,9 +113,14 @@ async def get_all_user_chats(user=Depends(get_current_user)):
@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_all_chats()
for chat in Chats.get_chats()
]
@@ -90,45 +141,6 @@ async def create_new_chat(form_data: ChatForm, user=Depends(get_current_user)):
)
############################
# GetAllTags
############################
@router.get("/tags/all", response_model=List[TagModel])
async def get_all_tags(user=Depends(get_current_user)):
try:
tags = Tags.get_tags_by_user_id(user.id)
return tags
except Exception as e:
log.exception(e)
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT()
)
############################
# 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
############################
@@ -176,17 +188,154 @@ 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"]
):
if user.role == "admin":
result = Chats.delete_chat_by_id(id)
return result
else:
if not request.app.state.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
############################
# ArchiveChat
############################
@router.get("/{id}/archive", response_model=Optional[ChatResponse])
async def archive_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 = Chats.toggle_chat_archive_by_id(id)
return ChatResponse(**{**chat.model_dump(), "chat": json.loads(chat.chat)})
else:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED, detail=ERROR_MESSAGES.DEFAULT()
)
############################
# ShareChatById
############################
@router.post("/{id}/share", response_model=Optional[ChatResponse])
async def share_chat_by_id(id: str, user=Depends(get_current_user)):
chat = Chats.get_chat_by_id_and_user_id(id, user.id)
if chat:
if chat.share_id:
shared_chat = Chats.update_shared_chat_by_chat_id(chat.id)
return ChatResponse(
**{**shared_chat.model_dump(), "chat": json.loads(shared_chat.chat)}
)
shared_chat = Chats.insert_shared_chat_by_chat_id(chat.id)
if not shared_chat:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=ERROR_MESSAGES.DEFAULT(),
)
return ChatResponse(
**{**shared_chat.model_dump(), "chat": json.loads(shared_chat.chat)}
)
else:
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
############################
# DeletedSharedChatById
############################
@router.delete("/{id}/share", response_model=Optional[bool])
async def delete_shared_chat_by_id(id: str, user=Depends(get_current_user)):
chat = Chats.get_chat_by_id_and_user_id(id, user.id)
if chat:
if not chat.share_id:
return False
result = Chats.delete_shared_chat_by_chat_id(id)
update_result = Chats.update_chat_share_id_by_id(id, None)
return result and update_result != None
else:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
)
############################
# 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
)
############################
# GetAllTags
############################
@router.get("/tags/all", response_model=List[TagModel])
async def get_all_tags(user=Depends(get_current_user)):
try:
tags = Tags.get_tags_by_user_id(user.id)
return tags
except Exception as e:
log.exception(e)
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT()
)
############################
# GetChatsByTags
############################
@router.get("/tags/tag/{tag_name}", response_model=List[ChatTitleIdResponse])
async def get_user_chat_list_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_list_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
############################
@@ -269,24 +418,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
+1 -1
View File
@@ -58,7 +58,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(
+82 -11
View File
@@ -1,22 +1,20 @@
from fastapi import APIRouter, UploadFile, File, BackgroundTasks
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
import requests
import os
import aiohttp
import json
from fpdf import FPDF
import markdown
from apps.web.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
router = APIRouter()
@@ -28,11 +26,84 @@ async def get_gravatar(
return get_gravatar_url(email)
class MarkdownForm(BaseModel):
md: str
@router.post("/markdown")
async def get_html_from_markdown(
form_data: MarkdownForm,
):
return {"html": markdown.markdown(form_data.md)}
class ChatForm(BaseModel):
title: str
messages: List[dict]
@router.post("/pdf")
async def download_chat_as_pdf(
form_data: ChatForm,
):
pdf = FPDF()
pdf.add_page()
STATIC_DIR = "./static"
FONTS_DIR = f"{STATIC_DIR}/fonts"
pdf.add_font("NotoSans", "", f"{FONTS_DIR}/NotoSans-Regular.ttf")
pdf.add_font("NotoSans", "b", f"{FONTS_DIR}/NotoSans-Bold.ttf")
pdf.add_font("NotoSans", "i", f"{FONTS_DIR}/NotoSans-Italic.ttf")
pdf.add_font("NotoSansKR", "", f"{FONTS_DIR}/NotoSansKR-Regular.ttf")
pdf.add_font("NotoSansJP", "", f"{FONTS_DIR}/NotoSansJP-Regular.ttf")
pdf.set_font("NotoSans", size=12)
pdf.set_fallback_fonts(["NotoSansKR", "NotoSansJP"])
pdf.set_auto_page_break(auto=True, margin=15)
# Adjust the effective page width for multi_cell
effective_page_width = (
pdf.w - 2 * pdf.l_margin - 10
) # Subtracted an additional 10 for extra padding
# Add chat messages
for message in form_data.messages:
role = message["role"]
content = message["content"]
pdf.set_font("NotoSans", "B", size=14) # Bold for the role
pdf.multi_cell(effective_page_width, 10, f"{role.upper()}", 0, "L")
pdf.ln(1) # Extra space between messages
pdf.set_font("NotoSans", size=10) # Regular for content
pdf.multi_cell(effective_page_width, 6, content, 0, "L")
pdf.ln(1.5) # Extra space between messages
# Save the pdf with name .pdf
pdf_bytes = pdf.output()
return Response(
content=bytes(pdf_bytes),
media_type="application/pdf",
headers={"Content-Disposition": f"attachment;filename=chat.pdf"},
)
@router.get("/db/download")
async def download_db(user=Depends(get_admin_user)):
if not ENABLE_ADMIN_EXPORT:
raise HTTPException(
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",
)
+241 -70
View File
@@ -18,6 +18,51 @@ from secrets import token_bytes
from constants import ERROR_MESSAGES
####################################
# LOGGING
####################################
log_levels = ["CRITICAL", "ERROR", "WARNING", "INFO", "DEBUG"]
GLOBAL_LOG_LEVEL = os.environ.get("GLOBAL_LOG_LEVEL", "").upper()
if GLOBAL_LOG_LEVEL in log_levels:
logging.basicConfig(stream=sys.stdout, level=GLOBAL_LOG_LEVEL, force=True)
else:
GLOBAL_LOG_LEVEL = "INFO"
log = logging.getLogger(__name__)
log.info(f"GLOBAL_LOG_LEVEL: {GLOBAL_LOG_LEVEL}")
log_sources = [
"AUDIO",
"COMFYUI",
"CONFIG",
"DB",
"IMAGES",
"LITELLM",
"MAIN",
"MODELS",
"OLLAMA",
"OPENAI",
"RAG",
"WEBHOOK",
]
SRC_LOG_LEVELS = {}
for source in log_sources:
log_env_var = source + "_LOG_LEVEL"
SRC_LOG_LEVELS[source] = os.environ.get(log_env_var, "").upper()
if SRC_LOG_LEVELS[source] not in log_levels:
SRC_LOG_LEVELS[source] = GLOBAL_LOG_LEVEL
log.info(f"{log_env_var}: {SRC_LOG_LEVELS[source]}")
log.setLevel(SRC_LOG_LEVELS["CONFIG"])
####################################
# Load .env file
####################################
try:
from dotenv import load_dotenv, find_dotenv
@@ -25,9 +70,11 @@ try:
except ImportError:
log.warning("dotenv not installed, skipping...")
WEBUI_NAME = "Open WebUI"
WEBUI_NAME = os.environ.get("WEBUI_NAME", "Open WebUI")
if WEBUI_NAME != "Open WebUI":
WEBUI_NAME += " (Open WebUI)"
WEBUI_FAVICON_URL = "https://openwebui.com/favicon.png"
shutil.copyfile("../build/favicon.png", "./static/favicon.png")
####################################
# ENV (dev,test,prod)
@@ -102,51 +149,37 @@ for version in soup.find_all("h2"):
CHANGELOG = changelog_json
####################################
# 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")))
try:
with open(f"{DATA_DIR}/config.json", "r") as f:
CONFIG_DATA = json.load(f)
except:
CONFIG_DATA = {}
####################################
# LOGGING
# Static DIR
####################################
log_levels = ["CRITICAL", "ERROR", "WARNING", "INFO", "DEBUG"]
GLOBAL_LOG_LEVEL = os.environ.get("GLOBAL_LOG_LEVEL", "").upper()
if GLOBAL_LOG_LEVEL in log_levels:
logging.basicConfig(stream=sys.stdout, level=GLOBAL_LOG_LEVEL, force=True)
STATIC_DIR = str(Path(os.getenv("STATIC_DIR", "./static")).resolve())
frontend_favicon = f"{FRONTEND_BUILD_DIR}/favicon.png"
if os.path.exists(frontend_favicon):
shutil.copyfile(frontend_favicon, f"{STATIC_DIR}/favicon.png")
else:
GLOBAL_LOG_LEVEL = "INFO"
log = logging.getLogger(__name__)
log.info(f"GLOBAL_LOG_LEVEL: {GLOBAL_LOG_LEVEL}")
log_sources = [
"AUDIO",
"CONFIG",
"DB",
"IMAGES",
"LITELLM",
"MAIN",
"MODELS",
"OLLAMA",
"OPENAI",
"RAG",
]
SRC_LOG_LEVELS = {}
for source in log_sources:
log_env_var = source + "_LOG_LEVEL"
SRC_LOG_LEVELS[source] = os.environ.get(log_env_var, "").upper()
if SRC_LOG_LEVELS[source] not in log_levels:
SRC_LOG_LEVELS[source] = GLOBAL_LOG_LEVEL
log.info(f"{log_env_var}: {SRC_LOG_LEVELS[source]}")
log.setLevel(SRC_LOG_LEVELS["CONFIG"])
logging.warning(f"Frontend favicon not found at {frontend_favicon}")
####################################
# CUSTOM_NAME
####################################
CUSTOM_NAME = os.environ.get("CUSTOM_NAME", "")
if CUSTOM_NAME:
try:
r = requests.get(f"https://api.openwebui.com/api/v1/custom/{CUSTOM_NAME}")
@@ -161,7 +194,7 @@ if CUSTOM_NAME:
r = requests.get(url, stream=True)
if r.status_code == 200:
with open("./static/favicon.png", "wb") as f:
with open(f"{STATIC_DIR}/favicon.png", "wb") as f:
r.raw.decode_content = True
shutil.copyfileobj(r.raw, f)
@@ -171,19 +204,6 @@ if CUSTOM_NAME:
pass
####################################
# 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")))
try:
with open(f"{DATA_DIR}/config.json", "r") as f:
CONFIG_DATA = json.load(f)
except:
CONFIG_DATA = {}
####################################
# File Upload DIR
####################################
@@ -204,7 +224,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,6 +271,7 @@ OLLAMA_API_BASE_URL = os.environ.get(
OLLAMA_BASE_URL = os.environ.get("OLLAMA_BASE_URL", "")
K8S_FLAG = os.environ.get("K8S_FLAG", "")
USE_OLLAMA_DOCKER = os.environ.get("USE_OLLAMA_DOCKER", "false")
if OLLAMA_BASE_URL == "" and OLLAMA_API_BASE_URL != "":
OLLAMA_BASE_URL = (
@@ -260,9 +281,13 @@ if OLLAMA_BASE_URL == "" and OLLAMA_API_BASE_URL != "":
)
if ENV == "prod":
if OLLAMA_BASE_URL == "/ollama":
OLLAMA_BASE_URL = "http://host.docker.internal:11434"
if OLLAMA_BASE_URL == "/ollama" and not K8S_FLAG:
if USE_OLLAMA_DOCKER.lower() == "true":
# if you use all-in-one docker container (Open WebUI + Ollama)
# with the docker build arg USE_OLLAMA=true (--build-arg="USE_OLLAMA=true") this only works with http://localhost:11434
OLLAMA_BASE_URL = "http://localhost:11434"
else:
OLLAMA_BASE_URL = "http://host.docker.internal:11434"
elif K8S_FLAG:
OLLAMA_BASE_URL = "http://ollama-service.open-webui.svc.cluster.local:11434"
@@ -300,6 +325,18 @@ OPENAI_API_BASE_URLS = [
for url in OPENAI_API_BASE_URLS.split(";")
]
OPENAI_API_KEY = ""
try:
OPENAI_API_KEY = OPENAI_API_KEYS[
OPENAI_API_BASE_URLS.index("https://api.openai.com/v1")
]
except:
pass
OPENAI_API_BASE_URL = "https://api.openai.com/v1"
####################################
# WEBUI
####################################
@@ -330,6 +367,17 @@ 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?",
},
]
)
@@ -342,13 +390,14 @@ USER_PERMISSIONS_CHAT_DELETION = (
USER_PERMISSIONS = {"chat": {"deletion": USER_PERMISSIONS_CHAT_DELETION}}
MODEL_FILTER_ENABLED = os.environ.get("MODEL_FILTER_ENABLED", "False").lower() == "true"
ENABLE_MODEL_FILTER = 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(";")]
WEBHOOK_URL = os.environ.get("WEBHOOK_URL", "")
ENABLE_ADMIN_EXPORT = os.environ.get("ENABLE_ADMIN_EXPORT", "True").lower() == "true"
####################################
# WEBUI_VERSION
####################################
@@ -360,6 +409,9 @@ WEBUI_VERSION = os.environ.get("WEBUI_VERSION", "v1.0.0-alpha.100")
####################################
WEBUI_AUTH = True
WEBUI_AUTH_TRUSTED_EMAIL_HEADER = os.environ.get(
"WEBUI_AUTH_TRUSTED_EMAIL_HEADER", None
)
####################################
# WEBUI_SECRET_KEY
@@ -380,21 +432,87 @@ if WEBUI_AUTH and WEBUI_SECRET_KEY == "":
####################################
CHROMA_DATA_PATH = f"{DATA_DIR}/vector_db"
# 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 (all-MiniLM-L6-v2)
RAG_EMBEDDING_MODEL = os.environ.get("RAG_EMBEDDING_MODEL", "all-MiniLM-L6-v2")
# device type ebbeding models - "cpu" (default), "cuda" (nvidia gpu required) or "mps" (apple silicon) - choosing this right can lead to better performance
RAG_EMBEDDING_MODEL_DEVICE_TYPE = os.environ.get(
"RAG_EMBEDDING_MODEL_DEVICE_TYPE", "cpu"
)
CHROMA_CLIENT = chromadb.PersistentClient(
path=CHROMA_DATA_PATH,
settings=Settings(allow_reset=True, anonymized_telemetry=False),
)
CHUNK_SIZE = 1500
CHUNK_OVERLAP = 100
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_TOP_K = int(os.environ.get("RAG_TOP_K", "5"))
RAG_RELEVANCE_THRESHOLD = float(os.environ.get("RAG_RELEVANCE_THRESHOLD", "0.0"))
RAG_TEMPLATE = """Use the following context as your learned knowledge, inside <context></context> XML tags.
ENABLE_RAG_HYBRID_SEARCH = (
os.environ.get("ENABLE_RAG_HYBRID_SEARCH", "").lower() == "true"
)
RAG_EMBEDDING_ENGINE = os.environ.get("RAG_EMBEDDING_ENGINE", "")
PDF_EXTRACT_IMAGES = os.environ.get("PDF_EXTRACT_IMAGES", "False").lower() == "true"
RAG_EMBEDDING_MODEL = os.environ.get(
"RAG_EMBEDDING_MODEL", "sentence-transformers/all-MiniLM-L6-v2"
)
log.info(f"Embedding model set: {RAG_EMBEDDING_MODEL}"),
RAG_EMBEDDING_MODEL_AUTO_UPDATE = (
os.environ.get("RAG_EMBEDDING_MODEL_AUTO_UPDATE", "").lower() == "true"
)
RAG_EMBEDDING_MODEL_TRUST_REMOTE_CODE = (
os.environ.get("RAG_EMBEDDING_MODEL_TRUST_REMOTE_CODE", "").lower() == "true"
)
RAG_RERANKING_MODEL = os.environ.get("RAG_RERANKING_MODEL", "")
if not RAG_RERANKING_MODEL == "":
log.info(f"Reranking model set: {RAG_RERANKING_MODEL}"),
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"
)
# 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")
if USE_CUDA.lower() == "true":
DEVICE_TYPE = "cuda"
else:
DEVICE_TYPE = "cpu"
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,
)
CHUNK_SIZE = int(os.environ.get("CHUNK_SIZE", "1500"))
CHUNK_OVERLAP = int(os.environ.get("CHUNK_OVERLAP", "100"))
DEFAULT_RAG_TEMPLATE = """Use the following context as your learned knowledge, inside <context></context> XML tags.
<context>
[context]
</context>
@@ -404,21 +522,74 @@ 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_TEMPLATE = os.environ.get("RAG_TEMPLATE", DEFAULT_RAG_TEMPLATE)
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)
ENABLE_LOCAL_WEB_FETCH = os.getenv("ENABLE_LOCAL_WEB_FETCH", "False").lower() == "true"
####################################
# Transcribe
####################################
WHISPER_MODEL = os.getenv("WHISPER_MODEL", "base")
WHISPER_MODEL_DIR = os.getenv("WHISPER_MODEL_DIR", f"{CACHE_DIR}/whisper/models")
WHISPER_MODEL_AUTO_UPDATE = (
os.environ.get("WHISPER_MODEL_AUTO_UPDATE", "").lower() == "true"
)
####################################
# Images
####################################
IMAGE_GENERATION_ENGINE = os.getenv("IMAGE_GENERATION_ENGINE", "")
ENABLE_IMAGE_GENERATION = (
os.environ.get("ENABLE_IMAGE_GENERATION", "").lower() == "true"
)
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
)
IMAGES_OPENAI_API_KEY = os.getenv("IMAGES_OPENAI_API_KEY", OPENAI_API_KEY)
IMAGE_SIZE = os.getenv("IMAGE_SIZE", "512x512")
IMAGE_STEPS = int(os.getenv("IMAGE_STEPS", 50))
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)
####################################
# LiteLLM
####################################
ENABLE_LITELLM = os.environ.get("ENABLE_LITELLM", "True").lower() == "true"
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
####################################
DATABASE_URL = os.environ.get("DATABASE_URL", f"sqlite:///{DATA_DIR}/webui.db")
+14 -1
View File
@@ -3,6 +3,10 @@ from enum import Enum
class MESSAGES(str, Enum):
DEFAULT = lambda msg="": f"{msg if msg else ''}"
MODEL_ADDED = lambda model="": f"The model '{model}' has been added successfully."
MODEL_DELETED = (
lambda model="": f"The model '{model}' has been deleted successfully."
)
class WEBHOOK_MESSAGES(str, Enum):
@@ -20,6 +24,7 @@ class ERROR_MESSAGES(str, Enum):
ENV_VAR_NOT_FOUND = "Required environment variable not found. Terminating now."
CREATE_USER_ERROR = "Oops! Something went wrong while creating your account. Please try again later. If the issue persists, contact support for assistance."
DELETE_USER_ERROR = "Oops! Something went wrong. We encountered an issue while trying to delete the user. Please give it another shot."
EMAIL_MISMATCH = "Uh-oh! This email does not match the email your provider is registered with. Please check your email and try again."
EMAIL_TAKEN = "Uh-oh! This email is already registered. Sign in with your existing account or choose another email to start anew."
USERNAME_TAKEN = (
"Uh-oh! This username is already registered. Please choose another username."
@@ -36,6 +41,7 @@ class ERROR_MESSAGES(str, Enum):
INVALID_PASSWORD = (
"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."
UNAUTHORIZED = "401 Unauthorized"
ACCESS_PROHIBITED = "You do not have permission to access this resource. Please contact your administrator for assistance."
ACTION_PROHIBITED = (
@@ -58,7 +64,14 @@ class ERROR_MESSAGES(str, Enum):
RATE_LIMIT_EXCEEDED = "API rate limit exceeded"
MODEL_NOT_FOUND = lambda name="": f"Model '{name}' was not found"
OPENAI_NOT_FOUND = lambda name="": f"OpenAI API was not found"
OPENAI_NOT_FOUND = lambda name="": "OpenAI API was not found"
OLLAMA_NOT_FOUND = "WebUI could not connect to Ollama"
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."
)
+35 -35
View File
@@ -1,36 +1,36 @@
{
"version": 0,
"ui": {
"default_locale": "en-US",
"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."
},
{
"title": [
"Give me ideas",
"for what to do with my kids' art"
],
"content": "What are 5 creative things I could do with my kids' art? I don't want to throw them away, but it's also so much clutter."
},
{
"title": [
"Tell me a fun fact",
"about the Roman Empire"
],
"content": "Tell me a random fun fact about the Roman Empire"
},
{
"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."
}
]
}
}
"version": 0,
"ui": {
"default_locale": "en-US",
"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."
},
{
"title": ["Give me ideas", "for what to do with my kids' art"],
"content": "What are 5 creative things I could do with my kids' art? I don't want to throw them away, but it's also so much clutter."
},
{
"title": ["Tell me a fun fact", "about the Roman Empire"],
"content": "Tell me a random fun fact about the Roman Empire"
},
{
"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."
}
]
}
}
Regular → Executable
View File
+95 -35
View File
@@ -5,6 +5,7 @@ import time
import os
import sys
import logging
import aiohttp
import requests
from fastapi import FastAPI, Request, Depends, status
@@ -18,12 +19,18 @@ from starlette.middleware.base import BaseHTTPMiddleware
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, startup as litellm_app_startup
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
import asyncio
from pydantic import BaseModel
from typing import List
@@ -38,11 +45,15 @@ from config import (
VERSION,
CHANGELOG,
FRONTEND_BUILD_DIR,
MODEL_FILTER_ENABLED,
CACHE_DIR,
STATIC_DIR,
ENABLE_LITELLM,
ENABLE_MODEL_FILTER,
MODEL_FILTER_LIST,
GLOBAL_LOG_LEVEL,
SRC_LOG_LEVELS,
WEBHOOK_URL,
ENABLE_ADMIN_EXPORT,
)
from constants import ERROR_MESSAGES
@@ -62,14 +73,28 @@ class SPAStaticFiles(StaticFiles):
raise ex
print(
f"""
___ __ __ _ _ _ ___
/ _ \ _ __ ___ _ __ \ \ / /__| |__ | | | |_ _|
| | | | '_ \ / _ \ '_ \ \ \ /\ / / _ \ '_ \| | | || |
| |_| | |_) | __/ | | | \ V V / __/ |_) | |_| || |
\___/| .__/ \___|_| |_| \_/\_/ \___|_.__/ \___/|___|
|_|
v{VERSION} - building the best open-source AI user interface.
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.ENABLE_MODEL_FILTER = ENABLE_MODEL_FILTER
app.state.MODEL_FILTER_LIST = MODEL_FILTER_LIST
app.state.WEBHOOK_URL = WEBHOOK_URL
origins = ["*"]
@@ -92,11 +117,14 @@ class RAGMiddleware(BaseHTTPMiddleware):
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.sentence_transformer_ef,
docs=data["docs"],
messages=data["messages"],
template=rag_app.state.RAG_TEMPLATE,
embedding_function=rag_app.state.EMBEDDING_FUNCTION,
k=rag_app.state.TOP_K,
reranking_function=rag_app.state.sentence_transformer_rf,
r=rag_app.state.RELEVANCE_THRESHOLD,
hybrid_search=rag_app.state.ENABLE_RAG_HYBRID_SEARCH,
)
del data["docs"]
@@ -148,7 +176,8 @@ async def check_url(request: Request, call_next):
@app.on_event("startup")
async def on_startup():
await litellm_app_startup()
if ENABLE_LITELLM:
asyncio.create_task(start_litellm_background())
app.mount("/api/v1", webui_app)
@@ -164,25 +193,30 @@ app.mount("/rag/api/v1", rag_app)
@app.get("/api/config")
async def get_app_config():
# Checking and Handling the Absence of 'ui' in CONFIG_DATA
default_locale = "en-US"
if "ui" in CONFIG_DATA:
default_locale = CONFIG_DATA["ui"].get("default_locale", "en-US")
# The Rest of the Function Now Uses the Variables Defined Above
return {
"status": True,
"name": WEBUI_NAME,
"version": VERSION,
"default_locale": (
CONFIG_DATA["ui"]["default_locale"]
if "default_locale" in CONFIG_DATA["ui"]
else "en-US"
),
"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,
}
@app.get("/api/config/model/filter")
async def get_model_filter_config(user=Depends(get_admin_user)):
return {
"enabled": app.state.MODEL_FILTER_ENABLED,
"enabled": app.state.ENABLE_MODEL_FILTER,
"models": app.state.MODEL_FILTER_LIST,
}
@@ -196,20 +230,20 @@ 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.ENABLE_MODEL_FILTER = 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.ENABLE_MODEL_FILTER = app.state.ENABLE_MODEL_FILTER
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.ENABLE_MODEL_FILTER = app.state.ENABLE_MODEL_FILTER
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.ENABLE_MODEL_FILTER = app.state.ENABLE_MODEL_FILTER
litellm_app.state.MODEL_FILTER_LIST = app.state.MODEL_FILTER_LIST
return {
"enabled": app.state.MODEL_FILTER_ENABLED,
"enabled": app.state.ENABLE_MODEL_FILTER,
"models": app.state.MODEL_FILTER_LIST,
}
@@ -251,26 +285,52 @@ async def get_app_changelog():
@app.get("/api/version/updates")
async def get_app_latest_release_version():
try:
response = requests.get(
f"https://api.github.com/repos/open-webui/open-webui/releases/latest"
)
response.raise_for_status()
latest_version = response.json()["tag_name"]
async with aiohttp.ClientSession() as session:
async with session.get(
"https://api.github.com/repos/open-webui/open-webui/releases/latest"
) as response:
response.raise_for_status()
data = await response.json()
latest_version = data["tag_name"]
return {"current": VERSION, "latest": latest_version[1:]}
except Exception as e:
return {"current": VERSION, "latest": latest_version[1:]}
except aiohttp.ClientError as e:
raise HTTPException(
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
detail=ERROR_MESSAGES.RATE_LIMIT_EXCEEDED,
)
app.mount("/static", StaticFiles(directory="static"), name="static")
app.mount("/cache", StaticFiles(directory="data/cache"), name="cache")
@app.get("/manifest.json")
async def get_manifest_json():
return {
"name": WEBUI_NAME,
"short_name": WEBUI_NAME,
"start_url": "/",
"display": "standalone",
"background_color": "#343541",
"theme_color": "#343541",
"orientation": "portrait-primary",
"icons": [{"src": "/static/logo.png", "type": "image/png", "sizes": "500x500"}],
}
app.mount(
"/",
SPAStaticFiles(directory=FRONTEND_BUILD_DIR, html=True),
name="spa-static-files",
)
app.mount("/static", StaticFiles(directory=STATIC_DIR), name="static")
app.mount("/cache", StaticFiles(directory=CACHE_DIR), name="cache")
if os.path.exists(FRONTEND_BUILD_DIR):
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."
)
@app.on_event("shutdown")
async def shutdown_event():
if ENABLE_LITELLM:
await shutdown_litellm_background()
+53 -39
View File
@@ -1,48 +1,62 @@
fastapi
uvicorn[standard]
pydantic
python-multipart
fastapi==0.109.2
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.0
python-socketio
python-jose
passlib[bcrypt]
uuid
python-socketio==5.11.2
python-jose==3.3.0
passlib[bcrypt]==1.7.4
uuid==1.30
requests
aiohttp
peewee
bcrypt
requests==2.31.0
aiohttp==3.9.5
peewee==3.17.3
peewee-migrate==1.12.2
psycopg2-binary==2.9.9
PyMySQL==1.1.0
bcrypt==4.1.2
litellm==1.30.7
argon2-cffi
apscheduler
google-generativeai
litellm==1.35.28
litellm[proxy]==1.35.28
langchain
langchain-community
fake_useragent
chromadb
sentence_transformers
pypdf
docx2txt
unstructured
markdown
pypandoc
pandas
openpyxl
pyxlsb
xlrd
boto3==1.34.95
opencv-python-headless
rapidocr-onnxruntime
argon2-cffi==23.1.0
APScheduler==3.10.4
google-generativeai==0.5.2
faster-whisper
langchain==0.1.16
langchain-community==0.0.34
langchain-chroma==0.1.0
PyJWT
pyjwt[crypto]
fake-useragent==1.5.1
chromadb==0.4.24
sentence-transformers==2.7.0
pypdf==4.2.0
docx2txt==0.8
unstructured==0.11.8
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
black
langfuse
opencv-python-headless==4.9.0.80
rapidocr-onnxruntime==1.2.3
fpdf2==2.7.8
rank-bm25==0.2.2
faster-whisper==1.0.1
PyJWT==2.8.0
PyJWT[crypto]==2.8.0
black==24.4.2
langfuse==2.27.3
youtube-transcript-api
+17 -6
View File
@@ -6,17 +6,28 @@ 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 "No WEBUI_SECRET_KEY provided"
if ! [ -e "$KEY_FILE" ]; then
echo Generating WEBUI_SECRET_KEY
echo "Generating WEBUI_SECRET_KEY"
# Generate a random value to use as a WEBUI_SECRET_KEY in case the user didn't provide one.
echo $(head -c 12 /dev/random | base64) > $KEY_FILE
echo $(head -c 12 /dev/random | base64) > "$KEY_FILE"
fi
echo Loading WEBUI_SECRET_KEY from $KEY_FILE
WEBUI_SECRET_KEY=`cat $KEY_FILE`
echo "Loading WEBUI_SECRET_KEY from $KEY_FILE"
WEBUI_SECRET_KEY=$(cat "$KEY_FILE")
fi
WEBUI_SECRET_KEY="$WEBUI_SECRET_KEY" exec uvicorn main:app --host 0.0.0.0 --port "$PORT" --forwarded-allow-ips '*'
if [ "$USE_OLLAMA_DOCKER" = "true" ]; then
echo "USE_OLLAMA is set to true, starting ollama serve."
ollama serve &
fi
if [ "$USE_CUDA_DOCKER" = "true" ]; then
echo "CUDA is enabled, appending LD_LIBRARY_PATH to include torch/cudnn & cublas libraries."
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 "$HOST" --port "$PORT" --forwarded-allow-ips '*'
+1 -1
View File
@@ -7,7 +7,7 @@ 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%"
Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.0 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

+1
View File
@@ -0,0 +1 @@
Name,Email,Password,Role
1 Name Email Password Role
Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

+28
View File
@@ -1,6 +1,8 @@
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from fastapi import HTTPException, status, Depends
from apps.web.models.users import Users
from pydantic import BaseModel
from typing import Union, Optional
from constants import ERROR_MESSAGES
@@ -8,6 +10,7 @@ from passlib.context import CryptContext
from datetime import datetime, timedelta
import requests
import jwt
import uuid
import logging
import config
@@ -58,6 +61,11 @@ def extract_token_from_auth_header(auth_header: str):
return auth_header[len("Bearer ") :]
def create_api_key():
key = str(uuid.uuid4()).replace("-", "")
return f"sk-{key}"
def get_http_authorization_cred(auth_header: str):
try:
scheme, credentials = auth_header.split(" ")
@@ -69,6 +77,10 @@ def get_http_authorization_cred(auth_header: str):
def get_current_user(
auth_token: HTTPAuthorizationCredentials = Depends(bearer_security),
):
# auth by api key
if auth_token.credentials.startswith("sk-"):
return get_current_user_by_api_key(auth_token.credentials)
# auth by jwt token
data = decode_token(auth_token.credentials)
if data != None and "id" in data:
user = Users.get_user_by_id(data["id"])
@@ -77,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(
@@ -85,6 +99,20 @@ 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
def get_verified_user(user=Depends(get_current_user)):
if user.role not in {"user", "admin"}:
raise HTTPException(
+9 -2
View File
@@ -1,6 +1,11 @@
import json
import requests
from config import VERSION, WEBUI_FAVICON_URL, WEBUI_NAME
import logging
from config import SRC_LOG_LEVELS, VERSION, WEBUI_FAVICON_URL, WEBUI_NAME
log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["WEBHOOK"])
def post_webhook(url: str, message: str, event_data: dict) -> bool:
@@ -39,9 +44,11 @@ def post_webhook(url: str, message: str, event_data: dict) -> bool:
else:
payload = {**event_data}
log.debug(f"payload: {payload}")
r = requests.post(url, json=payload)
r.raise_for_status()
log.debug(f"r.text: {r.text}")
return True
except Exception as e:
print(e)
log.exception(e)
return False
+6 -1
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
View File
@@ -0,0 +1,8 @@
import { defineConfig } from 'cypress';
export default defineConfig({
e2e: {
baseUrl: 'http://localhost:8080'
},
video: true
});
+46
View File
@@ -0,0 +1,46 @@
// 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('div[role="menuitem"]').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('div[role="menuitem"]').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');
});
});
});
+52
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();
}
});
});
});
+88
View File
@@ -0,0 +1,88 @@
// 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('/');
// Open the sidebar if it is not already open
cy.get('[aria-label="Open sidebar"]').then(() => {
cy.get('button[id="sidebar-toggle-button"]').click();
});
// Click on the profile link
cy.get('button').contains(adminUser.name).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
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
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
View File
@@ -0,0 +1,7 @@
{
"extends": "../tsconfig.json",
"compilerOptions": {
"inlineSourceMap": true,
"sourceMap": false
}
}
+8
View File
@@ -0,0 +1,8 @@
services:
ollama:
devices:
- /dev/kfd:/dev/kfd
- /dev/dri:/dev/dri
image: ollama/ollama:${OLLAMA_DOCKER_TAG-rocm}
environment:
- 'HSA_OVERRIDE_GFX_VERSION=${HSA_OVERRIDE_GFX_VERSION-11.0.0}'
+2 -2
View File
@@ -8,7 +8,7 @@ services:
pull_policy: always
tty: true
restart: unless-stopped
image: ollama/ollama:latest
image: ollama/ollama:${OLLAMA_DOCKER_TAG-latest}
open-webui:
build:
@@ -16,7 +16,7 @@ services:
args:
OLLAMA_BASE_URL: '/ollama'
dockerfile: Dockerfile
image: ghcr.io/open-webui/open-webui:main
image: ghcr.io/open-webui/open-webui:${WEBUI_DOCKER_TAG-main}
container_name: open-webui
volumes:
- open-webui:/app/backend/data
+5 -1
View File
@@ -7,7 +7,11 @@ ollama
{{- end -}}
{{- define "ollama.url" -}}
{{- printf "http://%s.%s.svc.cluster.local:%d/api" (include "ollama.name" .) (.Release.Namespace) (.Values.ollama.service.port | int) }}
{{- if .Values.ollama.externalHost }}
{{- printf .Values.ollama.externalHost }}
{{- else }}
{{- printf "http://%s.%s.svc.cluster.local:%d" (include "ollama.name" .) (.Release.Namespace) (.Values.ollama.service.port | int) }}
{{- end }}
{{- end }}
{{- define "chart.name" -}}
@@ -1,3 +1,4 @@
{{- if not .Values.ollama.externalHost }}
apiVersion: v1
kind: Service
metadata:
@@ -19,3 +20,4 @@ spec:
port: {{ .port }}
targetPort: http
{{- end }}
{{- end }}
@@ -1,3 +1,4 @@
{{- if not .Values.ollama.externalHost }}
apiVersion: apps/v1
kind: StatefulSet
metadata:
@@ -94,3 +95,4 @@ spec:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- end }}
{{- end }}
+2
View File
@@ -17,7 +17,9 @@ spec:
resources:
requests:
storage: {{ .Values.webui.persistence.size }}
{{- if .Values.webui.persistence.storageClass }}
storageClassName: {{ .Values.webui.persistence.storageClass }}
{{- end }}
{{- with .Values.webui.persistence.selector }}
selector:
{{- toYaml . | nindent 4 }}
+1
View File
@@ -1,6 +1,7 @@
nameOverride: ""
ollama:
externalHost: ""
annotations: {}
podAnnotations: {}
replicaCount: 1
+1635 -11
View File
File diff suppressed because it is too large Load Diff
+8 -4
View File
@@ -1,6 +1,6 @@
{
"name": "open-webui",
"version": "0.1.116",
"version": "0.1.123",
"private": true,
"scripts": {
"dev": "vite dev --host",
@@ -13,8 +13,9 @@
"lint:types": "npm run check",
"lint:backend": "pylint backend/",
"format": "prettier --plugin-search-dir --write '**/*.{js,ts,svelte,css,md,html,json}'",
"format:backend": "yapf --recursive backend -p -i",
"i18n:parse": "i18next --config i18next-parser.config.ts && prettier --write 'src/lib/i18n/**/*.{js,json}'"
"format:backend": "black . --exclude \"/venv/\"",
"i18n:parse": "i18next --config i18next-parser.config.ts && prettier --write 'src/lib/i18n/**/*.{js,json}'",
"cy:open": "cypress open"
},
"devDependencies": {
"@sveltejs/adapter-auto": "^2.0.0",
@@ -25,8 +26,10 @@
"@typescript-eslint/eslint-plugin": "^6.17.0",
"@typescript-eslint/parser": "^6.17.0",
"autoprefixer": "^10.4.16",
"cypress": "^13.8.1",
"eslint": "^8.56.0",
"eslint-config-prettier": "^8.5.0",
"eslint-plugin-cypress": "^3.0.2",
"eslint-plugin-svelte": "^2.30.0",
"i18next-parser": "^8.13.0",
"postcss": "^8.4.31",
@@ -46,6 +49,7 @@
"async": "^3.2.5",
"bits-ui": "^0.19.7",
"dayjs": "^1.11.10",
"eventsource-parser": "^1.1.2",
"file-saver": "^2.0.5",
"highlight.js": "^11.9.0",
"i18next": "^23.10.0",
@@ -59,4 +63,4 @@
"tippy.js": "^6.3.7",
"uuid": "^9.0.1"
}
}
}
+2 -1
View File
@@ -82,6 +82,7 @@ usage() {
echo "Examples:"
echo " $0 --drop"
echo " $0 --enable-gpu[count=1]"
echo " $0 --enable-gpu[count=all]"
echo " $0 --enable-api[port=11435]"
echo " $0 --enable-gpu[count=1] --enable-api[port=12345] --webui[port=3000]"
echo " $0 --enable-gpu[count=1] --enable-api[port=12345] --webui[port=3000] --data[folder=./ollama-data]"
@@ -160,7 +161,7 @@ else
if [[ $enable_gpu == true ]]; then
# Validate and process command-line arguments
if [[ -n $gpu_count ]]; then
if ! [[ $gpu_count =~ ^[0-9]+$ ]]; then
if ! [[ $gpu_count =~ ^([0-9]+|all)$ ]]; then
echo "Invalid GPU count: $gpu_count"
exit 1
fi
+4
View File
@@ -78,3 +78,7 @@ select {
/* for Chrome */
-webkit-appearance: none;
}
.katex-mathml {
display: none;
}
+38 -1
View File
@@ -3,7 +3,7 @@
<head>
<meta charset="utf-8" />
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
<link rel="manifest" href="%sveltekit.assets%/manifest.json" />
<link rel="manifest" href="%sveltekit.assets%/manifest.json" crossorigin="use-credentials" />
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1" />
<meta name="robots" content="noindex,nofollow" />
<script>
@@ -43,9 +43,46 @@
})();
</script>
<title>Open WebUI</title>
%sveltekit.head%
</head>
<body data-sveltekit-preload-data="hover">
<div style="display: contents">%sveltekit.body%</div>
<div
id="splash-screen"
style="
position: fixed;
z-index: 100;
background: #fff;
top: 0;
left: 0;
width: 100%;
height: 100%;
"
>
<style type="text/css" nonce="">
html {
overflow-y: scroll !important;
}
</style>
<img
style="
position: absolute;
width: 6rem;
height: 6rem;
top: 46%;
left: 50%;
margin: -40px 0 0 -40px;
"
src="/logo.svg"
/>
<!-- <span style="position: absolute; bottom: 32px; left: 50%; margin: -36px 0 0 -36px">
Footer content
</span> -->
</div>
</body>
</html>
+100 -1
View File
@@ -1,11 +1,73 @@
import { AUDIO_API_BASE_URL } from '$lib/constants';
export const getAudioConfig = async (token: string) => {
let error = null;
const res = await fetch(`${AUDIO_API_BASE_URL}/config`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`
}
})
.then(async (res) => {
if (!res.ok) throw await res.json();
return res.json();
})
.catch((err) => {
console.log(err);
error = err.detail;
return null;
});
if (error) {
throw error;
}
return res;
};
type OpenAIConfigForm = {
url: string;
key: string;
};
export const updateAudioConfig = async (token: string, payload: OpenAIConfigForm) => {
let error = null;
const res = await fetch(`${AUDIO_API_BASE_URL}/config/update`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`
},
body: JSON.stringify({
...payload
})
})
.then(async (res) => {
if (!res.ok) throw await res.json();
return res.json();
})
.catch((err) => {
console.log(err);
error = err.detail;
return null;
});
if (error) {
throw error;
}
return res;
};
export const transcribeAudio = async (token: string, file: File) => {
const data = new FormData();
data.append('file', file);
let error = null;
const res = await fetch(`${AUDIO_API_BASE_URL}/transcribe`, {
const res = await fetch(`${AUDIO_API_BASE_URL}/transcriptions`, {
method: 'POST',
headers: {
Accept: 'application/json',
@@ -29,3 +91,40 @@ export const transcribeAudio = async (token: string, file: File) => {
return res;
};
export const synthesizeOpenAISpeech = async (
token: string = '',
speaker: string = 'alloy',
text: string = ''
) => {
let error = null;
const res = await fetch(`${AUDIO_API_BASE_URL}/speech`, {
method: 'POST',
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
model: 'tts-1',
input: text,
voice: speaker
})
})
.then(async (res) => {
if (!res.ok) throw await res.json();
return res;
})
.catch((err) => {
error = err.detail;
console.log(err);
return null;
});
if (error) {
throw error;
}
return res;
};
+122 -2
View File
@@ -58,7 +58,12 @@ export const userSignIn = async (email: string, password: string) => {
return res;
};
export const userSignUp = async (name: string, email: string, password: string) => {
export const userSignUp = async (
name: string,
email: string,
password: string,
profile_image_url: string
) => {
let error = null;
const res = await fetch(`${WEBUI_API_BASE_URL}/auths/signup`, {
@@ -69,7 +74,47 @@ export const userSignUp = async (name: string, email: string, password: string)
body: JSON.stringify({
name: name,
email: email,
password: password
password: password,
profile_image_url: profile_image_url
})
})
.then(async (res) => {
if (!res.ok) throw await res.json();
return res.json();
})
.catch((err) => {
console.log(err);
error = err.detail;
return null;
});
if (error) {
throw error;
}
return res;
};
export const addUser = async (
token: string,
name: string,
email: string,
password: string,
role: string = 'pending'
) => {
let error = null;
const res = await fetch(`${WEBUI_API_BASE_URL}/auths/add`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
...(token && { authorization: `Bearer ${token}` })
},
body: JSON.stringify({
name: name,
email: email,
password: password,
role: role
})
})
.then(async (res) => {
@@ -318,3 +363,78 @@ export const updateJWTExpiresDuration = async (token: string, duration: string)
return res;
};
export const createAPIKey = async (token: string) => {
let error = null;
const res = await fetch(`${WEBUI_API_BASE_URL}/auths/api_key`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`
}
})
.then(async (res) => {
if (!res.ok) throw await res.json();
return res.json();
})
.catch((err) => {
console.log(err);
error = err.detail;
return null;
});
if (error) {
throw error;
}
return res.api_key;
};
export const getAPIKey = async (token: string) => {
let error = null;
const res = await fetch(`${WEBUI_API_BASE_URL}/auths/api_key`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`
}
})
.then(async (res) => {
if (!res.ok) throw await res.json();
return res.json();
})
.catch((err) => {
console.log(err);
error = err.detail;
return null;
});
if (error) {
throw error;
}
return res.api_key;
};
export const deleteAPIKey = async (token: string) => {
let error = null;
const res = await fetch(`${WEBUI_API_BASE_URL}/auths/api_key`, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`
}
})
.then(async (res) => {
if (!res.ok) throw await res.json();
return res.json();
})
.catch((err) => {
console.log(err);
error = err.detail;
return null;
});
if (error) {
throw error;
}
return res;
};
+190
View File
@@ -62,6 +62,68 @@ export const getChatList = async (token: string = '') => {
return res;
};
export const getChatListByUserId = async (token: string = '', userId: string) => {
let error = null;
const res = await fetch(`${WEBUI_API_BASE_URL}/chats/list/user/${userId}`, {
method: 'GET',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
...(token && { authorization: `Bearer ${token}` })
}
})
.then(async (res) => {
if (!res.ok) throw await res.json();
return res.json();
})
.then((json) => {
return json;
})
.catch((err) => {
error = err;
console.log(err);
return null;
});
if (error) {
throw error;
}
return res;
};
export const getArchivedChatList = async (token: string = '') => {
let error = null;
const res = await fetch(`${WEBUI_API_BASE_URL}/chats/archived`, {
method: 'GET',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
...(token && { authorization: `Bearer ${token}` })
}
})
.then(async (res) => {
if (!res.ok) throw await res.json();
return res.json();
})
.then((json) => {
return json;
})
.catch((err) => {
error = err;
console.log(err);
return null;
});
if (error) {
throw error;
}
return res;
};
export const getAllChats = async (token: string) => {
let error = null;
@@ -218,6 +280,134 @@ export const getChatById = async (token: string, id: string) => {
return res;
};
export const getChatByShareId = async (token: string, share_id: string) => {
let error = null;
const res = await fetch(`${WEBUI_API_BASE_URL}/chats/share/${share_id}`, {
method: 'GET',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
...(token && { authorization: `Bearer ${token}` })
}
})
.then(async (res) => {
if (!res.ok) throw await res.json();
return res.json();
})
.then((json) => {
return json;
})
.catch((err) => {
error = err;
console.log(err);
return null;
});
if (error) {
throw error;
}
return res;
};
export const shareChatById = async (token: string, id: string) => {
let error = null;
const res = await fetch(`${WEBUI_API_BASE_URL}/chats/${id}/share`, {
method: 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
...(token && { authorization: `Bearer ${token}` })
}
})
.then(async (res) => {
if (!res.ok) throw await res.json();
return res.json();
})
.then((json) => {
return json;
})
.catch((err) => {
error = err;
console.log(err);
return null;
});
if (error) {
throw error;
}
return res;
};
export const archiveChatById = async (token: string, id: string) => {
let error = null;
const res = await fetch(`${WEBUI_API_BASE_URL}/chats/${id}/archive`, {
method: 'GET',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
...(token && { authorization: `Bearer ${token}` })
}
})
.then(async (res) => {
if (!res.ok) throw await res.json();
return res.json();
})
.then((json) => {
return json;
})
.catch((err) => {
error = err;
console.log(err);
return null;
});
if (error) {
throw error;
}
return res;
};
export const deleteSharedChatById = async (token: string, id: string) => {
let error = null;
const res = await fetch(`${WEBUI_API_BASE_URL}/chats/${id}/share`, {
method: 'DELETE',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
...(token && { authorization: `Bearer ${token}` })
}
})
.then(async (res) => {
if (!res.ok) throw await res.json();
return res.json();
})
.then((json) => {
return json;
})
.catch((err) => {
error = err;
console.log(err);
return null;
});
if (error) {
throw error;
}
return res;
};
export const updateChatById = async (token: string, id: string, chat: object) => {
let error = null;
+7 -6
View File
@@ -72,10 +72,10 @@ export const updateImageGenerationConfig = async (
return res;
};
export const getOpenAIKey = async (token: string = '') => {
export const getOpenAIConfig = async (token: string = '') => {
let error = null;
const res = await fetch(`${IMAGES_API_BASE_URL}/key`, {
const res = await fetch(`${IMAGES_API_BASE_URL}/openai/config`, {
method: 'GET',
headers: {
Accept: 'application/json',
@@ -101,13 +101,13 @@ export const getOpenAIKey = async (token: string = '') => {
throw error;
}
return res.OPENAI_API_KEY;
return res;
};
export const updateOpenAIKey = async (token: string = '', key: string) => {
export const updateOpenAIConfig = async (token: string = '', url: string, key: string) => {
let error = null;
const res = await fetch(`${IMAGES_API_BASE_URL}/key/update`, {
const res = await fetch(`${IMAGES_API_BASE_URL}/openai/config/update`, {
method: 'POST',
headers: {
Accept: 'application/json',
@@ -115,6 +115,7 @@ export const updateOpenAIKey = async (token: string = '', key: string) => {
...(token && { authorization: `Bearer ${token}` })
},
body: JSON.stringify({
url: url,
key: key
})
})
@@ -136,7 +137,7 @@ export const updateOpenAIKey = async (token: string = '', key: string) => {
throw error;
}
return res.OPENAI_API_KEY;
return res;
};
export const getImageGenerationEngineUrls = async (token: string = '') => {
+28 -1
View File
@@ -1,4 +1,5 @@
import { OLLAMA_API_BASE_URL } from '$lib/constants';
import { promptTemplate } from '$lib/utils';
export const getOllamaUrls = async (token: string = '') => {
let error = null;
@@ -144,7 +145,7 @@ export const generateTitle = async (
) => {
let error = null;
template = template.replace(/{{prompt}}/g, prompt);
template = promptTemplate(template, prompt);
console.log(template);
@@ -219,6 +220,32 @@ export const generatePrompt = async (token: string = '', model: string, conversa
return res;
};
export const generateEmbeddings = async (token: string = '', model: string, text: string) => {
let error = null;
const res = await fetch(`${OLLAMA_API_BASE_URL}/api/embeddings`, {
method: 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`
},
body: JSON.stringify({
model: model,
prompt: text
})
}).catch((err) => {
error = err;
return null;
});
if (error) {
throw error;
}
return res;
};
export const generateTextCompletion = async (token: string = '', model: string, text: string) => {
let error = null;
+6 -3
View File
@@ -1,4 +1,5 @@
import { OPENAI_API_BASE_URL } from '$lib/constants';
import { promptTemplate } from '$lib/utils';
export const getOpenAIUrls = async (token: string = '') => {
let error = null;
@@ -210,10 +211,12 @@ export const generateOpenAIChatCompletion = async (
token: string = '',
body: object,
url: string = OPENAI_API_BASE_URL
) => {
): Promise<[Response | null, AbortController]> => {
const controller = new AbortController();
let error = null;
const res = await fetch(`${url}/chat/completions`, {
signal: controller.signal,
method: 'POST',
headers: {
Authorization: `Bearer ${token}`,
@@ -230,7 +233,7 @@ export const generateOpenAIChatCompletion = async (
throw error;
}
return res;
return [res, controller];
};
export const synthesizeOpenAISpeech = async (
@@ -273,7 +276,7 @@ export const generateTitle = async (
) => {
let error = null;
template = template.replace(/{{prompt}}/g, prompt);
template = promptTemplate(template, prompt);
console.log(template);
+161
View File
@@ -123,6 +123,7 @@ export const getQuerySettings = async (token: string) => {
type QuerySettings = {
k: number | null;
r: number | null;
template: string | null;
};
@@ -220,6 +221,37 @@ export const uploadWebToVectorDB = async (token: string, collection_name: string
return res;
};
export const uploadYoutubeTranscriptionToVectorDB = async (token: string, url: string) => {
let error = null;
const res = await fetch(`${RAG_API_BASE_URL}/youtube`, {
method: 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
authorization: `Bearer ${token}`
},
body: JSON.stringify({
url: url
})
})
.then(async (res) => {
if (!res.ok) throw await res.json();
return res.json();
})
.catch((err) => {
error = err.detail;
console.log(err);
return null;
});
if (error) {
throw error;
}
return res;
};
export const queryDoc = async (
token: string,
collection_name: string,
@@ -345,3 +377,132 @@ export const resetVectorDB = async (token: string) => {
return res;
};
export const getEmbeddingConfig = async (token: string) => {
let error = null;
const res = await fetch(`${RAG_API_BASE_URL}/embedding`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`
}
})
.then(async (res) => {
if (!res.ok) throw await res.json();
return res.json();
})
.catch((err) => {
console.log(err);
error = err.detail;
return null;
});
if (error) {
throw error;
}
return res;
};
type OpenAIConfigForm = {
key: string;
url: string;
};
type EmbeddingModelUpdateForm = {
openai_config?: OpenAIConfigForm;
embedding_engine: string;
embedding_model: string;
};
export const updateEmbeddingConfig = async (token: string, payload: EmbeddingModelUpdateForm) => {
let error = null;
const res = await fetch(`${RAG_API_BASE_URL}/embedding/update`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`
},
body: JSON.stringify({
...payload
})
})
.then(async (res) => {
if (!res.ok) throw await res.json();
return res.json();
})
.catch((err) => {
console.log(err);
error = err.detail;
return null;
});
if (error) {
throw error;
}
return res;
};
export const getRerankingConfig = async (token: string) => {
let error = null;
const res = await fetch(`${RAG_API_BASE_URL}/reranking`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`
}
})
.then(async (res) => {
if (!res.ok) throw await res.json();
return res.json();
})
.catch((err) => {
console.log(err);
error = err.detail;
return null;
});
if (error) {
throw error;
}
return res;
};
type RerankingModelUpdateForm = {
reranking_model: string;
};
export const updateRerankingConfig = async (token: string, payload: RerankingModelUpdateForm) => {
let error = null;
const res = await fetch(`${RAG_API_BASE_URL}/reranking/update`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`
},
body: JSON.stringify({
...payload
})
})
.then(async (res) => {
if (!res.ok) throw await res.json();
return res.json();
})
.catch((err) => {
console.log(err);
error = err.detail;
return null;
});
if (error) {
throw error;
}
return res;
};
+84
View File
@@ -0,0 +1,84 @@
import { EventSourceParserStream } from 'eventsource-parser/stream';
import type { ParsedEvent } from 'eventsource-parser';
type TextStreamUpdate = {
done: boolean;
value: string;
};
// createOpenAITextStream takes a responseBody with a SSE response,
// and returns an async generator that emits delta updates with large deltas chunked into random sized chunks
export async function createOpenAITextStream(
responseBody: ReadableStream<Uint8Array>,
splitLargeDeltas: boolean
): Promise<AsyncGenerator<TextStreamUpdate>> {
const eventStream = responseBody
.pipeThrough(new TextDecoderStream())
.pipeThrough(new EventSourceParserStream())
.getReader();
let iterator = openAIStreamToIterator(eventStream);
if (splitLargeDeltas) {
iterator = streamLargeDeltasAsRandomChunks(iterator);
}
return iterator;
}
async function* openAIStreamToIterator(
reader: ReadableStreamDefaultReader<ParsedEvent>
): AsyncGenerator<TextStreamUpdate> {
while (true) {
const { value, done } = await reader.read();
if (done) {
yield { done: true, value: '' };
break;
}
if (!value) {
continue;
}
const data = value.data;
if (data.startsWith('[DONE]')) {
yield { done: true, value: '' };
break;
}
try {
const parsedData = JSON.parse(data);
console.log(parsedData);
yield { done: false, value: parsedData.choices?.[0]?.delta?.content ?? '' };
} catch (e) {
console.error('Error extracting delta from SSE event:', e);
}
}
}
// streamLargeDeltasAsRandomChunks will chunk large deltas (length > 5) into random sized chunks between 1-3 characters
// This is to simulate a more fluid streaming, even though some providers may send large chunks of text at once
async function* streamLargeDeltasAsRandomChunks(
iterator: AsyncGenerator<TextStreamUpdate>
): AsyncGenerator<TextStreamUpdate> {
for await (const textStreamUpdate of iterator) {
if (textStreamUpdate.done) {
yield textStreamUpdate;
return;
}
let content = textStreamUpdate.value;
if (content.length < 5) {
yield { done: false, value: content };
continue;
}
while (content != '') {
const chunkSize = Math.min(Math.floor(Math.random() * 3) + 1, content.length);
const chunk = content.slice(0, chunkSize);
yield { done: false, value: chunk };
// Do not sleep if the tab is hidden
// Timers are throttled to 1s in hidden tabs
if (document?.visibilityState !== 'hidden') {
await sleep(5);
}
content = content.slice(chunkSize);
}
}
}
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
+58 -3
View File
@@ -22,6 +22,57 @@ export const getGravatarUrl = async (email: string) => {
return res;
};
export const downloadChatAsPDF = async (chat: object) => {
let error = null;
const blob = await fetch(`${WEBUI_API_BASE_URL}/utils/pdf`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
title: chat.title,
messages: chat.messages
})
})
.then(async (res) => {
if (!res.ok) throw await res.json();
return res.blob();
})
.catch((err) => {
console.log(err);
error = err;
return null;
});
return blob;
};
export const getHTMLFromMarkdown = async (md: string) => {
let error = null;
const res = await fetch(`${WEBUI_API_BASE_URL}/utils/markdown`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
md: md
})
})
.then(async (res) => {
if (!res.ok) throw await res.json();
return res.json();
})
.catch((err) => {
console.log(err);
error = err;
return null;
});
return res.html;
};
export const downloadDatabase = async (token: string) => {
let error = null;
@@ -32,9 +83,9 @@ export const downloadDatabase = async (token: string) => {
Authorization: `Bearer ${token}`
}
})
.then((response) => {
.then(async (response) => {
if (!response.ok) {
throw new Error('Network response was not ok');
throw await response.json();
}
return response.blob();
})
@@ -49,7 +100,11 @@ export const downloadDatabase = async (token: string) => {
})
.catch((err) => {
console.log(err);
error = err;
error = err.detail;
return null;
});
if (error) {
throw error;
}
};
+4 -3
View File
@@ -22,7 +22,7 @@
</script>
<Modal bind:show>
<div class="px-5 py-4 dark:text-gray-300">
<div class="px-5 py-4 dark:text-gray-300 text-gray-700">
<div class="flex justify-between items-start">
<div class="text-xl font-bold">
{$i18n.t('What’s New in')}
@@ -32,6 +32,7 @@
<button
class="self-center"
on:click={() => {
localStorage.version = $config.version;
show = false;
}}
>
@@ -58,7 +59,7 @@
<hr class=" dark:border-gray-800" />
<div class=" w-full p-4 px-5">
<div class=" w-full p-4 px-5 text-gray-700 dark:text-gray-100">
<div class=" overflow-y-scroll max-h-80">
<div class="mb-3">
{#if changelog}
@@ -109,7 +110,7 @@
localStorage.version = $config.version;
show = false;
}}
class=" px-4 py-2 bg-emerald-600 hover:bg-emerald-700 text-gray-100 transition rounded"
class=" px-4 py-2 bg-emerald-700 hover:bg-emerald-800 text-gray-100 transition rounded-lg"
>
<span class="relative">{$i18n.t("Okay, Let's Go!")}</span>
</button>
@@ -0,0 +1,334 @@
<script lang="ts">
import { toast } from 'svelte-sonner';
import { createEventDispatcher } from 'svelte';
import { onMount, getContext } from 'svelte';
import { addUser } from '$lib/apis/auths';
import Modal from '../common/Modal.svelte';
import { WEBUI_BASE_URL } from '$lib/constants';
const i18n = getContext('i18n');
const dispatch = createEventDispatcher();
export let show = false;
let loading = false;
let tab = '';
let inputFiles;
let _user = {
name: '',
email: '',
password: '',
role: 'user'
};
$: if (show) {
_user = {
name: '',
email: '',
password: '',
role: 'user'
};
}
const submitHandler = async () => {
const stopLoading = () => {
dispatch('save');
loading = false;
};
if (tab === '') {
loading = true;
const res = await addUser(
localStorage.token,
_user.name,
_user.email,
_user.password,
_user.role
).catch((error) => {
toast.error(error);
});
if (res) {
stopLoading();
show = false;
}
} else {
if (inputFiles) {
loading = true;
const file = inputFiles[0];
const reader = new FileReader();
reader.onload = async (e) => {
const csv = e.target.result;
const rows = csv.split('\n');
let userCount = 0;
for (const [idx, row] of rows.entries()) {
const columns = row.split(',').map((col) => col.trim());
console.log(idx, columns);
if (idx > 0) {
if (columns.length === 4 && ['admin', 'user', 'pending'].includes(columns[3])) {
const res = await addUser(
localStorage.token,
columns[0],
columns[1],
columns[2],
columns[3]
).catch((error) => {
toast.error(`Row ${idx + 1}: ${error}`);
return null;
});
if (res) {
userCount = userCount + 1;
}
} else {
toast.error(`Row ${idx + 1}: invalid format.`);
}
}
}
toast.success(`Successfully imported ${userCount} users.`);
inputFiles = null;
const uploadInputElement = document.getElementById('upload-user-csv-input');
if (uploadInputElement) {
uploadInputElement.value = null;
}
stopLoading();
};
reader.readAsText(file);
} else {
toast.error(`File not found.`);
}
}
};
</script>
<Modal size="sm" bind:show>
<div>
<div class=" flex justify-between dark:text-gray-300 px-5 pt-4 pb-2">
<div class=" text-lg font-medium self-center">{$i18n.t('Add User')}</div>
<button
class="self-center"
on:click={() => {
show = false;
}}
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
class="w-5 h-5"
>
<path
d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z"
/>
</svg>
</button>
</div>
<div class="flex flex-col md:flex-row w-full px-5 pb-4 md:space-x-4 dark:text-gray-200">
<div class=" flex flex-col w-full sm:flex-row sm:justify-center sm:space-x-6">
<form
class="flex flex-col w-full"
on:submit|preventDefault={() => {
submitHandler();
}}
>
<div class="flex text-center text-sm font-medium rounded-xl bg-transparent/10 p-1 mb-2">
<button
class="w-full rounded-lg p-1.5 {tab === '' ? 'bg-gray-50 dark:bg-gray-850' : ''}"
type="button"
on:click={() => {
tab = '';
}}>Form</button
>
<button
class="w-full rounded-lg p-1 {tab === 'import' ? 'bg-gray-50 dark:bg-gray-850' : ''}"
type="button"
on:click={() => {
tab = 'import';
}}>CSV Import</button
>
</div>
<div class="px-1">
{#if tab === ''}
<div class="flex flex-col w-full">
<div class=" mb-1 text-xs text-gray-500">{$i18n.t('Role')}</div>
<div class="flex-1">
<select
class="w-full capitalize rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 disabled:text-gray-500 dark:disabled:text-gray-500 outline-none"
bind:value={_user.role}
placeholder={$i18n.t('Enter Your Role')}
required
>
<option value="pending"> pending </option>
<option value="user"> user </option>
<option value="admin"> admin </option>
</select>
</div>
</div>
<div class="flex flex-col w-full mt-2">
<div class=" mb-1 text-xs text-gray-500">{$i18n.t('Name')}</div>
<div class="flex-1">
<input
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 disabled:text-gray-500 dark:disabled:text-gray-500 outline-none"
type="text"
bind:value={_user.name}
placeholder={$i18n.t('Enter Your Full Name')}
autocomplete="off"
required
/>
</div>
</div>
<hr class=" dark:border-gray-800 my-3 w-full" />
<div class="flex flex-col w-full">
<div class=" mb-1 text-xs text-gray-500">{$i18n.t('Email')}</div>
<div class="flex-1">
<input
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 disabled:text-gray-500 dark:disabled:text-gray-500 outline-none"
type="email"
bind:value={_user.email}
placeholder={$i18n.t('Enter Your Email')}
autocomplete="off"
required
/>
</div>
</div>
<div class="flex flex-col w-full mt-2">
<div class=" mb-1 text-xs text-gray-500">{$i18n.t('Password')}</div>
<div class="flex-1">
<input
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 disabled:text-gray-500 dark:disabled:text-gray-500 outline-none"
type="password"
bind:value={_user.password}
placeholder={$i18n.t('Enter Your Password')}
autocomplete="off"
/>
</div>
</div>
{:else if tab === 'import'}
<div>
<div class="mb-3 w-full">
<input
id="upload-user-csv-input"
hidden
bind:files={inputFiles}
type="file"
accept=".csv"
/>
<button
class="w-full text-sm font-medium py-3 bg-transparent hover:bg-gray-100 border border-dashed dark:border-gray-800 dark:hover:bg-gray-850 text-center rounded-xl"
type="button"
on:click={() => {
document.getElementById('upload-user-csv-input')?.click();
}}
>
{#if inputFiles}
{inputFiles.length > 0 ? `${inputFiles.length}` : ''} document(s) selected.
{:else}
{$i18n.t('Click here to select a csv file.')}
{/if}
</button>
</div>
<div class=" text-xs text-gray-500">
ⓘ {$i18n.t(
'Ensure your CSV file includes 4 columns in this order: Name, Email, Password, Role.'
)}
<a
class="underline dark:text-gray-200"
href="{WEBUI_BASE_URL}/static/user-import.csv"
>
Click here to download user import template file.
</a>
</div>
</div>
{/if}
</div>
<div class="flex justify-end pt-3 text-sm font-medium">
<button
class=" px-4 py-2 bg-emerald-700 hover:bg-emerald-800 text-gray-100 transition rounded-lg flex flex-row space-x-1 items-center {loading
? ' cursor-not-allowed'
: ''}"
type="submit"
disabled={loading}
>
{$i18n.t('Submit')}
{#if loading}
<div class="ml-2 self-center">
<svg
class=" w-4 h-4"
viewBox="0 0 24 24"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg"
><style>
.spinner_ajPY {
transform-origin: center;
animation: spinner_AtaB 0.75s infinite linear;
}
@keyframes spinner_AtaB {
100% {
transform: rotate(360deg);
}
}
</style><path
d="M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,19a8,8,0,1,1,8-8A8,8,0,0,1,12,20Z"
opacity=".25"
/><path
d="M10.14,1.16a11,11,0,0,0-9,8.92A1.59,1.59,0,0,0,2.46,12,1.52,1.52,0,0,0,4.11,10.7a8,8,0,0,1,6.66-6.61A1.42,1.42,0,0,0,12,2.69h0A1.57,1.57,0,0,0,10.14,1.16Z"
class="spinner_ajPY"
/></svg
>
</div>
{/if}
</button>
</div>
</form>
</div>
</div>
</div>
</Modal>
<style>
input::-webkit-outer-spin-button,
input::-webkit-inner-spin-button {
/* display: none; <- Crashes Chrome on hover */
-webkit-appearance: none;
margin: 0; /* <-- Apparently some margin are still there even though it's hidden */
}
.tabs::-webkit-scrollbar {
display: none; /* for Chrome, Safari and Opera */
}
.tabs {
-ms-overflow-style: none; /* IE and Edge */
scrollbar-width: none; /* Firefox */
}
input[type='number'] {
-moz-appearance: textfield; /* Firefox */
}
</style>
@@ -86,7 +86,7 @@
<div class="text-xs text-gray-500">
{$i18n.t('Created at')}
{dayjs(selectedUser.timestamp * 1000).format($i18n.t('MMMM DD, YYYY'))}
{dayjs(selectedUser.created_at * 1000).format($i18n.t('MMMM DD, YYYY'))}
</div>
</div>
</div>
@@ -139,7 +139,7 @@
<div class="flex justify-end pt-3 text-sm font-medium">
<button
class=" px-4 py-2 bg-emerald-600 hover:bg-emerald-700 text-gray-100 transition rounded"
class=" px-4 py-2 bg-emerald-700 hover:bg-emerald-800 text-gray-100 transition rounded-lg"
type="submit"
>
{$i18n.t('Save')}
@@ -1,6 +1,8 @@
<script lang="ts">
import { downloadDatabase } from '$lib/apis/utils';
import { onMount, getContext } from 'svelte';
import { config } from '$lib/stores';
import { toast } from 'svelte-sonner';
const i18n = getContext('i18n');
@@ -24,42 +26,47 @@
<div class=" flex w-full justify-between">
<!-- <div class=" self-center text-xs font-medium">{$i18n.t('Allow Chat Deletion')}</div> -->
<button
class=" flex rounded-md py-1.5 px-3 w-full hover:bg-gray-200 dark:hover:bg-gray-800 transition"
type="button"
on:click={() => {
// exportAllUserChats();
{#if $config?.admin_export_enabled ?? true}
<button
class=" flex rounded-md py-1.5 px-3 w-full hover:bg-gray-200 dark:hover:bg-gray-800 transition"
type="button"
on:click={() => {
// exportAllUserChats();
downloadDatabase(localStorage.token);
}}
>
<div class=" self-center mr-3">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="currentColor"
class="w-4 h-4"
>
<path d="M2 3a1 1 0 0 1 1-1h10a1 1 0 0 1 1 1v1a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V3Z" />
<path
fill-rule="evenodd"
d="M13 6H3v6a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V6ZM8.75 7.75a.75.75 0 0 0-1.5 0v2.69L6.03 9.22a.75.75 0 0 0-1.06 1.06l2.5 2.5a.75.75 0 0 0 1.06 0l2.5-2.5a.75.75 0 1 0-1.06-1.06l-1.22 1.22V7.75Z"
clip-rule="evenodd"
/>
</svg>
</div>
<div class=" self-center text-sm font-medium">{$i18n.t('Download Database')}</div>
</button>
downloadDatabase(localStorage.token).catch((error) => {
toast.error(error);
});
}}
>
<div class=" self-center mr-3">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="currentColor"
class="w-4 h-4"
>
<path d="M2 3a1 1 0 0 1 1-1h10a1 1 0 0 1 1 1v1a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V3Z" />
<path
fill-rule="evenodd"
d="M13 6H3v6a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V6ZM8.75 7.75a.75.75 0 0 0-1.5 0v2.69L6.03 9.22a.75.75 0 0 0-1.06 1.06l2.5 2.5a.75.75 0 0 0 1.06 0l2.5-2.5a.75.75 0 1 0-1.06-1.06l-1.22 1.22V7.75Z"
clip-rule="evenodd"
/>
</svg>
</div>
<div class=" self-center text-sm font-medium">{$i18n.t('Download Database')}</div>
</button>
{/if}
</div>
</div>
</div>
<!-- <div class="flex justify-end pt-3 text-sm font-medium">
<button
class=" px-4 py-2 bg-emerald-600 hover:bg-emerald-700 text-gray-100 transition rounded"
class=" px-4 py-2 bg-emerald-700 hover:bg-emerald-800 text-gray-100 transition rounded-lg"
type="submit"
>
Save
</button>
</div> -->
</form>
@@ -159,7 +159,7 @@
<div class="flex justify-end pt-3 text-sm font-medium">
<button
class=" px-4 py-2 bg-emerald-600 hover:bg-emerald-700 text-gray-100 transition rounded"
class=" px-4 py-2 bg-emerald-700 hover:bg-emerald-800 text-gray-100 transition rounded-lg"
type="submit"
>
{$i18n.t('Save')}
@@ -190,7 +190,7 @@
<div class="flex justify-end pt-3 text-sm font-medium">
<button
class=" px-4 py-2 bg-emerald-600 hover:bg-emerald-700 text-gray-100 transition rounded"
class=" px-4 py-2 bg-emerald-700 hover:bg-emerald-800 text-gray-100 transition rounded-lg"
type="submit"
>
{$i18n.t('Save')}
@@ -15,7 +15,7 @@
<Modal bind:show>
<div>
<div class=" flex justify-between dark:text-gray-300 px-5 py-4">
<div class=" flex justify-between dark:text-gray-300 px-5 pt-4 pb-2">
<div class=" text-lg font-medium self-center">{$i18n.t('Admin Settings')}</div>
<button
class="self-center"
@@ -35,7 +35,6 @@
</svg>
</button>
</div>
<hr class=" dark:border-gray-800" />
<div class="flex flex-col md:flex-row w-full p-4 md:space-x-4">
<div
@@ -0,0 +1,144 @@
<script lang="ts">
import { toast } from 'svelte-sonner';
import dayjs from 'dayjs';
import { getContext, createEventDispatcher } from 'svelte';
const dispatch = createEventDispatcher();
import Modal from '$lib/components/common/Modal.svelte';
import { getChatListByUserId, deleteChatById, getArchivedChatList } from '$lib/apis/chats';
import Tooltip from '$lib/components/common/Tooltip.svelte';
const i18n = getContext('i18n');
export let show = false;
export let user;
let chats = [];
const deleteChatHandler = async (chatId) => {
const res = await deleteChatById(localStorage.token, chatId).catch((error) => {
toast.error(error);
});
chats = await getChatListByUserId(localStorage.token, user.id);
};
$: if (show) {
(async () => {
if (user.id) {
chats = await getChatListByUserId(localStorage.token, user.id);
}
})();
}
</script>
<Modal size="lg" bind:show>
<div>
<div class=" flex justify-between dark:text-gray-300 px-5 py-4">
<div class=" text-lg font-medium self-center capitalize">
{$i18n.t("{{user}}'s Chats", { user: user.name })}
</div>
<button
class="self-center"
on:click={() => {
show = false;
}}
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
class="w-5 h-5"
>
<path
d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z"
/>
</svg>
</button>
</div>
<hr class=" dark:border-gray-850" />
<div class="flex flex-col md:flex-row w-full px-5 py-4 md:space-x-4 dark:text-gray-200">
<div class=" flex flex-col w-full sm:flex-row sm:justify-center sm:space-x-6">
{#if chats.length > 0}
<div class="text-left text-sm w-full mb-4 max-h-[22rem] overflow-y-scroll">
<div class="relative overflow-x-auto">
<table class="w-full text-sm text-left text-gray-600 dark:text-gray-400 table-auto">
<thead
class="text-xs text-gray-700 uppercase bg-transparent dark:text-gray-200 border-b-2 dark:border-gray-800"
>
<tr>
<th scope="col" class="px-3 py-2"> {$i18n.t('Name')} </th>
<th scope="col" class="px-3 py-2 hidden md:flex"> {$i18n.t('Created at')} </th>
<th scope="col" class="px-3 py-2 text-right" />
</tr>
</thead>
<tbody>
{#each chats as chat, idx}
<tr
class="bg-transparent {idx !== chats.length - 1 &&
'border-b'} dark:bg-gray-900 dark:border-gray-850 text-xs"
>
<td class="px-3 py-1 w-2/3">
<a href="/s/{chat.id}" target="_blank">
<div class=" underline line-clamp-1">
{chat.title}
</div>
</a>
</td>
<td class=" px-3 py-1 hidden md:flex h-[2.5rem]">
<div class="my-auto">
{dayjs(chat.created_at * 1000).format($i18n.t('MMMM DD, YYYY HH:mm'))}
</div>
</td>
<td class="px-3 py-1 text-right">
<div class="flex justify-end w-full">
<Tooltip content={$i18n.t('Delete Chat')}>
<button
class="self-center w-fit text-sm px-2 py-2 hover:bg-black/5 dark:hover:bg-white/5 rounded-xl"
on:click={async () => {
deleteChatHandler(chat.id);
}}
>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="w-4 h-4"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="m14.74 9-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 0 1-2.244 2.077H8.084a2.25 2.25 0 0 1-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 0 0-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 0 1 3.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 0 0-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 0 0-7.5 0"
/>
</svg>
</button>
</Tooltip>
</div>
</td>
</tr>
{/each}
</tbody>
</table>
</div>
<!-- {#each chats as chat}
<div>
{JSON.stringify(chat)}
</div>
{/each} -->
</div>
{:else}
<div class="text-left text-sm w-full mb-8">
{user.name}
{$i18n.t('has no conversations.')}
</div>
{/if}
</div>
</div>
</div>
</Modal>

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