Compare commits

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

This allows DOCS_DIR to be mounted from a different path outside of DATA_DIR, or falls back to the previous behaviour if DOCS_DIR is not in the environment
2024-04-25 00:40:39 +02:00
Timothy Jaeryang Baek 488f448f04 Merge pull request #1732 from dannyl1u/fix/chat-link 2024-04-24 14:36:13 -07:00
Danny Liu 95295061d0 fix: config var not defined 2024-04-24 14:31:03 -07:00
Steven Kreitzer adb009f388 Merge branch 'dev' into buroa/hybrid-search 2024-04-24 14:51:49 -05:00
Steven Kreitzer c0259aad67 feat: hybrid search and reranking support 2024-04-24 07:55:10 -05:00
Silentoplayz 53b277e575 Update pull_request_template.md
fix
2024-04-24 06:02:25 +00:00
Silentoplayz 29c6b253ca Update pull_request_template.md
Okay, no warning then.
2024-04-23 11:17:32 +00:00
Silentoplayz 4a536484a6 Update pull_request_template.md
Added failure to follow template warning
Made more concise
2024-04-23 11:14:54 +00:00
Steven Kreitzer db801aee79 Merge branch 'dev' into buroa/hybrid-search 2024-04-22 18:35:32 -05:00
Steven Kreitzer 4e0b32b505 feat: hybrid search 2024-04-22 18:33:43 -05:00
Silentoplayz a615308111 Update pull_request_template.md
Fix
2024-04-22 07:19:09 +00:00
Silentoplayz dcb32f78fe Update pull_request_template.md
Overhaul
2024-04-22 07:11:59 +00:00
Entaigner 4570f6fb0e Chose between "docker-compose" and "docker compose" in confirm_remove.sh 2024-04-19 20:19:24 +02:00
Entaigner e1e66f708f Chose between "docker-compose" and "docker compose" in Makefile 2024-04-19 19:30:25 +02:00
179 changed files with 14678 additions and 3460 deletions
+2 -1
View File
@@ -10,7 +10,8 @@ OPENAI_API_KEY=''
# DO NOT TRACK
SCARF_NO_ANALYTICS=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"
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',
+12
View File
@@ -0,0 +1,12 @@
version: 2
updates:
- package-ecosystem: pip
directory: '/backend'
schedule:
interval: daily
time: '13:00'
- package-ecosystem: 'github-actions'
directory: '/'
schedule:
# Check for updates to GitHub Actions every week
interval: 'weekly'
+25 -6
View File
@@ -1,15 +1,18 @@
## Pull Request Checklist
- [ ] **Target branch:** Pull requests should target the `dev` branch.
- [ ] **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 +20,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]
@@ -29,6 +29,9 @@ jobs:
- name: Format Frontend
run: npm run format
- name: Run i18next
run: npm run i18n:parse
- name: Check for Changes After Format
run: git diff --exit-code
+195
View File
@@ -0,0 +1,195 @@
name: Integration Test
on:
push:
branches:
- main
- dev
pull_request:
branches:
- main
- dev
jobs:
cypress-run:
name: Run Cypress Integration Tests
runs-on: ubuntu-latest
steps:
- name: Checkout Repository
uses: actions/checkout@v4
- name: Build and run Compose Stack
run: |
docker compose --file docker-compose.yaml --file docker-compose.api.yaml up --detach --build
- name: Wait for Ollama to be up
timeout-minutes: 5
run: |
until curl --output /dev/null --silent --fail http://localhost:11434; do
printf '.'
sleep 1
done
echo "Service is up!"
- name: Preload Ollama model
run: |
docker exec ollama ollama pull qwen:0.5b-chat-v1.5-q2_K
- name: Cypress run
uses: cypress-io/github-action@v6
with:
browser: chrome
wait-on: 'http://localhost:3000'
config: baseUrl=http://localhost:3000
- uses: actions/upload-artifact@v4
if: always()
name: Upload Cypress videos
with:
name: cypress-videos
path: cypress/videos
if-no-files-found: ignore
- name: Extract Compose logs
if: always()
run: |
docker compose logs > compose-logs.txt
- uses: actions/upload-artifact@v4
if: always()
name: Upload Compose logs
with:
name: compose-logs
path: compose-logs.txt
if-no-files-found: ignore
migration_test:
name: Run Migration Tests
runs-on: ubuntu-latest
services:
postgres:
image: postgres
env:
POSTGRES_PASSWORD: postgres
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 5432:5432
# mysql:
# image: mysql
# env:
# MYSQL_ROOT_PASSWORD: mysql
# MYSQL_DATABASE: mysql
# options: >-
# --health-cmd "mysqladmin ping -h localhost"
# --health-interval 10s
# --health-timeout 5s
# --health-retries 5
# ports:
# - 3306:3306
steps:
- name: Checkout Repository
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@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
+72
View File
@@ -5,6 +5,78 @@ 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.124] - 2024-05-08
### Added
- **🖼️ Improved Chat Sidebar**: Now conveniently displays time ranges and organizes chats by today, yesterday, and more.
- **📜 Citations in RAG Feature**: Easily track the context fed to the LLM with added citations in the RAG feature.
- **🔒 Auth Disable Option**: Introducing the ability to disable authentication. Set 'WEBUI_AUTH' to False to disable authentication. Note: Only applicable for fresh installations without existing users.
- **📹 Enhanced YouTube RAG Pipeline**: Now supports non-English videos for an enriched experience.
- **🔊 Specify OpenAI TTS Models**: Customize your TTS experience by specifying OpenAI TTS models.
- **🔧 Additional Environment Variables**: Discover more environment variables in our comprehensive documentation at Open WebUI Documentation (https://docs.openwebui.com).
- **🌐 Language Support**: Arabic, Finnish, and Hindi added; Improved support for German, Vietnamese, and Chinese.
### Fixed
- **🛠️ Model Selector Styling**: Addressed styling issues for improved user experience.
- **⚠️ Warning Messages**: Resolved backend warning messages.
### Changed
- **📝 Title Generation**: Limited output to 50 tokens.
- **📦 Helm Charts**: Removed Helm charts, now available in a separate repository (https://github.com/open-webui/helm-charts).
## [0.1.123] - 2024-05-02
### Added
- **🎨 New Landing Page Design**: Refreshed design for a more modern look and optimized use of screen space.
- **📹 Youtube RAG Pipeline**: Introduces dedicated RAG pipeline for Youtube videos, enabling interaction with video transcriptions directly.
- **🔧 Enhanced Admin Panel**: Streamlined user management with options to add users directly or in bulk via CSV import.
- **👥 '@' Model Integration**: Easily switch to specific models during conversations; old collaborative chat feature phased out.
- **🌐 Language Enhancements**: Swedish translation added, plus improvements to German, Spanish, and the addition of Doge translation.
### Fixed
- **🗑️ Delete Chat Shortcut**: Addressed issue where shortcut wasn't functioning.
- **🖼️ Modal Closing Bug**: Resolved unexpected closure of modal when dragging from within.
- **✏️ Edit Button Styling**: Fixed styling inconsistency with edit buttons.
- **🌐 Image Generation Compatibility Issue**: Rectified image generation compatibility issue with third-party APIs.
- **📱 iOS PWA Icon Fix**: Corrected iOS PWA home screen icon shape.
- **🔍 Scroll Gesture Bug**: Adjusted gesture sensitivity to prevent accidental activation when scrolling through code on mobile; now requires scrolling from the leftmost side to open the sidebar.
### Changed
- **🔄 Unlimited Context Length**: Advanced settings now allow unlimited max context length (previously limited to 16000).
- **👑 Super Admin Assignment**: The first signup is automatically assigned a super admin role, unchangeable by other admins.
- **🛡️ Admin User Restrictions**: User action buttons from the admin panel are now disabled for users with admin roles.
- **🔝 Default Model Selector**: Set as default model option now exclusively available on the landing page.
## [0.1.122] - 2024-04-27
### Added
- **🌟 Enhanced RAG Pipeline**: Now with hybrid searching via 'BM25', reranking powered by 'CrossEncoder', and configurable relevance score thresholds.
- **🛢️ External Database Support**: Seamlessly connect to custom SQLite or Postgres databases using the 'DATABASE_URL' environment variable.
- **🌐 Remote ChromaDB Support**: Introducing the capability to connect to remote ChromaDB servers.
- **👨‍💼 Improved Admin Panel**: Admins can now conveniently check users' chat lists and last active status directly from the admin panel.
- **🎨 Splash Screen**: Introducing a loading splash screen for a smoother user experience.
- **🌍 Language Support Expansion**: Added support for Bangla (bn-BD), along with enhancements to Chinese, Spanish, and Ukrainian translations.
- **💻 Improved LaTeX Rendering Performance**: Enjoy faster rendering times for LaTeX equations.
- **🔧 More Environment Variables**: Explore additional environment variables in our documentation (https://docs.openwebui.com), including the 'ENABLE_LITELLM' option to manage memory usage.
### Fixed
- **🔧 Ollama Compatibility**: Resolved errors occurring when Ollama server version isn't an integer, such as SHA builds or RCs.
- **🐛 Various OpenAI API Issues**: Addressed several issues related to the OpenAI API.
- **🛑 Stop Sequence Issue**: Fixed the problem where the stop sequence with a backslash '\' was not functioning.
- **🔤 Font Fallback**: Corrected font fallback issue.
### Changed
- **⌨️ Prompt Input Behavior on Mobile**: Enter key prompt submission disabled on mobile devices for improved user experience.
## [0.1.121] - 2024-04-24
### Fixed
+16 -5
View File
@@ -8,8 +8,9 @@ ARG USE_CUDA_VER=cu121
# any sentence transformer model; models to use can be found at https://huggingface.co/models?library=sentence-transformers
# Leaderboard: https://huggingface.co/spaces/mteb/leaderboard
# for better performance and multilangauge support use "intfloat/multilingual-e5-large" (~2.5GB) or "intfloat/multilingual-e5-base" (~1.5GB)
# IMPORTANT: If you change the default model (sentence-transformers/all-MiniLM-L6-v2) and vice versa, you aren't able to use RAG Chat with your previous documents loaded in the WebUI! You need to re-embed them.
# IMPORTANT: If you change the embedding model (sentence-transformers/all-MiniLM-L6-v2) and vice versa, you aren't able to use RAG Chat with your previous documents loaded in the WebUI! You need to re-embed them.
ARG USE_EMBEDDING_MODEL=sentence-transformers/all-MiniLM-L6-v2
ARG USE_RERANKING_MODEL=""
######## WebUI frontend ########
FROM --platform=$BUILDPLATFORM node:21-alpine3.19 as build
@@ -30,6 +31,7 @@ ARG USE_CUDA
ARG USE_OLLAMA
ARG USE_CUDA_VER
ARG USE_EMBEDDING_MODEL
ARG USE_RERANKING_MODEL
## Basis ##
ENV ENV=prod \
@@ -38,7 +40,8 @@ ENV ENV=prod \
USE_OLLAMA_DOCKER=${USE_OLLAMA} \
USE_CUDA_DOCKER=${USE_CUDA} \
USE_CUDA_DOCKER_VER=${USE_CUDA_VER} \
USE_EMBEDDING_MODEL_DOCKER=${USE_EMBEDDING_MODEL}
USE_EMBEDDING_MODEL_DOCKER=${USE_EMBEDDING_MODEL} \
USE_RERANKING_MODEL_DOCKER=${USE_RERANKING_MODEL}
## Basis URL Config ##
ENV OLLAMA_BASE_URL="/ollama" \
@@ -48,7 +51,8 @@ ENV OLLAMA_BASE_URL="/ollama" \
ENV OPENAI_API_KEY="" \
WEBUI_SECRET_KEY="" \
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
@@ -62,12 +66,19 @@ ENV WHISPER_MODEL="base" \
## RAG Embedding model settings ##
ENV RAG_EMBEDDING_MODEL="$USE_EMBEDDING_MODEL_DOCKER" \
RAG_EMBEDDING_MODEL_DIR="/app/backend/data/cache/embedding/models" \
RAG_RERANKING_MODEL="$USE_RERANKING_MODEL_DOCKER" \
SENTENCE_TRANSFORMERS_HOME="/app/backend/data/cache/embedding/models"
## Hugging Face download cache ##
ENV HF_HOME="/app/backend/data/cache/embedding/models"
#### Other models ##########################################################
WORKDIR /app/backend
ENV HOME /root
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
@@ -123,4 +134,4 @@ COPY ./backend .
EXPOSE 8080
CMD [ "bash", "start.sh"]
CMD [ "bash", "start.sh"]
+14 -8
View File
@@ -1,27 +1,33 @@
ifneq ($(shell which docker-compose 2>/dev/null),)
DOCKER_COMPOSE := docker-compose
else
DOCKER_COMPOSE := docker compose
endif
install:
@docker-compose up -d
$(DOCKER_COMPOSE) up -d
remove:
@chmod +x confirm_remove.sh
@./confirm_remove.sh
start:
@docker-compose start
$(DOCKER_COMPOSE) start
startAndBuild:
docker-compose up -d --build
$(DOCKER_COMPOSE) up -d --build
stop:
@docker-compose stop
$(DOCKER_COMPOSE) stop
update:
# Calls the LLM update script
chmod +x update_ollama_models.sh
@./update_ollama_models.sh
@git pull
@docker-compose down
$(DOCKER_COMPOSE) down
# Make sure the ollama-webui container is stopped before rebuilding
@docker stop open-webui || true
@docker-compose up --build -d
@docker-compose start
$(DOCKER_COMPOSE) up --build -d
$(DOCKER_COMPOSE) start
+1 -1
View File
@@ -152,7 +152,7 @@ We offer various installation alternatives, including non-Docker methods, Docker
### Troubleshooting
Encountering connection issues? Our [Open WebUI Documentation](https://docs.openwebui.com/getting-started/troubleshooting/) has got you covered. For further assistance and to join our vibrant community, visit the [Open WebUI Discord](https://discord.gg/5rJgQTnV4s).
Encountering connection issues? Our [Open WebUI Documentation](https://docs.openwebui.com/troubleshooting/) has got you covered. For further assistance and to join our vibrant community, visit the [Open WebUI Discord](https://discord.gg/5rJgQTnV4s).
### Keeping Your Docker Installation Up-to-Date
+12
View File
@@ -43,6 +43,8 @@ from config import (
DEVICE_TYPE,
AUDIO_OPENAI_API_BASE_URL,
AUDIO_OPENAI_API_KEY,
AUDIO_OPENAI_API_MODEL,
AUDIO_OPENAI_API_VOICE,
)
log = logging.getLogger(__name__)
@@ -60,6 +62,8 @@ app.add_middleware(
app.state.OPENAI_API_BASE_URL = AUDIO_OPENAI_API_BASE_URL
app.state.OPENAI_API_KEY = AUDIO_OPENAI_API_KEY
app.state.OPENAI_API_MODEL = AUDIO_OPENAI_API_MODEL
app.state.OPENAI_API_VOICE = AUDIO_OPENAI_API_VOICE
# setting device type for whisper model
whisper_device_type = DEVICE_TYPE if DEVICE_TYPE and DEVICE_TYPE == "cuda" else "cpu"
@@ -72,6 +76,8 @@ SPEECH_CACHE_DIR.mkdir(parents=True, exist_ok=True)
class OpenAIConfigUpdateForm(BaseModel):
url: str
key: str
model: str
speaker: str
@app.get("/config")
@@ -79,6 +85,8 @@ async def get_openai_config(user=Depends(get_admin_user)):
return {
"OPENAI_API_BASE_URL": app.state.OPENAI_API_BASE_URL,
"OPENAI_API_KEY": app.state.OPENAI_API_KEY,
"OPENAI_API_MODEL": app.state.OPENAI_API_MODEL,
"OPENAI_API_VOICE": app.state.OPENAI_API_VOICE,
}
@@ -91,11 +99,15 @@ async def update_openai_config(
app.state.OPENAI_API_BASE_URL = form_data.url
app.state.OPENAI_API_KEY = form_data.key
app.state.OPENAI_API_MODEL = form_data.model
app.state.OPENAI_API_VOICE = form_data.speaker
return {
"status": True,
"OPENAI_API_BASE_URL": app.state.OPENAI_API_BASE_URL,
"OPENAI_API_KEY": app.state.OPENAI_API_KEY,
"OPENAI_API_MODEL": app.state.OPENAI_API_MODEL,
"OPENAI_API_VOICE": app.state.OPENAI_API_VOICE,
}
+59 -28
View File
@@ -24,6 +24,7 @@ from utils.misc import calculate_sha256
from typing import Optional
from pydantic import BaseModel
from pathlib import Path
import mimetypes
import uuid
import base64
import json
@@ -32,11 +33,15 @@ import logging
from config import (
SRC_LOG_LEVELS,
CACHE_DIR,
IMAGE_GENERATION_ENGINE,
ENABLE_IMAGE_GENERATION,
AUTOMATIC1111_BASE_URL,
COMFYUI_BASE_URL,
IMAGES_OPENAI_API_BASE_URL,
IMAGES_OPENAI_API_KEY,
IMAGE_GENERATION_MODEL,
IMAGE_SIZE,
IMAGE_STEPS,
)
@@ -55,21 +60,21 @@ app.add_middleware(
allow_headers=["*"],
)
app.state.ENGINE = ""
app.state.ENGINE = IMAGE_GENERATION_ENGINE
app.state.ENABLED = ENABLE_IMAGE_GENERATION
app.state.OPENAI_API_BASE_URL = IMAGES_OPENAI_API_BASE_URL
app.state.OPENAI_API_KEY = IMAGES_OPENAI_API_KEY
app.state.MODEL = ""
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")
@@ -311,35 +316,61 @@ class GenerateImageForm(BaseModel):
def save_b64_image(b64_str):
image_id = str(uuid.uuid4())
file_path = IMAGE_CACHE_DIR.joinpath(f"{image_id}.png")
try:
# Split the base64 string to get the actual image data
img_data = base64.b64decode(b64_str)
image_id = str(uuid.uuid4())
# Write the image data to a file
with open(file_path, "wb") as f:
f.write(img_data)
if "," in b64_str:
header, encoded = b64_str.split(",", 1)
mime_type = header.split(";")[0]
img_data = base64.b64decode(encoded)
image_format = mimetypes.guess_extension(mime_type)
image_filename = f"{image_id}{image_format}"
file_path = IMAGE_CACHE_DIR / f"{image_filename}"
with open(file_path, "wb") as f:
f.write(img_data)
return image_filename
else:
image_filename = f"{image_id}.png"
file_path = IMAGE_CACHE_DIR.joinpath(image_filename)
img_data = base64.b64decode(b64_str)
# Write the image data to a file
with open(file_path, "wb") as f:
f.write(img_data)
return image_filename
return image_id
except Exception as e:
log.error(f"Error saving image: {e}")
log.exception(f"Error saving image: {e}")
return None
def save_url_image(url):
image_id = str(uuid.uuid4())
file_path = IMAGE_CACHE_DIR.joinpath(f"{image_id}.png")
try:
r = requests.get(url)
r.raise_for_status()
if r.headers["content-type"].split("/")[0] == "image":
with open(file_path, "wb") as image_file:
image_file.write(r.content)
mime_type = r.headers["content-type"]
image_format = mimetypes.guess_extension(mime_type)
if not image_format:
raise ValueError("Could not determine image type from MIME type")
image_filename = f"{image_id}{image_format}"
file_path = IMAGE_CACHE_DIR.joinpath(f"{image_filename}")
with open(file_path, "wb") as image_file:
for chunk in r.iter_content(chunk_size=8192):
image_file.write(chunk)
return image_filename
else:
log.error(f"Url does not point to an image.")
return None
return image_id
except Exception as e:
log.exception(f"Error saving image: {e}")
return None
@@ -381,9 +412,9 @@ def generate_image(
images = []
for image in res["data"]:
image_id = save_b64_image(image["b64_json"])
images.append({"url": f"/cache/image/generations/{image_id}.png"})
file_body_path = IMAGE_CACHE_DIR.joinpath(f"{image_id}.json")
image_filename = save_b64_image(image["b64_json"])
images.append({"url": f"/cache/image/generations/{image_filename}"})
file_body_path = IMAGE_CACHE_DIR.joinpath(f"{image_filename}.json")
with open(file_body_path, "w") as f:
json.dump(data, f)
@@ -418,9 +449,9 @@ def generate_image(
images = []
for image in res["data"]:
image_id = save_url_image(image["url"])
images.append({"url": f"/cache/image/generations/{image_id}.png"})
file_body_path = IMAGE_CACHE_DIR.joinpath(f"{image_id}.json")
image_filename = save_url_image(image["url"])
images.append({"url": f"/cache/image/generations/{image_filename}"})
file_body_path = IMAGE_CACHE_DIR.joinpath(f"{image_filename}.json")
with open(file_body_path, "w") as f:
json.dump(data.model_dump(exclude_none=True), f)
@@ -456,9 +487,9 @@ def generate_image(
images = []
for image in res["images"]:
image_id = save_b64_image(image)
images.append({"url": f"/cache/image/generations/{image_id}.png"})
file_body_path = IMAGE_CACHE_DIR.joinpath(f"{image_id}.json")
image_filename = save_b64_image(image)
images.append({"url": f"/cache/image/generations/{image_filename}"})
file_body_path = IMAGE_CACHE_DIR.joinpath(f"{image_filename}.json")
with open(file_body_path, "w") as f:
json.dump({**data, "info": res["info"]}, f)
+63 -38
View File
@@ -21,18 +21,25 @@ 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,
)
import warnings
warnings.simplefilter("ignore")
from litellm.utils import get_llm_provider
import asyncio
@@ -57,11 +64,20 @@ LITELLM_CONFIG_DIR = f"{DATA_DIR}/litellm/config.yaml"
with open(LITELLM_CONFIG_DIR, "r") as file:
litellm_config = yaml.safe_load(file)
app.state.ENABLE = ENABLE_LITELLM
app.state.CONFIG = litellm_config
# 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 run_background_process(command):
global background_process
@@ -70,9 +86,11 @@ async def run_background_process(command):
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
*command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, env=env
)
background_process = process
log.info("Subprocess started successfully.")
@@ -130,7 +148,7 @@ async def startup_event():
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
@@ -198,49 +216,56 @@ async def update_config(form_data: LiteLLMConfigForm, user=Depends(get_admin_use
@app.get("/models")
@app.get("/v1/models")
async def get_models(user=Depends(get_current_user)):
while not background_process:
await asyncio.sleep(0.1)
url = f"http://localhost:{LITELLM_PROXY_PORT}/v1"
r = None
try:
r = requests.request(method="GET", url=f"{url}/models")
r.raise_for_status()
if app.state.ENABLE:
while not background_process:
await asyncio.sleep(0.1)
data = r.json()
url = f"http://localhost:{LITELLM_PROXY_PORT}/v1"
r = None
try:
r = requests.request(method="GET", url=f"{url}/models")
r.raise_for_status()
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"],
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:
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}"
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": [
{
"id": model["model_name"],
"object": "model",
"created": int(time.time()),
"owned_by": "openai",
}
for model in app.state.CONFIG["model_list"]
],
"data": [],
"object": "list",
}
+92 -12
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
@@ -24,19 +25,25 @@ import uuid
import aiohttp
import asyncio
import logging
import time
from urllib.parse import urlparse
from typing import Optional, List, Union
from apps.web.models.users import Users
from constants import ERROR_MESSAGES
from utils.utils import decode_token, get_current_user, get_admin_user
from utils.utils import (
decode_token,
get_current_user,
get_verified_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 +62,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
@@ -163,12 +170,12 @@ async def get_all_models():
@app.get("/api/tags")
@app.get("/api/tags/{url_idx}")
async def get_ollama_tags(
url_idx: Optional[int] = None, user=Depends(get_current_user)
url_idx: Optional[int] = None, user=Depends(get_verified_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["models"] = list(
filter(
@@ -216,7 +223,9 @@ 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("-")[0].split("."))),
key=lambda x: tuple(
map(int, re.sub(r"^v|-.*", "", x["version"]).split("."))
),
)
return {"version": lowest_version["version"]}
@@ -560,7 +569,7 @@ async def delete_model(
@app.post("/api/show")
async def show_model_info(form_data: ModelNameForm, user=Depends(get_current_user)):
async def show_model_info(form_data: ModelNameForm, user=Depends(get_verified_user)):
if form_data.name not in app.state.MODELS:
raise HTTPException(
status_code=400,
@@ -609,7 +618,7 @@ class GenerateEmbeddingsForm(BaseModel):
async def generate_embeddings(
form_data: GenerateEmbeddingsForm,
url_idx: Optional[int] = None,
user=Depends(get_current_user),
user=Depends(get_verified_user),
):
if url_idx == None:
model = form_data.model
@@ -727,7 +736,7 @@ class GenerateCompletionForm(BaseModel):
async def generate_completion(
form_data: GenerateCompletionForm,
url_idx: Optional[int] = None,
user=Depends(get_current_user),
user=Depends(get_verified_user),
):
if url_idx == None:
@@ -830,7 +839,7 @@ class GenerateChatCompletionForm(BaseModel):
async def generate_chat_completion(
form_data: GenerateChatCompletionForm,
url_idx: Optional[int] = None,
user=Depends(get_current_user),
user=Depends(get_verified_user),
):
if url_idx == None:
@@ -939,7 +948,7 @@ class OpenAIChatCompletionForm(BaseModel):
async def generate_openai_chat_completion(
form_data: OpenAIChatCompletionForm,
url_idx: Optional[int] = None,
user=Depends(get_current_user),
user=Depends(get_verified_user),
):
if url_idx == None:
@@ -1023,6 +1032,75 @@ async def generate_openai_chat_completion(
)
@app.get("/v1/models")
@app.get("/v1/models/{url_idx}")
async def get_openai_models(
url_idx: Optional[int] = None,
user=Depends(get_verified_user),
):
if url_idx == None:
models = await get_all_models()
if app.state.ENABLE_MODEL_FILTER:
if user.role == "user":
models["models"] = list(
filter(
lambda model: model["name"] in app.state.MODEL_FILTER_LIST,
models["models"],
)
)
return {
"data": [
{
"id": model["model"],
"object": "model",
"created": int(time.time()),
"owned_by": "openai",
}
for model in models["models"]
],
"object": "list",
}
else:
url = app.state.OLLAMA_BASE_URLS[url_idx]
try:
r = requests.request(method="GET", url=f"{url}/api/tags")
r.raise_for_status()
models = r.json()
return {
"data": [
{
"id": model["model"],
"object": "model",
"created": int(time.time()),
"owned_by": "openai",
}
for model in models["models"]
],
"object": "list",
}
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 HTTPException(
status_code=r.status_code if r else 500,
detail=error_detail,
)
class UrlForm(BaseModel):
url: str
@@ -1238,7 +1316,9 @@ def upload_model(file: UploadFile = File(...), url_idx: Optional[int] = None):
@app.api_route("/{path:path}", methods=["GET", "POST", "PUT", "DELETE"])
async def deprecated_proxy(path: str, request: Request, user=Depends(get_current_user)):
async def deprecated_proxy(
path: str, request: Request, user=Depends(get_verified_user)
):
url = app.state.OLLAMA_BASE_URLS[0]
target_url = f"{url}/{path}"
+8 -5
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
@@ -171,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):
@@ -199,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,
)
@@ -225,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(
+305 -111
View File
@@ -28,9 +28,15 @@ 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
@@ -39,8 +45,6 @@ import json
import sentence_transformers
from apps.ollama.main import generate_ollama_embeddings, GenerateEmbeddingsForm
from apps.web.models.documents import (
Documents,
DocumentForm,
@@ -48,9 +52,12 @@ from apps.web.models.documents import (
)
from apps.rag.utils import (
query_embeddings_doc,
query_embeddings_collection,
generate_openai_embeddings,
get_model_path,
get_embedding_function,
query_doc,
query_doc_with_hybrid_search,
query_collection,
query_collection_with_hybrid_search,
)
from utils.misc import (
@@ -60,13 +67,23 @@ 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_AUTO_UPDATE,
RAG_EMBEDDING_MODEL_TRUST_REMOTE_CODE,
ENABLE_RAG_HYBRID_SEARCH,
ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION,
RAG_RERANKING_MODEL,
PDF_EXTRACT_IMAGES,
RAG_RERANKING_MODEL_AUTO_UPDATE,
RAG_RERANKING_MODEL_TRUST_REMOTE_CODE,
RAG_OPENAI_API_BASE_URL,
RAG_OPENAI_API_KEY,
DEVICE_TYPE,
@@ -74,6 +91,8 @@ from config import (
CHUNK_SIZE,
CHUNK_OVERLAP,
RAG_TEMPLATE,
ENABLE_RAG_LOCAL_WEB_FETCH,
YOUTUBE_LOADER_LANGUAGE,
)
from constants import ERROR_MESSAGES
@@ -83,31 +102,83 @@ log.setLevel(SRC_LOG_LEVELS["RAG"])
app = FastAPI()
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.ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION = (
ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION
)
app.state.TOP_K = 4
app.state.CHUNK_SIZE = CHUNK_SIZE
app.state.CHUNK_OVERLAP = CHUNK_OVERLAP
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 = False
app.state.PDF_EXTRACT_IMAGES = PDF_EXTRACT_IMAGES
if app.state.RAG_EMBEDDING_ENGINE == "":
app.state.sentence_transformer_ef = sentence_transformers.SentenceTransformer(
app.state.RAG_EMBEDDING_MODEL,
device=DEVICE_TYPE,
trust_remote_code=RAG_EMBEDDING_MODEL_TRUST_REMOTE_CODE,
)
app.state.YOUTUBE_LOADER_LANGUAGE = YOUTUBE_LOADER_LANGUAGE
app.state.YOUTUBE_LOADER_TRANSLATION = None
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,
@@ -121,7 +192,7 @@ class CollectionNameForm(BaseModel):
collection_name: Optional[str] = "test"
class StoreWebForm(CollectionNameForm):
class UrlForm(CollectionNameForm):
url: str
@@ -134,6 +205,7 @@ async def get_status():
"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,
}
@@ -150,6 +222,11 @@ async def get_embedding_config(user=Depends(get_admin_user)):
}
@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
@@ -170,22 +247,22 @@ async def update_embedding_config(
)
try:
app.state.RAG_EMBEDDING_ENGINE = form_data.embedding_engine
app.state.RAG_EMBEDDING_MODEL = form_data.embedding_model
if app.state.RAG_EMBEDDING_ENGINE in ["ollama", "openai"]:
app.state.RAG_EMBEDDING_MODEL = form_data.embedding_model
app.state.sentence_transformer_ef = None
if form_data.openai_config != None:
app.state.OPENAI_API_BASE_URL = form_data.openai_config.url
app.state.OPENAI_API_KEY = form_data.openai_config.key
else:
sentence_transformer_ef = sentence_transformers.SentenceTransformer(
app.state.RAG_EMBEDDING_MODEL,
device=DEVICE_TYPE,
trust_remote_code=RAG_EMBEDDING_MODEL_TRUST_REMOTE_CODE,
)
app.state.RAG_EMBEDDING_MODEL = form_data.embedding_model
app.state.sentence_transformer_ef = sentence_transformer_ef
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,
@@ -196,7 +273,6 @@ async def update_embedding_config(
"key": app.state.OPENAI_API_KEY,
},
}
except Exception as e:
log.exception(f"Problem updating embedding model: {e}")
raise HTTPException(
@@ -205,6 +281,34 @@ async def update_embedding_config(
)
class RerankingModelUpdateForm(BaseModel):
reranking_model: str
@app.post("/reranking/update")
async def update_reranking_config(
form_data: RerankingModelUpdateForm, user=Depends(get_admin_user)
):
log.info(
f"Updating reranking model: {app.state.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")
async def get_rag_config(user=Depends(get_admin_user)):
return {
@@ -214,6 +318,11 @@ async def get_rag_config(user=Depends(get_admin_user)):
"chunk_size": app.state.CHUNK_SIZE,
"chunk_overlap": app.state.CHUNK_OVERLAP,
},
"web_loader_ssl_verification": app.state.ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION,
"youtube": {
"language": app.state.YOUTUBE_LOADER_LANGUAGE,
"translation": app.state.YOUTUBE_LOADER_TRANSLATION,
},
}
@@ -222,16 +331,53 @@ class ChunkParamUpdateForm(BaseModel):
chunk_overlap: int
class YoutubeLoaderConfig(BaseModel):
language: List[str]
translation: Optional[str] = None
class ConfigUpdateForm(BaseModel):
pdf_extract_images: bool
chunk: ChunkParamUpdateForm
pdf_extract_images: Optional[bool] = None
chunk: Optional[ChunkParamUpdateForm] = None
web_loader_ssl_verification: Optional[bool] = None
youtube: Optional[YoutubeLoaderConfig] = None
@app.post("/config/update")
async def update_rag_config(form_data: ConfigUpdateForm, user=Depends(get_admin_user)):
app.state.PDF_EXTRACT_IMAGES = form_data.pdf_extract_images
app.state.CHUNK_SIZE = form_data.chunk.chunk_size
app.state.CHUNK_OVERLAP = form_data.chunk.chunk_overlap
app.state.PDF_EXTRACT_IMAGES = (
form_data.pdf_extract_images
if form_data.pdf_extract_images != None
else app.state.PDF_EXTRACT_IMAGES
)
app.state.CHUNK_SIZE = (
form_data.chunk.chunk_size if form_data.chunk != None else app.state.CHUNK_SIZE
)
app.state.CHUNK_OVERLAP = (
form_data.chunk.chunk_overlap
if form_data.chunk != None
else app.state.CHUNK_OVERLAP
)
app.state.ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION = (
form_data.web_loader_ssl_verification
if form_data.web_loader_ssl_verification != None
else app.state.ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION
)
app.state.YOUTUBE_LOADER_LANGUAGE = (
form_data.youtube.language
if form_data.youtube != None
else app.state.YOUTUBE_LOADER_LANGUAGE
)
app.state.YOUTUBE_LOADER_TRANSLATION = (
form_data.youtube.translation
if form_data.youtube != None
else app.state.YOUTUBE_LOADER_TRANSLATION
)
return {
"status": True,
@@ -240,6 +386,11 @@ async def update_rag_config(form_data: ConfigUpdateForm, user=Depends(get_admin_
"chunk_size": app.state.CHUNK_SIZE,
"chunk_overlap": app.state.CHUNK_OVERLAP,
},
"web_loader_ssl_verification": app.state.ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION,
"youtube": {
"language": app.state.YOUTUBE_LOADER_LANGUAGE,
"translation": app.state.YOUTUBE_LOADER_TRANSLATION,
},
}
@@ -257,12 +408,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")
@@ -271,13 +426,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")
@@ -286,34 +451,22 @@ def query_doc_handler(
user=Depends(get_current_user),
):
try:
if app.state.RAG_EMBEDDING_ENGINE == "":
query_embeddings = app.state.sentence_transformer_ef.encode(
form_data.query
).tolist()
elif app.state.RAG_EMBEDDING_ENGINE == "ollama":
query_embeddings = generate_ollama_embeddings(
GenerateEmbeddingsForm(
**{
"model": app.state.RAG_EMBEDDING_MODEL,
"prompt": form_data.query,
}
)
if app.state.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,
)
elif app.state.RAG_EMBEDDING_ENGINE == "openai":
query_embeddings = generate_openai_embeddings(
model=app.state.RAG_EMBEDDING_MODEL,
text=form_data.query,
key=app.state.OPENAI_API_KEY,
url=app.state.OPENAI_API_BASE_URL,
else:
return query_doc(
collection_name=form_data.collection_name,
query=form_data.query,
embedding_function=app.state.EMBEDDING_FUNCTION,
k=form_data.k if form_data.k else app.state.TOP_K,
)
return query_embeddings_doc(
collection_name=form_data.collection_name,
query=form_data.query,
query_embeddings=query_embeddings,
k=form_data.k if form_data.k else app.state.TOP_K,
)
except Exception as e:
log.exception(e)
raise HTTPException(
@@ -326,6 +479,8 @@ class QueryCollectionsForm(BaseModel):
collection_names: List[str]
query: str
k: Optional[int] = None
r: Optional[float] = None
hybrid: Optional[bool] = None
@app.post("/query/collection")
@@ -334,33 +489,23 @@ def query_collection_handler(
user=Depends(get_current_user),
):
try:
if app.state.RAG_EMBEDDING_ENGINE == "":
query_embeddings = app.state.sentence_transformer_ef.encode(
form_data.query
).tolist()
elif app.state.RAG_EMBEDDING_ENGINE == "ollama":
query_embeddings = generate_ollama_embeddings(
GenerateEmbeddingsForm(
**{
"model": app.state.RAG_EMBEDDING_MODEL,
"prompt": form_data.query,
}
)
if app.state.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,
)
elif app.state.RAG_EMBEDDING_ENGINE == "openai":
query_embeddings = generate_openai_embeddings(
model=app.state.RAG_EMBEDDING_MODEL,
text=form_data.query,
key=app.state.OPENAI_API_KEY,
url=app.state.OPENAI_API_BASE_URL,
else:
return query_collection(
collection_names=form_data.collection_names,
query=form_data.query,
embedding_function=app.state.EMBEDDING_FUNCTION,
k=form_data.k if form_data.k else app.state.TOP_K,
)
return query_embeddings_collection(
collection_names=form_data.collection_names,
query_embeddings=query_embeddings,
k=form_data.k if form_data.k else app.state.TOP_K,
)
except Exception as e:
log.exception(e)
raise HTTPException(
@@ -369,11 +514,15 @@ def query_collection_handler(
)
@app.post("/web")
def store_web(form_data: StoreWebForm, user=Depends(get_current_user)):
# "https://www.gutenberg.org/files/1727/1727-h/1727-h.htm"
@app.post("/youtube")
def store_youtube_video(form_data: UrlForm, user=Depends(get_current_user)):
try:
loader = WebBaseLoader(form_data.url)
loader = YoutubeLoader.from_youtube_url(
form_data.url,
add_video_info=True,
language=app.state.YOUTUBE_LOADER_LANGUAGE,
translation=app.state.YOUTUBE_LOADER_TRANSLATION,
)
data = loader.load()
collection_name = form_data.collection_name
@@ -394,6 +543,64 @@ def store_web(form_data: StoreWebForm, user=Depends(get_current_user)):
)
@app.post("/web")
def store_web(form_data: UrlForm, user=Depends(get_current_user)):
# "https://www.gutenberg.org/files/1727/1727-h/1727-h.htm"
try:
loader = get_web_loader(
form_data.url, verify_ssl=app.state.ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION
)
data = loader.load()
collection_name = form_data.collection_name
if collection_name == "":
collection_name = calculate_sha256_string(form_data.url)[:63]
store_data_in_vector_db(data, collection_name, overwrite=True)
return {
"status": True,
"collection_name": collection_name,
"filename": form_data.url,
}
except Exception as e:
log.exception(e)
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.DEFAULT(e),
)
def get_web_loader(url: str, verify_ssl: bool = True):
# Check if the URL is valid
if isinstance(validators.url(url), validators.ValidationError):
raise ValueError(ERROR_MESSAGES.INVALID_URL)
if not ENABLE_RAG_LOCAL_WEB_FETCH:
# Local web fetch is disabled, filter out any URLs that resolve to private IP addresses
parsed_url = urllib.parse.urlparse(url)
# Get IPv4 and IPv6 addresses
ipv4_addresses, ipv6_addresses = resolve_hostname(parsed_url.hostname)
# Check if any of the resolved addresses are private
# This is technically still vulnerable to DNS rebinding attacks, as we don't control WebBaseLoader
for ip in ipv4_addresses:
if validators.ipv4(ip, private=True):
raise ValueError(ERROR_MESSAGES.INVALID_URL)
for ip in ipv6_addresses:
if validators.ipv6(ip, private=True):
raise ValueError(ERROR_MESSAGES.INVALID_URL)
return WebBaseLoader(url, verify_ssl=verify_ssl)
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(
@@ -427,8 +634,6 @@ def store_docs_in_vector_db(docs, collection_name, overwrite: bool = False) -> b
log.info(f"store_docs_in_vector_db {docs} {collection_name}")
texts = [doc.page_content for doc in docs]
texts = list(map(lambda x: x.replace("\n", " "), texts))
metadatas = [doc.metadata for doc in docs]
try:
@@ -440,31 +645,20 @@ def store_docs_in_vector_db(docs, collection_name, overwrite: bool = False) -> b
collection = CHROMA_CLIENT.create_collection(name=collection_name)
if app.state.RAG_EMBEDDING_ENGINE == "":
embeddings = app.state.sentence_transformer_ef.encode(texts).tolist()
elif app.state.RAG_EMBEDDING_ENGINE == "ollama":
embeddings = [
generate_ollama_embeddings(
GenerateEmbeddingsForm(
**{"model": app.state.RAG_EMBEDDING_MODEL, "prompt": text}
)
)
for text in texts
]
elif app.state.RAG_EMBEDDING_ENGINE == "openai":
embeddings = [
generate_openai_embeddings(
model=app.state.RAG_EMBEDDING_MODEL,
text=text,
key=app.state.OPENAI_API_KEY,
url=app.state.OPENAI_API_BASE_URL,
)
for text in texts
]
embedding_func = get_embedding_function(
app.state.RAG_EMBEDDING_ENGINE,
app.state.RAG_EMBEDDING_MODEL,
app.state.sentence_transformer_ef,
app.state.OPENAI_API_KEY,
app.state.OPENAI_API_BASE_URL,
)
embedding_texts = list(map(lambda x: x.replace("\n", " "), texts))
embeddings = embedding_func(embedding_texts)
for batch in create_batches(
api=CHROMA_CLIENT,
ids=[str(uuid.uuid1()) for _ in texts],
ids=[str(uuid.uuid4()) for _ in texts],
metadatas=metadatas,
embeddings=embeddings,
documents=texts,
+348 -78
View File
@@ -1,3 +1,4 @@
import os
import logging
import requests
@@ -8,6 +9,16 @@ from apps.ollama.main import (
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
@@ -15,88 +26,164 @@ log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["RAG"])
def query_embeddings_doc(collection_name: str, query: str, query_embeddings, k: int):
def query_doc(
collection_name: str,
query: str,
embedding_function,
k: int,
):
try:
# if you use docker use the model from the environment variable
log.info(f"query_embeddings_doc {query_embeddings}")
collection = CHROMA_CLIENT.get_collection(name=collection_name)
query_embeddings = embedding_function(query)
result = collection.query(
query_embeddings=[query_embeddings],
n_results=k,
)
log.info(f"query_embeddings_doc:result {result}")
log.info(f"query_doc:result {result}")
return result
except Exception as e:
raise e
def merge_and_sort_query_results(query_results, k):
def query_doc_with_hybrid_search(
collection_name: str,
query: str,
embedding_function,
k: int,
reranking_function,
r: float,
):
try:
collection = CHROMA_CLIENT.get_collection(name=collection_name)
documents = collection.get() # get all documents
bm25_retriever = BM25Retriever.from_texts(
texts=documents.get("documents"),
metadatas=documents.get("metadatas"),
)
bm25_retriever.k = k
chroma_retriever = ChromaRetriever(
collection=collection,
embedding_function=embedding_function,
top_n=k,
)
ensemble_retriever = EnsembleRetriever(
retrievers=[bm25_retriever, chroma_retriever], weights=[0.5, 0.5]
)
compressor = RerankCompressor(
embedding_function=embedding_function,
top_n=k,
reranking_function=reranking_function,
r_score=r,
)
compression_retriever = ContextualCompressionRetriever(
base_compressor=compressor, base_retriever=ensemble_retriever
)
result = compression_retriever.invoke(query)
result = {
"distances": [[d.metadata.get("score") for d in result]],
"documents": [[d.page_content for d in result]],
"metadatas": [[d.metadata for d in result]],
}
log.info(f"query_doc_with_hybrid_search:result {result}")
return result
except Exception as e:
raise e
def merge_and_sort_query_results(query_results, k, reverse=False):
# Initialize lists to store combined data
combined_ids = []
combined_distances = []
combined_metadatas = []
combined_documents = []
combined_metadatas = []
# Combine data from each dictionary
for data in query_results:
combined_ids.extend(data["ids"][0])
combined_distances.extend(data["distances"][0])
combined_metadatas.extend(data["metadatas"][0])
combined_documents.extend(data["documents"][0])
combined_metadatas.extend(data["metadatas"][0])
# Create a list of tuples (distance, id, metadata, document)
combined = list(
zip(combined_distances, combined_ids, combined_metadatas, combined_documents)
)
# Create a list of tuples (distance, document, metadata)
combined = list(zip(combined_distances, combined_documents, combined_metadatas))
# Sort the list based on distances
combined.sort(key=lambda x: x[0])
combined.sort(key=lambda x: x[0], reverse=reverse)
# Unzip the sorted list
sorted_distances, sorted_ids, sorted_metadatas, sorted_documents = zip(*combined)
# We don't have anything :-(
if not combined:
sorted_distances = []
sorted_documents = []
sorted_metadatas = []
else:
# Unzip the sorted list
sorted_distances, sorted_documents, sorted_metadatas = zip(*combined)
# Slicing the lists to include only k elements
sorted_distances = list(sorted_distances)[:k]
sorted_ids = list(sorted_ids)[:k]
sorted_metadatas = list(sorted_metadatas)[:k]
sorted_documents = list(sorted_documents)[:k]
# Slicing the lists to include only k elements
sorted_distances = list(sorted_distances)[:k]
sorted_documents = list(sorted_documents)[:k]
sorted_metadatas = list(sorted_metadatas)[:k]
# Create the output dictionary
merged_query_results = {
"ids": [sorted_ids],
result = {
"distances": [sorted_distances],
"metadatas": [sorted_metadatas],
"documents": [sorted_documents],
"embeddings": None,
"uris": None,
"data": None,
"metadatas": [sorted_metadatas],
}
return merged_query_results
return result
def query_embeddings_collection(
collection_names: List[str], query: str, query_embeddings, k: int
def query_collection(
collection_names: List[str],
query: str,
embedding_function,
k: int,
):
results = []
log.info(f"query_embeddings_collection {query_embeddings}")
for collection_name in collection_names:
try:
result = query_embeddings_doc(
result = query_doc(
collection_name=collection_name,
query=query,
query_embeddings=query_embeddings,
k=k,
embedding_function=embedding_function,
)
results.append(result)
except:
pass
return merge_and_sort_query_results(results, k=k)
return merge_and_sort_query_results(results, k)
def query_collection_with_hybrid_search(
collection_names: List[str],
query: str,
embedding_function,
k: int,
reranking_function,
r: float,
):
results = []
for collection_name in collection_names:
try:
result = query_doc_with_hybrid_search(
collection_name=collection_name,
query=query,
embedding_function=embedding_function,
k=k,
reranking_function=reranking_function,
r=r,
)
results.append(result)
except:
pass
return merge_and_sort_query_results(results, k=k, reverse=True)
def rag_template(template: str, context: str, query: str):
@@ -105,20 +192,53 @@ def rag_template(template: str, context: str, query: str):
return template
def rag_messages(
docs,
messages,
template,
k,
def get_embedding_function(
embedding_engine,
embedding_model,
embedding_function,
openai_key,
openai_url,
):
log.debug(
f"docs: {docs} {messages} {embedding_engine} {embedding_model} {embedding_function} {openai_key} {openai_url}"
)
if embedding_engine == "":
return lambda query: embedding_function.encode(query).tolist()
elif embedding_engine in ["ollama", "openai"]:
if embedding_engine == "ollama":
func = lambda query: generate_ollama_embeddings(
GenerateEmbeddingsForm(
**{
"model": embedding_model,
"prompt": query,
}
)
)
elif embedding_engine == "openai":
func = lambda query: generate_openai_embeddings(
model=embedding_model,
text=query,
key=openai_key,
url=openai_url,
)
def generate_multiple(query, f):
if isinstance(query, list):
return [f(q) for q in query]
else:
return f(query)
return lambda query: generate_multiple(query, func)
def rag_messages(
docs,
messages,
template,
embedding_function,
k,
reranking_function,
r,
hybrid_search,
):
log.debug(f"docs: {docs} {messages} {embedding_function} {reranking_function}")
last_user_message_idx = None
for i in range(len(messages) - 1, -1, -1):
@@ -145,62 +265,74 @@ def rag_messages(
content_type = None
query = ""
extracted_collections = []
relevant_contexts = []
for doc in docs:
context = None
try:
collection_names = (
doc["collection_names"]
if doc["type"] == "collection"
else [doc["collection_name"]]
)
collection_names = set(collection_names).difference(extracted_collections)
if not collection_names:
log.debug(f"skipping {doc} as it has already been extracted")
continue
try:
if doc["type"] == "text":
context = doc["content"]
else:
if embedding_engine == "":
query_embeddings = embedding_function.encode(query).tolist()
elif embedding_engine == "ollama":
query_embeddings = generate_ollama_embeddings(
GenerateEmbeddingsForm(
**{
"model": embedding_model,
"prompt": query,
}
)
)
elif embedding_engine == "openai":
query_embeddings = generate_openai_embeddings(
model=embedding_model,
text=query,
key=openai_key,
url=openai_url,
)
if doc["type"] == "collection":
context = query_embeddings_collection(
collection_names=doc["collection_names"],
if hybrid_search:
context = query_collection_with_hybrid_search(
collection_names=collection_names,
query=query,
query_embeddings=query_embeddings,
embedding_function=embedding_function,
k=k,
reranking_function=reranking_function,
r=r,
)
else:
context = query_embeddings_doc(
collection_name=doc["collection_name"],
context = query_collection(
collection_names=collection_names,
query=query,
query_embeddings=query_embeddings,
embedding_function=embedding_function,
k=k,
)
except Exception as e:
log.exception(e)
context = None
relevant_contexts.append(context)
if context:
relevant_contexts.append({**context, "source": doc})
log.debug(f"relevant_contexts: {relevant_contexts}")
extracted_collections.extend(collection_names)
context_string = ""
citations = []
for context in relevant_contexts:
if context:
context_string += " ".join(context["documents"][0]) + "\n"
try:
if "documents" in context:
context_string += "\n\n".join(
[text for text in context["documents"][0] if text is not None]
)
if "metadatas" in context:
citations.append(
{
"source": context["source"],
"document": context["documents"][0],
"metadata": context["metadatas"][0],
}
)
except Exception as e:
log.exception(e)
context_string = context_string.strip()
ra_content = rag_template(
template=template,
@@ -208,6 +340,8 @@ def rag_messages(
query=query,
)
log.debug(f"ra_content: {ra_content}")
if content_type == "list":
new_content = []
for content_item in user_message["content"]:
@@ -226,7 +360,45 @@ def rag_messages(
messages[last_user_message_idx] = new_user_message
return messages
return messages, citations
def get_model_path(model: str, update_model: bool = False):
# Construct huggingface_hub kwargs with local_files_only to return the snapshot path
cache_dir = os.getenv("SENTENCE_TRANSFORMERS_HOME")
local_files_only = not update_model
snapshot_kwargs = {
"cache_dir": cache_dir,
"local_files_only": local_files_only,
}
log.debug(f"model: {model}")
log.debug(f"snapshot_kwargs: {snapshot_kwargs}")
# Inspiration from upstream sentence_transformers
if (
os.path.exists(model)
or ("\\" in model or model.count("/") > 1)
and local_files_only
):
# If fully qualified path exists, return input, else set repo_id
return model
elif "/" not in model:
# Set valid repo_id for model short-name
model = "sentence-transformers" + "/" + model
snapshot_kwargs["repo_id"] = model
# Attempt to query the huggingface_hub library to determine the local path and/or to update
try:
model_repo_path = snapshot_download(**snapshot_kwargs)
log.debug(f"model_repo_path: {model_repo_path}")
return model_repo_path
except Exception as e:
log.exception(f"Cannot determine model snapshot path: {e}")
return model
def generate_openai_embeddings(
@@ -250,3 +422,101 @@ def generate_openai_embeddings(
except Exception as e:
print(e)
return None
from typing import Any
from langchain_core.retrievers import BaseRetriever
from langchain_core.callbacks import CallbackManagerForRetrieverRun
class ChromaRetriever(BaseRetriever):
collection: Any
embedding_function: Any
top_n: int
def _get_relevant_documents(
self,
query: str,
*,
run_manager: CallbackManagerForRetrieverRun,
) -> List[Document]:
query_embeddings = self.embedding_function(query)
results = self.collection.query(
query_embeddings=[query_embeddings],
n_results=self.top_n,
)
ids = results["ids"][0]
metadatas = results["metadatas"][0]
documents = results["documents"][0]
results = []
for idx in range(len(ids)):
results.append(
Document(
metadata=metadatas[idx],
page_content=documents[idx],
)
)
return results
import operator
from typing import Optional, Sequence
from langchain_core.documents import BaseDocumentCompressor, Document
from langchain_core.callbacks import Callbacks
from langchain_core.pydantic_v1 import Extra
from sentence_transformers import util
class RerankCompressor(BaseDocumentCompressor):
embedding_function: Any
top_n: int
reranking_function: Any
r_score: float
class Config:
extra = Extra.forbid
arbitrary_types_allowed = True
def compress_documents(
self,
documents: Sequence[Document],
query: str,
callbacks: Optional[Callbacks] = None,
) -> Sequence[Document]:
reranking = self.reranking_function is not None
if reranking:
scores = self.reranking_function.predict(
[(query, doc.page_content) for doc in documents]
)
else:
query_embedding = self.embedding_function(query)
document_embedding = self.embedding_function(
[doc.page_content for doc in documents]
)
scores = util.cos_sim(query_embedding, document_embedding)[0]
docs_with_scores = list(zip(documents, scores.tolist()))
if self.r_score:
docs_with_scores = [
(d, s) for d, s in docs_with_scores if s >= self.r_score
]
result = sorted(docs_with_scores, key=operator.itemgetter(1), reverse=True)
final_results = []
for doc, doc_score in result[: self.top_n]:
metadata = doc.metadata
metadata["score"] = doc_score
doc = Document(
page_content=doc.page_content,
metadata=metadata,
)
final_results.append(doc)
return final_results
+5 -4
View File
@@ -1,6 +1,7 @@
from peewee import *
from peewee_migrate import Router
from config import SRC_LOG_LEVELS, DATA_DIR
from playhouse.db_url import connect
from config import SRC_LOG_LEVELS, DATA_DIR, DATABASE_URL
import os
import logging
@@ -11,12 +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(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)
@@ -37,6 +37,18 @@ with suppress(ImportError):
def migrate(migrator: Migrator, database: pw.Database, *, fake=False):
"""Write your migrations here."""
# We perform different migrations for SQLite and other databases
# This is because SQLite is very loose with enforcing its schema, and trying to migrate other databases like SQLite
# will require per-database SQL queries.
# Instead, we assume that because external DB support was added at a later date, it is safe to assume a newer base
# schema instead of trying to migrate from an older schema.
if isinstance(database, pw.SqliteDatabase):
migrate_sqlite(migrator, database, fake=fake)
else:
migrate_external(migrator, database, fake=fake)
def migrate_sqlite(migrator: Migrator, database: pw.Database, *, fake=False):
@migrator.create_model
class Auth(pw.Model):
id = pw.CharField(max_length=255, unique=True)
@@ -53,7 +65,7 @@ def migrate(migrator: Migrator, database: pw.Database, *, fake=False):
user_id = pw.CharField(max_length=255)
title = pw.CharField()
chat = pw.TextField()
timestamp = pw.DateField()
timestamp = pw.BigIntegerField()
class Meta:
table_name = "chat"
@@ -64,7 +76,7 @@ def migrate(migrator: Migrator, database: pw.Database, *, fake=False):
tag_name = pw.CharField(max_length=255)
chat_id = pw.CharField(max_length=255)
user_id = pw.CharField(max_length=255)
timestamp = pw.DateField()
timestamp = pw.BigIntegerField()
class Meta:
table_name = "chatidtag"
@@ -78,7 +90,7 @@ def migrate(migrator: Migrator, database: pw.Database, *, fake=False):
filename = pw.CharField()
content = pw.TextField(null=True)
user_id = pw.CharField(max_length=255)
timestamp = pw.DateField()
timestamp = pw.BigIntegerField()
class Meta:
table_name = "document"
@@ -89,7 +101,7 @@ def migrate(migrator: Migrator, database: pw.Database, *, fake=False):
tag_name = pw.CharField(max_length=255, unique=True)
user_id = pw.CharField(max_length=255)
modelfile = pw.TextField()
timestamp = pw.DateField()
timestamp = pw.BigIntegerField()
class Meta:
table_name = "modelfile"
@@ -101,7 +113,7 @@ def migrate(migrator: Migrator, database: pw.Database, *, fake=False):
user_id = pw.CharField(max_length=255)
title = pw.CharField()
content = pw.TextField()
timestamp = pw.DateField()
timestamp = pw.BigIntegerField()
class Meta:
table_name = "prompt"
@@ -123,7 +135,100 @@ def migrate(migrator: Migrator, database: pw.Database, *, fake=False):
email = pw.CharField(max_length=255)
role = pw.CharField(max_length=255)
profile_image_url = pw.CharField(max_length=255)
timestamp = pw.DateField()
timestamp = pw.BigIntegerField()
class Meta:
table_name = "user"
def migrate_external(migrator: Migrator, database: pw.Database, *, fake=False):
@migrator.create_model
class Auth(pw.Model):
id = pw.CharField(max_length=255, unique=True)
email = pw.CharField(max_length=255)
password = pw.TextField()
active = pw.BooleanField()
class Meta:
table_name = "auth"
@migrator.create_model
class Chat(pw.Model):
id = pw.CharField(max_length=255, unique=True)
user_id = pw.CharField(max_length=255)
title = pw.TextField()
chat = pw.TextField()
timestamp = pw.BigIntegerField()
class Meta:
table_name = "chat"
@migrator.create_model
class ChatIdTag(pw.Model):
id = pw.CharField(max_length=255, unique=True)
tag_name = pw.CharField(max_length=255)
chat_id = pw.CharField(max_length=255)
user_id = pw.CharField(max_length=255)
timestamp = pw.BigIntegerField()
class Meta:
table_name = "chatidtag"
@migrator.create_model
class Document(pw.Model):
id = pw.AutoField()
collection_name = pw.CharField(max_length=255, unique=True)
name = pw.CharField(max_length=255, unique=True)
title = pw.TextField()
filename = pw.TextField()
content = pw.TextField(null=True)
user_id = pw.CharField(max_length=255)
timestamp = pw.BigIntegerField()
class Meta:
table_name = "document"
@migrator.create_model
class Modelfile(pw.Model):
id = pw.AutoField()
tag_name = pw.CharField(max_length=255, unique=True)
user_id = pw.CharField(max_length=255)
modelfile = pw.TextField()
timestamp = pw.BigIntegerField()
class Meta:
table_name = "modelfile"
@migrator.create_model
class Prompt(pw.Model):
id = pw.AutoField()
command = pw.CharField(max_length=255, unique=True)
user_id = pw.CharField(max_length=255)
title = pw.TextField()
content = pw.TextField()
timestamp = pw.BigIntegerField()
class Meta:
table_name = "prompt"
@migrator.create_model
class Tag(pw.Model):
id = pw.CharField(max_length=255, unique=True)
name = pw.CharField(max_length=255)
user_id = pw.CharField(max_length=255)
data = pw.TextField(null=True)
class Meta:
table_name = "tag"
@migrator.create_model
class User(pw.Model):
id = pw.CharField(max_length=255, unique=True)
name = pw.CharField(max_length=255)
email = pw.CharField(max_length=255)
role = pw.CharField(max_length=255)
profile_image_url = pw.TextField()
timestamp = pw.BigIntegerField()
class Meta:
table_name = "user"
@@ -37,6 +37,13 @@ with suppress(ImportError):
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",
@@ -60,9 +67,40 @@ def migrate(migrator: Migrator, database: pw.Database, *, fake=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))
@@ -75,3 +113,18 @@ def rollback(migrator: Migrator, database: pw.Database, *, fake=False):
# 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))
+5 -1
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:
@@ -89,6 +89,10 @@ class SignupForm(BaseModel):
profile_image_url: Optional[str] = "/user.png"
class AddUserForm(SignupForm):
role: Optional[str] = "pending"
class AuthsTable:
def __init__(self, db):
self.db = db
+26 -21
View File
@@ -17,11 +17,11 @@ from apps.web.internal.db import DB
class Chat(Model):
id = CharField(unique=True)
user_id = CharField()
title = CharField()
title = TextField()
chat = TextField() # Save Chat JSON as Text
created_at = DateTimeField()
updated_at = DateTimeField()
created_at = BigIntegerField()
updated_at = BigIntegerField()
share_id = CharField(null=True, unique=True)
archived = BooleanField(default=False)
@@ -191,7 +191,7 @@ class ChatTable:
except:
return None
def get_archived_chat_lists_by_user_id(
def get_archived_chat_list_by_user_id(
self, user_id: str, skip: int = 0, limit: int = 50
) -> List[ChatModel]:
return [
@@ -204,7 +204,7 @@ class ChatTable:
# .offset(skip)
]
def get_chat_lists_by_user_id(
def get_chat_list_by_user_id(
self, user_id: str, skip: int = 0, limit: int = 50
) -> List[ChatModel]:
return [
@@ -217,7 +217,7 @@ class ChatTable:
# .offset(skip)
]
def get_chat_lists_by_chat_ids(
def get_chat_list_by_chat_ids(
self, chat_ids: List[str], skip: int = 0, limit: int = 50
) -> List[ChatModel]:
return [
@@ -228,20 +228,6 @@ class ChatTable:
.order_by(Chat.updated_at.desc())
]
def get_all_chats(self) -> List[ChatModel]:
return [
ChatModel(**model_to_dict(chat))
for chat in Chat.select().order_by(Chat.updated_at.desc())
]
def get_all_chats_by_user_id(self, user_id: str) -> List[ChatModel]:
return [
ChatModel(**model_to_dict(chat))
for chat in Chat.select()
.where(Chat.user_id == user_id)
.order_by(Chat.updated_at.desc())
]
def get_chat_by_id(self, id: str) -> Optional[ChatModel]:
try:
chat = Chat.get(Chat.id == id)
@@ -271,9 +257,28 @@ class ChatTable:
def get_chats(self, skip: int = 0, limit: int = 50) -> List[ChatModel]:
return [
ChatModel(**model_to_dict(chat))
for chat in Chat.select().limit(limit).offset(skip)
for chat in Chat.select().order_by(Chat.updated_at.desc())
# .limit(limit).offset(skip)
]
def get_chats_by_user_id(self, user_id: str) -> List[ChatModel]:
return [
ChatModel(**model_to_dict(chat))
for chat in Chat.select()
.where(Chat.user_id == user_id)
.order_by(Chat.updated_at.desc())
# .limit(limit).offset(skip)
]
def delete_chat_by_id(self, id: str) -> bool:
try:
query = Chat.delete().where((Chat.id == id))
query.execute() # Remove the rows, return number of rows removed.
return True and self.delete_shared_chat_by_chat_id(id)
except:
return False
def delete_chat_by_id_and_user_id(self, id: str, user_id: str) -> bool:
try:
query = Chat.delete().where((Chat.id == id) & (Chat.user_id == user_id))
+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
+1 -1
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
+31 -4
View File
@@ -18,8 +18,12 @@ 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:
@@ -32,7 +36,11 @@ class UserModel(BaseModel):
email: str
role: str = "pending"
profile_image_url: str
timestamp: int # timestamp in epoch
last_active_at: int # timestamp in epoch
updated_at: int # timestamp in epoch
created_at: int # timestamp in epoch
api_key: Optional[str] = None
@@ -73,7 +81,9 @@ class UsersTable:
"email": email,
"role": role,
"profile_image_url": profile_image_url,
"timestamp": int(time.time()),
"last_active_at": int(time.time()),
"created_at": int(time.time()),
"updated_at": int(time.time()),
}
)
result = User.create(**user.model_dump())
@@ -113,6 +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)
@@ -137,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)
+67 -2
View File
@@ -1,14 +1,19 @@
from fastapi import Request
import logging
from fastapi import Request, UploadFile, File
from fastapi import Depends, HTTPException, status
from fastapi import APIRouter
from pydantic import BaseModel
import re
import uuid
import csv
from apps.web.models.auths import (
SigninForm,
SignupForm,
AddUserForm,
UpdateProfileForm,
UpdatePasswordForm,
UserResponse,
@@ -28,7 +33,7 @@ from utils.utils import (
from utils.misc import parse_duration, validate_email_format
from utils.webhook import post_webhook
from constants import ERROR_MESSAGES, WEBHOOK_MESSAGES
from config import WEBUI_AUTH_TRUSTED_EMAIL_HEADER
from config import WEBUI_AUTH, WEBUI_AUTH_TRUSTED_EMAIL_HEADER
router = APIRouter()
@@ -113,6 +118,21 @@ async def signin(request: Request, form_data: SigninForm):
),
)
user = Auths.authenticate_user_by_trusted_header(trusted_email)
if WEBUI_AUTH == False:
if Users.get_num_users() != 0:
raise HTTPException(400, detail=ERROR_MESSAGES.EXISTING_USERS)
admin_email = "admin@localhost"
admin_password = "admin"
if not Users.get_user_by_email(admin_email.lower()):
await signup(
request,
SignupForm(email=admin_email, password=admin_password, name="User"),
)
user = Auths.authenticate_user(admin_email.lower(), admin_password)
else:
user = Auths.authenticate_user(form_data.email.lower(), form_data.password)
@@ -203,6 +223,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
############################
+111 -87
View File
@@ -36,15 +36,49 @@ 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)
############################
# 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)
############################
@@ -53,22 +87,47 @@ async def get_user_chats(
@router.get("/archived", response_model=List[ChatTitleIdResponse])
async def get_archived_user_chats(
async def get_archived_session_user_chat_list(
user=Depends(get_current_user), skip: int = 0, limit: int = 50
):
return Chats.get_archived_chat_lists_by_user_id(user.id, skip, limit)
return Chats.get_archived_chat_list_by_user_id(user.id, skip, limit)
############################
# GetAllChats
# 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
)
############################
# 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)
]
@@ -86,7 +145,7 @@ async def get_all_user_chats_in_db(user=Depends(get_admin_user)):
)
return [
ChatResponse(**{**chat.model_dump(), "chat": json.loads(chat.chat)})
for chat in Chats.get_all_chats()
for chat in Chats.get_chats()
]
@@ -107,6 +166,38 @@ async def create_new_chat(form_data: ChatForm, user=Depends(get_current_user)):
)
############################
# GetChatsByTags
############################
class TagNameForm(BaseModel):
name: str
skip: Optional[int] = 0
limit: Optional[int] = 50
@router.post("/tags", response_model=List[ChatTitleIdResponse])
async def get_user_chat_list_by_tag_name(
form_data: TagNameForm, user=Depends(get_current_user)
):
print(form_data)
chat_ids = [
chat_id_tag.chat_id
for chat_id_tag in Tags.get_chat_ids_by_tag_name_and_user_id(
form_data.name, user.id
)
]
chats = Chats.get_chat_list_by_chat_ids(chat_ids, form_data.skip, form_data.limit)
if len(chats) == 0:
Tags.delete_tag_by_tag_name_and_user_id(form_data.name, user.id)
return chats
############################
# GetAllTags
############################
@@ -124,28 +215,6 @@ async def get_all_tags(user=Depends(get_current_user)):
)
############################
# GetChatsByTags
############################
@router.get("/tags/tag/{tag_name}", response_model=List[ChatTitleIdResponse])
async def get_user_chats_by_tag_name(
tag_name: str, user=Depends(get_current_user), skip: int = 0, limit: int = 50
):
chat_ids = [
chat_id_tag.chat_id
for chat_id_tag in Tags.get_chat_ids_by_tag_name_and_user_id(tag_name, user.id)
]
chats = Chats.get_chat_lists_by_chat_ids(chat_ids, skip, limit)
if len(chats) == 0:
Tags.delete_tag_by_tag_name_and_user_id(tag_name, user.id)
return chats
############################
# GetChatById
############################
@@ -193,17 +262,18 @@ async def update_chat_by_id(
@router.delete("/{id}", response_model=bool)
async def delete_chat_by_id(request: Request, id: str, user=Depends(get_current_user)):
if (
user.role == "user"
and not request.app.state.USER_PERMISSIONS["chat"]["deletion"]
):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
)
if user.role == "admin":
result = Chats.delete_chat_by_id(id)
return result
else:
if not request.app.state.USER_PERMISSIONS["chat"]["deletion"]:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
)
result = Chats.delete_chat_by_id_and_user_id(id, user.id)
return result
result = Chats.delete_chat_by_id_and_user_id(id, user.id)
return result
############################
@@ -278,31 +348,6 @@ async def delete_shared_chat_by_id(id: str, user=Depends(get_current_user)):
)
############################
# GetSharedChatById
############################
@router.get("/share/{share_id}", response_model=Optional[ChatResponse])
async def get_shared_chat_by_id(share_id: str, user=Depends(get_current_user)):
if user.role == "pending":
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED, detail=ERROR_MESSAGES.NOT_FOUND
)
if user.role == "user":
chat = Chats.get_chat_by_share_id(share_id)
elif user.role == "admin":
chat = Chats.get_chat_by_id(share_id)
if chat:
return ChatResponse(**{**chat.model_dump(), "chat": json.loads(chat.chat)})
else:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED, detail=ERROR_MESSAGES.NOT_FOUND
)
############################
# GetChatTagsById
############################
@@ -383,24 +428,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(
+9 -3
View File
@@ -1,5 +1,6 @@
from fastapi import APIRouter, UploadFile, File, Response
from fastapi import Depends, HTTPException, status
from peewee import SqliteDatabase
from starlette.responses import StreamingResponse, FileResponse
from pydantic import BaseModel
@@ -7,11 +8,11 @@ from pydantic import BaseModel
from fpdf import FPDF
import markdown
from apps.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
@@ -96,8 +97,13 @@ async def download_db(user=Depends(get_admin_user)):
status_code=status.HTTP_401_UNAUTHORIZED,
detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
)
if not isinstance(DB, SqliteDatabase):
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.DB_NOT_SQLITE,
)
return FileResponse(
f"{DATA_DIR}/webui.db",
DB.database,
media_type="application/octet-stream",
filename="webui.db",
)
+132 -28
View File
@@ -18,6 +18,18 @@ from secrets import token_bytes
from constants import ERROR_MESSAGES
####################################
# Load .env file
####################################
try:
from dotenv import load_dotenv, find_dotenv
load_dotenv(find_dotenv("../.env"))
except ImportError:
print("dotenv not installed, skipping...")
####################################
# LOGGING
####################################
@@ -59,20 +71,16 @@ for source in log_sources:
log.setLevel(SRC_LOG_LEVELS["CONFIG"])
####################################
# Load .env file
####################################
try:
from dotenv import load_dotenv, find_dotenv
load_dotenv(find_dotenv("../.env"))
except ImportError:
log.warning("dotenv not installed, skipping...")
WEBUI_NAME = os.environ.get("WEBUI_NAME", "Open WebUI")
if WEBUI_NAME != "Open WebUI":
WEBUI_NAME += " (Open WebUI)"
WEBUI_URL = os.environ.get("WEBUI_URL", "http://localhost:3000")
WEBUI_FAVICON_URL = "https://openwebui.com/favicon.png"
####################################
# ENV (dev,test,prod)
####################################
@@ -165,7 +173,11 @@ except:
STATIC_DIR = str(Path(os.getenv("STATIC_DIR", "./static")).resolve())
shutil.copyfile(f"{FRONTEND_BUILD_DIR}/favicon.png", f"{STATIC_DIR}/favicon.png")
frontend_favicon = f"{FRONTEND_BUILD_DIR}/favicon.png"
if os.path.exists(frontend_favicon):
shutil.copyfile(frontend_favicon, f"{STATIC_DIR}/favicon.png")
else:
logging.warning(f"Frontend favicon not found at {frontend_favicon}")
####################################
# CUSTOM_NAME
@@ -195,9 +207,6 @@ if CUSTOM_NAME:
except Exception as e:
log.exception(e)
pass
else:
if WEBUI_NAME != "Open WebUI":
WEBUI_NAME += " (Open WebUI)"
####################################
@@ -220,7 +229,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)
@@ -363,6 +372,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?",
},
]
)
@@ -375,8 +395,7 @@ 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(";")]
@@ -394,7 +413,7 @@ WEBUI_VERSION = os.environ.get("WEBUI_VERSION", "v1.0.0-alpha.100")
# WEBUI_AUTH (Required for security)
####################################
WEBUI_AUTH = True
WEBUI_AUTH = os.environ.get("WEBUI_AUTH", "True").lower() == "true"
WEBUI_AUTH_TRUSTED_EMAIL_HEADER = os.environ.get(
"WEBUI_AUTH_TRUSTED_EMAIL_HEADER", None
)
@@ -418,19 +437,82 @@ if WEBUI_AUTH and WEBUI_SECRET_KEY == "":
####################################
CHROMA_DATA_PATH = f"{DATA_DIR}/vector_db"
CHROMA_TENANT = os.environ.get("CHROMA_TENANT", chromadb.DEFAULT_TENANT)
CHROMA_DATABASE = os.environ.get("CHROMA_DATABASE", chromadb.DEFAULT_DATABASE)
CHROMA_HTTP_HOST = os.environ.get("CHROMA_HTTP_HOST", "")
CHROMA_HTTP_PORT = int(os.environ.get("CHROMA_HTTP_PORT", "8000"))
# Comma-separated list of header=value pairs
CHROMA_HTTP_HEADERS = os.environ.get("CHROMA_HTTP_HEADERS", "")
if CHROMA_HTTP_HEADERS:
CHROMA_HTTP_HEADERS = dict(
[pair.split("=") for pair in CHROMA_HTTP_HEADERS.split(",")]
)
else:
CHROMA_HTTP_HEADERS = None
CHROMA_HTTP_SSL = os.environ.get("CHROMA_HTTP_SSL", "false").lower() == "true"
# this uses the model defined in the Dockerfile ENV variable. If you dont use docker or docker based deployments such as k8s, the default embedding model will be used (sentence-transformers/all-MiniLM-L6-v2)
RAG_TOP_K = int(os.environ.get("RAG_TOP_K", "5"))
RAG_RELEVANCE_THRESHOLD = float(os.environ.get("RAG_RELEVANCE_THRESHOLD", "0.0"))
ENABLE_RAG_HYBRID_SEARCH = (
os.environ.get("ENABLE_RAG_HYBRID_SEARCH", "").lower() == "true"
)
ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION = (
os.environ.get("ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION", "True").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"
)
if CHROMA_HTTP_HOST != "":
CHROMA_CLIENT = chromadb.HttpClient(
host=CHROMA_HTTP_HOST,
port=CHROMA_HTTP_PORT,
headers=CHROMA_HTTP_HEADERS,
ssl=CHROMA_HTTP_SSL,
tenant=CHROMA_TENANT,
database=CHROMA_DATABASE,
settings=Settings(allow_reset=True, anonymized_telemetry=False),
)
else:
CHROMA_CLIENT = chromadb.PersistentClient(
path=CHROMA_DATA_PATH,
settings=Settings(allow_reset=True, anonymized_telemetry=False),
tenant=CHROMA_TENANT,
database=CHROMA_DATABASE,
)
# device type embedding models - "cpu" (default), "cuda" (nvidia gpu required) or "mps" (apple silicon) - choosing this right can lead to better performance
USE_CUDA = os.environ.get("USE_CUDA_DOCKER", "false")
@@ -440,15 +522,10 @@ else:
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
CHUNK_SIZE = int(os.environ.get("CHUNK_SIZE", "1500"))
CHUNK_OVERLAP = int(os.environ.get("CHUNK_OVERLAP", "100"))
RAG_TEMPLATE = """Use the following context as your learned knowledge, inside <context></context> XML tags.
DEFAULT_RAG_TEMPLATE = """Use the following context as your learned knowledge, inside <context></context> XML tags.
<context>
[context]
</context>
@@ -458,13 +535,21 @@ 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_RAG_LOCAL_WEB_FETCH = (
os.getenv("ENABLE_RAG_LOCAL_WEB_FETCH", "False").lower() == "true"
)
YOUTUBE_LOADER_LANGUAGE = os.getenv("YOUTUBE_LOADER_LANGUAGE", "en").split(",")
####################################
# Transcribe
####################################
@@ -480,18 +565,25 @@ WHISPER_MODEL_AUTO_UPDATE = (
# 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", "")
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
@@ -499,12 +591,24 @@ IMAGES_OPENAI_API_KEY = os.getenv("IMAGES_OPENAI_API_KEY", OPENAI_API_KEY)
AUDIO_OPENAI_API_BASE_URL = os.getenv("AUDIO_OPENAI_API_BASE_URL", OPENAI_API_BASE_URL)
AUDIO_OPENAI_API_KEY = os.getenv("AUDIO_OPENAI_API_KEY", OPENAI_API_KEY)
AUDIO_OPENAI_API_MODEL = os.getenv("AUDIO_OPENAI_API_MODEL", "tts-1")
AUDIO_OPENAI_API_VOICE = os.getenv("AUDIO_OPENAI_API_VOICE", "alloy")
####################################
# 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")
+9
View File
@@ -42,6 +42,9 @@ class ERROR_MESSAGES(str, Enum):
"The password provided is incorrect. Please check for typos and try again."
)
INVALID_TRUSTED_HEADER = "Your provider has not provided a trusted header. Please contact your administrator for assistance."
EXISTING_USERS = "You can't turn off authentication because there are existing users. If you want to disable WEBUI_AUTH, make sure your web interface doesn't have any existing users and is a fresh installation."
UNAUTHORIZED = "401 Unauthorized"
ACCESS_PROHIBITED = "You do not have permission to access this resource. Please contact your administrator for assistance."
ACTION_PROHIBITED = (
@@ -69,3 +72,9 @@ class ERROR_MESSAGES(str, Enum):
CREATE_API_KEY_ERROR = "Oops! Something went wrong while creating your API key. Please try again later. If the issue persists, contact support for assistance."
EMPTY_CONTENT = "The content provided is empty. Please ensure that there is text or data present before proceeding."
DB_NOT_SQLITE = "This feature is only available when running with SQLite databases."
INVALID_URL = (
"Oops! The URL you provided is invalid. Please double-check and try again."
)
+12
View File
@@ -18,6 +18,18 @@
{
"title": ["Show me a code snippet", "of a website's sticky header"],
"content": "Show me a code snippet of a website's sticky header in CSS and JavaScript."
},
{
"title": ["Explain options trading", "if I'm familiar with buying and selling stocks"],
"content": "Explain options trading in simple terms if I'm familiar with buying and selling stocks."
},
{
"title": ["Overcome procrastination", "give me tips"],
"content": "Could you start by asking me about instances when I procrastinate the most and then give me some suggestions to overcome it?"
},
{
"title": ["Grammar check", "rewrite it for better readability "],
"content": "Check the following sentence for grammar and clarity: \"[sentence]\". Rewrite it for better readability while maintaining its original meaning."
}
]
}
Regular → Executable
View File
+89 -29
View File
@@ -15,7 +15,7 @@ from fastapi.middleware.wsgi import WSGIMiddleware
from fastapi.middleware.cors import CORSMiddleware
from starlette.exceptions import HTTPException as StarletteHTTPException
from starlette.middleware.base import BaseHTTPMiddleware
from starlette.responses import StreamingResponse, Response
from apps.ollama.main import app as ollama_app
from apps.openai.main import app as openai_app
@@ -25,6 +25,8 @@ from apps.litellm.main import (
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
@@ -41,13 +43,16 @@ from apps.rag.utils import rag_messages
from config import (
CONFIG_DATA,
WEBUI_NAME,
WEBUI_URL,
WEBUI_AUTH,
ENV,
VERSION,
CHANGELOG,
FRONTEND_BUILD_DIR,
CACHE_DIR,
STATIC_DIR,
MODEL_FILTER_ENABLED,
ENABLE_LITELLM,
ENABLE_MODEL_FILTER,
MODEL_FILTER_LIST,
GLOBAL_LOG_LEVEL,
SRC_LOG_LEVELS,
@@ -73,7 +78,7 @@ class SPAStaticFiles(StaticFiles):
print(
f"""
rf"""
___ __ __ _ _ _ ___
/ _ \ _ __ ___ _ __ \ \ / /__| |__ | | | |_ _|
| | | | '_ \ / _ \ '_ \ \ \ /\ / / _ \ '_ \| | | || |
@@ -89,7 +94,7 @@ 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
@@ -99,6 +104,8 @@ origins = ["*"]
class RAGMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request: Request, call_next):
return_citations = False
if request.method == "POST" and (
"/api/chat" in request.url.path or "/chat/completions" in request.url.path
):
@@ -111,24 +118,29 @@ class RAGMiddleware(BaseHTTPMiddleware):
# Parse string to JSON
data = json.loads(body_str) if body_str else {}
return_citations = data.get("citations", False)
if "citations" in data:
del data["citations"]
# Example: Add a new key-value pair or modify existing ones
# data["modified"] = True # Example modification
if "docs" in data:
data = {**data}
data["messages"] = rag_messages(
data["docs"],
data["messages"],
rag_app.state.RAG_TEMPLATE,
rag_app.state.TOP_K,
rag_app.state.RAG_EMBEDDING_ENGINE,
rag_app.state.RAG_EMBEDDING_MODEL,
rag_app.state.sentence_transformer_ef,
rag_app.state.OPENAI_API_KEY,
rag_app.state.OPENAI_API_BASE_URL,
data["messages"], citations = rag_messages(
docs=data["docs"],
messages=data["messages"],
template=rag_app.state.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"]
log.debug(f"data['messages']: {data['messages']}")
log.debug(
f"data['messages']: {data['messages']}, citations: {citations}"
)
modified_body_bytes = json.dumps(data).encode("utf-8")
@@ -146,11 +158,36 @@ class RAGMiddleware(BaseHTTPMiddleware):
]
response = await call_next(request)
if return_citations:
# Inject the citations into the response
if isinstance(response, StreamingResponse):
# If it's a streaming response, inject it as SSE event or NDJSON line
content_type = response.headers.get("Content-Type")
if "text/event-stream" in content_type:
return StreamingResponse(
self.openai_stream_wrapper(response.body_iterator, citations),
)
if "application/x-ndjson" in content_type:
return StreamingResponse(
self.ollama_stream_wrapper(response.body_iterator, citations),
)
return response
async def _receive(self, body: bytes):
return {"type": "http.request", "body": body, "more_body": False}
async def openai_stream_wrapper(self, original_generator, citations):
yield f"data: {json.dumps({'citations': citations})}\n\n"
async for data in original_generator:
yield data
async def ollama_stream_wrapper(self, original_generator, citations):
yield f"{json.dumps({'citations': citations})}\n"
async for data in original_generator:
yield data
app.add_middleware(RAGMiddleware)
@@ -176,7 +213,8 @@ async def check_url(request: Request, call_next):
@app.on_event("startup")
async def on_startup():
asyncio.create_task(start_litellm_background())
if ENABLE_LITELLM:
asyncio.create_task(start_litellm_background())
app.mount("/api/v1", webui_app)
@@ -203,6 +241,7 @@ async def get_app_config():
"status": True,
"name": WEBUI_NAME,
"version": VERSION,
"auth": WEBUI_AUTH,
"default_locale": default_locale,
"images": images_app.state.ENABLED,
"default_models": webui_app.state.DEFAULT_MODELS,
@@ -215,7 +254,7 @@ async def get_app_config():
@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,
}
@@ -229,20 +268,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,
}
@@ -310,20 +349,41 @@ async def get_manifest_json():
"background_color": "#343541",
"theme_color": "#343541",
"orientation": "portrait-primary",
"icons": [{"src": "/favicon.png", "type": "image/png", "sizes": "844x884"}],
"icons": [{"src": "/static/logo.png", "type": "image/png", "sizes": "500x500"}],
}
@app.get("/opensearch.xml")
async def get_opensearch_xml():
xml_content = rf"""
<OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.1/" xmlns:moz="http://www.mozilla.org/2006/browser/search/">
<ShortName>{WEBUI_NAME}</ShortName>
<Description>Search {WEBUI_NAME}</Description>
<InputEncoding>UTF-8</InputEncoding>
<Image width="16" height="16" type="image/x-icon">{WEBUI_URL}/favicon.png</Image>
<Url type="text/html" method="get" template="{WEBUI_URL}/?q={"{searchTerms}"}"/>
<moz:SearchForm>{WEBUI_URL}</moz:SearchForm>
</OpenSearchDescription>
"""
return Response(content=xml_content, media_type="application/xml")
app.mount("/static", StaticFiles(directory=STATIC_DIR), name="static")
app.mount("/cache", StaticFiles(directory=CACHE_DIR), name="cache")
app.mount(
"/",
SPAStaticFiles(directory=FRONTEND_BUILD_DIR, html=True),
name="spa-static-files",
)
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():
await shutdown_litellm_background()
if ENABLE_LITELLM:
await shutdown_litellm_background()
+48 -45
View File
@@ -1,57 +1,60 @@
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
requests
aiohttp
peewee
peewee-migrate
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.35.17
litellm[proxy]==1.35.17
litellm[proxy]==1.35.28
boto3
boto3==1.34.95
argon2-cffi
apscheduler
google-generativeai
argon2-cffi==23.1.0
APScheduler==3.10.4
google-generativeai==0.5.2
langchain
langchain-chroma
langchain-community
fake_useragent
chromadb
sentence_transformers
pypdf
docx2txt
unstructured
markdown
pypandoc
pandas
openpyxl
pyxlsb
xlrd
langchain==0.1.16
langchain-community==0.0.34
langchain-chroma==0.1.0
opencv-python-headless
rapidocr-onnxruntime
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
fpdf2
rank_bm25
opencv-python-headless==4.9.0.80
rapidocr-onnxruntime==1.2.3
faster-whisper
fpdf2==2.7.8
rank-bm25==0.2.2
PyJWT
pyjwt[crypto]
faster-whisper==1.0.1
black
langfuse
PyJWT[crypto]==2.8.0
black==24.4.2
langfuse==2.27.3
youtube-transcript-api==0.6.2
pytube
+3 -2
View File
@@ -6,8 +6,9 @@ cd "$SCRIPT_DIR" || exit
KEY_FILE=.webui_secret_key
PORT="${PORT:-8080}"
HOST="${HOST:-0.0.0.0}"
if test "$WEBUI_SECRET_KEY $WEBUI_JWT_SECRET_KEY" = " "; then
echo "No WEBUI_SECRET_KEY provided"
echo "Loading WEBUI_SECRET_KEY from file, not provided as an environment variable."
if ! [ -e "$KEY_FILE" ]; then
echo "Generating WEBUI_SECRET_KEY"
@@ -29,4 +30,4 @@ if [ "$USE_CUDA_DOCKER" = "true" ]; then
export LD_LIBRARY_PATH="$LD_LIBRARY_PATH:/usr/local/lib/python3.11/site-packages/torch/lib:/usr/local/lib/python3.11/site-packages/nvidia/cudnn/lib"
fi
WEBUI_SECRET_KEY="$WEBUI_SECRET_KEY" exec uvicorn main:app --host 0.0.0.0 --port "$PORT" --forwarded-allow-ips '*'
WEBUI_SECRET_KEY="$WEBUI_SECRET_KEY" exec uvicorn main:app --host "$HOST" --port "$PORT" --forwarded-allow-ips '*'
+2 -2
View File
@@ -7,13 +7,13 @@ SET "SCRIPT_DIR=%~dp0"
cd /d "%SCRIPT_DIR%" || exit /b
SET "KEY_FILE=.webui_secret_key"
SET "PORT=%PORT:8080%"
IF "%PORT%"=="" SET PORT=8080
SET "WEBUI_SECRET_KEY=%WEBUI_SECRET_KEY%"
SET "WEBUI_JWT_SECRET_KEY=%WEBUI_JWT_SECRET_KEY%"
:: Check if WEBUI_SECRET_KEY and WEBUI_JWT_SECRET_KEY are not set
IF "%WEBUI_SECRET_KEY%%WEBUI_JWT_SECRET_KEY%" == " " (
echo No WEBUI_SECRET_KEY provided
echo Loading WEBUI_SECRET_KEY from file, not provided as an environment variable.
IF NOT EXIST "%KEY_FILE%" (
echo Generating WEBUI_SECRET_KEY
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

+4 -3
View File
@@ -38,9 +38,10 @@ def calculate_sha256_string(string):
def validate_email_format(email: str) -> bool:
if not re.match(r"[^@]+@[^@]+\.[^@]+", email):
return False
return True
if email.endswith("@localhost"):
return True
return bool(re.match(r"[^@]+@[^@]+\.[^@]+", email))
def sanitize_filename(file_name):
+6
View File
@@ -89,6 +89,8 @@ def get_current_user(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=ERROR_MESSAGES.INVALID_TOKEN,
)
else:
Users.update_user_last_active_by_id(user.id)
return user
else:
raise HTTPException(
@@ -99,11 +101,15 @@ def get_current_user(
def get_current_user_by_api_key(api_key: str):
user = Users.get_user_by_api_key(api_key)
if user is None:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=ERROR_MESSAGES.INVALID_TOKEN,
)
else:
Users.update_user_last_active_by_id(user.id)
return user
+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('button[aria-label="model-item"]').first().click();
});
it('user can perform text chat', () => {
// Click on the model selector
cy.get('button[aria-label="Select a model"]').click();
// Select the first model
cy.get('button[aria-label="model-item"]').first().click();
// Type a message
cy.get('#chat-textarea').type('Hi, what can you do? A single sentence only please.', {
force: true
});
// Send the message
cy.get('button[type="submit"]').click();
// User's message should be visible
cy.get('.chat-user').should('exist');
// Wait for the response
cy.get('.chat-assistant', { timeout: 120_000 }) // .chat-assistant is created after the first token is received
.find('div[aria-label="Generation Info"]', { timeout: 120_000 }) // Generation Info is created after the stop token is received
.should('exist');
});
});
});
+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
}
}
-1
View File
@@ -1 +0,0 @@
values-minikube.yaml
-21
View File
@@ -1,21 +0,0 @@
apiVersion: v2
name: open-webui
version: 1.0.0
appVersion: "latest"
home: https://www.openwebui.com/
icon: https://raw.githubusercontent.com/open-webui/open-webui/main/static/favicon.png
description: "Open WebUI: A User-Friendly Web Interface for Chat Interactions 👋"
keywords:
- llm
- chat
- web-ui
sources:
- https://github.com/open-webui/open-webui/tree/main/kubernetes/helm
- https://hub.docker.com/r/ollama/ollama
- https://github.com/open-webui/open-webui/pkgs/container/open-webui
annotations:
licenses: MIT
+4
View File
@@ -0,0 +1,4 @@
# Helm Charts
Open WebUI Helm Charts are now hosted in a separate repo, which can be found here: https://github.com/open-webui/helm-charts
The charts are released at https://helm.openwebui.com.
-51
View File
@@ -1,51 +0,0 @@
{{- define "open-webui.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
{{- end -}}
{{- define "ollama.name" -}}
ollama
{{- end -}}
{{- define "ollama.url" -}}
{{- 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" -}}
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
{{- end }}
{{- define "base.labels" -}}
helm.sh/chart: {{ include "chart.name" . }}
{{- if .Chart.AppVersion }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
{{- end }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- end }}
{{- define "base.selectorLabels" -}}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end -}}
{{- define "open-webui.selectorLabels" -}}
{{ include "base.selectorLabels" . }}
app.kubernetes.io/component: {{ .Chart.Name }}
{{- end }}
{{- define "open-webui.labels" -}}
{{ include "base.labels" . }}
{{ include "open-webui.selectorLabels" . }}
{{- end }}
{{- define "ollama.selectorLabels" -}}
{{ include "base.selectorLabels" . }}
app.kubernetes.io/component: {{ include "ollama.name" . }}
{{- end }}
{{- define "ollama.labels" -}}
{{ include "base.labels" . }}
{{ include "ollama.selectorLabels" . }}
{{- end }}
@@ -1,23 +0,0 @@
{{- if not .Values.ollama.externalHost }}
apiVersion: v1
kind: Service
metadata:
name: {{ include "ollama.name" . }}
labels:
{{- include "ollama.labels" . | nindent 4 }}
{{- with .Values.ollama.service.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
selector:
{{- include "ollama.selectorLabels" . | nindent 4 }}
{{- with .Values.ollama.service }}
type: {{ .type }}
ports:
- protocol: TCP
name: http
port: {{ .port }}
targetPort: http
{{- end }}
{{- end }}
@@ -1,98 +0,0 @@
{{- if not .Values.ollama.externalHost }}
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: {{ include "ollama.name" . }}
labels:
{{- include "ollama.labels" . | nindent 4 }}
{{- with .Values.ollama.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
serviceName: {{ include "ollama.name" . }}
replicas: {{ .Values.ollama.replicaCount }}
selector:
matchLabels:
{{- include "ollama.selectorLabels" . | nindent 6 }}
template:
metadata:
labels:
{{- include "ollama.labels" . | nindent 8 }}
{{- with .Values.ollama.podAnnotations }}
annotations:
{{- toYaml . | nindent 8 }}
{{- end }}
spec:
enableServiceLinks: false
automountServiceAccountToken: false
{{- with .Values.ollama.runtimeClassName }}
runtimeClassName: {{ . }}
{{- end }}
containers:
- name: {{ include "ollama.name" . }}
{{- with .Values.ollama.image }}
image: {{ .repository }}:{{ .tag }}
imagePullPolicy: {{ .pullPolicy }}
{{- end }}
tty: true
ports:
- name: http
containerPort: {{ .Values.ollama.service.containerPort }}
env:
{{- if .Values.ollama.gpu.enabled }}
- name: PATH
value: /usr/local/nvidia/bin:/usr/local/cuda/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
- name: LD_LIBRARY_PATH
value: /usr/local/nvidia/lib:/usr/local/nvidia/lib64
- name: NVIDIA_DRIVER_CAPABILITIES
value: compute,utility
{{- end }}
{{- with .Values.ollama.resources }}
resources: {{- toYaml . | nindent 10 }}
{{- end }}
volumeMounts:
- name: data
mountPath: /root/.ollama
{{- with .Values.ollama.nodeSelector }}
nodeSelector:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.ollama.tolerations }}
tolerations:
{{- toYaml . | nindent 8 }}
{{- end }}
volumes:
{{- if and .Values.ollama.persistence.enabled .Values.ollama.persistence.existingClaim }}
- name: data
persistentVolumeClaim:
claimName: {{ .Values.ollama.persistence.existingClaim }}
{{- else if not .Values.ollama.persistence.enabled }}
- name: data
emptyDir: {}
{{- else if and .Values.ollama.persistence.enabled (not .Values.ollama.persistence.existingClaim) }}
[]
volumeClaimTemplates:
- metadata:
name: data
labels:
{{- include "ollama.selectorLabels" . | nindent 8 }}
{{- with .Values.ollama.persistence.annotations }}
annotations:
{{- toYaml . | nindent 8 }}
{{- end }}
spec:
accessModes:
{{- range .Values.ollama.persistence.accessModes }}
- {{ . | quote }}
{{- end }}
resources:
requests:
storage: {{ .Values.ollama.persistence.size | quote }}
storageClassName: {{ .Values.ollama.persistence.storageClass }}
{{- with .Values.ollama.persistence.selector }}
selector:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- end }}
{{- end }}
@@ -1,62 +0,0 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "open-webui.name" . }}
labels:
{{- include "open-webui.labels" . | nindent 4 }}
{{- with .Values.webui.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
replicas: {{ .Values.webui.replicaCount }}
selector:
matchLabels:
{{- include "open-webui.selectorLabels" . | nindent 6 }}
template:
metadata:
labels:
{{- include "open-webui.labels" . | nindent 8 }}
{{- with .Values.webui.podAnnotations }}
annotations:
{{- toYaml . | nindent 8 }}
{{- end }}
spec:
enableServiceLinks: false
automountServiceAccountToken: false
containers:
- name: {{ .Chart.Name }}
{{- with .Values.webui.image }}
image: {{ .repository }}:{{ .tag | default $.Chart.AppVersion }}
imagePullPolicy: {{ .pullPolicy }}
{{- end }}
ports:
- name: http
containerPort: {{ .Values.webui.service.containerPort }}
{{- with .Values.webui.resources }}
resources: {{- toYaml . | nindent 10 }}
{{- end }}
volumeMounts:
- name: data
mountPath: /app/backend/data
env:
- name: OLLAMA_BASE_URL
value: {{ include "ollama.url" . | quote }}
tty: true
{{- with .Values.webui.nodeSelector }}
nodeSelector:
{{- toYaml . | nindent 8 }}
{{- end }}
volumes:
{{- if and .Values.webui.persistence.enabled .Values.webui.persistence.existingClaim }}
- name: data
persistentVolumeClaim:
claimName: {{ .Values.webui.persistence.existingClaim }}
{{- else if not .Values.webui.persistence.enabled }}
- name: data
emptyDir: {}
{{- else if and .Values.webui.persistence.enabled (not .Values.webui.persistence.existingClaim) }}
- name: data
persistentVolumeClaim:
claimName: {{ include "open-webui.name" . }}
{{- end }}
@@ -1,33 +0,0 @@
{{- if .Values.webui.ingress.enabled }}
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: {{ include "open-webui.name" . }}
labels:
{{- include "open-webui.labels" . | nindent 4 }}
{{- with .Values.webui.ingress.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
{{- with .Values.webui.ingress.class }}
ingressClassName: {{ . }}
{{- end }}
{{- if .Values.webui.ingress.tls }}
tls:
- hosts:
- {{ .Values.webui.ingress.host | quote }}
secretName: {{ default (printf "%s-tls" .Release.Name) .Values.webui.ingress.existingSecret }}
{{- end }}
rules:
- host: {{ .Values.webui.ingress.host }}
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: {{ include "open-webui.name" . }}
port:
name: http
{{- end }}
-27
View File
@@ -1,27 +0,0 @@
{{- if and .Values.webui.persistence.enabled (not .Values.webui.persistence.existingClaim) }}
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: {{ include "open-webui.name" . }}
labels:
{{- include "open-webui.selectorLabels" . | nindent 4 }}
{{- with .Values.webui.persistence.annotations }}
annotations:
{{- toYaml . | nindent 8 }}
{{- end }}
spec:
accessModes:
{{- range .Values.webui.persistence.accessModes }}
- {{ . | quote }}
{{- end }}
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 }}
{{- end }}
{{- end }}
@@ -1,29 +0,0 @@
apiVersion: v1
kind: Service
metadata:
name: {{ include "open-webui.name" . }}
labels:
{{- include "open-webui.labels" . | nindent 4 }}
{{- with .Values.webui.service.labels }}
{{- toYaml . | nindent 4 }}
{{- end }}
{{- with .Values.webui.service.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
selector:
{{- include "open-webui.selectorLabels" . | nindent 4 }}
type: {{ .Values.webui.service.type | default "ClusterIP" }}
ports:
- protocol: TCP
name: http
port: {{ .Values.webui.service.port }}
targetPort: http
{{- if .Values.webui.service.nodePort }}
nodePort: {{ .Values.webui.service.nodePort | int }}
{{- end }}
{{- if .Values.webui.service.loadBalancerClass }}
loadBalancerClass: {{ .Values.webui.service.loadBalancerClass | quote }}
{{- end }}
-27
View File
@@ -1,27 +0,0 @@
ollama:
resources:
requests:
cpu: "2000m"
memory: "2Gi"
limits:
cpu: "4000m"
memory: "4Gi"
nvidia.com/gpu: "0"
service:
type: ClusterIP
gpu:
enabled: false
webui:
resources:
requests:
cpu: "500m"
memory: "500Mi"
limits:
cpu: "1000m"
memory: "1Gi"
ingress:
enabled: true
host: open-webui.minikube.local
service:
type: NodePort
-75
View File
@@ -1,75 +0,0 @@
nameOverride: ""
ollama:
externalHost: ""
annotations: {}
podAnnotations: {}
replicaCount: 1
image:
repository: ollama/ollama
tag: latest
pullPolicy: Always
resources: {}
persistence:
enabled: true
size: 30Gi
existingClaim: ""
accessModes:
- ReadWriteOnce
storageClass: ""
selector: {}
annotations: {}
nodeSelector: {}
# -- If using a special runtime container such as nvidia, set it here.
runtimeClassName: ""
tolerations:
- key: nvidia.com/gpu
operator: Exists
effect: NoSchedule
service:
type: ClusterIP
annotations: {}
port: 80
containerPort: 11434
gpu:
# -- Enable additional ENV values to help Ollama discover GPU usage
enabled: false
webui:
annotations: {}
podAnnotations: {}
replicaCount: 1
image:
repository: ghcr.io/open-webui/open-webui
tag: ""
pullPolicy: Always
resources: {}
ingress:
enabled: false
class: ""
# -- Use appropriate annotations for your Ingress controller, e.g., for NGINX:
# nginx.ingress.kubernetes.io/rewrite-target: /
annotations: {}
host: ""
tls: false
existingSecret: ""
persistence:
enabled: true
size: 2Gi
existingClaim: ""
# -- If using multiple replicas, you must update accessModes to ReadWriteMany
accessModes:
- ReadWriteOnce
storageClass: ""
selector: {}
annotations: {}
nodeSelector: {}
tolerations: []
service:
type: ClusterIP
annotations: {}
port: 80
containerPort: 8080
nodePort: ""
labels: {}
loadBalancerClass: ""
+1609 -169
View File
File diff suppressed because it is too large Load Diff
+6 -3
View File
@@ -1,6 +1,6 @@
{
"name": "open-webui",
"version": "0.1.121",
"version": "0.1.124",
"private": true,
"scripts": {
"dev": "vite dev --host",
@@ -14,7 +14,8 @@
"lint:backend": "pylint backend/",
"format": "prettier --plugin-search-dir --write '**/*.{js,ts,svelte,css,md,html,json}'",
"format:backend": "black . --exclude \"/venv/\"",
"i18n:parse": "i18next --config i18next-parser.config.ts && prettier --write 'src/lib/i18n/**/*.{js,json}'"
"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",
@@ -53,7 +57,6 @@
"i18next-resources-to-backend": "^1.2.0",
"idb": "^7.1.1",
"js-sha256": "^0.10.1",
"jspdf": "^2.5.1",
"katex": "^0.16.9",
"marked": "^9.1.0",
"svelte-sonner": "^0.3.19",
+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
+9
View File
@@ -82,3 +82,12 @@ select {
.katex-mathml {
display: none;
}
.scrollbar-none:active::-webkit-scrollbar-thumb,
.scrollbar-none:focus::-webkit-scrollbar-thumb,
.scrollbar-none:hover::-webkit-scrollbar-thumb {
visibility: visible;
}
.scrollbar-none::-webkit-scrollbar-thumb {
visibility: hidden;
}
+43
View File
@@ -6,6 +6,12 @@
<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" />
<link
rel="search"
type="application/opensearchdescription+xml"
title="Open WebUI"
href="/opensearch.xml"
/>
<script>
// On page load or when changing themes, best to add inline in `head` to avoid FOUC
(() => {
@@ -43,9 +49,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>
+5 -2
View File
@@ -30,6 +30,8 @@ export const getAudioConfig = async (token: string) => {
type OpenAIConfigForm = {
url: string;
key: string;
model: string;
speaker: string;
};
export const updateAudioConfig = async (token: string, payload: OpenAIConfigForm) => {
@@ -95,7 +97,8 @@ export const transcribeAudio = async (token: string, file: File) => {
export const synthesizeOpenAISpeech = async (
token: string = '',
speaker: string = 'alloy',
text: string = ''
text: string = '',
model: string = 'tts-1'
) => {
let error = null;
@@ -106,7 +109,7 @@ export const synthesizeOpenAISpeech = async (
'Content-Type': 'application/json'
},
body: JSON.stringify({
model: 'tts-1',
model: model,
input: text,
voice: speaker
})
+39
View File
@@ -95,6 +95,45 @@ export const userSignUp = async (
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) => {
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 updateUserProfile = async (token: string, name: string, profileImageUrl: string) => {
let error = null;
+49 -5
View File
@@ -1,4 +1,5 @@
import { WEBUI_API_BASE_URL } from '$lib/constants';
import { getTimeRange } from '$lib/utils';
export const createNewChat = async (token: string, chat: object) => {
let error = null;
@@ -59,7 +60,44 @@ export const getChatList = async (token: string = '') => {
throw error;
}
return res;
return res.map((chat) => ({
...chat,
time_range: getTimeRange(chat.updated_at)
}));
};
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.map((chat) => ({
...chat,
time_range: getTimeRange(chat.updated_at)
}));
};
export const getArchivedChatList = async (token: string = '') => {
@@ -189,13 +227,16 @@ export const getAllChatTags = async (token: string) => {
export const getChatListByTagName = async (token: string = '', tagName: string) => {
let error = null;
const res = await fetch(`${WEBUI_API_BASE_URL}/chats/tags/tag/${tagName}`, {
method: 'GET',
const res = await fetch(`${WEBUI_API_BASE_URL}/chats/tags`, {
method: 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
...(token && { authorization: `Bearer ${token}` })
}
},
body: JSON.stringify({
name: tagName
})
})
.then(async (res) => {
if (!res.ok) throw await res.json();
@@ -214,7 +255,10 @@ export const getChatListByTagName = async (token: string = '', tagName: string)
throw error;
}
return res;
return res.map((chat) => ({
...chat,
time_range: getTimeRange(chat.updated_at)
}));
};
export const getChatById = async (token: string, id: string) => {
+6 -2
View File
@@ -159,7 +159,11 @@ export const generateTitle = async (
body: JSON.stringify({
model: model,
prompt: template,
stream: false
stream: false,
options: {
// Restrict the number of tokens generated to 50
num_predict: 50
}
})
})
.then(async (res) => {
@@ -178,7 +182,7 @@ export const generateTitle = async (
throw error;
}
return res?.response ?? 'New Chat';
return res?.response.replace(/["']/g, '') ?? 'New Chat';
};
export const generatePrompt = async (token: string = '', model: string, conversation: string) => {
+11 -6
View File
@@ -211,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}`,
@@ -231,13 +233,14 @@ export const generateOpenAIChatCompletion = async (
throw error;
}
return res;
return [res, controller];
};
export const synthesizeOpenAISpeech = async (
token: string = '',
speaker: string = 'alloy',
text: string = ''
text: string = '',
model: string = 'tts-1'
) => {
let error = null;
@@ -248,7 +251,7 @@ export const synthesizeOpenAISpeech = async (
'Content-Type': 'application/json'
},
body: JSON.stringify({
model: 'tts-1',
model: model,
input: text,
voice: speaker
})
@@ -293,7 +296,9 @@ export const generateTitle = async (
content: template
}
],
stream: false
stream: false,
// Restricting the max tokens to 50 to avoid long titles
max_tokens: 50
})
})
.then(async (res) => {
@@ -312,5 +317,5 @@ export const generateTitle = async (
throw error;
}
return res?.choices[0]?.message?.content ?? 'New Chat';
return res?.choices[0]?.message?.content.replace(/["']/g, '') ?? 'New Chat';
};
+102 -2
View File
@@ -32,9 +32,16 @@ type ChunkConfigForm = {
chunk_overlap: number;
};
type YoutubeConfigForm = {
language: string[];
translation?: string | null;
};
type RAGConfigForm = {
pdf_extract_images: boolean;
chunk: ChunkConfigForm;
pdf_extract_images?: boolean;
chunk?: ChunkConfigForm;
web_loader_ssl_verification?: boolean;
youtube?: YoutubeConfigForm;
};
export const updateRAGConfig = async (token: string, payload: RAGConfigForm) => {
@@ -123,6 +130,7 @@ export const getQuerySettings = async (token: string) => {
type QuerySettings = {
k: number | null;
r: number | null;
template: string | null;
};
@@ -220,6 +228,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,
@@ -413,3 +452,64 @@ export const updateEmbeddingConfig = async (token: string, payload: EmbeddingMod
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;
};
+41 -16
View File
@@ -1,15 +1,24 @@
import { EventSourceParserStream } from 'eventsource-parser/stream';
import type { ParsedEvent } from 'eventsource-parser';
type TextStreamUpdate = {
done: boolean;
value: string;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
citations?: any;
};
// createOpenAITextStream takes a ReadableStreamDefaultReader from an SSE response,
// 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(
messageStream: ReadableStreamDefaultReader,
responseBody: ReadableStream<Uint8Array>,
splitLargeDeltas: boolean
): Promise<AsyncGenerator<TextStreamUpdate>> {
let iterator = openAIStreamToIterator(messageStream);
const eventStream = responseBody
.pipeThrough(new TextDecoderStream())
.pipeThrough(new EventSourceParserStream())
.getReader();
let iterator = openAIStreamToIterator(eventStream);
if (splitLargeDeltas) {
iterator = streamLargeDeltasAsRandomChunks(iterator);
}
@@ -17,7 +26,7 @@ export async function createOpenAITextStream(
}
async function* openAIStreamToIterator(
reader: ReadableStreamDefaultReader
reader: ReadableStreamDefaultReader<ParsedEvent>
): AsyncGenerator<TextStreamUpdate> {
while (true) {
const { value, done } = await reader.read();
@@ -25,19 +34,27 @@ async function* openAIStreamToIterator(
yield { done: true, value: '' };
break;
}
const lines = value.split('\n');
for (const line of lines) {
if (line !== '') {
console.log(line);
if (line === 'data: [DONE]') {
yield { done: true, value: '' };
} else {
const data = JSON.parse(line.replace(/^data: /, ''));
console.log(data);
if (!value) {
continue;
}
const data = value.data;
if (data.startsWith('[DONE]')) {
yield { done: true, value: '' };
break;
}
yield { done: false, value: data.choices[0].delta.content ?? '' };
}
try {
const parsedData = JSON.parse(data);
console.log(parsedData);
if (parsedData.citations) {
yield { done: false, value: '', citations: parsedData.citations };
continue;
}
yield { done: false, value: parsedData.choices?.[0]?.delta?.content ?? '' };
} catch (e) {
console.error('Error extracting delta from SSE event:', e);
}
}
}
@@ -52,6 +69,10 @@ async function* streamLargeDeltasAsRandomChunks(
yield textStreamUpdate;
return;
}
if (textStreamUpdate.citations) {
yield textStreamUpdate;
continue;
}
let content = textStreamUpdate.value;
if (content.length < 5) {
yield { done: false, value: content };
@@ -61,7 +82,11 @@ async function* streamLargeDeltasAsRandomChunks(
const chunkSize = Math.min(Math.floor(Math.random() * 3) + 1, content.length);
const chunk = content.slice(0, chunkSize);
yield { done: false, value: chunk };
await sleep(5);
// 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);
}
}
+7 -3
View File
@@ -83,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();
})
@@ -100,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 -6
View File
@@ -22,7 +22,7 @@
</script>
<Modal bind:show>
<div class="px-5 py-4 dark:text-gray-300">
<div class="px-5 pt-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')}
@@ -57,10 +57,8 @@
</div>
</div>
<hr class=" dark:border-gray-800" />
<div class=" w-full p-4 px-5">
<div class=" overflow-y-scroll max-h-80">
<div class=" w-full p-4 px-5 text-gray-700 dark:text-gray-100">
<div class=" overflow-y-scroll max-h-80 scrollbar-none">
<div class="mb-3">
{#if changelog}
{#each Object.keys(changelog) as version}
@@ -110,7 +108,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($i18n.t('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')}
@@ -2,6 +2,7 @@
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');
@@ -32,7 +33,9 @@
on:click={() => {
// exportAllUserChats();
downloadDatabase(localStorage.token);
downloadDatabase(localStorage.token).catch((error) => {
toast.error(error);
});
}}
>
<div class=" self-center mr-3">
@@ -59,10 +62,11 @@
<!-- <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>
File diff suppressed because it is too large Load Diff
@@ -24,7 +24,7 @@
{
name: 'All Documents',
type: 'collection',
title: 'All Documents',
title: $i18n.t('All Documents'),
collection_names: $documents.map((doc) => doc.collection_name)
}
]
@@ -87,6 +87,17 @@
chatInputElement?.focus();
await tick();
};
const confirmSelectYoutube = async (url) => {
dispatch('youtube', url);
prompt = removeFirstHashWord(prompt);
const chatInputElement = document.getElementById('chat-textarea');
await tick();
chatInputElement?.focus();
await tick();
};
</script>
{#if filteredItems.length > 0 || prompt.split(' ')?.at(0)?.substring(1).startsWith('http')}
@@ -132,7 +143,30 @@
</button>
{/each}
{#if prompt.split(' ')?.at(0)?.substring(1).startsWith('http')}
{#if prompt.split(' ')?.at(0)?.substring(1).startsWith('https://www.youtube.com')}
<button
class="px-3 py-1.5 rounded-xl w-full text-left bg-gray-100 selected-command-option-button"
type="button"
on:click={() => {
const url = prompt.split(' ')?.at(0)?.substring(1);
if (isValidHttpUrl(url)) {
confirmSelectYoutube(url);
} else {
toast.error(
$i18n.t(
'Oops! Looks like the URL is invalid. Please double-check and try again.'
)
);
}
}}
>
<div class=" font-medium text-black line-clamp-1">
{prompt.split(' ')?.at(0)?.substring(1)}
</div>
<div class=" text-xs text-gray-600 line-clamp-1">{$i18n.t('Youtube')}</div>
</button>
{:else if prompt.split(' ')?.at(0)?.substring(1).startsWith('http')}
<button
class="px-3 py-1.5 rounded-xl w-full text-left bg-gray-100 selected-command-option-button"
type="button"
@@ -1,4 +1,6 @@
<script lang="ts">
import { createEventDispatcher } from 'svelte';
import { generatePrompt } from '$lib/apis/ollama';
import { models } from '$lib/stores';
import { splitStream } from '$lib/utils';
@@ -7,6 +9,8 @@
const i18n = getContext('i18n');
const dispatch = createEventDispatcher();
export let prompt = '';
export let user = null;
@@ -17,12 +21,7 @@
let filteredModels = [];
$: filteredModels = $models
.filter(
(p) =>
p.name !== 'hr' &&
!p.external &&
p.name.includes(prompt.split(' ')?.at(0)?.substring(1) ?? '')
)
.filter((p) => p.name.includes(prompt.split(' ')?.at(0)?.substring(1) ?? ''))
.sort((a, b) => a.name.localeCompare(b.name));
$: if (prompt) {
@@ -38,6 +37,11 @@
};
const confirmSelect = async (model) => {
prompt = '';
dispatch('select', model);
};
const confirmSelectCollaborativeChat = async (model) => {
// dispatch('select', model);
prompt = '';
user = JSON.parse(JSON.stringify(model.name));
@@ -127,40 +131,42 @@
};
</script>
{#if filteredModels.length > 0}
<div class="md:px-2 mb-3 text-left w-full absolute bottom-0 left-0 right-0">
<div class="flex w-full px-2">
<div class=" bg-gray-100 dark:bg-gray-700 w-10 rounded-l-xl text-center">
<div class=" text-lg font-semibold mt-2">@</div>
</div>
{#if prompt.charAt(0) === '@'}
{#if filteredModels.length > 0}
<div class="md:px-2 mb-3 text-left w-full absolute bottom-0 left-0 right-0">
<div class="flex w-full px-2">
<div class=" bg-gray-100 dark:bg-gray-700 w-10 rounded-l-xl text-center">
<div class=" text-lg font-semibold mt-2">@</div>
</div>
<div class="max-h-60 flex flex-col w-full rounded-r-xl bg-white">
<div class="m-1 overflow-y-auto p-1 rounded-r-xl space-y-0.5">
{#each filteredModels as model, modelIdx}
<button
class=" px-3 py-1.5 rounded-xl w-full text-left {modelIdx === selectedIdx
? ' bg-gray-100 selected-command-option-button'
: ''}"
type="button"
on:click={() => {
confirmSelect(model);
}}
on:mousemove={() => {
selectedIdx = modelIdx;
}}
on:focus={() => {}}
>
<div class=" font-medium text-black line-clamp-1">
{model.name}
</div>
<div class="max-h-60 flex flex-col w-full rounded-r-xl bg-white">
<div class="m-1 overflow-y-auto p-1 rounded-r-xl space-y-0.5">
{#each filteredModels as model, modelIdx}
<button
class=" px-3 py-1.5 rounded-xl w-full text-left {modelIdx === selectedIdx
? ' bg-gray-100 selected-command-option-button'
: ''}"
type="button"
on:click={() => {
confirmSelect(model);
}}
on:mousemove={() => {
selectedIdx = modelIdx;
}}
on:focus={() => {}}
>
<div class=" font-medium text-black line-clamp-1">
{model.name}
</div>
<!-- <div class=" text-xs text-gray-600 line-clamp-1">
<!-- <div class=" text-xs text-gray-600 line-clamp-1">
{doc.title}
</div> -->
</button>
{/each}
</button>
{/each}
</div>
</div>
</div>
</div>
</div>
{/if}
{/if}
@@ -1,56 +1,118 @@
<script lang="ts">
import Bolt from '$lib/components/icons/Bolt.svelte';
import { onMount, getContext } from 'svelte';
const i18n = getContext('i18n');
export let submitPrompt: Function;
export let suggestionPrompts = [];
let prompts = [];
$: prompts =
suggestionPrompts.length <= 4
? suggestionPrompts
: suggestionPrompts.sort(() => Math.random() - 0.5).slice(0, 4);
$: prompts = suggestionPrompts
.reduce((acc, current) => [...acc, ...[current]], [])
.sort(() => Math.random() - 0.5);
// suggestionPrompts.length <= 4
// ? suggestionPrompts
// : suggestionPrompts.sort(() => Math.random() - 0.5).slice(0, 4);
onMount(() => {
const containerElement = document.getElementById('suggestions-container');
if (containerElement) {
containerElement.addEventListener('wheel', function (event) {
if (event.deltaY !== 0) {
// If scrolling vertically, prevent default behavior
event.preventDefault();
// Adjust horizontal scroll position based on vertical scroll
containerElement.scrollLeft += event.deltaY;
}
});
}
});
</script>
<div class=" mb-3 md:p-1 text-left w-full">
<div class=" flex flex-wrap-reverse px-2 text-left">
{#if prompts.length > 0}
<div class="mb-2 flex gap-1 text-sm font-medium items-center text-gray-400 dark:text-gray-600">
<Bolt />
{$i18n.t('Suggested')}
</div>
{/if}
<div class="w-full">
<div
class="relative w-full flex gap-2 snap-x snap-mandatory md:snap-none overflow-x-auto tabs"
id="suggestions-container"
>
{#each prompts as prompt, promptIdx}
<div
class="{promptIdx > 1 ? 'hidden sm:inline-flex' : ''} basis-full sm:basis-1/2 p-[5px] px-1"
>
<div class="snap-center shrink-0">
<button
class=" flex-1 flex justify-between w-full h-full px-4 py-2.5 bg-gray-50 hover:bg-gray-100 dark:bg-gray-850 dark:hover:bg-gray-800 rounded-2xl transition group"
class="flex flex-col flex-1 shrink-0 w-64 justify-between h-36 p-5 px-6 bg-gray-50 hover:bg-gray-100 dark:bg-gray-850 dark:hover:bg-gray-800 rounded-3xl transition group"
on:click={() => {
submitPrompt(prompt.content);
}}
>
<div class="flex flex-col text-left self-center">
<div class="flex flex-col text-left">
{#if prompt.title && prompt.title[0] !== ''}
<div class="text-sm font-medium dark:text-gray-300">{prompt.title[0]}</div>
<div class="text-sm text-gray-500 line-clamp-1">{prompt.title[1]}</div>
<div
class=" font-medium dark:text-gray-300 dark:group-hover:text-gray-200 transition"
>
{prompt.title[0]}
</div>
<div class="text-sm text-gray-600 font-normal line-clamp-2">{prompt.title[1]}</div>
{:else}
<div class=" self-center text-sm font-medium dark:text-gray-300 line-clamp-2">
<div
class=" self-center text-sm font-medium dark:text-gray-300 dark:group-hover:text-gray-100 transition line-clamp-2"
>
{prompt.content}
</div>
{/if}
</div>
<div
class="self-center p-1 rounded-lg text-gray-50 group-hover:text-gray-800 dark:text-gray-850 dark:group-hover:text-gray-100 transition"
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="currentColor"
class="w-4 h-4"
<div class="w-full flex justify-between">
<div
class="text-xs text-gray-400 group-hover:text-gray-500 dark:text-gray-600 dark:group-hover:text-gray-500 transition self-center"
>
<path
fill-rule="evenodd"
d="M8 14a.75.75 0 0 1-.75-.75V4.56L4.03 7.78a.75.75 0 0 1-1.06-1.06l4.5-4.5a.75.75 0 0 1 1.06 0l4.5 4.5a.75.75 0 0 1-1.06 1.06L8.75 4.56v8.69A.75.75 0 0 1 8 14Z"
clip-rule="evenodd"
/>
</svg>
{$i18n.t('Prompt')}
</div>
<div
class="self-end p-1 rounded-lg text-gray-300 group-hover:text-gray-800 dark:text-gray-700 dark:group-hover:text-gray-100 transition"
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="currentColor"
class="size-4"
>
<path
fill-rule="evenodd"
d="M8 14a.75.75 0 0 1-.75-.75V4.56L4.03 7.78a.75.75 0 0 1-1.06-1.06l4.5-4.5a.75.75 0 0 1 1.06 0l4.5 4.5a.75.75 0 0 1-1.06 1.06L8.75 4.56v8.69A.75.75 0 0 1 8 14Z"
clip-rule="evenodd"
/>
</svg>
</div>
</div>
</button>
</div>
{/each}
<!-- <div class="snap-center shrink-0">
<img
class="shrink-0 w-80 h-40 rounded-lg shadow-xl bg-white"
src="https://images.unsplash.com/photo-1604999565976-8913ad2ddb7c?ixlib=rb-1.2.1&amp;ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&amp;auto=format&amp;fit=crop&amp;w=320&amp;h=160&amp;q=80"
/>
</div> -->
</div>
</div>
<style>
.tabs::-webkit-scrollbar {
display: none; /* for Chrome, Safari and Opera */
}
.tabs {
-ms-overflow-style: none; /* IE and Edge */
scrollbar-width: none; /* Firefox */
}
</style>
+106 -101
View File
@@ -12,7 +12,7 @@
import Placeholder from './Messages/Placeholder.svelte';
import Spinner from '../common/Spinner.svelte';
import { imageGenerations } from '$lib/apis/images';
import { copyToClipboard } from '$lib/utils';
import { copyToClipboard, findWordIndices } from '$lib/utils';
const i18n = getContext('i18n');
@@ -22,6 +22,8 @@
export let continueGeneration: Function;
export let regenerateResponse: Function;
export let prompt;
export let suggestionPrompts;
export let processing = '';
export let bottomPadding = false;
export let autoScroll;
@@ -236,108 +238,111 @@
history: history
});
};
// const messageDeleteHandler = async (messageId) => {
// const message = history.messages[messageId];
// const parentId = message.parentId;
// const childrenIds = message.childrenIds ?? [];
// const grandchildrenIds = [];
// // Iterate through childrenIds to find grandchildrenIds
// for (const childId of childrenIds) {
// const childMessage = history.messages[childId];
// const grandChildrenIds = childMessage.childrenIds ?? [];
// for (const grandchildId of grandchildrenIds) {
// const childMessage = history.messages[grandchildId];
// childMessage.parentId = parentId;
// }
// grandchildrenIds.push(...grandChildrenIds);
// }
// history.messages[parentId].childrenIds.push(...grandchildrenIds);
// history.messages[parentId].childrenIds = history.messages[parentId].childrenIds.filter(
// (id) => id !== messageId
// );
// // Select latest message
// let currentMessageId = grandchildrenIds.at(-1);
// if (currentMessageId) {
// let messageChildrenIds = history.messages[currentMessageId].childrenIds;
// while (messageChildrenIds.length !== 0) {
// currentMessageId = messageChildrenIds.at(-1);
// messageChildrenIds = history.messages[currentMessageId].childrenIds;
// }
// history.currentId = currentMessageId;
// }
// await updateChatById(localStorage.token, chatId, { messages, history });
// };
</script>
{#if messages.length == 0}
<Placeholder models={selectedModels} modelfiles={selectedModelfiles} />
{:else}
<div class=" pb-10">
{#key chatId}
{#each messages as message, messageIdx}
<div class=" w-full">
<div
class="flex flex-col justify-between px-5 mb-3 {$settings?.fullScreenMode ?? null
? 'max-w-full'
: 'max-w-3xl'} mx-auto rounded-lg group"
>
{#if message.role === 'user'}
<UserMessage
on:delete={() => messageDeleteHandler(message.id)}
user={$user}
{readOnly}
{message}
isFirstMessage={messageIdx === 0}
siblings={message.parentId !== null
? history.messages[message.parentId]?.childrenIds ?? []
: Object.values(history.messages)
.filter((message) => message.parentId === null)
.map((message) => message.id) ?? []}
{confirmEditMessage}
{showPreviousMessage}
{showNextMessage}
copyToClipboard={copyToClipboardWithToast}
/>
{:else}
<ResponseMessage
{message}
modelfiles={selectedModelfiles}
siblings={history.messages[message.parentId]?.childrenIds ?? []}
isLastMessage={messageIdx + 1 === messages.length}
{readOnly}
{updateChatMessages}
{confirmEditResponseMessage}
{showPreviousMessage}
{showNextMessage}
{rateMessage}
copyToClipboard={copyToClipboardWithToast}
{continueGeneration}
{regenerateResponse}
on:save={async (e) => {
console.log('save', e);
<div class="h-full flex mb-16">
{#if messages.length == 0}
<Placeholder
models={selectedModels}
modelfiles={selectedModelfiles}
{suggestionPrompts}
submitPrompt={async (p) => {
let text = p;
const message = e.detail;
history.messages[message.id] = message;
await updateChatById(localStorage.token, chatId, {
messages: messages,
history: history
});
}}
/>
{/if}
if (p.includes('{{CLIPBOARD}}')) {
const clipboardText = await navigator.clipboard.readText().catch((err) => {
toast.error($i18n.t('Failed to read clipboard contents'));
return '{{CLIPBOARD}}';
});
text = p.replaceAll('{{CLIPBOARD}}', clipboardText);
}
prompt = text;
await tick();
const chatInputElement = document.getElementById('chat-textarea');
if (chatInputElement) {
prompt = p;
chatInputElement.style.height = '';
chatInputElement.style.height = Math.min(chatInputElement.scrollHeight, 200) + 'px';
chatInputElement.focus();
const words = findWordIndices(prompt);
if (words.length > 0) {
const word = words.at(0);
chatInputElement.setSelectionRange(word?.startIndex, word.endIndex + 1);
}
}
await tick();
}}
/>
{:else}
<div class="w-full pt-2">
{#key chatId}
{#each messages as message, messageIdx}
<div class=" w-full {messageIdx === messages.length - 1 ? 'pb-28' : ''}">
<div
class="flex flex-col justify-between px-5 mb-3 {$settings?.fullScreenMode ?? null
? 'max-w-full'
: 'max-w-5xl'} mx-auto rounded-lg group"
>
{#if message.role === 'user'}
<UserMessage
on:delete={() => messageDeleteHandler(message.id)}
user={$user}
{readOnly}
{message}
isFirstMessage={messageIdx === 0}
siblings={message.parentId !== null
? history.messages[message.parentId]?.childrenIds ?? []
: Object.values(history.messages)
.filter((message) => message.parentId === null)
.map((message) => message.id) ?? []}
{confirmEditMessage}
{showPreviousMessage}
{showNextMessage}
copyToClipboard={copyToClipboardWithToast}
/>
{:else}
<ResponseMessage
{message}
modelfiles={selectedModelfiles}
siblings={history.messages[message.parentId]?.childrenIds ?? []}
isLastMessage={messageIdx + 1 === messages.length}
{readOnly}
{updateChatMessages}
{confirmEditResponseMessage}
{showPreviousMessage}
{showNextMessage}
{rateMessage}
copyToClipboard={copyToClipboardWithToast}
{continueGeneration}
{regenerateResponse}
on:save={async (e) => {
console.log('save', e);
const message = e.detail;
history.messages[message.id] = message;
await updateChatById(localStorage.token, chatId, {
messages: messages,
history: history
});
}}
/>
{/if}
</div>
</div>
</div>
{/each}
{/each}
{#if bottomPadding}
<div class=" mb-10" />
{/if}
{/key}
</div>
{/if}
{#if bottomPadding}
<div class=" pb-20" />
{/if}
{/key}
</div>
{/if}
</div>
@@ -0,0 +1,77 @@
<script lang="ts">
import { getContext, onMount, tick } from 'svelte';
import Modal from '$lib/components/common/Modal.svelte';
const i18n = getContext('i18n');
export let show = false;
export let citation;
let mergedDocuments = [];
$: if (citation) {
mergedDocuments = citation.document?.map((c, i) => {
return {
source: citation.source,
document: c,
metadata: citation.metadata?.[i]
};
});
}
</script>
<Modal size="lg" 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 capitalize">
{$i18n.t('Citation')}
</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-6 pb-5 md:space-x-4">
<div
class="flex flex-col w-full dark:text-gray-200 overflow-y-scroll max-h-[22rem] scrollbar-none"
>
{#each mergedDocuments as document, documentIdx}
<div class="flex flex-col w-full">
<div class="text-sm font-medium dark:text-gray-300">
{$i18n.t('Source')}
</div>
<div class="text-sm dark:text-gray-400">
{document.source?.name ?? $i18n.t('No source available')}
</div>
</div>
<div class="flex flex-col w-full">
<div class=" text-sm font-medium dark:text-gray-300">
{$i18n.t('Content')}
</div>
<pre class="text-sm dark:text-gray-400 whitespace-pre-line">
{document.document}
</pre>
</div>
{#if documentIdx !== mergedDocuments.length - 1}
<hr class=" dark:border-gray-850 my-3" />
{/if}
{/each}
</div>
</div>
</div>
</Modal>
@@ -31,7 +31,9 @@
>
</div>
<pre class=" rounded-b-lg hljs p-4 px-5 overflow-x-auto rounded-t-none"><code
<pre
class=" hljs p-4 px-5 overflow-x-auto"
style="border-top-left-radius: 0px; border-top-right-radius: 0px;"><code
class="language-{lang} rounded-t-none whitespace-pre">{@html highlightedCode || code}</code
></pre>
</div>
@@ -3,11 +3,19 @@
import { user } from '$lib/stores';
import { onMount, getContext } from 'svelte';
import { blur, fade } from 'svelte/transition';
import Suggestions from '../MessageInput/Suggestions.svelte';
const i18n = getContext('i18n');
export let models = [];
export let modelfiles = [];
export let submitPrompt;
export let suggestionPrompts;
let mounted = false;
let modelfile = null;
let selectedModelIdx = 0;
@@ -17,12 +25,16 @@
$: if (models.length > 0) {
selectedModelIdx = models.length - 1;
}
onMount(() => {
mounted = true;
});
</script>
{#if models.length > 0}
<div class="m-auto text-center max-w-md px-2">
<div class="flex justify-center mt-8">
<div class="flex -space-x-4 mb-1">
{#key mounted}
<div class="m-auto w-full max-w-6xl px-8 lg:px-24 pb-16">
<div class="flex justify-start">
<div class="flex -space-x-4 mb-1" in:fade={{ duration: 200 }}>
{#each models as model, modelIdx}
<button
on:click={() => {
@@ -33,15 +45,15 @@
<img
src={modelfiles[model]?.imageUrl ?? `${WEBUI_BASE_URL}/static/favicon.png`}
alt="modelfile"
class=" size-12 rounded-full border-[1px] border-gray-200 dark:border-none"
class=" size-[2.7rem] rounded-full border-[1px] border-gray-200 dark:border-none"
draggable="false"
/>
{:else}
<img
src={models.length === 1
? `${WEBUI_BASE_URL}/static/favicon.png`
src={$i18n.language === 'dg-DG'
? `/doge.png`
: `${WEBUI_BASE_URL}/static/favicon.png`}
class=" size-12 rounded-full border-[1px] border-gray-200 dark:border-none"
class=" size-[2.7rem] rounded-full border-[1px] border-gray-200 dark:border-none"
alt="logo"
draggable="false"
/>
@@ -50,26 +62,42 @@
{/each}
</div>
</div>
<div class=" mt-2 mb-5 text-2xl text-gray-800 dark:text-gray-100 font-semibold">
{#if modelfile}
<span class=" capitalize">
{modelfile.title}
</span>
<div class="mt-0.5 text-base font-normal text-gray-600 dark:text-gray-400">
{modelfile.desc}
</div>
{#if modelfile.user}
<div class="mt-0.5 text-sm font-normal text-gray-500 dark:text-gray-500">
By <a href="https://openwebui.com/m/{modelfile.user.username}"
>{modelfile.user.name ? modelfile.user.name : `@${modelfile.user.username}`}</a
>
</div>
{/if}
{:else}
<div class=" line-clamp-1">{$i18n.t('Hello, {{name}}', { name: $user.name })}</div>
<div>{$i18n.t('How can I help you today?')}</div>
{/if}
<div
class=" mt-2 mb-4 text-3xl text-gray-800 dark:text-gray-100 font-semibold text-left flex items-center gap-4"
>
<div>
<div class=" capitalize line-clamp-1" in:fade={{ duration: 200 }}>
{#if modelfile}
{modelfile.title}
{:else}
{$i18n.t('Hello, {{name}}', { name: $user.name })}
{/if}
</div>
<div in:fade={{ duration: 200, delay: 200 }}>
{#if modelfile}
<div class="mt-0.5 text-base font-normal text-gray-500 dark:text-gray-400">
{modelfile.desc}
</div>
{#if modelfile.user}
<div class="mt-0.5 text-sm font-normal text-gray-400 dark:text-gray-500">
By <a href="https://openwebui.com/m/{modelfile.user.username}"
>{modelfile.user.name ? modelfile.user.name : `@${modelfile.user.username}`}</a
>
</div>
{/if}
{:else}
<div class=" font-medium text-gray-400 dark:text-gray-500">
{$i18n.t('How can I help you today?')}
</div>
{/if}
</div>
</div>
</div>
<div class=" w-full" in:fade={{ duration: 200, delay: 300 }}>
<Suggestions {suggestionPrompts} {submitPrompt} />
</div>
</div>
{/if}
{/key}
@@ -123,7 +123,7 @@
submitHandler();
}}
>
Submit
{$i18n.t('Submit')}
</button>
</div>
</div>
@@ -23,15 +23,16 @@
revertSanitizedResponseContent,
sanitizeResponseContent
} from '$lib/utils';
import { WEBUI_BASE_URL } from '$lib/constants';
import Name from './Name.svelte';
import ProfileImage from './ProfileImage.svelte';
import Skeleton from './Skeleton.svelte';
import CodeBlock from './CodeBlock.svelte';
import Image from '$lib/components/common/Image.svelte';
import { WEBUI_BASE_URL } from '$lib/constants';
import Tooltip from '$lib/components/common/Tooltip.svelte';
import RateComment from './RateComment.svelte';
import CitationsModal from '$lib/components/chat/Messages/CitationsModal.svelte';
export let modelfiles = [];
export let message;
@@ -65,6 +66,9 @@
let showRateComment = false;
let showCitationModal = false;
let selectedCitation = null;
$: tokens = marked.lexer(sanitizeResponseContent(message.content));
const renderer = new marked.Renderer();
@@ -133,23 +137,26 @@
};
const renderLatex = () => {
let chatMessageElements = document.getElementsByClassName('chat-assistant');
// let lastChatMessageElement = chatMessageElements[chatMessageElements.length - 1];
let chatMessageElements = document
.getElementById(`message-${message.id}`)
?.getElementsByClassName('chat-assistant');
for (const element of chatMessageElements) {
auto_render(element, {
// customised options
// • auto-render specific keys, e.g.:
delimiters: [
{ left: '$$', right: '$$', display: false },
{ left: '$ ', right: ' $', display: false },
{ left: '\\(', right: '\\)', display: false },
{ left: '\\[', right: '\\]', display: false },
{ left: '[ ', right: ' ]', display: false }
],
// • rendering keys, e.g.:
throwOnError: false
});
if (chatMessageElements) {
for (const element of chatMessageElements) {
auto_render(element, {
// customised options
// • auto-render specific keys, e.g.:
delimiters: [
{ left: '$$', right: '$$', display: false },
{ left: '$ ', right: ' $', display: false },
{ left: '\\(', right: '\\)', display: false },
{ left: '\\[', right: '\\]', display: false },
{ left: '[ ', right: ' ]', display: false }
],
// • rendering keys, e.g.:
throwOnError: false
});
}
}
};
@@ -220,7 +227,8 @@
const res = await synthesizeOpenAISpeech(
localStorage.token,
$settings?.audio?.speaker,
sentence
sentence,
$settings?.audio?.model
).catch((error) => {
toast.error(error);
@@ -321,10 +329,13 @@
});
</script>
<CitationsModal bind:show={showCitationModal} citation={selectedCitation} />
{#key message.id}
<div class=" flex w-full message-{message.id}">
<div class=" flex w-full message-{message.id}" id="message-{message.id}">
<ProfileImage
src={modelfiles[message.model]?.imageUrl ?? `${WEBUI_BASE_URL}/static/favicon.png`}
src={modelfiles[message.model]?.imageUrl ??
($i18n.language === 'dg-DG' ? `/doge.png` : `${WEBUI_BASE_URL}/static/favicon.png`)}
/>
<div class="w-full overflow-hidden">
@@ -342,156 +353,193 @@
{/if}
</Name>
{#if message.content === ''}
<Skeleton />
{:else}
{#if message.files}
<div class="my-2.5 w-full flex overflow-x-auto gap-2 flex-wrap">
{#each message.files as file}
<div>
{#if file.type === 'image'}
<Image src={file.url} />
{/if}
</div>
{/each}
</div>
{/if}
{#if message.files}
<div class="my-2.5 w-full flex overflow-x-auto gap-2 flex-wrap">
{#each message.files as file}
<div>
{#if file.type === 'image'}
<Image src={file.url} />
{/if}
</div>
{/each}
</div>
{/if}
<div
class="prose chat-{message.role} w-full max-w-full dark:prose-invert prose-headings:my-0 prose-p:m-0 prose-p:-mb-6 prose-pre:my-0 prose-table:my-0 prose-blockquote:my-0 prose-img:my-0 prose-ul:-my-4 prose-ol:-my-4 prose-li:-my-3 prose-ul:-mb-6 prose-ol:-mb-8 prose-ol:p-0 prose-li:-mb-4 whitespace-pre-line"
>
<div>
{#if edit === true}
<div class=" w-full">
<textarea
id="message-edit-{message.id}"
bind:this={editTextAreaElement}
class=" bg-transparent outline-none w-full resize-none"
bind:value={editedContent}
on:input={(e) => {
e.target.style.height = '';
e.target.style.height = `${e.target.scrollHeight}px`;
<div
class="prose chat-{message.role} w-full max-w-full dark:prose-invert prose-headings:my-0 prose-p:m-0 prose-p:-mb-6 prose-pre:my-0 prose-table:my-0 prose-blockquote:my-0 prose-img:my-0 prose-ul:-my-4 prose-ol:-my-4 prose-li:-my-3 prose-ul:-mb-6 prose-ol:-mb-8 prose-ol:p-0 prose-li:-mb-4 whitespace-pre-line"
>
<div>
{#if edit === true}
<div class=" w-full">
<textarea
id="message-edit-{message.id}"
bind:this={editTextAreaElement}
class=" bg-transparent outline-none w-full resize-none"
bind:value={editedContent}
on:input={(e) => {
e.target.style.height = '';
e.target.style.height = `${e.target.scrollHeight}px`;
}}
/>
<div class=" mt-2 mb-1 flex justify-center space-x-2 text-sm font-medium">
<button
class="px-4 py-2 bg-emerald-700 hover:bg-emerald-800 text-gray-100 transition rounded-lg"
on:click={() => {
editMessageConfirmHandler();
}}
/>
>
{$i18n.t('Save')}
</button>
<div class=" mt-2 mb-1 flex justify-center space-x-2 text-sm font-medium">
<button
class="px-4 py-2 bg-emerald-600 hover:bg-emerald-700 text-gray-100 transition rounded-lg"
on:click={() => {
editMessageConfirmHandler();
}}
>
{$i18n.t('Save')}
</button>
<button
class=" px-4 py-2 hover:bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-100 transition outline outline-1 outline-gray-200 dark:outline-gray-600 rounded-lg"
on:click={() => {
cancelEditMessage();
}}
>
{$i18n.t('Cancel')}
</button>
</div>
<button
class=" px-4 py-2 hover:bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-100 transition outline outline-1 outline-gray-200 dark:outline-gray-600 rounded-lg"
on:click={() => {
cancelEditMessage();
}}
>
{$i18n.t('Cancel')}
</button>
</div>
{:else}
<div class="w-full">
{#if message?.error === true}
<div
class="flex mt-2 mb-4 space-x-2 border px-4 py-3 border-red-800 bg-red-800/30 font-medium rounded-lg"
</div>
{:else}
<div class="w-full">
{#if message?.error === true}
<div
class="flex mt-2 mb-4 space-x-2 border px-4 py-3 border-red-800 bg-red-800/30 font-medium rounded-lg"
>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="w-5 h-5 self-center"
>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="w-5 h-5 self-center"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M12 9v3.75m9-.75a9 9 0 11-18 0 9 9 0 0118 0zm-9 3.75h.008v.008H12v-.008z"
/>
</svg>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M12 9v3.75m9-.75a9 9 0 11-18 0 9 9 0 0118 0zm-9 3.75h.008v.008H12v-.008z"
/>
</svg>
<div class=" self-center">
{message.content}
</div>
<div class=" self-center">
{message.content}
</div>
{:else}
{#each tokens as token}
{#if token.type === 'code'}
<CodeBlock
lang={token.lang}
code={revertSanitizedResponseContent(token.text)}
/>
{:else}
{@html marked.parse(token.raw, {
...defaults,
gfm: true,
breaks: true,
renderer
})}
{/if}
{/each}
<!-- {@html marked(message.content.replaceAll('\\', '\\\\'))} -->
{/if}
</div>
{:else if message.content === ''}
<Skeleton />
{:else}
{#each tokens as token}
{#if token.type === 'code'}
<CodeBlock
lang={token.lang}
code={revertSanitizedResponseContent(token.text)}
/>
{:else}
{@html marked.parse(token.raw, {
...defaults,
gfm: true,
breaks: true,
renderer
})}
{/if}
{/each}
{/if}
{#if message.done}
<div
class=" flex justify-start space-x-1 overflow-x-auto buttons text-gray-700 dark:text-gray-500"
>
{#if siblings.length > 1}
<div class="flex self-center min-w-fit">
<button
class="self-center dark:hover:text-white hover:text-black transition"
on:click={() => {
showPreviousMessage(message);
}}
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
class="w-4 h-4"
>
<path
fill-rule="evenodd"
d="M12.79 5.23a.75.75 0 01-.02 1.06L8.832 10l3.938 3.71a.75.75 0 11-1.04 1.08l-4.5-4.25a.75.75 0 010-1.08l4.5-4.25a.75.75 0 011.06.02z"
clip-rule="evenodd"
/>
</svg>
</button>
{#if message.citations}
<div class="mt-1 mb-2 w-full flex gap-1 items-center">
{#each message.citations.reduce((acc, citation) => {
citation.document.forEach((document, index) => {
const metadata = citation.metadata?.[index];
const id = metadata?.source ?? 'N/A';
<div class="text-xs font-bold self-center min-w-fit dark:text-gray-100">
{siblings.indexOf(message.id) + 1} / {siblings.length}
const existingSource = acc.find((item) => item.id === id);
if (existingSource) {
existingSource.document.push(document);
existingSource.metadata.push(metadata);
} else {
acc.push( { id: id, source: citation?.source, document: [document], metadata: metadata ? [metadata] : [] } );
}
});
return acc;
}, []) as citation, idx}
<div class="flex gap-1 text-xs font-semibold">
<button
class="flex dark:text-gray-300 py-1 px-1 bg-gray-50 hover:bg-gray-100 dark:bg-gray-850 dark:hover:bg-gray-800 transition rounded-xl"
on:click={() => {
showCitationModal = true;
selectedCitation = citation;
}}
>
<div class="bg-white dark:bg-gray-700 rounded-full size-4">
{idx + 1}
</div>
<div class=" mx-2">
{citation.source.name}
</div>
</button>
</div>
{/each}
</div>
{/if}
<button
class="self-center dark:hover:text-white hover:text-black transition"
on:click={() => {
showNextMessage(message);
}}
{#if message.done || siblings.length > 1}
<div
class=" flex justify-start space-x-1 overflow-x-auto buttons text-gray-700 dark:text-gray-500"
>
{#if siblings.length > 1}
<div class="flex self-center min-w-fit">
<button
class="self-center dark:hover:text-white hover:text-black transition"
on:click={() => {
showPreviousMessage(message);
}}
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
class="w-4 h-4"
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
class="w-4 h-4"
>
<path
fill-rule="evenodd"
d="M7.21 14.77a.75.75 0 01.02-1.06L11.168 10 7.23 6.29a.75.75 0 111.04-1.08l4.5 4.25a.75.75 0 010 1.08l-4.5 4.25a.75.75 0 01-1.06-.02z"
clip-rule="evenodd"
/>
</svg>
</button>
</div>
{/if}
<path
fill-rule="evenodd"
d="M12.79 5.23a.75.75 0 01-.02 1.06L8.832 10l3.938 3.71a.75.75 0 11-1.04 1.08l-4.5-4.25a.75.75 0 010-1.08l4.5-4.25a.75.75 0 011.06.02z"
clip-rule="evenodd"
/>
</svg>
</button>
<div class="text-xs font-bold self-center min-w-fit dark:text-gray-100">
{siblings.indexOf(message.id) + 1} / {siblings.length}
</div>
<button
class="self-center dark:hover:text-white hover:text-black transition"
on:click={() => {
showNextMessage(message);
}}
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
class="w-4 h-4"
>
<path
fill-rule="evenodd"
d="M7.21 14.77a.75.75 0 01.02-1.06L11.168 10 7.23 6.29a.75.75 0 111.04-1.08l4.5 4.25a.75.75 0 010 1.08l-4.5 4.25a.75.75 0 01-1.06-.02z"
clip-rule="evenodd"
/>
</svg>
</button>
</div>
{/if}
{#if message.done}
{#if !readOnly}
<Tooltip content="Edit" placement="bottom">
<Tooltip content={$i18n.t('Edit')} placement="bottom">
<button
class="{isLastMessage
? 'visible'
@@ -518,7 +566,7 @@
</Tooltip>
{/if}
<Tooltip content="Copy" placement="bottom">
<Tooltip content={$i18n.t('Copy')} placement="bottom">
<button
class="{isLastMessage
? 'visible'
@@ -545,7 +593,7 @@
</Tooltip>
{#if !readOnly}
<Tooltip content="Good Response" placement="bottom">
<Tooltip content={$i18n.t('Good Response')} placement="bottom">
<button
class="{isLastMessage
? 'visible'
@@ -580,7 +628,7 @@
</button>
</Tooltip>
<Tooltip content="Bad Response" placement="bottom">
<Tooltip content={$i18n.t('Bad Response')} placement="bottom">
<button
class="{isLastMessage
? 'visible'
@@ -615,7 +663,7 @@
</Tooltip>
{/if}
<Tooltip content="Read Aloud" placement="bottom">
<Tooltip content={$i18n.t('Read Aloud')} placement="bottom">
<button
id="speak-button-{message.id}"
class="{isLastMessage
@@ -764,7 +812,7 @@
{/if}
{#if message.info}
<Tooltip content="Generation Info" placement="bottom">
<Tooltip content={$i18n.t('Generation Info')} placement="bottom">
<button
class=" {isLastMessage
? 'visible'
@@ -793,7 +841,7 @@
{/if}
{#if isLastMessage && !readOnly}
<Tooltip content="Continue Response" placement="bottom">
<Tooltip content={$i18n.t('Continue Response')} placement="bottom">
<button
type="button"
class="{isLastMessage
@@ -825,7 +873,7 @@
</button>
</Tooltip>
<Tooltip content="Regenerate" placement="bottom">
<Tooltip content={$i18n.t('Regenerate')} placement="bottom">
<button
type="button"
class="{isLastMessage
@@ -850,24 +898,24 @@
</button>
</Tooltip>
{/if}
</div>
{/if}
{/if}
</div>
{/if}
{#if showRateComment}
<RateComment
messageId={message.id}
bind:show={showRateComment}
bind:message
on:submit={() => {
updateChatMessages();
}}
/>
{/if}
</div>
{/if}
</div>
{#if showRateComment}
<RateComment
messageId={message.id}
bind:show={showRateComment}
bind:message
on:submit={() => {
updateChatMessages();
}}
/>
{/if}
</div>
{/if}
</div>
{/if}
</div>
</div>
</div>
{/key}
@@ -1,4 +1,4 @@
<div class="w-full mt-3">
<div class="w-full mt-3 mb-4">
<div class="animate-pulse flex w-full">
<div class="space-y-2 w-full">
<div class="h-2 bg-gray-200 dark:bg-gray-600 rounded mr-14" />
@@ -193,7 +193,7 @@
<div class=" mt-2 mb-1 flex justify-center space-x-2 text-sm font-medium">
<button
id="save-edit-message-button"
class="px-4 py-2 bg-emerald-600 hover:bg-emerald-700 text-gray-100 transition rounded-lg"
class="px-4 py-2 bg-emerald-700 hover:bg-emerald-800 text-gray-100 transition rounded-lg"
on:click={() => {
editMessageConfirmHandler();
}}
@@ -266,7 +266,7 @@
{/if}
{#if !readOnly}
<Tooltip content="Edit" placement="bottom">
<Tooltip content={$i18n.t('Edit')} placement="bottom">
<button
class="invisible group-hover:visible p-1 rounded dark:hover:text-white hover:text-black transition edit-user-message-button"
on:click={() => {
@@ -291,7 +291,7 @@
</Tooltip>
{/if}
<Tooltip content="Copy" placement="bottom">
<Tooltip content={$i18n.t('Copy')} placement="bottom">
<button
class="invisible group-hover:visible p-1 rounded dark:hover:text-white hover:text-black transition"
on:click={() => {
@@ -316,7 +316,7 @@
</Tooltip>
{#if !isFirstMessage && !readOnly}
<Tooltip content="Delete" placement="bottom">
<Tooltip content={$i18n.t('Delete')} placement="bottom">
<button
class="invisible group-hover:visible p-1 rounded dark:hover:text-white hover:text-black transition"
on:click={() => {

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