Compare commits

..

415 Commits

Author SHA1 Message Date
Timothy Jaeryang Baek 693dc3107a Merge pull request #4811 from open-webui/dev
Create and publish Docker images with specific build args / build-main-image (linux/amd64) (push) Waiting to run
Create and publish Docker images with specific build args / build-main-image (linux/arm64) (push) Waiting to run
Create and publish Docker images with specific build args / build-cuda-image (linux/amd64) (push) Waiting to run
Create and publish Docker images with specific build args / build-cuda-image (linux/arm64) (push) Waiting to run
Create and publish Docker images with specific build args / build-ollama-image (linux/amd64) (push) Waiting to run
Create and publish Docker images with specific build args / build-ollama-image (linux/arm64) (push) Waiting to run
Create and publish Docker images with specific build args / merge-main-images (push) Blocked by required conditions
Create and publish Docker images with specific build args / merge-cuda-images (push) Blocked by required conditions
Create and publish Docker images with specific build args / merge-ollama-images (push) Blocked by required conditions
0.3.16
2024-08-27 18:49:04 +02:00
Timothy J. Baek 63c0772135 Update uv.lock 2024-08-27 18:48:56 +02:00
Timothy J. Baek fb9b7275ad doc: changelog 2024-08-27 18:45:35 +02:00
Timothy J. Baek 7a024fbe1e refac 2024-08-27 18:18:40 +02:00
Timothy Jaeryang Baek 9dade91ef5 Merge pull request #4917 from Yanyutin753/upload_files_limit
🤖 Limit the size and number of uploaded files
2024-08-27 17:09:52 +02:00
Timothy J. Baek 35fa278b37 chore: format 2024-08-27 17:07:47 +02:00
Timothy J. Baek 6a21a77ee9 refac 2024-08-27 17:05:24 +02:00
Timothy J. Baek 628310b12b refac 2024-08-27 15:58:02 +02:00
Timothy J. Baek ef28330c1a refac: do NOT change default behaviour in a PR 2024-08-27 15:56:47 +02:00
Timothy J. Baek 69c4687a53 refac 2024-08-27 15:53:29 +02:00
Timothy J. Baek 09cba5b87a refac: rm sub standard code 2024-08-27 15:51:40 +02:00
Timothy J. Baek 600409682e refac: do not change default behaviour 2024-08-27 15:30:57 +02:00
Timothy J. Baek 689b05a73d enh: add content-type: application/x-ndjson to ollama /api/chat 2024-08-27 14:06:58 +02:00
Timothy J. Baek 062649e483 refac: endpoints regarding db operations 2024-08-27 14:01:00 +02:00
Timothy Jaeryang Baek bbeed7cd85 Merge pull request #4938 from KarlLee830/translate
i18n: Update Chinese translation
2024-08-27 13:30:42 +02:00
Karl Lee 6c49a15e20 i18n: Update Chinese translation 2024-08-27 19:12:47 +08:00
Timothy Jaeryang Baek 18a6b00083 Merge pull request #4929 from jannikstdl/improve-rag-status
fix: reset status on error + styling
2024-08-27 13:08:46 +02:00
Timothy Jaeryang Baek 7c841c9f63 Merge pull request #4924 from Peter-De-Ath/fix-duplicate-system-prompt
fix: stop system prompt being duplicated
2024-08-27 13:07:26 +02:00
Jannik Streidl 97d73d3d33 fix: reset status on error + styling 2024-08-27 11:24:05 +02:00
Peter De-Ath b2e682e263 fix: stop system prompt being duplicated 2024-08-26 21:25:57 +01:00
Clivia 29cbdbcadd 💄Fix format 2024-08-26 23:59:53 +08:00
Clivia ebca735f5e 💄Fix format 2024-08-26 23:53:51 +08:00
Clivia 775478534a 👀 Fix Common users cannot upload files
💄Fix format

💄Fix format i18

 Feat paste upload files and make restrictions

 Feat paste upload files and make restrictions
2024-08-26 23:36:13 +08:00
Clivia b01d72ade3 💄Fix format
💄Fix format
2024-08-26 23:36:13 +08:00
Clivia b6da4baa97 💄 Limit the size and number of uploaded files
💄 Limit the size and number of uploaded files
2024-08-26 23:36:13 +08:00
Timothy J. Baek 7fa9f381e1 chore: format 2024-08-26 15:38:42 +02:00
Timothy J. Baek a9673c793a fix 2024-08-26 15:37:11 +02:00
Timothy Jaeryang Baek b148865ee8 Merge pull request #4886 from kiosion/dev
feat: Add control for how message content is split for TTS generation requests
2024-08-26 15:02:30 +02:00
Timothy J. Baek f4f7adb377 refac 2024-08-26 15:01:29 +02:00
Timothy J. Baek b96239fb0b enh: block api user with model filter 2024-08-26 14:24:56 +02:00
Timothy J. Baek 7fc049a513 enh: codespan click to copy content 2024-08-26 14:17:33 +02:00
Timothy J. Baek faeabfb3d4 fix: include __files__ param to pipe function 2024-08-26 12:36:04 +02:00
Timothy J. Baek de6b5a7bbe refac 2024-08-26 12:27:00 +02:00
Timothy J. Baek efd4b03f78 enh: repopulate tools & functions from db if non existent 2024-08-26 12:18:12 +02:00
Timothy J. Baek e98a20fce9 fix: .md processing issue 2024-08-26 12:08:55 +02:00
Timothy J. Baek 5a0e1c5f75 refac: disable signups when ENABLE_LOGIN_FORM is set to false 2024-08-26 11:54:55 +02:00
Timothy Jaeryang Baek c224f1105e Merge pull request #4859 from 0xThresh/main
Enhancement: Update Docker image metadata
2024-08-26 11:38:16 +02:00
Timothy Jaeryang Baek 95185aaaec Merge pull request #4904 from que-nguyen/dev
Fix and update Vietnamese translations for better accuracy.
2024-08-26 11:35:01 +02:00
Que Nguyen cb7ee6212e Fix and update Vietnamese translations for better accuracy. 2024-08-26 14:14:00 +07:00
kiosion d78c35c9ba refac: Tidy Chat.svelte 2024-08-25 20:27:50 -04:00
kiosion 73998a70cc i18n: Add new strings for en, fr locales 2024-08-25 20:03:58 -04:00
kiosion 3967c34261 feat: Add control for how message content is split for TTS generation reqs 2024-08-25 20:03:21 -04:00
kiosion f30428754f fix: Safely retrieve settings from LocalStorage 2024-08-25 20:00:57 -04:00
James W. 56c9552ab3 Reset package.json to correct version 2024-08-25 14:46:21 -07:00
James W. e414ba2d8c Merge pull request #28 from 0xThresh/dev
Test updating version with all images
2024-08-25 15:34:37 -06:00
0xThresh.eth b1355e16bc Test updating version with all images 2024-08-25 15:33:08 -06:00
James W. 882b76cefa Merge pull request #27 from 0xThresh/dev
Add image updates to cuda and ollama image jobs
2024-08-25 15:28:51 -06:00
0xThresh.eth 63d82dbece Add image updates to cuda and ollama image jobs 2024-08-25 15:28:09 -06:00
Timothy J. Baek f568389235 refac 2024-08-25 18:42:27 +02:00
Timothy Jaeryang Baek 814473878f Merge pull request #4899 from open-webui/config-db-migration
feat: config.json db migration
2024-08-25 17:59:01 +02:00
Timothy J. Baek a44bae2d3a fix 2024-08-25 17:54:51 +02:00
Timothy J. Baek fd0370d801 fix 2024-08-25 16:57:01 +02:00
Timothy J. Baek 58cf1be20c feat: config.json db migration 2024-08-25 16:52:36 +02:00
Timothy Jaeryang Baek 072945c40b Merge pull request #4867 from El-Tatane/improve-fr-translations
i18n: Improve French translations
2024-08-25 14:50:24 +02:00
El-Tatane 7c7d407f34 i18n: Improve French translations 2024-08-23 22:00:05 +02:00
James W. c63a2dfbcd Merge pull request #26 from 0xThresh/dev
Test bumping the Open WebUI version
2024-08-23 12:11:24 -06:00
0xThresh.eth e1022b3a28 Test bumping the Open WebUI version 2024-08-23 12:10:39 -06:00
James W. 0022902e8d Merge pull request #25 from 0xThresh/dev
Try removing push by digest
2024-08-23 12:03:42 -06:00
0xThresh.eth 9be7c8b969 Try removing push by digest 2024-08-23 12:03:06 -06:00
James W. 913b454fc3 Merge pull request #24 from 0xThresh/dev
Move version tags to latest when no new release is created
2024-08-23 11:54:09 -06:00
0xThresh.eth ff1ea70dfa Only add version tag to main 2024-08-23 11:46:30 -06:00
0xThresh.eth 550386f52a Test moving tags in Actions 2024-08-23 11:40:02 -06:00
Timothy J. Baek 7a1fecbdb3 fix 2024-08-23 18:22:50 +02:00
Timothy J. Baek 8a99eaa68f fix 2024-08-23 18:21:19 +02:00
Timothy J. Baek e442b3b169 fix: async image gen automatic1111 2024-08-23 16:51:34 +02:00
Timothy J. Baek 553293f4d5 refac: styling 2024-08-23 16:45:07 +02:00
Timothy J. Baek 48503573c1 refac: call overlay styling 2024-08-23 16:42:36 +02:00
Timothy J. Baek 4519ddd0e9 refac: files rbac 2024-08-23 16:19:04 +02:00
Timothy J. Baek 8b3d5e8b80 chore: format 2024-08-23 15:56:50 +02:00
Timothy J. Baek 0a5a2e67e8 fix: uploaded files leaking to other user chats issue
#4601
2024-08-23 15:02:23 +02:00
Timothy J. Baek 7b91be21b4 refac 2024-08-23 14:43:32 +02:00
Timothy J. Baek 591962d906 refac: input commands 2024-08-23 14:31:39 +02:00
Timothy Jaeryang Baek 64c0157271 Merge pull request #4842 from KarlLee830/translate
i18n: Update Chinese translation
2024-08-23 14:08:48 +02:00
Timothy Jaeryang Baek f5c0e670aa Merge pull request #4847 from michaelpoluektov/fix-tools-param
fix: `__tools__` param
2024-08-23 14:08:32 +02:00
Michael Poluektov 83de28bac2 fix: tools param 2024-08-23 12:58:43 +01:00
Karl Lee c6885a8bcf i18n: Update Chinese translation 2024-08-23 18:01:26 +08:00
Timothy J. Baek 5d8dbff486 refac 2024-08-22 17:37:47 +02:00
Timothy J. Baek 0939cb5ed6 refac: styling 2024-08-22 17:34:28 +02:00
Timothy J. Baek 153a2876b4 fix: mediaStream not stopping after exiting call mode 2024-08-22 17:27:22 +02:00
Timothy J. Baek 61503a654c enh: call=true url search param 2024-08-22 17:17:32 +02:00
Timothy J. Baek c268a4e217 refac: only activate wakelock in call mode 2024-08-22 17:12:31 +02:00
Timothy J. Baek 70ab7735ba chore: format 2024-08-22 16:38:48 +02:00
Timothy J. Baek 25de3e753d fix 2024-08-22 16:34:12 +02:00
Timothy Jaeryang Baek 670672c067 Merge pull request #4813 from jannikstdl/rag-knowledge-status
feat: show rag status when using models with knowledge collections
2024-08-22 16:31:42 +02:00
Timothy J. Baek 85f8a80389 refac 2024-08-22 16:31:33 +02:00
Timothy J. Baek 673b893a8a refac 2024-08-22 16:11:19 +02:00
Timothy J. Baek d8d2f3529f refac 2024-08-22 16:08:03 +02:00
Timothy J. Baek 63ba8145b9 refac 2024-08-22 16:02:29 +02:00
Timothy Jaeryang Baek 99db82a161 Merge pull request #4815 from michaelpoluektov/fix-user-valves
fix: Fix user valves
2024-08-22 15:25:45 +02:00
Timothy J. Baek a3089e0472 fix 2024-08-22 15:24:48 +02:00
Timothy Jaeryang Baek 9186abf9c1 Merge branch 'dev' into fix-user-valves 2024-08-22 15:23:59 +02:00
Timothy J. Baek 97808ba1c3 refac 2024-08-22 15:23:32 +02:00
Timothy J. Baek 589420c208 refac 2024-08-22 15:20:19 +02:00
Timothy J. Baek 3bf7d569c6 refac 2024-08-22 15:11:31 +02:00
Timothy J. Baek 14187b027d refac 2024-08-22 15:09:06 +02:00
Timothy J. Baek 06f067fda9 refacfix 2024-08-22 15:03:39 +02:00
Michael Poluektov 9b5dfe64b7 fix: is not None 2024-08-22 13:58:10 +01:00
Timothy J. Baek 1c89c91f07 refac 2024-08-22 14:53:14 +02:00
Timothy J. Baek ed5761f18f refac: rye -> uv 2024-08-22 14:38:05 +02:00
Michael Poluektov 16ec25d296 fix user valves 2024-08-22 13:34:35 +01:00
Jannik Streidl abe17ab4b5 feat: show rag status when using models with knowledge collections 2024-08-22 14:30:11 +02:00
Timothy Jaeryang Baek 14f0e6a2ba Merge pull request #4783 from open-webui/dependabot/pip/backend/dev/langchain-community-0.2.12
chore(deps): bump langchain-community from 0.2.10 to 0.2.12 in /backend
2024-08-22 14:26:41 +02:00
Timothy Jaeryang Baek f1f2f034f8 Merge pull request #4785 from open-webui/dependabot/pip/backend/dev/markdown-3.7
chore(deps): bump markdown from 3.6 to 3.7 in /backend
2024-08-22 14:26:33 +02:00
Timothy J. Baek 63418a583a fix: latex rendering issue 2024-08-22 14:24:49 +02:00
dependabot[bot] 4031cb9eda chore(deps): bump langchain-community from 0.2.10 to 0.2.12 in /backend
Bumps [langchain-community](https://github.com/langchain-ai/langchain) from 0.2.10 to 0.2.12.
- [Release notes](https://github.com/langchain-ai/langchain/releases)
- [Commits](https://github.com/langchain-ai/langchain/compare/langchain-community==0.2.10...langchain-community==0.2.12)

---
updated-dependencies:
- dependency-name: langchain-community
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-08-22 12:10:43 +00:00
dependabot[bot] ee6b8c5b72 chore(deps): bump markdown from 3.6 to 3.7 in /backend
Bumps [markdown](https://github.com/Python-Markdown/markdown) from 3.6 to 3.7.
- [Release notes](https://github.com/Python-Markdown/markdown/releases)
- [Changelog](https://github.com/Python-Markdown/markdown/blob/master/docs/changelog.md)
- [Commits](https://github.com/Python-Markdown/markdown/compare/3.6...3.7)

---
updated-dependencies:
- dependency-name: markdown
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-08-22 12:07:04 +00:00
Timothy Jaeryang Baek 11aecbe79a Merge pull request #4768 from zhaofengli/db-url-fixes
fix: Fix parameter handling for PostgreSQL URLs
2024-08-22 14:01:28 +02:00
Timothy Jaeryang Baek a9bace0f97 Merge pull request #4784 from open-webui/dependabot/pip/backend/dev/langchain-0.2.14
chore(deps): bump langchain from 0.2.12 to 0.2.14 in /backend
2024-08-22 13:59:27 +02:00
Timothy Jaeryang Baek 7e4fed2451 Merge pull request #4786 from open-webui/dependabot/pip/backend/dev/unstructured-0.15.7
chore(deps): bump unstructured from 0.15.5 to 0.15.7 in /backend
2024-08-22 13:59:03 +02:00
Timothy Jaeryang Baek ac8a16ec5b Merge pull request #4787 from open-webui/dependabot/pip/backend/dev/langfuse-2.44.0
chore(deps): bump langfuse from 2.43.3 to 2.44.0 in /backend
2024-08-22 13:58:48 +02:00
Timothy Jaeryang Baek 95d016dea4 Merge pull request #4800 from aleixdorca/dev
i18n: Update catalan translation.json
2024-08-22 13:58:14 +02:00
Timothy Jaeryang Baek abe36a3e67 Merge pull request #4803 from CJDaniel96/dev
fix: DeprecationWarning for datetime.utcnow() by using datetime.now(UTC)
2024-08-22 13:57:57 +02:00
USIGLOBAL\daniel_tsai 89ebbed67b fix: DeprecationWarning for datetime.utcnow() by using datetime.now(UTC) 2024-08-22 15:12:40 +08:00
Aleix Dorca ec075e2612 Update catalan translation.json 2024-08-22 07:12:42 +02:00
Timothy Jaeryang Baek e2b7296786 Merge pull request #4798 from open-webui/dev
fix: filter compatibility issue
2024-08-22 01:14:34 +02:00
Timothy J. Baek 6fcd40d4d8 fix: filter compatibility issue 2024-08-22 01:08:59 +02:00
Timothy Jaeryang Baek 847ca66001 Merge pull request #4795 from open-webui/dev
Create and publish Docker images with specific build args / build-main-image (linux/amd64) (push) Waiting to run
Create and publish Docker images with specific build args / build-main-image (linux/arm64) (push) Waiting to run
Create and publish Docker images with specific build args / build-cuda-image (linux/amd64) (push) Waiting to run
Create and publish Docker images with specific build args / build-cuda-image (linux/arm64) (push) Waiting to run
Create and publish Docker images with specific build args / build-ollama-image (linux/amd64) (push) Waiting to run
Create and publish Docker images with specific build args / build-ollama-image (linux/arm64) (push) Waiting to run
Create and publish Docker images with specific build args / merge-main-images (push) Blocked by required conditions
Create and publish Docker images with specific build args / merge-cuda-images (push) Blocked by required conditions
Create and publish Docker images with specific build args / merge-ollama-images (push) Blocked by required conditions
0.3.15
2024-08-22 00:28:30 +02:00
Timothy J. Baek 85b4129219 fix 2024-08-22 00:25:43 +02:00
Timothy J. Baek c36f83df5b doc: changelog 2024-08-22 00:23:40 +02:00
Timothy J. Baek bb026cdd9c fix: many model chat backward compatibility 2024-08-22 00:22:40 +02:00
Timothy J. Baek 8843898a8c fix: older many model chat compatibility 2024-08-22 00:03:58 +02:00
Timothy J. Baek f036aa0a48 doc: changelog 2024-08-21 23:47:33 +02:00
Timothy J. Baek 1e928e463f chore: format 2024-08-21 23:35:54 +02:00
Timothy J. Baek 36e895c135 fix 2024-08-21 23:33:32 +02:00
Timothy J. Baek 3447f233fa fix 2024-08-21 23:27:23 +02:00
Timothy J. Baek f3e8dd1f2e enh: comfyui logs 2024-08-21 23:22:12 +02:00
Timothy J. Baek 3f0b7b29db fix 2024-08-21 23:11:12 +02:00
Timothy J. Baek 95cf90d787 enh: comfyui seed node support 2024-08-21 23:05:20 +02:00
Timothy J. Baek f3f6941205 fix: many model chat actions not working 2024-08-21 22:54:56 +02:00
Timothy J. Baek 4a21c5c5e7 refac 2024-08-21 22:42:25 +02:00
Timothy J. Baek 180f2b9a83 fix: functions 2024-08-21 22:39:36 +02:00
Timothy J. Baek 94eb91063c fix 2024-08-21 22:27:21 +02:00
Timothy J. Baek dc4c6b3b14 enh: temporary-chat url search param 2024-08-21 18:55:12 +02:00
Timothy Jaeryang Baek 50d53c6f8d Merge pull request #4790 from open-webui/dev
fix
2024-08-21 18:33:57 +02:00
Timothy J. Baek a93b0ac143 fix 2024-08-21 18:33:31 +02:00
Timothy Jaeryang Baek ed92205d7d Merge pull request #4789 from open-webui/dev
fix
2024-08-21 18:30:10 +02:00
Timothy J. Baek acaf135a2f fix 2024-08-21 18:29:52 +02:00
Timothy Jaeryang Baek bf6c6afb21 Merge pull request #4788 from open-webui/main
dev
2024-08-21 17:41:15 +02:00
dependabot[bot] f5994e3a44 chore(deps): bump langfuse from 2.43.3 to 2.44.0 in /backend
Bumps [langfuse](https://github.com/langfuse/langfuse) from 2.43.3 to 2.44.0.
- [Release notes](https://github.com/langfuse/langfuse/releases)
- [Commits](https://github.com/langfuse/langfuse/commits/v2.44.0)

---
updated-dependencies:
- dependency-name: langfuse
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-08-21 15:38:07 +00:00
dependabot[bot] ccf5bd1492 chore(deps): bump unstructured from 0.15.5 to 0.15.7 in /backend
Bumps [unstructured](https://github.com/Unstructured-IO/unstructured) from 0.15.5 to 0.15.7.
- [Release notes](https://github.com/Unstructured-IO/unstructured/releases)
- [Changelog](https://github.com/Unstructured-IO/unstructured/blob/0.15.7/CHANGELOG.md)
- [Commits](https://github.com/Unstructured-IO/unstructured/compare/0.15.5...0.15.7)

---
updated-dependencies:
- dependency-name: unstructured
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-08-21 15:38:05 +00:00
dependabot[bot] 510a0f3a2f chore(deps): bump langchain from 0.2.12 to 0.2.14 in /backend
Bumps [langchain](https://github.com/langchain-ai/langchain) from 0.2.12 to 0.2.14.
- [Release notes](https://github.com/langchain-ai/langchain/releases)
- [Commits](https://github.com/langchain-ai/langchain/compare/langchain==0.2.12...langchain==0.2.14)

---
updated-dependencies:
- dependency-name: langchain
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-08-21 15:37:56 +00:00
Timothy Jaeryang Baek 8a620cab44 Merge pull request #4623 from open-webui/dev
Create and publish Docker images with specific build args / build-main-image (linux/amd64) (push) Waiting to run
Create and publish Docker images with specific build args / build-main-image (linux/arm64) (push) Waiting to run
Create and publish Docker images with specific build args / build-cuda-image (linux/amd64) (push) Waiting to run
Create and publish Docker images with specific build args / build-cuda-image (linux/arm64) (push) Waiting to run
Create and publish Docker images with specific build args / build-ollama-image (linux/amd64) (push) Waiting to run
Create and publish Docker images with specific build args / build-ollama-image (linux/arm64) (push) Waiting to run
Create and publish Docker images with specific build args / merge-main-images (push) Blocked by required conditions
Create and publish Docker images with specific build args / merge-cuda-images (push) Blocked by required conditions
Create and publish Docker images with specific build args / merge-ollama-images (push) Blocked by required conditions
0.3.14
2024-08-21 17:36:58 +02:00
Timothy J. Baek fcffb0adf2 fix 2024-08-21 17:28:09 +02:00
Timothy J. Baek efcb56f0dc refac 2024-08-21 17:27:39 +02:00
Timothy J. Baek 86ee19178e doc: changelog 2024-08-21 17:09:18 +02:00
Timothy J. Baek 1fc306acd0 chore: backend bump 2024-08-21 15:47:03 +02:00
Timothy J. Baek da1d5ad917 fix 2024-08-21 15:42:33 +02:00
Timothy J. Baek b45db22a17 chore: format 2024-08-21 15:16:37 +02:00
Timothy J. Baek 19455ab04e fix 2024-08-21 15:15:29 +02:00
Timothy J. Baek 71b1661f00 fix 2024-08-21 14:49:54 +02:00
Timothy J. Baek 99764bfd29 fix 2024-08-21 14:49:17 +02:00
Timothy J. Baek 063e006446 feat: custom comfyui workflow
Co-Authored-By: John Karabudak <hello@johnthenerd.com>
2024-08-21 14:44:47 +02:00
Timothy J. Baek 436f009dab refac 2024-08-21 14:20:52 +02:00
Timothy J. Baek 71a66ed4cf refac 2024-08-21 14:18:47 +02:00
Timothy J. Baek 29bef261be refac 2024-08-21 14:16:33 +02:00
Timothy J. Baek e2291f7148 refac 2024-08-21 01:39:30 +02:00
Timothy J. Baek 8b5aed7a2b refac 2024-08-21 01:21:03 +02:00
Timothy J. Baek 95057d2368 refac: image gen 2024-08-21 00:35:42 +02:00
Timothy Jaeryang Baek 20dadf9b5a Merge pull request #4760 from michaelpoluektov/tools-refac-2.1
Fix: tools filter
2024-08-20 18:51:28 +02:00
Michael Poluektov 3d6ac3a7db Merge branch 'dev' of https://github.com/open-webui/open-webui into tools-refac-2.1 2024-08-20 17:42:25 +01:00
Michael Poluektov bd47bbbce9 fix tools filter 2024-08-20 17:41:51 +01:00
Timothy Jaeryang Baek ddebfc7413 Merge pull request #4757 from OriginalSimon/dev
i18n: Update of the Ukrainian translation
2024-08-20 18:17:40 +02:00
Timothy J. Baek c5310e84db feat: custom COMFYUI_WORKFLOW
deprecates several comfyui env vars
2024-08-20 18:17:15 +02:00
Simon 0c8301e79c Update translation.json 2024-08-20 17:44:20 +02:00
Timothy J. Baek 73faa8dc80 refac: styling 2024-08-20 17:03:20 +02:00
Timothy Jaeryang Baek ee526b4b07 Merge pull request #4724 from michaelpoluektov/tools-refac-2.1
feat: Add `__tools__` optional param for function pipes
2024-08-20 17:01:13 +02:00
Michael Poluektov 454f59d59a undo frontend change 2024-08-20 15:48:14 +01:00
Michael Poluektov 2e3146263c put tool_ids and files in metadata 2024-08-20 15:41:49 +01:00
Michael Poluektov bcbcd5fde9 Merge branch 'dev' of https://github.com/open-webui/open-webui into tools-refac-2.1 2024-08-20 14:56:47 +01:00
Timothy J. Baek 83a596612a fix 2024-08-20 14:28:19 +02:00
Timothy J. Baek 2b896989b8 refac 2024-08-20 14:27:14 +02:00
Timothy Jaeryang Baek 27109d22e4 Merge pull request #4743 from 5E-324/add-num_gpu
fix: Changes to num_gpu made in Settings > General won't be saved
2024-08-20 13:38:11 +02:00
Timothy Jaeryang Baek 22d8f8f1ef Merge pull request #4746 from 5E-324/fix-type-errors
chore: Fix type errors.
2024-08-20 13:37:08 +02:00
Zhuoran fd26e5635d Fix type errors. 2024-08-20 08:44:09 +08:00
Zhuoran b350b0023f Add num_gpu in General.svelte 2024-08-20 07:47:20 +08:00
Timothy J. Baek 330eb0fbb1 enh: user message edit 2024-08-19 21:51:10 +02:00
Timothy J. Baek d79d3f1352 enh: mermaid dark theme 2024-08-19 19:42:31 +02:00
Michael Poluektov 44966db505 avoid ugly exception 2024-08-19 17:04:57 +01:00
Timothy J. Baek 1e8abea753 refac 2024-08-19 17:58:54 +02:00
Timothy J. Baek 21d8ff61bb refac 2024-08-19 17:57:47 +02:00
Michael Poluektov 9652c8f8af dont delete files and tool_ids 2024-08-19 16:57:29 +01:00
Timothy J. Baek de8f5b9c13 enh: default title gen prompt 2024-08-19 17:54:34 +02:00
Michael Poluektov c89df923c5 fix import error 2024-08-19 16:52:42 +01:00
Michael Poluektov 556bc8669a remove config options for now 2024-08-19 16:50:52 +01:00
Timothy J. Baek 89ba98e927 enh: better pdf CJK language support 2024-08-19 17:50:42 +02:00
Timothy J. Baek 6c8a15fae2 refac 2024-08-19 17:47:28 +02:00
Timothy J. Baek 68d8fd69c0 fix 2024-08-19 17:44:14 +02:00
Timothy J. Baek b31de299e4 fix: mermaid rendering issue 2024-08-19 17:28:38 +02:00
Michael Poluektov 9d7037b730 add pydantic model from json 2024-08-19 16:27:38 +01:00
Michael Poluektov 5edc211392 pass docstring to function 2024-08-19 16:27:21 +01:00
Timothy J. Baek 1329eea5e5 refac 2024-08-19 17:00:48 +02:00
Timothy J. Baek 28022b056b chore: format 2024-08-19 16:51:17 +02:00
Timothy J. Baek cbadf39d7d enh: user chat edit permission 2024-08-19 16:49:40 +02:00
Timothy Jaeryang Baek ec99ac7121 Update SECURITY.md 2024-08-19 09:18:40 -05:00
Timothy J. Baek dfa5041b6f refac: styling 2024-08-19 15:30:41 +02:00
Timothy J. Baek 0fa85c5c64 enh: enable message rating setting 2024-08-19 15:16:49 +02:00
Timothy J. Baek 5abe1076ed enh: model workspace shiftKey quick actions 2024-08-19 14:58:15 +02:00
Timothy Jaeryang Baek 7feda56e7b Merge pull request #4718 from open-webui/dependabot/pip/backend/dev/bcrypt-4.2.0
chore(deps): bump bcrypt from 4.1.3 to 4.2.0 in /backend
2024-08-19 14:17:00 +02:00
dependabot[bot] a54d3ad512 chore(deps): bump bcrypt from 4.1.3 to 4.2.0 in /backend
Bumps [bcrypt](https://github.com/pyca/bcrypt) from 4.1.3 to 4.2.0.
- [Changelog](https://github.com/pyca/bcrypt/blob/main/release.py)
- [Commits](https://github.com/pyca/bcrypt/compare/4.1.3...4.2.0)

---
updated-dependencies:
- dependency-name: bcrypt
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-08-19 12:05:37 +00:00
Timothy Jaeryang Baek cdf9f5a4ec Merge pull request #4720 from open-webui/dependabot/pip/backend/dev/boto3-1.35.0
chore(deps): bump boto3 from 1.34.153 to 1.35.0 in /backend
2024-08-19 14:04:41 +02:00
Timothy Jaeryang Baek a472bfe6dc Merge pull request #4719 from open-webui/dependabot/pip/backend/dev/faster-whisper-1.0.3
chore(deps): bump faster-whisper from 1.0.2 to 1.0.3 in /backend
2024-08-19 14:04:31 +02:00
Timothy Jaeryang Baek f88d994c35 Merge pull request #4717 from open-webui/dependabot/pip/backend/dev/uvicorn-standard--0.30.6
chore(deps): bump uvicorn[standard] from 0.22.0 to 0.30.6 in /backend
2024-08-19 14:04:22 +02:00
Timothy Jaeryang Baek ad92aded84 Merge pull request #4716 from open-webui/dependabot/pip/backend/dev/sqlalchemy-2.0.32
chore(deps): bump sqlalchemy from 2.0.31 to 2.0.32 in /backend
2024-08-19 14:03:43 +02:00
Timothy J. Baek b4de0a52f8 fix
Co-Authored-By: elad_pt <124190990+elad-pticha@users.noreply.github.com>
2024-08-19 14:01:21 +02:00
Timothy Jaeryang Baek bd23dac92e Merge pull request #4714 from crizCraig/set-cors
sec: Allow setting CORS origin
2024-08-19 13:55:28 +02:00
Michael Poluektov 528df12bf1 fix: nonetype error 2024-08-19 11:15:22 +01:00
Michael Poluektov a933319adb import error? 2024-08-19 11:11:00 +01:00
Michael Poluektov 13c03bfd7d add __tools__ custom param 2024-08-19 11:08:27 +01:00
Michael Poluektov 18965dcdac delete keys if envvars are set 2024-08-19 11:03:55 +01:00
Michael Poluektov a4a7d678f9 move tools utils to utils.tools 2024-08-19 10:53:12 +01:00
Michael Poluektov fd422d2e3c use filters envvars 2024-08-19 10:46:52 +01:00
Michael Poluektov ce7a1a73ac remove more nesting 2024-08-19 10:34:44 +01:00
Michael Poluektov 32874a816d add filter toggle envvars 2024-08-19 10:26:16 +01:00
Michael Poluektov 3164354c0b refactor into single wrapper 2024-08-19 10:23:40 +01:00
dependabot[bot] f96aaf1177 chore(deps): bump boto3 from 1.34.153 to 1.35.0 in /backend
Bumps [boto3](https://github.com/boto/boto3) from 1.34.153 to 1.35.0.
- [Release notes](https://github.com/boto/boto3/releases)
- [Commits](https://github.com/boto/boto3/compare/1.34.153...1.35.0)

---
updated-dependencies:
- dependency-name: boto3
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-08-19 03:07:09 +00:00
dependabot[bot] 589c79f3c2 chore(deps): bump faster-whisper from 1.0.2 to 1.0.3 in /backend
Bumps [faster-whisper](https://github.com/SYSTRAN/faster-whisper) from 1.0.2 to 1.0.3.
- [Release notes](https://github.com/SYSTRAN/faster-whisper/releases)
- [Commits](https://github.com/SYSTRAN/faster-whisper/compare/v1.0.2...v1.0.3)

---
updated-dependencies:
- dependency-name: faster-whisper
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-08-19 03:07:03 +00:00
dependabot[bot] a0f6864216 chore(deps): bump uvicorn[standard] from 0.22.0 to 0.30.6 in /backend
Bumps [uvicorn[standard]](https://github.com/encode/uvicorn) from 0.22.0 to 0.30.6.
- [Release notes](https://github.com/encode/uvicorn/releases)
- [Changelog](https://github.com/encode/uvicorn/blob/master/CHANGELOG.md)
- [Commits](https://github.com/encode/uvicorn/compare/0.22.0...0.30.6)

---
updated-dependencies:
- dependency-name: uvicorn[standard]
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-08-19 03:06:54 +00:00
dependabot[bot] a4cc2c5c48 chore(deps): bump sqlalchemy from 2.0.31 to 2.0.32 in /backend
Bumps [sqlalchemy](https://github.com/sqlalchemy/sqlalchemy) from 2.0.31 to 2.0.32.
- [Release notes](https://github.com/sqlalchemy/sqlalchemy/releases)
- [Changelog](https://github.com/sqlalchemy/sqlalchemy/blob/main/CHANGES.rst)
- [Commits](https://github.com/sqlalchemy/sqlalchemy/commits)

---
updated-dependencies:
- dependency-name: sqlalchemy
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-08-19 03:06:51 +00:00
Craig Quiter 0550d12106 Black format 2024-08-18 15:04:01 -07:00
Craig Quiter 845628c100 Fix tab format causing build failure 2024-08-18 15:00:49 -07:00
Craig Quiter 7bcdc10539 Optimize imports 2024-08-18 14:19:29 -07:00
Craig Quiter d2f10d50bf Allow seting CORS origin 2024-08-18 14:19:29 -07:00
Timothy Jaeryang Baek 446b2a334a Merge pull request #4713 from FINNSEEFLY/task/adjust-localization
i18n: Update ru-RU and uk-UA localizations
2024-08-18 22:57:34 +02:00
FINNSEEFLY b0fd90ada9 Adjust controls to ensure proper display of localized strings 2024-08-18 22:42:58 +03:00
FINNSEEFLY 6bfd48e412 Adjust uk localization 2024-08-18 22:40:18 +03:00
FINNSEEFLY 07ffd6e231 Adjust ru localization 2024-08-18 22:39:36 +03:00
Timothy J. Baek c675beeda4 refac 2024-08-18 21:14:22 +02:00
Timothy J. Baek fe0cf9506c refac 2024-08-18 21:11:59 +02:00
Timothy J. Baek 7c81509804 feat: merge responses 2024-08-18 20:59:59 +02:00
Timothy J. Baek 65923006a8 chore: format 2024-08-18 18:29:30 +02:00
Timothy J. Baek b02f6db475 refac 2024-08-18 18:28:58 +02:00
Timothy J. Baek 7f394f3f00 refac 2024-08-18 18:21:59 +02:00
Timothy J. Baek fc8524bbfd refac: styling 2024-08-18 18:20:36 +02:00
Timothy J. Baek c9505531fd refac: styling 2024-08-18 17:40:26 +02:00
Timothy J. Baek 1b809fe42e refac: styling 2024-08-18 17:16:22 +02:00
Timothy J. Baek a1b4df1b85 fix: cloned many model chat freezing issue 2024-08-18 16:47:12 +02:00
Timothy Jaeryang Baek 079f37a2d6 Merge pull request #4701 from ndrsfel/fix-rag-embedding-openai-batch-size-environment-variable
fix: RAG with OpenAI embedding models and batch_size environment variable fails silently
2024-08-18 13:24:18 +02:00
Andreas Feldl 0980066363 fix conversion 2024-08-18 12:46:47 +02:00
Timothy Jaeryang Baek a3b6654cbb Merge pull request #4688 from amirsubhi/dev
i18n: Updated ms-MY Translation
2024-08-17 21:57:52 +02:00
amirsubhi f4328325be Update i18n ms-MY Translation
Update Bahasa Malaysia Translation to reflect new update, and some spelling error
2024-08-18 02:17:51 +08:00
Timothy J. Baek 7badff49d8 refac 2024-08-17 19:43:04 +02:00
Timothy J. Baek 176c689f8d fix: unstructured md file parsing issue 2024-08-17 17:20:35 +02:00
Timothy J. Baek fa20b1dc09 chore: format 2024-08-17 17:17:45 +02:00
Timothy J. Baek 2ca9989d20 refac 2024-08-17 17:15:44 +02:00
Timothy J. Baek 3420818c52 refac 2024-08-17 17:11:58 +02:00
Timothy J. Baek 0ae6ca608c refac 2024-08-17 17:01:35 +02:00
Timothy J. Baek 536b40890a refac 2024-08-17 16:57:27 +02:00
Timothy J. Baek d5337917db refac 2024-08-17 16:46:04 +02:00
Timothy J. Baek 15f3ebba93 refac 2024-08-17 16:41:34 +02:00
Timothy J. Baek e71f55e58f refac 2024-08-17 16:32:39 +02:00
Timothy J. Baek fe747382c1 refac 2024-08-17 16:27:11 +02:00
Timothy J. Baek c4946d42e0 refac 2024-08-17 16:24:11 +02:00
Timothy J. Baek c1823b4b73 refac 2024-08-17 16:02:46 +02:00
Timothy J. Baek 862a30842c refac: chat_completion_inlets_handler -> chat_completion_filter_functions_handler 2024-08-17 16:00:18 +02:00
Timothy Jaeryang Baek cbb0940ff8 Merge pull request #4602 from michaelpoluektov/tools-refac-1
refactor, perf: Tools refactor (progress PR 1)
2024-08-17 15:50:40 +02:00
Timothy Jaeryang Baek bd8df3583d Merge pull request #4674 from crizCraig/sanitize-11labs-voiceid
sec: Sanitize 11labs voice id to address semgrep security issue: tainted-path-traversal-stdlib-fastapi
2024-08-17 15:49:56 +02:00
Craig Quiter 5f36807dbe Note tts defaults are from openai 2024-08-16 15:42:15 -07:00
Craig Quiter 442f50303a Sanitize voice_id 2024-08-16 15:10:53 -07:00
Craig Quiter 4560f3b1ae Return a dict from get_available_voices 2024-08-16 15:10:51 -07:00
Craig Quiter 59d2c670ba Optimize imports 2024-08-16 15:10:47 -07:00
Craig Quiter 02577f6a45 Cache elevenlabs voice call (can take 1s) 2024-08-16 15:10:41 -07:00
Timothy J. Baek 094fdd8943 fix 2024-08-16 19:23:36 +02:00
Timothy J. Baek 17169dff1f fix 2024-08-16 19:00:10 +02:00
Timothy J. Baek 28e3e6e8cb refac: many model chat 2024-08-16 18:54:30 +02:00
Timothy J. Baek 4f47053e93 refac 2024-08-16 17:51:50 +02:00
Timothy J. Baek 9025e9dbb1 fix: pseudo html rendering issue 2024-08-16 15:44:18 +02:00
Timothy J. Baek eee1dad217 refac: styling 2024-08-16 15:37:17 +02:00
Timothy J. Baek 769df698be fix: visible backtick in codespan 2024-08-16 15:37:11 +02:00
Timothy J. Baek 4ef042e966 refac 2024-08-16 15:33:14 +02:00
Timothy J. Baek 92062ff722 refac 2024-08-16 15:19:47 +02:00
Timothy J. Baek 623aa08b9e refac 2024-08-16 15:15:06 +02:00
Timothy J. Baek a529343b2b refac 2024-08-16 15:10:21 +02:00
Timothy J. Baek 2161903163 refac 2024-08-16 14:42:51 +02:00
Timothy Jaeryang Baek 3de9a1a130 Merge pull request #4651 from KarlLee830/translate
i18n: Update Chinese translation
2024-08-16 14:12:47 +02:00
Karl Lee 587c1a3ca2 i18n: Update Chinese translation 2024-08-16 16:37:02 +08:00
Timothy J. Baek d224566957 refac: styling 2024-08-16 00:21:57 +02:00
Timothy J. Baek 8ea1a10525 enh: action __event_emitter__ support 2024-08-15 23:55:31 +02:00
Michael Poluektov b6d6094018 Merge branch 'dev' into tools-refac-1 2024-08-15 21:35:31 +01:00
Timothy J. Baek e5e1bac242 chore: format 2024-08-15 17:31:47 +02:00
Timothy J. Baek 8c2ba7f7ea enh: Actions __webui__ flag support 2024-08-15 17:28:43 +02:00
Timothy J. Baek dc6ca61548 enh: temp chat
deprecates chat history setting and introduces temp chat from model selector
2024-08-15 16:54:16 +02:00
Timothy Jaeryang Baek 723caf2a09 Merge pull request #4621 from nthe/main
feat: Set content-type header in Ollama backend
2024-08-15 15:45:51 +02:00
Timothy J. Baek 439cb66672 refac: fuzzy search threshold 2024-08-15 15:44:38 +02:00
Timothy J. Baek dbd5b4c9f1 enh: render markdown user message 2024-08-15 15:41:02 +02:00
Timothy Jaeryang Baek 4dd404ac3b Merge pull request #4614 from sebdanielsson/pwa-maskable-icon
fix: Make PWA icon maskable
2024-08-15 13:25:00 +02:00
Timothy J. Baek ba370438b2 refac: "any maskable" is discouraged 2024-08-15 13:24:47 +02:00
Juraj Onuska f73a60d96c fix: set content-type header in ollama backend 2024-08-15 13:15:12 +02:00
Sebastian afe1f13c5b Make PWA icon maskable 2024-08-15 00:46:22 +02:00
Timothy J. Baek 0554cc6128 enh: shiftkey quick delete for tools and functions 2024-08-15 00:44:23 +02:00
Timothy J. Baek 5a6ece9513 refac: enhanced response content sanitisation
'<' and '>' can be correctly displayed now
2024-08-15 00:08:15 +02:00
Michael Poluektov 4042219b3e minor refac 2024-08-14 21:40:00 +01:00
Michael Poluektov fdc89cbcee tool calling refactor 2024-08-14 21:40:00 +01:00
Michael Poluektov 6df6170c44 add get_configured_tools 2024-08-14 21:40:00 +01:00
Michael Poluektov d598d4bb93 typing and tweaks 2024-08-14 21:40:00 +01:00
Michael Poluektov 790bdcf9fc rename tool calling helpers to use 'tool' instead of 'function' 2024-08-14 21:40:00 +01:00
Michael Poluektov 2efcda837c add try: except back 2024-08-14 21:40:00 +01:00
Michael Poluektov e86688284a factor out get_function_calling_payload 2024-08-14 21:40:00 +01:00
Michael Poluektov ff9d899f9c fix more LSP errors 2024-08-14 21:40:00 +01:00
Michael Poluektov a68b918cbb refactor get_function_call_response 2024-08-14 21:40:00 +01:00
Michael Poluektov 9fb70969d7 factor out get_content_from_response 2024-08-14 21:40:00 +01:00
Michael Poluektov 0c9119d619 move task to metadata 2024-08-14 21:40:00 +01:00
Michael Poluektov 556141cdd8 refactor task 2024-08-14 21:40:00 +01:00
Michael Poluektov 60003c976a rename to chat_completions_inlet_handler for clarity 2024-08-14 21:40:00 +01:00
Michael Poluektov 23f1bee7bd cleanup 2024-08-14 21:40:00 +01:00
Michael Poluektov 589efcdc5f is_chat_completion_request helper, remove nesting 2024-08-14 21:40:00 +01:00
Michael Poluektov 3befadb29f remove unnecessary nesting, remove unused endpoint 2024-08-14 21:40:00 +01:00
Timothy Jaeryang Baek 13b0e7d64a Merge pull request #4434 from open-webui/dev
Create and publish Docker images with specific build args / build-main-image (linux/amd64) (push) Waiting to run
Create and publish Docker images with specific build args / build-main-image (linux/arm64) (push) Waiting to run
Create and publish Docker images with specific build args / build-cuda-image (linux/amd64) (push) Waiting to run
Create and publish Docker images with specific build args / build-cuda-image (linux/arm64) (push) Waiting to run
Create and publish Docker images with specific build args / build-ollama-image (linux/amd64) (push) Waiting to run
Create and publish Docker images with specific build args / build-ollama-image (linux/arm64) (push) Waiting to run
Create and publish Docker images with specific build args / merge-main-images (push) Blocked by required conditions
Create and publish Docker images with specific build args / merge-cuda-images (push) Blocked by required conditions
Create and publish Docker images with specific build args / merge-ollama-images (push) Blocked by required conditions
0.3.13
2024-08-14 21:45:19 +02:00
Timothy J. Baek c8badfe21f refac 2024-08-14 21:42:43 +02:00
Timothy J. Baek 2a1b9cae91 doc: changelog 2024-08-14 21:39:11 +02:00
Timothy J. Baek 5ba7bbdd98 refac 2024-08-14 21:34:08 +02:00
Timothy J. Baek 9435d2044a refac: more robust mermaid chart rendering 2024-08-14 21:16:07 +02:00
Timothy J. Baek 609a42c29c refac: mermaid chart rendering 2024-08-14 17:34:44 +02:00
Timothy J. Baek 6a1e7ab038 chore: version bump 2024-08-14 17:13:37 +02:00
Timothy J. Baek 53daa15b9a chore: format 2024-08-14 17:07:16 +02:00
Timothy J. Baek 55097410f6 feat: haptic feedback on support devices (android)
Co-Authored-By: Danny Liu <dannyjialiliu@gmail.com>
2024-08-14 17:05:43 +02:00
Timothy J. Baek 04e2b6e2bd chore: format 2024-08-14 16:39:02 +02:00
Timothy Jaeryang Baek 9f0c9d973c Merge pull request #4597 from michaelpoluektov/cleanup
refactor: search and replace-able cleanup
2024-08-14 16:30:27 +02:00
Timothy J. Baek 1597e33a74 refac: mermaid chart rendering 2024-08-14 16:27:55 +02:00
Timothy Jaeryang Baek 0b218bbb72 Merge pull request #4598 from michaelpoluektov/cleanup-frontend
refactor: Remove unused frontend functions migrated to backend
2024-08-14 16:13:39 +02:00
Timothy J. Baek 7b21b718fe refac 2024-08-14 16:09:16 +02:00
Timothy J. Baek 6e5b557a1f refac 2024-08-14 16:08:23 +02:00
Timothy J. Baek 6aefc79807 refac: latex 2024-08-14 16:07:39 +02:00
Michael Poluektov ec9e0dadea remove frontend functions migrated to backend 2024-08-14 14:33:20 +01:00
Michael Poluektov 0470146d7b replace Tuple with tuple 2024-08-14 13:58:37 +01:00
Michael Poluektov a518d50477 format backend 2024-08-14 13:49:18 +01:00
Michael Poluektov 29f904db45 remove List imports 2024-08-14 13:46:31 +01:00
Michael Poluektov 038fc48ac0 replace == None with is None 2024-08-14 13:39:53 +01:00
Michael Poluektov 6f72def1ac replace except: with except Exception: 2024-08-14 13:38:19 +01:00
Timothy J. Baek 0ec1f9e331 fix: ENABLE_ADMIN_CHAT_ACCESS disabling user deletion 2024-08-14 12:23:24 +02:00
Timothy Jaeryang Baek 9682806476 Merge pull request #4372 from JTHesse/main
build: Adding ability to install requirements from frontmatter for tools and functions
2024-08-13 18:18:26 +02:00
Timothy Jaeryang Baek fc6fa7887b Merge pull request #4579 from alexandregodard/main
Update main.py
2024-08-13 18:17:27 +02:00
Timothy J. Baek e1e69cfbcb refac: sft -> default to safetensors 2024-08-13 17:15:20 +01:00
Alexandre GODARD 7a8f8960c5 Update main.py
Fix typo in update_reranking_model
2024-08-13 17:51:25 +02:00
Timothy Jaeryang Baek 30c44d431b Merge pull request #4538 from open-webui/dependabot/pip/backend/dev/chromadb-0.5.5
chore(deps): bump chromadb from 0.5.4 to 0.5.5 in /backend
2024-08-13 12:22:56 +02:00
Timothy Jaeryang Baek b177976f29 Merge pull request #4539 from open-webui/dependabot/pip/backend/dev/langchain-0.2.12
chore(deps): bump langchain from 0.2.11 to 0.2.12 in /backend
2024-08-13 12:22:48 +02:00
Timothy J. Baek af6b92a6fa chore: format 2024-08-13 11:22:08 +01:00
Timothy J. Baek a1888b3757 refac 2024-08-13 11:21:17 +01:00
Timothy Jaeryang Baek c75e77cdde Merge pull request #4554 from 5E-324/add-num_gpu
feat: Add advanced parameter num_gpu (Ollama)
2024-08-13 12:15:44 +02:00
Timothy Jaeryang Baek 6447b484c1 Merge pull request #4555 from 5E-324/ollama-params-fix
fix: Ollama parameters not shown in Chat Controls even if the current user is admin.
2024-08-13 12:13:40 +02:00
Timothy Jaeryang Baek 0c5236ac27 Merge pull request #4562 from simonaszilinskas/patch-5
Update Lithuanian translations
2024-08-13 12:13:11 +02:00
Timothy J. Baek 7ef5aa520c chore: format 2024-08-13 11:12:35 +01:00
Simonas d620bcf516 Update Lithuanian translations 2024-08-13 03:10:08 -05:00
Zhuoran b0c2e607b8 Set property "admin" of AdvancedParams in Controls 2024-08-13 10:06:51 +08:00
Zhuoran 56e2d579f2 Add advanced parameter num_gpu (Ollama) 2024-08-13 09:46:38 +08:00
Zhaofeng Li e63d5778a8 fix: Decode URL-encoded characters in passwords
This enables using passwords containing special characters.
2024-08-12 08:52:16 -06:00
Zhaofeng Li a53c2a8c6b fix: Pass all parsed options to ReconnectingPostgresqlDatabase 2024-08-12 08:52:16 -06:00
Timothy J. Baek 70f580ec45 fix 2024-08-12 15:10:08 +01:00
Timothy J. Baek eae35dddc2 refac 2024-08-12 12:47:54 +01:00
Timothy Jaeryang Baek bb979c9a78 Merge pull request #4540 from open-webui/dependabot/pip/backend/dev/pytest-approx-eq-8.3.2
chore(deps): update pytest requirement from ~=8.2.2 to ~=8.3.2 in /backend
2024-08-12 13:31:02 +02:00
Timothy Jaeryang Baek 4f959c31de Merge pull request #4537 from open-webui/dependabot/pip/backend/dev/langfuse-2.43.3
chore(deps): bump langfuse from 2.39.2 to 2.43.3 in /backend
2024-08-12 13:30:52 +02:00
Timothy Jaeryang Baek 3bbe065db1 Merge pull request #4541 from open-webui/dependabot/pip/backend/dev/pyjwt-crypto--2.9.0
chore(deps): bump pyjwt[crypto] from 2.8.0 to 2.9.0 in /backend
2024-08-12 13:30:43 +02:00
dependabot[bot] 41460e1335 chore(deps): bump pyjwt[crypto] from 2.8.0 to 2.9.0 in /backend
Bumps [pyjwt[crypto]](https://github.com/jpadilla/pyjwt) from 2.8.0 to 2.9.0.
- [Release notes](https://github.com/jpadilla/pyjwt/releases)
- [Changelog](https://github.com/jpadilla/pyjwt/blob/master/CHANGELOG.rst)
- [Commits](https://github.com/jpadilla/pyjwt/compare/2.8.0...2.9.0)

---
updated-dependencies:
- dependency-name: pyjwt[crypto]
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-08-12 03:15:42 +00:00
dependabot[bot] ffdb44f887 chore(deps): update pytest requirement in /backend
Updates the requirements on [pytest](https://github.com/pytest-dev/pytest) to permit the latest version.
- [Release notes](https://github.com/pytest-dev/pytest/releases)
- [Changelog](https://github.com/pytest-dev/pytest/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/pytest-dev/pytest/compare/8.2.2...8.3.2)

---
updated-dependencies:
- dependency-name: pytest
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-08-12 03:15:40 +00:00
dependabot[bot] 549d3b4d10 chore(deps): bump langchain from 0.2.11 to 0.2.12 in /backend
Bumps [langchain](https://github.com/langchain-ai/langchain) from 0.2.11 to 0.2.12.
- [Release notes](https://github.com/langchain-ai/langchain/releases)
- [Commits](https://github.com/langchain-ai/langchain/compare/langchain==0.2.11...langchain==0.2.12)

---
updated-dependencies:
- dependency-name: langchain
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-08-12 03:15:35 +00:00
dependabot[bot] 879f85802a chore(deps): bump chromadb from 0.5.4 to 0.5.5 in /backend
Bumps [chromadb](https://github.com/chroma-core/chroma) from 0.5.4 to 0.5.5.
- [Release notes](https://github.com/chroma-core/chroma/releases)
- [Changelog](https://github.com/chroma-core/chroma/blob/main/RELEASE_PROCESS.md)
- [Commits](https://github.com/chroma-core/chroma/compare/0.5.4...0.5.5)

---
updated-dependencies:
- dependency-name: chromadb
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-08-12 03:15:30 +00:00
dependabot[bot] d652a1bbb1 chore(deps): bump langfuse from 2.39.2 to 2.43.3 in /backend
Bumps [langfuse](https://github.com/langfuse/langfuse) from 2.39.2 to 2.43.3.
- [Release notes](https://github.com/langfuse/langfuse/releases)
- [Commits](https://github.com/langfuse/langfuse/commits)

---
updated-dependencies:
- dependency-name: langfuse
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-08-12 03:15:25 +00:00
Timothy Jaeryang Baek 9c2429ff97 Merge pull request #4402 from michaelpoluektov/remove-ollama
refactor: re-use utils in Ollama
2024-08-12 00:45:15 +02:00
Timothy Jaeryang Baek d0645d3c4f Merge pull request #4503 from open-webui/dependabot/pip/pip-621ce9c937
chore(deps): bump the pip group across 2 directories with 1 update
2024-08-12 00:40:46 +02:00
Timothy Jaeryang Baek 748be3e637 Merge pull request #4529 from cheahjs/fix/integration-tests
ci: fix cypress integration tests
2024-08-11 23:41:01 +02:00
Jun Siang Cheah 153ba168a0 tests: update share test to actually share 2024-08-11 22:30:25 +01:00
Jun Siang Cheah c70b18b2ef tests: change how cypress detects chat messages 2024-08-11 16:22:33 +01:00
Jun Siang Cheah af6420e06d ci: increase usable disk space for integration test 2024-08-11 14:17:32 +01:00
Michael Poluektov 547611b703 Merge branch 'dev' of https://github.com/open-webui/open-webui into remove-ollama 2024-08-10 11:47:20 +01:00
Timothy Jaeryang Baek 1b2ae7bb77 Merge pull request #4504 from OriginalSimon/dev
i18n: Update of the Ukrainian translation
2024-08-09 21:28:07 +02:00
Simon 5cd8011d53 Update translation.json 2024-08-09 21:03:19 +02:00
dependabot[bot] 208833d9f2 chore(deps): bump the pip group across 2 directories with 1 update
Bumps the pip group with 1 update in the / directory: [aiohttp](https://github.com/aio-libs/aiohttp).
Bumps the pip group with 1 update in the /backend directory: [aiohttp](https://github.com/aio-libs/aiohttp).


Updates `aiohttp` from 3.9.5 to 3.10.2
- [Release notes](https://github.com/aio-libs/aiohttp/releases)
- [Changelog](https://github.com/aio-libs/aiohttp/blob/master/CHANGES.rst)
- [Commits](https://github.com/aio-libs/aiohttp/compare/v3.9.5...v3.10.2)

Updates `aiohttp` from 3.9.5 to 3.10.2
- [Release notes](https://github.com/aio-libs/aiohttp/releases)
- [Changelog](https://github.com/aio-libs/aiohttp/blob/master/CHANGES.rst)
- [Commits](https://github.com/aio-libs/aiohttp/compare/v3.9.5...v3.10.2)

---
updated-dependencies:
- dependency-name: aiohttp
  dependency-type: direct:production
  dependency-group: pip
- dependency-name: aiohttp
  dependency-type: direct:production
  dependency-group: pip
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-08-09 18:21:50 +00:00
Timothy Jaeryang Baek 6863028540 Merge pull request #4500 from KarlLee830/translate
i18n: Update Chinese translation
2024-08-09 17:13:42 +02:00
Karl Lee 5e97a52cbe i18n: Update Chinese translation 2024-08-09 22:04:33 +08:00
Timothy Jaeryang Baek af865ab035 Merge pull request #4474 from aleixdorca/dev
i18n: Updated catalan translation
2024-08-09 12:01:43 +02:00
Timothy J. Baek 01f622866f fix 2024-08-09 00:14:41 +02:00
Timothy J. Baek e9d14601a1 refac 2024-08-09 00:08:20 +02:00
Timothy J. Baek 92e77d7b33 refac: katex 2024-08-09 00:01:38 +02:00
Timothy J. Baek 3b370bbcb3 fix 2024-08-08 20:46:39 +02:00
Aleix Dorca 20b1753a91 i18n: Update catalan translation.json 2024-08-08 17:18:59 +02:00
Aleix Dorca b2379c7104 i18n: Update catalan translation.json 2024-08-08 17:16:52 +02:00
Timothy J. Baek e491e96f88 fix: markdown '$$' rendering issue 2024-08-08 15:27:51 +02:00
Timothy J. Baek 8afc8c5714 chore: format 2024-08-08 15:07:59 +02:00
Timothy J. Baek d388c20373 enh: codeblock i18n 2024-08-08 15:07:24 +02:00
Timothy J. Baek 13403cd7dc enh: codeblock i18n 2024-08-08 14:24:47 +02:00
Timothy Jaeryang Baek 52fd701f2e Merge pull request #4465 from open-webui/main
dev
2024-08-08 14:23:33 +02:00
Michael Poluektov 204a4fbe7a fix: backend format test 2024-08-08 12:45:23 +01:00
Michael Poluektov 549817627f Merge branch 'dev' into remove-ollama 2024-08-08 12:42:37 +01:00
Michael Poluektov fa4d1d42a5 fix: backend format test 2024-08-08 12:41:41 +01:00
Michael Poluektov 309cd645f1 undo del 2024-08-08 12:30:07 +01:00
Michael Poluektov a725801e55 fix: formatting test errors, remove print, merge dev 2024-08-08 11:34:48 +01:00
Michael Poluektov 8cdf9814bd fix: name differences 2024-08-08 11:01:00 +01:00
Michael Poluektov e6bbce439d fix: repeat_penalty 2024-08-08 10:52:09 +01:00
Timothy Jaeryang Baek 8d257ed596 Merge commit from fork
SSRF Fix
2024-08-08 11:47:33 +02:00
Timothy Jaeryang Baek a7063a598d Merge pull request #4448 from JohnTheNerd/comfyui-async-fix
fix: ComfyUI generation no longer causes FastAPI to stall for all users
2024-08-08 11:36:19 +02:00
Jan-Timo Hesse 367fa039a0 added install_frontmatter_requirements 2024-08-08 09:46:14 +02:00
Jan-Timo Hesse 71d88fe35d revert 2024-08-08 09:45:52 +02:00
John Karabudak 958fe9639a fix: ComfyUI generation no longer causes FastAPI to stall for all users
as the get_images() function involves a `while True` loop while waiting for a response from ComfyUI and is not async, when image generation is running the entire UI becomes unresponsive for all users.

furthermore, when image generation takes too long, the Docker health check starts failing.

this is certainly a bad fix as it does not convert everything to async, but rather just puts the blocking loop in a separate thread. however, it works and it at least fixes the problem for now.
2024-08-07 22:24:55 -02:30
Timothy Jaeryang Baek 670f28d694 Merge branch 'dev' into remove-ollama 2024-08-07 23:06:11 +02:00
Timothy Jaeryang Baek 3715994c25 Merge pull request #4439 from cdgco/dev
feat: Add OAuth Email Claim Variable
2024-08-07 22:13:10 +02:00
Carter Roeser d72d5d0e8e feat: Add OAuth Email Claim Variable
Add an `OAUTH_EMAIL_CLAIM` variable to override the default "email" claim value.
2024-08-07 11:39:51 -07:00
Timothy J. Baek dbe463a53d fix 2024-08-07 17:39:54 +02:00
Timothy J. Baek 678dd780ee chore: package 2024-08-07 17:39:00 +02:00
Timothy Jaeryang Baek 44c870447f Merge pull request #4385 from candidosales/sveltekit-2v
build: Migrated to SvelteKit 2v
2024-08-07 17:37:08 +02:00
Timothy Jaeryang Baek 70a8f6e707 Merge pull request #4431 from Nowheresly/fix4158_reconnect
feat: #4158 allow reconnection when websocket is closed
2024-08-07 17:13:57 +02:00
Sylvere Richard 2fb4d3356c fix: #4158 allow reconnection when websocket is closed
log reconnection attempts
mark session_id as mandatory on client-side
2024-08-07 17:05:55 +02:00
root 590fd129c8 SSRF Fix Updated 2024-08-07 10:59:22 -04:00
Timothy Jaeryang Baek 99d10d1189 Merge pull request #4430 from open-webui/dev
fix
2024-08-07 15:53:11 +02:00
Timothy J. Baek ad9a7cb1e2 refac 2024-08-07 15:52:03 +02:00
Timothy J. Baek 8187922ef1 fix: "metadata" issue 2024-08-07 15:49:48 +02:00
Michael Poluektov f62281a0c7 Merge branch 'dev' of https://github.com/open-webui/open-webui into remove-ollama 2024-08-07 11:47:46 +01:00
root 1f8d08eaa2 SSRF Fix 2024-08-07 03:30:21 -04:00
Candido Sales Gomes 4105c9735e revert files 2024-08-06 09:38:42 -04:00
Michael Poluektov ed205d82e8 fix: pop 2024-08-06 12:25:00 +01:00
Michael Poluektov fc31267a54 refac: re-use utils.misc 2024-08-06 11:31:45 +01:00
Michael Poluektov 44c781f414 cleanup 2024-08-06 10:50:22 +01:00
Michael Poluektov 831fe9f509 cleanup 2024-08-06 10:15:29 +01:00
Candido Sales Gomes b289eef523 typo 2024-08-05 11:16:28 -04:00
Candido Sales Gomes 5315fb201d changelod updated 2024-08-05 11:10:31 -04:00
Candido Sales Gomes 6a012d290e migration to SvelteKit 2 2024-08-05 11:00:30 -04:00
Jan-Timo Hesse cec5fdd144 Added EXTRA_MODULES argument 2024-08-05 09:42:16 +02:00
199 changed files with 13360 additions and 9392 deletions
+2 -2
View File
@@ -3,10 +3,10 @@ updates:
- package-ecosystem: pip
directory: '/backend'
schedule:
interval: weekly
interval: monthly
target-branch: 'dev'
- package-ecosystem: 'github-actions'
directory: '/'
schedule:
# Check for updates to GitHub Actions every week
interval: 'weekly'
interval: monthly
+55 -53
View File
@@ -10,61 +10,63 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Checkout repository
uses: actions/checkout@v4
- name: Check for changes in package.json
run: |
git diff --cached --diff-filter=d package.json || {
echo "No changes to package.json"
exit 1
}
- name: Get version number from package.json
id: get_version
run: |
VERSION=$(jq -r '.version' package.json)
echo "::set-output name=version::$VERSION"
- name: Check for changes in package.json
run: |
git diff --cached --diff-filter=d package.json || {
echo "No changes to package.json"
exit 1
}
- name: Extract latest CHANGELOG entry
id: changelog
run: |
CHANGELOG_CONTENT=$(awk 'BEGIN {print_section=0;} /^## \[/ {if (print_section == 0) {print_section=1;} else {exit;}} print_section {print;}' CHANGELOG.md)
CHANGELOG_ESCAPED=$(echo "$CHANGELOG_CONTENT" | sed ':a;N;$!ba;s/\n/%0A/g')
echo "Extracted latest release notes from CHANGELOG.md:"
echo -e "$CHANGELOG_CONTENT"
echo "::set-output name=content::$CHANGELOG_ESCAPED"
- name: Get version number from package.json
id: get_version
run: |
VERSION=$(jq -r '.version' package.json)
echo "::set-output name=version::$VERSION"
- name: Create GitHub release
uses: actions/github-script@v7
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const changelog = `${{ steps.changelog.outputs.content }}`;
const release = await github.rest.repos.createRelease({
owner: context.repo.owner,
repo: context.repo.repo,
tag_name: `v${{ steps.get_version.outputs.version }}`,
name: `v${{ steps.get_version.outputs.version }}`,
body: changelog,
})
console.log(`Created release ${release.data.html_url}`)
- name: Extract latest CHANGELOG entry
id: changelog
run: |
CHANGELOG_CONTENT=$(awk 'BEGIN {print_section=0;} /^## \[/ {if (print_section == 0) {print_section=1;} else {exit;}} print_section {print;}' CHANGELOG.md)
CHANGELOG_ESCAPED=$(echo "$CHANGELOG_CONTENT" | sed ':a;N;$!ba;s/\n/%0A/g')
echo "Extracted latest release notes from CHANGELOG.md:"
echo -e "$CHANGELOG_CONTENT"
echo "::set-output name=content::$CHANGELOG_ESCAPED"
- name: Upload package to GitHub release
uses: actions/upload-artifact@v4
with:
name: package
path: .
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Create GitHub release
uses: actions/github-script@v7
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const changelog = `${{ steps.changelog.outputs.content }}`;
const release = await github.rest.repos.createRelease({
owner: context.repo.owner,
repo: context.repo.repo,
tag_name: `v${{ steps.get_version.outputs.version }}`,
name: `v${{ steps.get_version.outputs.version }}`,
body: changelog,
})
console.log(`Created release ${release.data.html_url}`)
- name: Trigger Docker build workflow
uses: actions/github-script@v7
with:
script: |
github.rest.actions.createWorkflowDispatch({
owner: context.repo.owner,
repo: context.repo.repo,
workflow_id: 'docker-build.yaml',
ref: 'v${{ steps.get_version.outputs.version }}',
})
- name: Upload package to GitHub release
uses: actions/upload-artifact@v4
with:
name: package
path: |
.
!.git
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Trigger Docker build workflow
uses: actions/github-script@v7
with:
script: |
github.rest.actions.createWorkflowDispatch({
owner: context.repo.owner,
repo: context.repo.repo,
workflow_id: 'docker-build.yaml',
ref: 'v${{ steps.get_version.outputs.version }}',
})
+44 -28
View File
@@ -56,19 +56,25 @@ jobs:
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata for Docker images (default latest tag)
- name: Get version number from package.json
id: get_version
run: |
VERSION=$(jq -r '.version' package.json)
echo "version=$VERSION" >> $GITHUB_OUTPUT
- name: Extract metadata for Docker images
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.FULL_IMAGE_NAME }}
tags: |
type=raw,value=latest,enable=${{ github.ref == 'refs/heads/main' }}
type=raw,value=${{ steps.get_version.outputs.version }},enable=${{ github.ref == 'refs/heads/main' }}
type=ref,event=branch
type=ref,event=tag
type=sha,prefix=git-
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
flavor: |
latest=${{ github.ref == 'refs/heads/main' }}
- name: Extract metadata for Docker cache
id: cache-meta
@@ -82,7 +88,7 @@ jobs:
prefix=cache-${{ matrix.platform }}-
latest=false
- name: Build Docker image (latest)
- name: Build Docker image
uses: docker/build-push-action@v5
id: build
with:
@@ -90,7 +96,8 @@ jobs:
push: true
platforms: ${{ matrix.platform }}
labels: ${{ steps.meta.outputs.labels }}
outputs: type=image,name=${{ env.FULL_IMAGE_NAME }},push-by-digest=true,name-canonical=true,push=true
tags: ${{ steps.meta.outputs.tags }}
outputs: type=image,name=${{ env.FULL_IMAGE_NAME }},push=true
cache-from: type=registry,ref=${{ steps.cache-meta.outputs.tags }}
cache-to: type=registry,ref=${{ steps.cache-meta.outputs.tags }},mode=max
build-args: |
@@ -153,21 +160,25 @@ jobs:
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata for Docker images (cuda tag)
- name: Get version number from package.json
id: get_version
run: |
VERSION=$(jq -r '.version' package.json)
echo "version=$VERSION" >> $GITHUB_OUTPUT
- name: Extract metadata for Docker images
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.FULL_IMAGE_NAME }}
tags: |
type=ref,event=branch
type=ref,event=tag
type=sha,prefix=git-
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=raw,enable=${{ github.ref == 'refs/heads/main' }},prefix=,suffix=,value=cuda
flavor: |
latest=${{ github.ref == 'refs/heads/main' }}
suffix=-cuda,onlatest=true
type=raw,value=latest-cuda,enable=${{ github.ref == 'refs/heads/main' }}
type=raw,value=${{ steps.get_version.outputs.version }}-cuda,enable=${{ github.ref == 'refs/heads/main' }}
type=ref,event=branch,suffix=-cuda
type=ref,event=tag,suffix=-cuda
type=sha,prefix=git-,suffix=-cuda
type=semver,pattern={{version}},suffix=-cuda
type=semver,pattern={{major}}.{{minor}},suffix=-cuda
- name: Extract metadata for Docker cache
id: cache-meta
@@ -189,7 +200,8 @@ jobs:
push: true
platforms: ${{ matrix.platform }}
labels: ${{ steps.meta.outputs.labels }}
outputs: type=image,name=${{ env.FULL_IMAGE_NAME }},push-by-digest=true,name-canonical=true,push=true
tags: ${{ steps.meta.outputs.tags }}
outputs: type=image,name=${{ env.FULL_IMAGE_NAME }},push=true
cache-from: type=registry,ref=${{ steps.cache-meta.outputs.tags }}
cache-to: type=registry,ref=${{ steps.cache-meta.outputs.tags }},mode=max
build-args: |
@@ -253,21 +265,25 @@ jobs:
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata for Docker images (ollama tag)
- name: Get version number from package.json
id: get_version
run: |
VERSION=$(jq -r '.version' package.json)
echo "version=$VERSION" >> $GITHUB_OUTPUT
- name: Extract metadata for Docker images
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.FULL_IMAGE_NAME }}
tags: |
type=ref,event=branch
type=ref,event=tag
type=sha,prefix=git-
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=raw,enable=${{ github.ref == 'refs/heads/main' }},prefix=,suffix=,value=ollama
flavor: |
latest=${{ github.ref == 'refs/heads/main' }}
suffix=-ollama,onlatest=true
type=raw,value=latest-ollama,enable=${{ github.ref == 'refs/heads/main' }}
type=raw,value=${{ steps.get_version.outputs.version }}-ollama,enable=${{ github.ref == 'refs/heads/main' }}
type=ref,event=branch,suffix=-ollama
type=ref,event=tag,suffix=-ollama
type=sha,prefix=git-,suffix=-ollama
type=semver,pattern={{version}},suffix=-ollama
type=semver,pattern={{major}}.{{minor}},suffix=-ollama
- name: Extract metadata for Docker cache
id: cache-meta
@@ -289,7 +305,8 @@ jobs:
push: true
platforms: ${{ matrix.platform }}
labels: ${{ steps.meta.outputs.labels }}
outputs: type=image,name=${{ env.FULL_IMAGE_NAME }},push-by-digest=true,name-canonical=true,push=true
tags: ${{ steps.meta.outputs.tags }}
outputs: type=image,name=${{ env.FULL_IMAGE_NAME }},push=true
cache-from: type=registry,ref=${{ steps.cache-meta.outputs.tags }}
cache-to: type=registry,ref=${{ steps.cache-meta.outputs.tags }},mode=max
build-args: |
@@ -309,7 +326,6 @@ jobs:
path: /tmp/digests/*
if-no-files-found: error
retention-days: 1
merge-main-images:
runs-on: ubuntu-latest
needs: [ build-main-image ]
+7
View File
@@ -15,6 +15,13 @@ jobs:
name: Run Cypress Integration Tests
runs-on: ubuntu-latest
steps:
- name: Maximize build space
uses: AdityaGarg8/remove-unwanted-software@v4.1
with:
remove-android: 'true'
remove-haskell: 'true'
remove-codeql: 'true'
- name: Checkout Repository
uses: actions/checkout@v4
+97
View File
@@ -5,6 +5,103 @@ 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.3.16] - 2024-08-27
### Added
- **🚀 Config DB Migration**: Migrated configuration handling from config.json to the database, enabling high-availability setups and load balancing across multiple Open WebUI instances.
- **🔗 Call Mode Activation via URL**: Added a 'call=true' URL search parameter enabling direct shortcuts to activate call mode, enhancing user interaction on mobile devices.
- **✨ TTS Content Control**: Added functionality to control how message content is segmented for Text-to-Speech (TTS) generation requests, allowing for more flexible speech output options.
- **😄 Show Knowledge Search Status**: Enhanced model usage transparency by displaying status when working with knowledge-augmented models, helping users understand the system's state during queries.
- **👆 Click-to-Copy for Codespan**: Enhanced interactive experience in the WebUI by allowing users to click to copy content from code spans directly.
- **🚫 API User Blocking via Model Filter**: Introduced the ability to block API users based on customized model filters, enhancing security and control over API access.
- **🎬 Call Overlay Styling**: Adjusted call overlay styling on large screens to not cover the entire interface, but only the chat control area, for a more unobtrusive interaction experience.
### Fixed
- **🔧 LaTeX Rendering Issue**: Addressed an issue that affected the correct rendering of LaTeX.
- **📁 File Leak Prevention**: Resolved the issue of uploaded files mistakenly being accessible across user chats.
- **🔧 Pipe Functions with '**files**' Param**: Fixed issues with '**files**' parameter not functioning correctly in pipe functions.
- **📝 Markdown Processing for RAG**: Fixed issues with processing Markdown in files.
- **🚫 Duplicate System Prompts**: Fixed bugs causing system prompts to duplicate.
### Changed
- **🔋 Wakelock Permission**: Optimized the activation of wakelock to only engage during call mode, conserving device resources and improving battery performance during idle periods.
- **🔍 Content-Type for Ollama Chats**: Added 'application/x-ndjson' content-type to '/api/chat' endpoint responses to match raw Ollama responses.
- **✋ Disable Signups Conditionally**: Implemented conditional logic to disable sign-ups when 'ENABLE_LOGIN_FORM' is set to false.
## [0.3.15] - 2024-08-21
### Added
- **🔗 Temporary Chat Activation**: Integrated a new URL parameter 'temporary-chat=true' to enable temporary chat sessions directly through the URL.
- **🌄 ComfyUI Seed Node Support**: Introduced seed node support in ComfyUI for image generation, allowing users to specify node IDs for randomized seed assignment.
### Fixed
- **🛠️ Tools and Functions**: Resolved a critical issue where Tools and Functions were not properly functioning, restoring full capability and reliability to these essential features.
- **🔘 Chat Action Button in Many Model Chat**: Fixed the malfunctioning of chat action buttons in many model chat environments, ensuring a smoother and more responsive user interaction.
- **⏪ Many Model Chat Compatibility**: Restored backward compatibility for many model chats.
## [0.3.14] - 2024-08-21
### Added
- **🛠️ Custom ComfyUI Workflow**: Deprecating several older environment variables, this enhancement introduces a new, customizable workflow for a more tailored user experience.
- **🔀 Merge Responses in Many Model Chat**: Enhances the dialogue by merging responses from multiple models into a single, coherent reply, improving the interaction quality in many model chats.
- **✅ Multiple Instances of Same Model in Chats**: Enhanced many model chat to support adding multiple instances of the same model.
- **🔧 Quick Actions in Model Workspace**: Enhanced Shift key quick actions for hiding/unhiding and deleting models, facilitating a smoother workflow.
- **🗨️ Markdown Rendering in User Messages**: User messages are now rendered in Markdown, enhancing readability and interaction.
- **💬 Temporary Chat Feature**: Introduced a temporary chat feature, deprecating the old chat history setting to enhance user interaction flexibility.
- **🖋️ User Message Editing**: Enhanced the user chat editing feature to allow saving changes without sending, providing more flexibility in message management.
- **🛡️ Security Enhancements**: Various security improvements implemented across the platform to ensure safer user experiences.
- **🌍 Updated Translations**: Enhanced translations for Chinese, Ukrainian, and Bahasa Malaysia, improving localization and user comprehension.
### Fixed
- **📑 Mermaid Rendering Issue**: Addressed issues with Mermaid chart rendering to ensure clean and clear visual data representation.
- **🎭 PWA Icon Maskability**: Fixed the Progressive Web App icon to be maskable, ensuring proper display on various device home screens.
- **🔀 Cloned Model Chat Freezing Issue**: Fixed a bug where cloning many model chats would cause freezing, enhancing stability and responsiveness.
- **🔍 Generic Error Handling and Refinements**: Various minor fixes and refinements to address previously untracked issues, ensuring smoother operations.
### Changed
- **🖼️ Image Generation Refactor**: Overhauled image generation processes for improved efficiency and quality.
- **🔨 Refactor Tool and Function Calling**: Refactored tool and function calling mechanisms for improved clarity and maintainability.
- **🌐 Backend Library Updates**: Updated critical backend libraries including SQLAlchemy, uvicorn[standard], faster-whisper, bcrypt, and boto3 for enhanced performance and security.
### Removed
- **🚫 Deprecated ComfyUI Environment Variables**: Removed several outdated environment variables related to ComfyUI settings, simplifying configuration management.
## [0.3.13] - 2024-08-14
### Added
- **🎨 Enhanced Markdown Rendering**: Significant improvements in rendering markdown, ensuring smooth and reliable display of LaTeX and Mermaid charts, enhancing user experience with more robust visual content.
- **🔄 Auto-Install Tools & Functions Python Dependencies**: For 'Tools' and 'Functions', Open WebUI now automatically install extra python requirements specified in the frontmatter, streamlining setup processes and customization.
- **🌀 OAuth Email Claim Customization**: Introduced an 'OAUTH_EMAIL_CLAIM' variable to allow customization of the default "email" claim within OAuth configurations, providing greater flexibility in authentication processes.
- **📶 Websocket Reconnection**: Enhanced reliability with the capability to automatically reconnect when a websocket is closed, ensuring consistent and stable communication.
- **🤳 Haptic Feedback on Support Devices**: Android devices now support haptic feedback for an immersive tactile experience during certain interactions.
### Fixed
- **🛠️ ComfyUI Performance Improvement**: Addressed an issue causing FastAPI to stall when ComfyUI image generation was active; now runs in a separate thread to prevent UI unresponsiveness.
- **🔀 Session Handling**: Fixed an issue mandating session_id on client-side to ensure smoother session management and transitions.
- **🖋️ Minor Bug Fixes and Format Corrections**: Various minor fixes including typo corrections, backend formatting improvements, and test amendments enhancing overall system stability and performance.
### Changed
- **🚀 Migration to SvelteKit 2**: Upgraded the underlying framework to SvelteKit version 2, offering enhanced speed, better code structure, and improved deployment capabilities.
- **🧹 General Cleanup and Refactoring**: Performed broad cleanup and refactoring across the platform, improving code efficiency and maintaining high standards of code health.
- **🚧 Integration Testing Improvements**: Modified how Cypress integration tests detect chat messages and updated sharing tests for better reliability and accuracy.
- **📁 Standardized '.safetensors' File Extension**: Renamed the '.sft' file extension to '.safetensors' for ComfyUI workflows, standardizing file formats across the platform.
### Removed
- **🗑️ Deprecated Frontend Functions**: Removed frontend functions that were migrated to backend to declutter the codebase and reduce redundancy.
## [0.3.12] - 2024-08-07
### Added
+81 -55
View File
@@ -1,5 +1,12 @@
import os
import hashlib
import json
import logging
import os
import uuid
from functools import lru_cache
from pathlib import Path
import requests
from fastapi import (
FastAPI,
Request,
@@ -8,34 +15,14 @@ from fastapi import (
status,
UploadFile,
File,
Form,
)
from fastapi.responses import StreamingResponse, JSONResponse, FileResponse
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import FileResponse
from pydantic import BaseModel
from typing import List
import uuid
import requests
import hashlib
from pathlib import Path
import json
from constants import ERROR_MESSAGES
from utils.utils import (
decode_token,
get_current_user,
get_verified_user,
get_admin_user,
)
from utils.misc import calculate_sha256
from config import (
SRC_LOG_LEVELS,
CACHE_DIR,
UPLOAD_DIR,
WHISPER_MODEL,
WHISPER_MODEL_DIR,
WHISPER_MODEL_AUTO_UPDATE,
@@ -50,7 +37,15 @@ from config import (
AUDIO_TTS_ENGINE,
AUDIO_TTS_MODEL,
AUDIO_TTS_VOICE,
AUDIO_TTS_SPLIT_ON,
AppConfig,
CORS_ALLOW_ORIGIN,
)
from constants import ERROR_MESSAGES
from utils.utils import (
get_current_user,
get_verified_user,
get_admin_user,
)
log = logging.getLogger(__name__)
@@ -59,7 +54,7 @@ log.setLevel(SRC_LOG_LEVELS["AUDIO"])
app = FastAPI()
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_origins=CORS_ALLOW_ORIGIN,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
@@ -78,6 +73,7 @@ app.state.config.TTS_ENGINE = AUDIO_TTS_ENGINE
app.state.config.TTS_MODEL = AUDIO_TTS_MODEL
app.state.config.TTS_VOICE = AUDIO_TTS_VOICE
app.state.config.TTS_API_KEY = AUDIO_TTS_API_KEY
app.state.config.TTS_SPLIT_ON = AUDIO_TTS_SPLIT_ON
# setting device type for whisper model
whisper_device_type = DEVICE_TYPE if DEVICE_TYPE and DEVICE_TYPE == "cuda" else "cpu"
@@ -94,6 +90,7 @@ class TTSConfigForm(BaseModel):
ENGINE: str
MODEL: str
VOICE: str
SPLIT_ON: str
class STTConfigForm(BaseModel):
@@ -145,6 +142,7 @@ async def get_audio_config(user=Depends(get_admin_user)):
"ENGINE": app.state.config.TTS_ENGINE,
"MODEL": app.state.config.TTS_MODEL,
"VOICE": app.state.config.TTS_VOICE,
"SPLIT_ON": app.state.config.TTS_SPLIT_ON,
},
"stt": {
"OPENAI_API_BASE_URL": app.state.config.STT_OPENAI_API_BASE_URL,
@@ -165,6 +163,7 @@ async def update_audio_config(
app.state.config.TTS_ENGINE = form_data.tts.ENGINE
app.state.config.TTS_MODEL = form_data.tts.MODEL
app.state.config.TTS_VOICE = form_data.tts.VOICE
app.state.config.TTS_SPLIT_ON = form_data.tts.SPLIT_ON
app.state.config.STT_OPENAI_API_BASE_URL = form_data.stt.OPENAI_API_BASE_URL
app.state.config.STT_OPENAI_API_KEY = form_data.stt.OPENAI_API_KEY
@@ -179,6 +178,7 @@ async def update_audio_config(
"ENGINE": app.state.config.TTS_ENGINE,
"MODEL": app.state.config.TTS_MODEL,
"VOICE": app.state.config.TTS_VOICE,
"SPLIT_ON": app.state.config.TTS_SPLIT_ON,
},
"stt": {
"OPENAI_API_BASE_URL": app.state.config.STT_OPENAI_API_BASE_URL,
@@ -244,7 +244,7 @@ async def speech(request: Request, user=Depends(get_verified_user)):
res = r.json()
if "error" in res:
error_detail = f"External: {res['error']['message']}"
except:
except Exception:
error_detail = f"External: {e}"
raise HTTPException(
@@ -261,6 +261,13 @@ async def speech(request: Request, user=Depends(get_verified_user)):
raise HTTPException(status_code=400, detail="Invalid JSON payload")
voice_id = payload.get("voice", "")
if voice_id not in get_available_voices():
raise HTTPException(
status_code=400,
detail="Invalid voice id",
)
url = f"https://api.elevenlabs.io/v1/text-to-speech/{voice_id}"
headers = {
@@ -299,7 +306,7 @@ async def speech(request: Request, user=Depends(get_verified_user)):
res = r.json()
if "error" in res:
error_detail = f"External: {res['error']['message']}"
except:
except Exception:
error_detail = f"External: {e}"
raise HTTPException(
@@ -353,7 +360,7 @@ def transcribe(
try:
model = WhisperModel(**whisper_kwargs)
except:
except Exception:
log.warning(
"WhisperModel initialization failed, attempting download with local_files_only=False"
)
@@ -421,7 +428,7 @@ def transcribe(
res = r.json()
if "error" in res:
error_detail = f"External: {res['error']['message']}"
except:
except Exception:
error_detail = f"External: {e}"
raise HTTPException(
@@ -438,7 +445,7 @@ def transcribe(
)
def get_available_models() -> List[dict]:
def get_available_models() -> list[dict]:
if app.state.config.TTS_ENGINE == "openai":
return [{"id": "tts-1"}, {"id": "tts-1-hd"}]
elif app.state.config.TTS_ENGINE == "elevenlabs":
@@ -466,39 +473,58 @@ async def get_models(user=Depends(get_verified_user)):
return {"models": get_available_models()}
def get_available_voices() -> List[dict]:
def get_available_voices() -> dict:
"""Returns {voice_id: voice_name} dict"""
ret = {}
if app.state.config.TTS_ENGINE == "openai":
return [
{"name": "alloy", "id": "alloy"},
{"name": "echo", "id": "echo"},
{"name": "fable", "id": "fable"},
{"name": "onyx", "id": "onyx"},
{"name": "nova", "id": "nova"},
{"name": "shimmer", "id": "shimmer"},
]
elif app.state.config.TTS_ENGINE == "elevenlabs":
headers = {
"xi-api-key": app.state.config.TTS_API_KEY,
"Content-Type": "application/json",
ret = {
"alloy": "alloy",
"echo": "echo",
"fable": "fable",
"onyx": "onyx",
"nova": "nova",
"shimmer": "shimmer",
}
elif app.state.config.TTS_ENGINE == "elevenlabs":
try:
response = requests.get(
"https://api.elevenlabs.io/v1/voices", headers=headers
)
response.raise_for_status()
voices_data = response.json()
ret = get_elevenlabs_voices()
except Exception as e:
# Avoided @lru_cache with exception
pass
voices = []
for voice in voices_data.get("voices", []):
voices.append({"name": voice["name"], "id": voice["voice_id"]})
return voices
except requests.RequestException as e:
log.error(f"Error fetching voices: {str(e)}")
return ret
return []
@lru_cache
def get_elevenlabs_voices() -> dict:
"""
Note, set the following in your .env file to use Elevenlabs:
AUDIO_TTS_ENGINE=elevenlabs
AUDIO_TTS_API_KEY=sk_... # Your Elevenlabs API key
AUDIO_TTS_VOICE=EXAVITQu4vr4xnSDxMaL # From https://api.elevenlabs.io/v1/voices
AUDIO_TTS_MODEL=eleven_multilingual_v2
"""
headers = {
"xi-api-key": app.state.config.TTS_API_KEY,
"Content-Type": "application/json",
}
try:
# TODO: Add retries
response = requests.get("https://api.elevenlabs.io/v1/voices", headers=headers)
response.raise_for_status()
voices_data = response.json()
voices = {}
for voice in voices_data.get("voices", []):
voices[voice["voice_id"]] = voice["name"]
except requests.RequestException as e:
# Avoid @lru_cache with exception
log.error(f"Error fetching voices: {str(e)}")
raise RuntimeError(f"Error fetching voices: {str(e)}")
return voices
@app.get("/voices")
async def get_voices(user=Depends(get_verified_user)):
return {"voices": get_available_voices()}
return {"voices": [{"id": k, "name": v} for k, v in get_available_voices().items()]}
+248 -266
View File
@@ -1,26 +1,10 @@
import re
import requests
import base64
from fastapi import (
FastAPI,
Request,
Depends,
HTTPException,
status,
UploadFile,
File,
Form,
)
from fastapi.middleware.cors import CORSMiddleware
from constants import ERROR_MESSAGES
from utils.utils import (
get_verified_user,
get_admin_user,
)
from apps.images.utils.comfyui import ImageGenerationPayload, comfyui_generate_image
from utils.misc import calculate_sha256
from typing import Optional
from pydantic import BaseModel
from pathlib import Path
@@ -29,7 +13,22 @@ import uuid
import base64
import json
import logging
import re
import requests
import asyncio
from utils.utils import (
get_verified_user,
get_admin_user,
)
from apps.images.utils.comfyui import (
ComfyUIWorkflow,
ComfyUIGenerateImageForm,
comfyui_generate_image,
)
from constants import ERROR_MESSAGES
from config import (
SRC_LOG_LEVELS,
CACHE_DIR,
@@ -38,18 +37,14 @@ from config import (
AUTOMATIC1111_BASE_URL,
AUTOMATIC1111_API_AUTH,
COMFYUI_BASE_URL,
COMFYUI_CFG_SCALE,
COMFYUI_SAMPLER,
COMFYUI_SCHEDULER,
COMFYUI_SD3,
COMFYUI_FLUX,
COMFYUI_FLUX_WEIGHT_DTYPE,
COMFYUI_FLUX_FP8_CLIP,
COMFYUI_WORKFLOW,
COMFYUI_WORKFLOW_NODES,
IMAGES_OPENAI_API_BASE_URL,
IMAGES_OPENAI_API_KEY,
IMAGE_GENERATION_MODEL,
IMAGE_SIZE,
IMAGE_STEPS,
CORS_ALLOW_ORIGIN,
AppConfig,
)
@@ -62,7 +57,7 @@ IMAGE_CACHE_DIR.mkdir(parents=True, exist_ok=True)
app = FastAPI()
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_origins=CORS_ALLOW_ORIGIN,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
@@ -81,20 +76,98 @@ app.state.config.MODEL = IMAGE_GENERATION_MODEL
app.state.config.AUTOMATIC1111_BASE_URL = AUTOMATIC1111_BASE_URL
app.state.config.AUTOMATIC1111_API_AUTH = AUTOMATIC1111_API_AUTH
app.state.config.COMFYUI_BASE_URL = COMFYUI_BASE_URL
app.state.config.COMFYUI_WORKFLOW = COMFYUI_WORKFLOW
app.state.config.COMFYUI_WORKFLOW_NODES = COMFYUI_WORKFLOW_NODES
app.state.config.IMAGE_SIZE = IMAGE_SIZE
app.state.config.IMAGE_STEPS = IMAGE_STEPS
app.state.config.COMFYUI_CFG_SCALE = COMFYUI_CFG_SCALE
app.state.config.COMFYUI_SAMPLER = COMFYUI_SAMPLER
app.state.config.COMFYUI_SCHEDULER = COMFYUI_SCHEDULER
app.state.config.COMFYUI_SD3 = COMFYUI_SD3
app.state.config.COMFYUI_FLUX = COMFYUI_FLUX
app.state.config.COMFYUI_FLUX_WEIGHT_DTYPE = COMFYUI_FLUX_WEIGHT_DTYPE
app.state.config.COMFYUI_FLUX_FP8_CLIP = COMFYUI_FLUX_FP8_CLIP
@app.get("/config")
async def get_config(request: Request, user=Depends(get_admin_user)):
return {
"enabled": app.state.config.ENABLED,
"engine": app.state.config.ENGINE,
"openai": {
"OPENAI_API_BASE_URL": app.state.config.OPENAI_API_BASE_URL,
"OPENAI_API_KEY": app.state.config.OPENAI_API_KEY,
},
"automatic1111": {
"AUTOMATIC1111_BASE_URL": app.state.config.AUTOMATIC1111_BASE_URL,
"AUTOMATIC1111_API_AUTH": app.state.config.AUTOMATIC1111_API_AUTH,
},
"comfyui": {
"COMFYUI_BASE_URL": app.state.config.COMFYUI_BASE_URL,
"COMFYUI_WORKFLOW": app.state.config.COMFYUI_WORKFLOW,
"COMFYUI_WORKFLOW_NODES": app.state.config.COMFYUI_WORKFLOW_NODES,
},
}
class OpenAIConfigForm(BaseModel):
OPENAI_API_BASE_URL: str
OPENAI_API_KEY: str
class Automatic1111ConfigForm(BaseModel):
AUTOMATIC1111_BASE_URL: str
AUTOMATIC1111_API_AUTH: str
class ComfyUIConfigForm(BaseModel):
COMFYUI_BASE_URL: str
COMFYUI_WORKFLOW: str
COMFYUI_WORKFLOW_NODES: list[dict]
class ConfigForm(BaseModel):
enabled: bool
engine: str
openai: OpenAIConfigForm
automatic1111: Automatic1111ConfigForm
comfyui: ComfyUIConfigForm
@app.post("/config/update")
async def update_config(form_data: ConfigForm, user=Depends(get_admin_user)):
app.state.config.ENGINE = form_data.engine
app.state.config.ENABLED = form_data.enabled
app.state.config.OPENAI_API_BASE_URL = form_data.openai.OPENAI_API_BASE_URL
app.state.config.OPENAI_API_KEY = form_data.openai.OPENAI_API_KEY
app.state.config.AUTOMATIC1111_BASE_URL = (
form_data.automatic1111.AUTOMATIC1111_BASE_URL
)
app.state.config.AUTOMATIC1111_API_AUTH = (
form_data.automatic1111.AUTOMATIC1111_API_AUTH
)
app.state.config.COMFYUI_BASE_URL = form_data.comfyui.COMFYUI_BASE_URL
app.state.config.COMFYUI_WORKFLOW = form_data.comfyui.COMFYUI_WORKFLOW
app.state.config.COMFYUI_WORKFLOW_NODES = form_data.comfyui.COMFYUI_WORKFLOW_NODES
return {
"enabled": app.state.config.ENABLED,
"engine": app.state.config.ENGINE,
"openai": {
"OPENAI_API_BASE_URL": app.state.config.OPENAI_API_BASE_URL,
"OPENAI_API_KEY": app.state.config.OPENAI_API_KEY,
},
"automatic1111": {
"AUTOMATIC1111_BASE_URL": app.state.config.AUTOMATIC1111_BASE_URL,
"AUTOMATIC1111_API_AUTH": app.state.config.AUTOMATIC1111_API_AUTH,
},
"comfyui": {
"COMFYUI_BASE_URL": app.state.config.COMFYUI_BASE_URL,
"COMFYUI_WORKFLOW": app.state.config.COMFYUI_WORKFLOW,
"COMFYUI_WORKFLOW_NODES": app.state.config.COMFYUI_WORKFLOW_NODES,
},
}
def get_automatic1111_api_auth():
if app.state.config.AUTOMATIC1111_API_AUTH == None:
if app.state.config.AUTOMATIC1111_API_AUTH is None:
return ""
else:
auth1111_byte_string = app.state.config.AUTOMATIC1111_API_AUTH.encode("utf-8")
@@ -103,164 +176,110 @@ def get_automatic1111_api_auth():
return f"Basic {auth1111_base64_encoded_string}"
@app.get("/config")
async def get_config(request: Request, user=Depends(get_admin_user)):
return {
"engine": app.state.config.ENGINE,
"enabled": app.state.config.ENABLED,
}
class ConfigUpdateForm(BaseModel):
engine: str
enabled: bool
@app.post("/config/update")
async def update_config(form_data: ConfigUpdateForm, user=Depends(get_admin_user)):
app.state.config.ENGINE = form_data.engine
app.state.config.ENABLED = form_data.enabled
return {
"engine": app.state.config.ENGINE,
"enabled": app.state.config.ENABLED,
}
class EngineUrlUpdateForm(BaseModel):
AUTOMATIC1111_BASE_URL: Optional[str] = None
AUTOMATIC1111_API_AUTH: Optional[str] = None
COMFYUI_BASE_URL: Optional[str] = None
@app.get("/url")
async def get_engine_url(user=Depends(get_admin_user)):
return {
"AUTOMATIC1111_BASE_URL": app.state.config.AUTOMATIC1111_BASE_URL,
"AUTOMATIC1111_API_AUTH": app.state.config.AUTOMATIC1111_API_AUTH,
"COMFYUI_BASE_URL": app.state.config.COMFYUI_BASE_URL,
}
@app.post("/url/update")
async def update_engine_url(
form_data: EngineUrlUpdateForm, user=Depends(get_admin_user)
):
if form_data.AUTOMATIC1111_BASE_URL == None:
app.state.config.AUTOMATIC1111_BASE_URL = AUTOMATIC1111_BASE_URL
else:
url = form_data.AUTOMATIC1111_BASE_URL.strip("/")
@app.get("/config/url/verify")
async def verify_url(user=Depends(get_admin_user)):
if app.state.config.ENGINE == "automatic1111":
try:
r = requests.head(url)
app.state.config.AUTOMATIC1111_BASE_URL = url
r = requests.get(
url=f"{app.state.config.AUTOMATIC1111_BASE_URL}/sdapi/v1/options",
headers={"authorization": get_automatic1111_api_auth()},
)
r.raise_for_status()
return True
except Exception as e:
app.state.config.ENABLED = False
raise HTTPException(status_code=400, detail=ERROR_MESSAGES.INVALID_URL)
elif app.state.config.ENGINE == "comfyui":
try:
r = requests.get(url=f"{app.state.config.COMFYUI_BASE_URL}/object_info")
r.raise_for_status()
return True
except Exception as e:
app.state.config.ENABLED = False
raise HTTPException(status_code=400, detail=ERROR_MESSAGES.INVALID_URL)
else:
return True
def set_image_model(model: str):
app.state.config.MODEL = model
if app.state.config.ENGINE in ["", "automatic1111"]:
api_auth = get_automatic1111_api_auth()
r = requests.get(
url=f"{app.state.config.AUTOMATIC1111_BASE_URL}/sdapi/v1/options",
headers={"authorization": api_auth},
)
options = r.json()
if model != options["sd_model_checkpoint"]:
options["sd_model_checkpoint"] = model
r = requests.post(
url=f"{app.state.config.AUTOMATIC1111_BASE_URL}/sdapi/v1/options",
json=options,
headers={"authorization": api_auth},
)
return app.state.config.MODEL
def get_image_model():
if app.state.config.ENGINE == "openai":
return app.state.config.MODEL if app.state.config.MODEL else "dall-e-2"
elif app.state.config.ENGINE == "comfyui":
return app.state.config.MODEL if app.state.config.MODEL else ""
elif app.state.config.ENGINE == "automatic1111" or app.state.config.ENGINE == "":
try:
r = requests.get(
url=f"{app.state.config.AUTOMATIC1111_BASE_URL}/sdapi/v1/options",
headers={"authorization": get_automatic1111_api_auth()},
)
options = r.json()
return options["sd_model_checkpoint"]
except Exception as e:
app.state.config.ENABLED = False
raise HTTPException(status_code=400, detail=ERROR_MESSAGES.DEFAULT(e))
if form_data.COMFYUI_BASE_URL == None:
app.state.config.COMFYUI_BASE_URL = COMFYUI_BASE_URL
else:
url = form_data.COMFYUI_BASE_URL.strip("/")
try:
r = requests.head(url)
app.state.config.COMFYUI_BASE_URL = url
except Exception as e:
raise HTTPException(status_code=400, detail=ERROR_MESSAGES.DEFAULT(e))
class ImageConfigForm(BaseModel):
MODEL: str
IMAGE_SIZE: str
IMAGE_STEPS: int
if form_data.AUTOMATIC1111_API_AUTH == None:
app.state.config.AUTOMATIC1111_API_AUTH = AUTOMATIC1111_API_AUTH
else:
app.state.config.AUTOMATIC1111_API_AUTH = form_data.AUTOMATIC1111_API_AUTH
@app.get("/image/config")
async def get_image_config(user=Depends(get_admin_user)):
return {
"AUTOMATIC1111_BASE_URL": app.state.config.AUTOMATIC1111_BASE_URL,
"AUTOMATIC1111_API_AUTH": app.state.config.AUTOMATIC1111_API_AUTH,
"COMFYUI_BASE_URL": app.state.config.COMFYUI_BASE_URL,
"status": True,
"MODEL": app.state.config.MODEL,
"IMAGE_SIZE": app.state.config.IMAGE_SIZE,
"IMAGE_STEPS": app.state.config.IMAGE_STEPS,
}
class OpenAIConfigUpdateForm(BaseModel):
url: str
key: str
@app.post("/image/config/update")
async def update_image_config(form_data: ImageConfigForm, user=Depends(get_admin_user)):
app.state.config.MODEL = form_data.MODEL
@app.get("/openai/config")
async def get_openai_config(user=Depends(get_admin_user)):
return {
"OPENAI_API_BASE_URL": app.state.config.OPENAI_API_BASE_URL,
"OPENAI_API_KEY": app.state.config.OPENAI_API_KEY,
}
@app.post("/openai/config/update")
async def update_openai_config(
form_data: OpenAIConfigUpdateForm, user=Depends(get_admin_user)
):
if form_data.key == "":
raise HTTPException(status_code=400, detail=ERROR_MESSAGES.API_KEY_NOT_FOUND)
app.state.config.OPENAI_API_BASE_URL = form_data.url
app.state.config.OPENAI_API_KEY = form_data.key
return {
"status": True,
"OPENAI_API_BASE_URL": app.state.config.OPENAI_API_BASE_URL,
"OPENAI_API_KEY": app.state.config.OPENAI_API_KEY,
}
class ImageSizeUpdateForm(BaseModel):
size: str
@app.get("/size")
async def get_image_size(user=Depends(get_admin_user)):
return {"IMAGE_SIZE": app.state.config.IMAGE_SIZE}
@app.post("/size/update")
async def update_image_size(
form_data: ImageSizeUpdateForm, user=Depends(get_admin_user)
):
pattern = r"^\d+x\d+$" # Regular expression pattern
if re.match(pattern, form_data.size):
app.state.config.IMAGE_SIZE = form_data.size
return {
"IMAGE_SIZE": app.state.config.IMAGE_SIZE,
"status": True,
}
pattern = r"^\d+x\d+$"
if re.match(pattern, form_data.IMAGE_SIZE):
app.state.config.IMAGE_SIZE = form_data.IMAGE_SIZE
else:
raise HTTPException(
status_code=400,
detail=ERROR_MESSAGES.INCORRECT_FORMAT(" (e.g., 512x512)."),
)
class ImageStepsUpdateForm(BaseModel):
steps: int
@app.get("/steps")
async def get_image_size(user=Depends(get_admin_user)):
return {"IMAGE_STEPS": app.state.config.IMAGE_STEPS}
@app.post("/steps/update")
async def update_image_size(
form_data: ImageStepsUpdateForm, user=Depends(get_admin_user)
):
if form_data.steps >= 0:
app.state.config.IMAGE_STEPS = form_data.steps
return {
"IMAGE_STEPS": app.state.config.IMAGE_STEPS,
"status": True,
}
if form_data.IMAGE_STEPS >= 0:
app.state.config.IMAGE_STEPS = form_data.IMAGE_STEPS
else:
raise HTTPException(
status_code=400,
detail=ERROR_MESSAGES.INCORRECT_FORMAT(" (e.g., 50)."),
)
return {
"MODEL": app.state.config.MODEL,
"IMAGE_SIZE": app.state.config.IMAGE_SIZE,
"IMAGE_STEPS": app.state.config.IMAGE_STEPS,
}
@app.get("/models")
def get_models(user=Depends(get_verified_user)):
@@ -271,18 +290,51 @@ def get_models(user=Depends(get_verified_user)):
{"id": "dall-e-3", "name": "DALL·E 3"},
]
elif app.state.config.ENGINE == "comfyui":
# TODO - get models from comfyui
r = requests.get(url=f"{app.state.config.COMFYUI_BASE_URL}/object_info")
info = r.json()
return list(
map(
lambda model: {"id": model, "name": model},
info["CheckpointLoaderSimple"]["input"]["required"]["ckpt_name"][0],
)
)
workflow = json.loads(app.state.config.COMFYUI_WORKFLOW)
model_node_id = None
else:
for node in app.state.config.COMFYUI_WORKFLOW_NODES:
if node["type"] == "model":
if node["node_ids"]:
model_node_id = node["node_ids"][0]
break
if model_node_id:
model_list_key = None
print(workflow[model_node_id]["class_type"])
for key in info[workflow[model_node_id]["class_type"]]["input"][
"required"
]:
if "_name" in key:
model_list_key = key
break
if model_list_key:
return list(
map(
lambda model: {"id": model, "name": model},
info[workflow[model_node_id]["class_type"]]["input"][
"required"
][model_list_key][0],
)
)
else:
return list(
map(
lambda model: {"id": model, "name": model},
info["CheckpointLoaderSimple"]["input"]["required"][
"ckpt_name"
][0],
)
)
elif (
app.state.config.ENGINE == "automatic1111" or app.state.config.ENGINE == ""
):
r = requests.get(
url=f"{app.state.config.AUTOMATIC1111_BASE_URL}/sdapi/v1/sd-models",
headers={"authorization": get_automatic1111_api_auth()},
@@ -299,69 +351,11 @@ def get_models(user=Depends(get_verified_user)):
raise HTTPException(status_code=400, detail=ERROR_MESSAGES.DEFAULT(e))
@app.get("/models/default")
async def get_default_model(user=Depends(get_admin_user)):
try:
if app.state.config.ENGINE == "openai":
return {
"model": (
app.state.config.MODEL if app.state.config.MODEL else "dall-e-2"
)
}
elif app.state.config.ENGINE == "comfyui":
return {"model": (app.state.config.MODEL if app.state.config.MODEL else "")}
else:
r = requests.get(
url=f"{app.state.config.AUTOMATIC1111_BASE_URL}/sdapi/v1/options",
headers={"authorization": get_automatic1111_api_auth()},
)
options = r.json()
return {"model": options["sd_model_checkpoint"]}
except Exception as e:
app.state.config.ENABLED = False
raise HTTPException(status_code=400, detail=ERROR_MESSAGES.DEFAULT(e))
class UpdateModelForm(BaseModel):
model: str
def set_model_handler(model: str):
if app.state.config.ENGINE in ["openai", "comfyui"]:
app.state.config.MODEL = model
return app.state.config.MODEL
else:
api_auth = get_automatic1111_api_auth()
r = requests.get(
url=f"{app.state.config.AUTOMATIC1111_BASE_URL}/sdapi/v1/options",
headers={"authorization": api_auth},
)
options = r.json()
if model != options["sd_model_checkpoint"]:
options["sd_model_checkpoint"] = model
r = requests.post(
url=f"{app.state.config.AUTOMATIC1111_BASE_URL}/sdapi/v1/options",
json=options,
headers={"authorization": api_auth},
)
return options
@app.post("/models/default/update")
def update_default_model(
form_data: UpdateModelForm,
user=Depends(get_verified_user),
):
return set_model_handler(form_data.model)
class GenerateImageForm(BaseModel):
model: Optional[str] = None
prompt: str
n: int = 1
size: Optional[str] = None
n: int = 1
negative_prompt: Optional[str] = None
@@ -477,7 +471,6 @@ async def image_generations(
return images
elif app.state.config.ENGINE == "comfyui":
data = {
"prompt": form_data.prompt,
"width": width,
@@ -491,32 +484,20 @@ async def image_generations(
if form_data.negative_prompt is not None:
data["negative_prompt"] = form_data.negative_prompt
if app.state.config.COMFYUI_CFG_SCALE:
data["cfg_scale"] = app.state.config.COMFYUI_CFG_SCALE
if app.state.config.COMFYUI_SAMPLER is not None:
data["sampler"] = app.state.config.COMFYUI_SAMPLER
if app.state.config.COMFYUI_SCHEDULER is not None:
data["scheduler"] = app.state.config.COMFYUI_SCHEDULER
if app.state.config.COMFYUI_SD3 is not None:
data["sd3"] = app.state.config.COMFYUI_SD3
if app.state.config.COMFYUI_FLUX is not None:
data["flux"] = app.state.config.COMFYUI_FLUX
if app.state.config.COMFYUI_FLUX_WEIGHT_DTYPE is not None:
data["flux_weight_dtype"] = app.state.config.COMFYUI_FLUX_WEIGHT_DTYPE
if app.state.config.COMFYUI_FLUX_FP8_CLIP is not None:
data["flux_fp8_clip"] = app.state.config.COMFYUI_FLUX_FP8_CLIP
data = ImageGenerationPayload(**data)
res = comfyui_generate_image(
form_data = ComfyUIGenerateImageForm(
**{
"workflow": ComfyUIWorkflow(
**{
"workflow": app.state.config.COMFYUI_WORKFLOW,
"nodes": app.state.config.COMFYUI_WORKFLOW_NODES,
}
),
**data,
}
)
res = await comfyui_generate_image(
app.state.config.MODEL,
data,
form_data,
user.id,
app.state.config.COMFYUI_BASE_URL,
)
@@ -530,13 +511,15 @@ async def image_generations(
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)
json.dump(form_data.model_dump(exclude_none=True), f)
log.debug(f"images: {images}")
return images
else:
elif (
app.state.config.ENGINE == "automatic1111" or app.state.config.ENGINE == ""
):
if form_data.model:
set_model_handler(form_data.model)
set_image_model(form_data.model)
data = {
"prompt": form_data.prompt,
@@ -551,14 +534,15 @@ async def image_generations(
if form_data.negative_prompt is not None:
data["negative_prompt"] = form_data.negative_prompt
r = requests.post(
# Use asyncio.to_thread for the requests.post call
r = await asyncio.to_thread(
requests.post,
url=f"{app.state.config.AUTOMATIC1111_BASE_URL}/sdapi/v1/txt2img",
json=data,
headers={"authorization": get_automatic1111_api_auth()},
)
res = r.json()
log.debug(f"res: {res}")
images = []
@@ -572,10 +556,8 @@ async def image_generations(
json.dump({**data, "info": res["info"]}, f)
return images
except Exception as e:
error = e
if r != None:
data = r.json()
if "error" in data:
+60 -308
View File
@@ -1,5 +1,5 @@
import asyncio
import websocket # NOTE: websocket-client (https://github.com/websocket-client/websocket-client)
import uuid
import json
import urllib.request
import urllib.parse
@@ -15,245 +15,6 @@ from pydantic import BaseModel
from typing import Optional
COMFYUI_DEFAULT_PROMPT = """
{
"3": {
"inputs": {
"seed": 0,
"steps": 20,
"cfg": 8,
"sampler_name": "euler",
"scheduler": "normal",
"denoise": 1,
"model": [
"4",
0
],
"positive": [
"6",
0
],
"negative": [
"7",
0
],
"latent_image": [
"5",
0
]
},
"class_type": "KSampler",
"_meta": {
"title": "KSampler"
}
},
"4": {
"inputs": {
"ckpt_name": "model.safetensors"
},
"class_type": "CheckpointLoaderSimple",
"_meta": {
"title": "Load Checkpoint"
}
},
"5": {
"inputs": {
"width": 512,
"height": 512,
"batch_size": 1
},
"class_type": "EmptyLatentImage",
"_meta": {
"title": "Empty Latent Image"
}
},
"6": {
"inputs": {
"text": "Prompt",
"clip": [
"4",
1
]
},
"class_type": "CLIPTextEncode",
"_meta": {
"title": "CLIP Text Encode (Prompt)"
}
},
"7": {
"inputs": {
"text": "Negative Prompt",
"clip": [
"4",
1
]
},
"class_type": "CLIPTextEncode",
"_meta": {
"title": "CLIP Text Encode (Prompt)"
}
},
"8": {
"inputs": {
"samples": [
"3",
0
],
"vae": [
"4",
2
]
},
"class_type": "VAEDecode",
"_meta": {
"title": "VAE Decode"
}
},
"9": {
"inputs": {
"filename_prefix": "ComfyUI",
"images": [
"8",
0
]
},
"class_type": "SaveImage",
"_meta": {
"title": "Save Image"
}
}
}
"""
FLUX_DEFAULT_PROMPT = """
{
"5": {
"inputs": {
"width": 1024,
"height": 1024,
"batch_size": 1
},
"class_type": "EmptyLatentImage"
},
"6": {
"inputs": {
"text": "Input Text Here",
"clip": [
"11",
0
]
},
"class_type": "CLIPTextEncode"
},
"8": {
"inputs": {
"samples": [
"13",
0
],
"vae": [
"10",
0
]
},
"class_type": "VAEDecode"
},
"9": {
"inputs": {
"filename_prefix": "ComfyUI",
"images": [
"8",
0
]
},
"class_type": "SaveImage"
},
"10": {
"inputs": {
"vae_name": "ae.sft"
},
"class_type": "VAELoader"
},
"11": {
"inputs": {
"clip_name1": "clip_l.safetensors",
"clip_name2": "t5xxl_fp16.safetensors",
"type": "flux"
},
"class_type": "DualCLIPLoader"
},
"12": {
"inputs": {
"unet_name": "flux1-dev.sft",
"weight_dtype": "default"
},
"class_type": "UNETLoader"
},
"13": {
"inputs": {
"noise": [
"25",
0
],
"guider": [
"22",
0
],
"sampler": [
"16",
0
],
"sigmas": [
"17",
0
],
"latent_image": [
"5",
0
]
},
"class_type": "SamplerCustomAdvanced"
},
"16": {
"inputs": {
"sampler_name": "euler"
},
"class_type": "KSamplerSelect"
},
"17": {
"inputs": {
"scheduler": "simple",
"steps": 20,
"denoise": 1,
"model": [
"12",
0
]
},
"class_type": "BasicScheduler"
},
"22": {
"inputs": {
"model": [
"12",
0
],
"conditioning": [
"6",
0
]
},
"class_type": "BasicGuider"
},
"25": {
"inputs": {
"noise_seed": 778937779713005
},
"class_type": "RandomNoise"
}
}
"""
def queue_prompt(prompt, client_id, base_url):
log.info("queue_prompt")
@@ -311,82 +72,71 @@ def get_images(ws, prompt, client_id, base_url):
return {"data": output_images}
class ImageGenerationPayload(BaseModel):
class ComfyUINodeInput(BaseModel):
type: Optional[str] = None
node_ids: list[str] = []
key: Optional[str] = "text"
value: Optional[str] = None
class ComfyUIWorkflow(BaseModel):
workflow: str
nodes: list[ComfyUINodeInput]
class ComfyUIGenerateImageForm(BaseModel):
workflow: ComfyUIWorkflow
prompt: str
negative_prompt: Optional[str] = ""
steps: Optional[int] = None
seed: Optional[int] = None
negative_prompt: Optional[str] = None
width: int
height: int
n: int = 1
cfg_scale: Optional[float] = None
sampler: Optional[str] = None
scheduler: Optional[str] = None
sd3: Optional[bool] = None
flux: Optional[bool] = None
flux_weight_dtype: Optional[str] = None
flux_fp8_clip: Optional[bool] = None
steps: Optional[int] = None
seed: Optional[int] = None
def comfyui_generate_image(
model: str, payload: ImageGenerationPayload, client_id, base_url
async def comfyui_generate_image(
model: str, payload: ComfyUIGenerateImageForm, client_id, base_url
):
ws_url = base_url.replace("http://", "ws://").replace("https://", "wss://")
workflow = json.loads(payload.workflow.workflow)
comfyui_prompt = json.loads(COMFYUI_DEFAULT_PROMPT)
if payload.cfg_scale:
comfyui_prompt["3"]["inputs"]["cfg"] = payload.cfg_scale
if payload.sampler:
comfyui_prompt["3"]["inputs"]["sampler"] = payload.sampler
if payload.scheduler:
comfyui_prompt["3"]["inputs"]["scheduler"] = payload.scheduler
if payload.sd3:
comfyui_prompt["5"]["class_type"] = "EmptySD3LatentImage"
if payload.steps:
comfyui_prompt["3"]["inputs"]["steps"] = payload.steps
comfyui_prompt["4"]["inputs"]["ckpt_name"] = model
comfyui_prompt["7"]["inputs"]["text"] = payload.negative_prompt
comfyui_prompt["3"]["inputs"]["seed"] = (
payload.seed if payload.seed else random.randint(0, 18446744073709551614)
)
# as Flux uses a completely different workflow, we must treat it specially
if payload.flux:
comfyui_prompt = json.loads(FLUX_DEFAULT_PROMPT)
comfyui_prompt["12"]["inputs"]["unet_name"] = model
comfyui_prompt["25"]["inputs"]["noise_seed"] = (
payload.seed if payload.seed else random.randint(0, 18446744073709551614)
)
if payload.sampler:
comfyui_prompt["16"]["inputs"]["sampler_name"] = payload.sampler
if payload.steps:
comfyui_prompt["17"]["inputs"]["steps"] = payload.steps
if payload.scheduler:
comfyui_prompt["17"]["inputs"]["scheduler"] = payload.scheduler
if payload.flux_weight_dtype:
comfyui_prompt["12"]["inputs"]["weight_dtype"] = payload.flux_weight_dtype
if payload.flux_fp8_clip:
comfyui_prompt["11"]["inputs"][
"clip_name2"
] = "t5xxl_fp8_e4m3fn.safetensors"
comfyui_prompt["5"]["inputs"]["batch_size"] = payload.n
comfyui_prompt["5"]["inputs"]["width"] = payload.width
comfyui_prompt["5"]["inputs"]["height"] = payload.height
# set the text prompt for our positive CLIPTextEncode
comfyui_prompt["6"]["inputs"]["text"] = payload.prompt
for node in payload.workflow.nodes:
if node.type:
if node.type == "model":
for node_id in node.node_ids:
workflow[node_id]["inputs"][node.key] = model
elif node.type == "prompt":
for node_id in node.node_ids:
workflow[node_id]["inputs"]["text"] = payload.prompt
elif node.type == "negative_prompt":
for node_id in node.node_ids:
workflow[node_id]["inputs"]["text"] = payload.negative_prompt
elif node.type == "width":
for node_id in node.node_ids:
workflow[node_id]["inputs"]["width"] = payload.width
elif node.type == "height":
for node_id in node.node_ids:
workflow[node_id]["inputs"]["height"] = payload.height
elif node.type == "n":
for node_id in node.node_ids:
workflow[node_id]["inputs"]["batch_size"] = payload.n
elif node.type == "steps":
for node_id in node.node_ids:
workflow[node_id]["inputs"]["steps"] = payload.steps
elif node.type == "seed":
seed = (
payload.seed
if payload.seed
else random.randint(0, 18446744073709551614)
)
for node_id in node.node_ids:
workflow[node_id]["inputs"][node.key] = seed
else:
for node_id in node.node_ids:
workflow[node_id]["inputs"][node.key] = node.value
try:
ws = websocket.WebSocket()
@@ -397,7 +147,9 @@ def comfyui_generate_image(
return None
try:
images = get_images(ws, comfyui_prompt, client_id, base_url)
log.info("Sending workflow to WebSocket server.")
log.info(f"Workflow: {workflow}")
images = await asyncio.to_thread(get_images, ws, workflow, client_id, base_url)
except Exception as e:
log.exception(f"Error while receiving images: {e}")
images = None
+129 -324
View File
@@ -1,47 +1,36 @@
from fastapi import (
FastAPI,
Request,
Response,
HTTPException,
Depends,
status,
UploadFile,
File,
BackgroundTasks,
)
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import StreamingResponse
from fastapi.concurrency import run_in_threadpool
from pydantic import BaseModel, ConfigDict
import os
import re
import copy
import random
import requests
import json
import uuid
import aiohttp
import asyncio
import logging
import time
from urllib.parse import urlparse
from typing import Optional, List, Union
from typing import Optional, Union
from starlette.background import BackgroundTask
from apps.webui.models.models import Models
from apps.webui.models.users import Users
from constants import ERROR_MESSAGES
from utils.utils import (
decode_token,
get_current_user,
get_verified_user,
get_admin_user,
)
from utils.task import prompt_template
from config import (
SRC_LOG_LEVELS,
@@ -52,8 +41,14 @@ from config import (
MODEL_FILTER_LIST,
UPLOAD_DIR,
AppConfig,
CORS_ALLOW_ORIGIN,
)
from utils.misc import (
calculate_sha256,
apply_model_params_to_body_ollama,
apply_model_params_to_body_openai,
apply_model_system_prompt_to_body,
)
from utils.misc import calculate_sha256, add_or_update_system_message
log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["OLLAMA"])
@@ -61,7 +56,7 @@ log.setLevel(SRC_LOG_LEVELS["OLLAMA"])
app = FastAPI()
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_origins=CORS_ALLOW_ORIGIN,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
@@ -120,7 +115,7 @@ async def get_ollama_api_urls(user=Depends(get_admin_user)):
class UrlUpdateForm(BaseModel):
urls: List[str]
urls: list[str]
@app.post("/urls/update")
@@ -153,20 +148,29 @@ async def cleanup_response(
await session.close()
async def post_streaming_url(url: str, payload: str, stream: bool = True):
async def post_streaming_url(
url: str, payload: Union[str, bytes], stream: bool = True, content_type=None
):
r = None
try:
session = aiohttp.ClientSession(
trust_env=True, timeout=aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT)
)
r = await session.post(url, data=payload)
r = await session.post(
url,
data=payload,
headers={"Content-Type": "application/json"},
)
r.raise_for_status()
if stream:
headers = dict(r.headers)
if content_type:
headers["Content-Type"] = content_type
return StreamingResponse(
r.content,
status_code=r.status,
headers=dict(r.headers),
headers=headers,
background=BackgroundTask(
cleanup_response, response=r, session=session
),
@@ -183,7 +187,7 @@ async def post_streaming_url(url: str, payload: str, stream: bool = True):
res = await r.json()
if "error" in res:
error_detail = f"Ollama: {res['error']}"
except:
except Exception:
error_detail = f"Ollama: {e}"
raise HTTPException(
@@ -238,7 +242,7 @@ async def get_all_models():
async def get_ollama_tags(
url_idx: Optional[int] = None, user=Depends(get_verified_user)
):
if url_idx == None:
if url_idx is None:
models = await get_all_models()
if app.state.config.ENABLE_MODEL_FILTER:
@@ -269,7 +273,7 @@ async def get_ollama_tags(
res = r.json()
if "error" in res:
error_detail = f"Ollama: {res['error']}"
except:
except Exception:
error_detail = f"Ollama: {e}"
raise HTTPException(
@@ -282,8 +286,7 @@ async def get_ollama_tags(
@app.get("/api/version/{url_idx}")
async def get_ollama_versions(url_idx: Optional[int] = None):
if app.state.config.ENABLE_OLLAMA_API:
if url_idx == None:
if url_idx is None:
# returns lowest version
tasks = [
fetch_url(f"{url}/api/version")
@@ -323,7 +326,7 @@ async def get_ollama_versions(url_idx: Optional[int] = None):
res = r.json()
if "error" in res:
error_detail = f"Ollama: {res['error']}"
except:
except Exception:
error_detail = f"Ollama: {e}"
raise HTTPException(
@@ -346,8 +349,6 @@ async def pull_model(
url = app.state.config.OLLAMA_BASE_URLS[url_idx]
log.info(f"url: {url}")
r = None
# Admin should be able to pull models from any source
payload = {**form_data.model_dump(exclude_none=True), "insecure": True}
@@ -367,7 +368,7 @@ async def push_model(
url_idx: Optional[int] = None,
user=Depends(get_admin_user),
):
if url_idx == None:
if url_idx is None:
if form_data.name in app.state.MODELS:
url_idx = app.state.MODELS[form_data.name]["urls"][0]
else:
@@ -417,7 +418,7 @@ async def copy_model(
url_idx: Optional[int] = None,
user=Depends(get_admin_user),
):
if url_idx == None:
if url_idx is None:
if form_data.source in app.state.MODELS:
url_idx = app.state.MODELS[form_data.source]["urls"][0]
else:
@@ -428,13 +429,14 @@ async def copy_model(
url = app.state.config.OLLAMA_BASE_URLS[url_idx]
log.info(f"url: {url}")
r = requests.request(
method="POST",
url=f"{url}/api/copy",
headers={"Content-Type": "application/json"},
data=form_data.model_dump_json(exclude_none=True).encode(),
)
try:
r = requests.request(
method="POST",
url=f"{url}/api/copy",
data=form_data.model_dump_json(exclude_none=True).encode(),
)
r.raise_for_status()
log.debug(f"r.text: {r.text}")
@@ -448,7 +450,7 @@ async def copy_model(
res = r.json()
if "error" in res:
error_detail = f"Ollama: {res['error']}"
except:
except Exception:
error_detail = f"Ollama: {e}"
raise HTTPException(
@@ -464,7 +466,7 @@ async def delete_model(
url_idx: Optional[int] = None,
user=Depends(get_admin_user),
):
if url_idx == None:
if url_idx is None:
if form_data.name in app.state.MODELS:
url_idx = app.state.MODELS[form_data.name]["urls"][0]
else:
@@ -476,12 +478,13 @@ async def delete_model(
url = app.state.config.OLLAMA_BASE_URLS[url_idx]
log.info(f"url: {url}")
r = requests.request(
method="DELETE",
url=f"{url}/api/delete",
headers={"Content-Type": "application/json"},
data=form_data.model_dump_json(exclude_none=True).encode(),
)
try:
r = requests.request(
method="DELETE",
url=f"{url}/api/delete",
data=form_data.model_dump_json(exclude_none=True).encode(),
)
r.raise_for_status()
log.debug(f"r.text: {r.text}")
@@ -495,7 +498,7 @@ async def delete_model(
res = r.json()
if "error" in res:
error_detail = f"Ollama: {res['error']}"
except:
except Exception:
error_detail = f"Ollama: {e}"
raise HTTPException(
@@ -516,12 +519,13 @@ async def show_model_info(form_data: ModelNameForm, user=Depends(get_verified_us
url = app.state.config.OLLAMA_BASE_URLS[url_idx]
log.info(f"url: {url}")
r = requests.request(
method="POST",
url=f"{url}/api/show",
headers={"Content-Type": "application/json"},
data=form_data.model_dump_json(exclude_none=True).encode(),
)
try:
r = requests.request(
method="POST",
url=f"{url}/api/show",
data=form_data.model_dump_json(exclude_none=True).encode(),
)
r.raise_for_status()
return r.json()
@@ -533,7 +537,7 @@ async def show_model_info(form_data: ModelNameForm, user=Depends(get_verified_us
res = r.json()
if "error" in res:
error_detail = f"Ollama: {res['error']}"
except:
except Exception:
error_detail = f"Ollama: {e}"
raise HTTPException(
@@ -556,7 +560,7 @@ async def generate_embeddings(
url_idx: Optional[int] = None,
user=Depends(get_verified_user),
):
if url_idx == None:
if url_idx is None:
model = form_data.model
if ":" not in model:
@@ -573,12 +577,13 @@ async def generate_embeddings(
url = app.state.config.OLLAMA_BASE_URLS[url_idx]
log.info(f"url: {url}")
r = requests.request(
method="POST",
url=f"{url}/api/embeddings",
headers={"Content-Type": "application/json"},
data=form_data.model_dump_json(exclude_none=True).encode(),
)
try:
r = requests.request(
method="POST",
url=f"{url}/api/embeddings",
data=form_data.model_dump_json(exclude_none=True).encode(),
)
r.raise_for_status()
return r.json()
@@ -590,7 +595,7 @@ async def generate_embeddings(
res = r.json()
if "error" in res:
error_detail = f"Ollama: {res['error']}"
except:
except Exception:
error_detail = f"Ollama: {e}"
raise HTTPException(
@@ -603,10 +608,9 @@ def generate_ollama_embeddings(
form_data: GenerateEmbeddingsForm,
url_idx: Optional[int] = None,
):
log.info(f"generate_ollama_embeddings {form_data}")
if url_idx == None:
if url_idx is None:
model = form_data.model
if ":" not in model:
@@ -623,12 +627,13 @@ def generate_ollama_embeddings(
url = app.state.config.OLLAMA_BASE_URLS[url_idx]
log.info(f"url: {url}")
r = requests.request(
method="POST",
url=f"{url}/api/embeddings",
headers={"Content-Type": "application/json"},
data=form_data.model_dump_json(exclude_none=True).encode(),
)
try:
r = requests.request(
method="POST",
url=f"{url}/api/embeddings",
data=form_data.model_dump_json(exclude_none=True).encode(),
)
r.raise_for_status()
data = r.json()
@@ -638,7 +643,7 @@ def generate_ollama_embeddings(
if "embedding" in data:
return data["embedding"]
else:
raise "Something went wrong :/"
raise Exception("Something went wrong :/")
except Exception as e:
log.exception(e)
error_detail = "Open WebUI: Server Connection Error"
@@ -647,16 +652,16 @@ def generate_ollama_embeddings(
res = r.json()
if "error" in res:
error_detail = f"Ollama: {res['error']}"
except:
except Exception:
error_detail = f"Ollama: {e}"
raise error_detail
raise Exception(error_detail)
class GenerateCompletionForm(BaseModel):
model: str
prompt: str
images: Optional[List[str]] = None
images: Optional[list[str]] = None
format: Optional[str] = None
options: Optional[dict] = None
system: Optional[str] = None
@@ -674,8 +679,7 @@ async def generate_completion(
url_idx: Optional[int] = None,
user=Depends(get_verified_user),
):
if url_idx == None:
if url_idx is None:
model = form_data.model
if ":" not in model:
@@ -700,12 +704,12 @@ async def generate_completion(
class ChatMessage(BaseModel):
role: str
content: str
images: Optional[List[str]] = None
images: Optional[list[str]] = None
class GenerateChatCompletionForm(BaseModel):
model: str
messages: List[ChatMessage]
messages: list[ChatMessage]
format: Optional[str] = None
options: Optional[dict] = None
template: Optional[str] = None
@@ -713,6 +717,18 @@ class GenerateChatCompletionForm(BaseModel):
keep_alive: Optional[Union[int, str]] = None
def get_ollama_url(url_idx: Optional[int], model: str):
if url_idx is None:
if model not in app.state.MODELS:
raise HTTPException(
status_code=400,
detail=ERROR_MESSAGES.MODEL_NOT_FOUND(model),
)
url_idx = random.choice(app.state.MODELS[model]["urls"])
url = app.state.config.OLLAMA_BASE_URLS[url_idx]
return url
@app.post("/api/chat")
@app.post("/api/chat/{url_idx}")
async def generate_chat_completion(
@@ -720,209 +736,47 @@ async def generate_chat_completion(
url_idx: Optional[int] = None,
user=Depends(get_verified_user),
):
log.debug(
"form_data.model_dump_json(exclude_none=True).encode(): {0} ".format(
form_data.model_dump_json(exclude_none=True).encode()
)
)
payload = {
**form_data.model_dump(exclude_none=True, exclude=["metadata"]),
}
payload = {**form_data.model_dump(exclude_none=True)}
log.debug(f"{payload = }")
if "metadata" in payload:
del payload["metadata"]
model_id = form_data.model
if app.state.config.ENABLE_MODEL_FILTER:
if user.role == "user" and model_id not in app.state.config.MODEL_FILTER_LIST:
raise HTTPException(
status_code=403,
detail="Model not found",
)
model_info = Models.get_model_by_id(model_id)
if model_info:
if model_info.base_model_id:
payload["model"] = model_info.base_model_id
model_info.params = model_info.params.model_dump()
params = model_info.params.model_dump()
if model_info.params:
if params:
if payload.get("options") is None:
payload["options"] = {}
if (
model_info.params.get("mirostat", None)
and payload["options"].get("mirostat") is None
):
payload["options"]["mirostat"] = model_info.params.get("mirostat", None)
if (
model_info.params.get("mirostat_eta", None)
and payload["options"].get("mirostat_eta") is None
):
payload["options"]["mirostat_eta"] = model_info.params.get(
"mirostat_eta", None
)
if (
model_info.params.get("mirostat_tau", None)
and payload["options"].get("mirostat_tau") is None
):
payload["options"]["mirostat_tau"] = model_info.params.get(
"mirostat_tau", None
)
if (
model_info.params.get("num_ctx", None)
and payload["options"].get("num_ctx") is None
):
payload["options"]["num_ctx"] = model_info.params.get("num_ctx", None)
if (
model_info.params.get("num_batch", None)
and payload["options"].get("num_batch") is None
):
payload["options"]["num_batch"] = model_info.params.get(
"num_batch", None
)
if (
model_info.params.get("num_keep", None)
and payload["options"].get("num_keep") is None
):
payload["options"]["num_keep"] = model_info.params.get("num_keep", None)
if (
model_info.params.get("repeat_last_n", None)
and payload["options"].get("repeat_last_n") is None
):
payload["options"]["repeat_last_n"] = model_info.params.get(
"repeat_last_n", None
)
if (
model_info.params.get("frequency_penalty", None)
and payload["options"].get("frequency_penalty") is None
):
payload["options"]["repeat_penalty"] = model_info.params.get(
"frequency_penalty", None
)
if (
model_info.params.get("temperature", None) is not None
and payload["options"].get("temperature") is None
):
payload["options"]["temperature"] = model_info.params.get(
"temperature", None
)
if (
model_info.params.get("seed", None) is not None
and payload["options"].get("seed") is None
):
payload["options"]["seed"] = model_info.params.get("seed", None)
if (
model_info.params.get("stop", None)
and payload["options"].get("stop") is None
):
payload["options"]["stop"] = (
[
bytes(stop, "utf-8").decode("unicode_escape")
for stop in model_info.params["stop"]
]
if model_info.params.get("stop", None)
else None
)
if (
model_info.params.get("tfs_z", None)
and payload["options"].get("tfs_z") is None
):
payload["options"]["tfs_z"] = model_info.params.get("tfs_z", None)
if (
model_info.params.get("max_tokens", None)
and payload["options"].get("max_tokens") is None
):
payload["options"]["num_predict"] = model_info.params.get(
"max_tokens", None
)
if (
model_info.params.get("top_k", None)
and payload["options"].get("top_k") is None
):
payload["options"]["top_k"] = model_info.params.get("top_k", None)
if (
model_info.params.get("top_p", None)
and payload["options"].get("top_p") is None
):
payload["options"]["top_p"] = model_info.params.get("top_p", None)
if (
model_info.params.get("min_p", None)
and payload["options"].get("min_p") is None
):
payload["options"]["min_p"] = model_info.params.get("min_p", None)
if (
model_info.params.get("use_mmap", None)
and payload["options"].get("use_mmap") is None
):
payload["options"]["use_mmap"] = model_info.params.get("use_mmap", None)
if (
model_info.params.get("use_mlock", None)
and payload["options"].get("use_mlock") is None
):
payload["options"]["use_mlock"] = model_info.params.get(
"use_mlock", None
)
if (
model_info.params.get("num_thread", None)
and payload["options"].get("num_thread") is None
):
payload["options"]["num_thread"] = model_info.params.get(
"num_thread", None
)
system = model_info.params.get("system", None)
if system:
system = prompt_template(
system,
**(
{
"user_name": user.name,
"user_location": (
user.info.get("location") if user.info else None
),
}
if user
else {}
),
payload["options"] = apply_model_params_to_body_ollama(
params, payload["options"]
)
payload = apply_model_system_prompt_to_body(params, payload, user)
if payload.get("messages"):
payload["messages"] = add_or_update_system_message(
system, payload["messages"]
)
if ":" not in payload["model"]:
payload["model"] = f"{payload['model']}:latest"
if url_idx == None:
if ":" not in payload["model"]:
payload["model"] = f"{payload['model']}:latest"
if payload["model"] in app.state.MODELS:
url_idx = random.choice(app.state.MODELS[payload["model"]]["urls"])
else:
raise HTTPException(
status_code=400,
detail=ERROR_MESSAGES.MODEL_NOT_FOUND(form_data.model),
)
url = app.state.config.OLLAMA_BASE_URLS[url_idx]
url = get_ollama_url(url_idx, payload["model"])
log.info(f"url: {url}")
log.debug(payload)
return await post_streaming_url(f"{url}/api/chat", json.dumps(payload))
return await post_streaming_url(
f"{url}/api/chat", json.dumps(payload), content_type="application/x-ndjson"
)
# TODO: we should update this part once Ollama supports other types
@@ -940,7 +794,7 @@ class OpenAIChatMessage(BaseModel):
class OpenAIChatCompletionForm(BaseModel):
model: str
messages: List[OpenAIChatMessage]
messages: list[OpenAIChatMessage]
model_config = ConfigDict(extra="allow")
@@ -952,83 +806,36 @@ async def generate_openai_chat_completion(
url_idx: Optional[int] = None,
user=Depends(get_verified_user),
):
form_data = OpenAIChatCompletionForm(**form_data)
payload = {**form_data.model_dump(exclude_none=True, exclude=["metadata"])}
completion_form = OpenAIChatCompletionForm(**form_data)
payload = {**completion_form.model_dump(exclude_none=True, exclude=["metadata"])}
if "metadata" in payload:
del payload["metadata"]
model_id = form_data.model
model_id = completion_form.model
if app.state.config.ENABLE_MODEL_FILTER:
if user.role == "user" and model_id not in app.state.config.MODEL_FILTER_LIST:
raise HTTPException(
status_code=403,
detail="Model not found",
)
model_info = Models.get_model_by_id(model_id)
if model_info:
if model_info.base_model_id:
payload["model"] = model_info.base_model_id
model_info.params = model_info.params.model_dump()
params = model_info.params.model_dump()
if model_info.params:
payload["temperature"] = model_info.params.get("temperature", None)
payload["top_p"] = model_info.params.get("top_p", None)
payload["max_tokens"] = model_info.params.get("max_tokens", None)
payload["frequency_penalty"] = model_info.params.get(
"frequency_penalty", None
)
payload["seed"] = model_info.params.get("seed", None)
payload["stop"] = (
[
bytes(stop, "utf-8").decode("unicode_escape")
for stop in model_info.params["stop"]
]
if model_info.params.get("stop", None)
else None
)
if params:
payload = apply_model_params_to_body_openai(params, payload)
payload = apply_model_system_prompt_to_body(params, payload, user)
system = model_info.params.get("system", None)
if ":" not in payload["model"]:
payload["model"] = f"{payload['model']}:latest"
if system:
system = prompt_template(
system,
**(
{
"user_name": user.name,
"user_location": (
user.info.get("location") if user.info else None
),
}
if user
else {}
),
)
# Check if the payload already has a system message
# If not, add a system message to the payload
if payload.get("messages"):
for message in payload["messages"]:
if message.get("role") == "system":
message["content"] = system + message["content"]
break
else:
payload["messages"].insert(
0,
{
"role": "system",
"content": system,
},
)
if url_idx == None:
if ":" not in payload["model"]:
payload["model"] = f"{payload['model']}:latest"
if payload["model"] in app.state.MODELS:
url_idx = random.choice(app.state.MODELS[payload["model"]]["urls"])
else:
raise HTTPException(
status_code=400,
detail=ERROR_MESSAGES.MODEL_NOT_FOUND(form_data.model),
)
url = app.state.config.OLLAMA_BASE_URLS[url_idx]
url = get_ollama_url(url_idx, payload["model"])
log.info(f"url: {url}")
return await post_streaming_url(
@@ -1044,7 +851,7 @@ async def get_openai_models(
url_idx: Optional[int] = None,
user=Depends(get_verified_user),
):
if url_idx == None:
if url_idx is None:
models = await get_all_models()
if app.state.config.ENABLE_MODEL_FILTER:
@@ -1099,7 +906,7 @@ async def get_openai_models(
res = r.json()
if "error" in res:
error_detail = f"Ollama: {res['error']}"
except:
except Exception:
error_detail = f"Ollama: {e}"
raise HTTPException(
@@ -1125,7 +932,6 @@ def parse_huggingface_url(hf_url):
path_components = parsed_url.path.split("/")
# Extract the desired output
user_repo = "/".join(path_components[1:3])
model_file = path_components[-1]
return model_file
@@ -1190,7 +996,6 @@ async def download_model(
url_idx: Optional[int] = None,
user=Depends(get_admin_user),
):
allowed_hosts = ["https://huggingface.co/", "https://github.com/"]
if not any(form_data.url.startswith(host) for host in allowed_hosts):
@@ -1199,7 +1004,7 @@ async def download_model(
detail="Invalid file_url. Only URLs from allowed hosts are permitted.",
)
if url_idx == None:
if url_idx is None:
url_idx = 0
url = app.state.config.OLLAMA_BASE_URLS[url_idx]
@@ -1222,7 +1027,7 @@ def upload_model(
url_idx: Optional[int] = None,
user=Depends(get_admin_user),
):
if url_idx == None:
if url_idx is None:
url_idx = 0
ollama_url = app.state.config.OLLAMA_BASE_URLS[url_idx]
+13 -7
View File
@@ -17,7 +17,10 @@ from utils.utils import (
get_verified_user,
get_admin_user,
)
from utils.misc import apply_model_params_to_body, apply_model_system_prompt_to_body
from utils.misc import (
apply_model_params_to_body_openai,
apply_model_system_prompt_to_body,
)
from config import (
SRC_LOG_LEVELS,
@@ -29,8 +32,9 @@ from config import (
ENABLE_MODEL_FILTER,
MODEL_FILTER_LIST,
AppConfig,
CORS_ALLOW_ORIGIN,
)
from typing import List, Optional, Literal, overload
from typing import Optional, Literal, overload
import hashlib
@@ -42,7 +46,7 @@ log.setLevel(SRC_LOG_LEVELS["OPENAI"])
app = FastAPI()
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_origins=CORS_ALLOW_ORIGIN,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
@@ -86,11 +90,11 @@ async def update_config(form_data: OpenAIConfigForm, user=Depends(get_admin_user
class UrlsUpdateForm(BaseModel):
urls: List[str]
urls: list[str]
class KeysUpdateForm(BaseModel):
keys: List[str]
keys: list[str]
@app.get("/urls")
@@ -356,7 +360,9 @@ async def generate_chat_completion(
):
idx = 0
payload = {**form_data}
payload.pop("metadata")
if "metadata" in payload:
del payload["metadata"]
model_id = form_data.get("model")
model_info = Models.get_model_by_id(model_id)
@@ -366,7 +372,7 @@ async def generate_chat_completion(
payload["model"] = model_info.base_model_id
params = model_info.params.model_dump()
payload = apply_model_params_to_body(params, payload)
payload = apply_model_params_to_body_openai(params, payload)
payload = apply_model_system_prompt_to_body(params, payload, user)
model = app.state.MODELS[payload.get("model")]
+37 -16
View File
@@ -13,7 +13,7 @@ import os, shutil, logging, re
from datetime import datetime
from pathlib import Path
from typing import List, Union, Sequence, Iterator, Any
from typing import Union, Sequence, Iterator, Any
from chromadb.utils.batch_utils import create_batches
from langchain_core.documents import Document
@@ -95,6 +95,8 @@ from config import (
TIKA_SERVER_URL,
RAG_TOP_K,
RAG_RELEVANCE_THRESHOLD,
RAG_FILE_MAX_SIZE,
RAG_FILE_MAX_COUNT,
RAG_EMBEDDING_ENGINE,
RAG_EMBEDDING_MODEL,
RAG_EMBEDDING_MODEL_AUTO_UPDATE,
@@ -129,6 +131,7 @@ from config import (
RAG_WEB_SEARCH_RESULT_COUNT,
RAG_WEB_SEARCH_CONCURRENT_REQUESTS,
RAG_EMBEDDING_OPENAI_BATCH_SIZE,
CORS_ALLOW_ORIGIN,
)
from constants import ERROR_MESSAGES
@@ -142,6 +145,8 @@ app.state.config = AppConfig()
app.state.config.TOP_K = RAG_TOP_K
app.state.config.RELEVANCE_THRESHOLD = RAG_RELEVANCE_THRESHOLD
app.state.config.FILE_MAX_SIZE = RAG_FILE_MAX_SIZE
app.state.config.FILE_MAX_COUNT = RAG_FILE_MAX_COUNT
app.state.config.ENABLE_RAG_HYBRID_SEARCH = ENABLE_RAG_HYBRID_SEARCH
app.state.config.ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION = (
@@ -240,12 +245,9 @@ app.state.EMBEDDING_FUNCTION = get_embedding_function(
app.state.config.RAG_EMBEDDING_OPENAI_BATCH_SIZE,
)
origins = ["*"]
app.add_middleware(
CORSMiddleware,
allow_origins=origins,
allow_origins=CORS_ALLOW_ORIGIN,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
@@ -376,7 +378,7 @@ async def update_reranking_config(
try:
app.state.config.RAG_RERANKING_MODEL = form_data.reranking_model
update_reranking_model(app.state.config.RAG_RERANKING_MODEL), True
update_reranking_model(app.state.config.RAG_RERANKING_MODEL, True)
return {
"status": True,
@@ -395,6 +397,10 @@ async def get_rag_config(user=Depends(get_admin_user)):
return {
"status": True,
"pdf_extract_images": app.state.config.PDF_EXTRACT_IMAGES,
"file": {
"max_size": app.state.config.FILE_MAX_SIZE,
"max_count": app.state.config.FILE_MAX_COUNT,
},
"content_extraction": {
"engine": app.state.config.CONTENT_EXTRACTION_ENGINE,
"tika_server_url": app.state.config.TIKA_SERVER_URL,
@@ -428,6 +434,11 @@ async def get_rag_config(user=Depends(get_admin_user)):
}
class FileConfig(BaseModel):
max_size: Optional[int] = None
max_count: Optional[int] = None
class ContentExtractionConfig(BaseModel):
engine: str = ""
tika_server_url: Optional[str] = None
@@ -439,7 +450,7 @@ class ChunkParamUpdateForm(BaseModel):
class YoutubeLoaderConfig(BaseModel):
language: List[str]
language: list[str]
translation: Optional[str] = None
@@ -466,6 +477,7 @@ class WebConfig(BaseModel):
class ConfigUpdateForm(BaseModel):
pdf_extract_images: Optional[bool] = None
file: Optional[FileConfig] = None
content_extraction: Optional[ContentExtractionConfig] = None
chunk: Optional[ChunkParamUpdateForm] = None
youtube: Optional[YoutubeLoaderConfig] = None
@@ -480,6 +492,10 @@ async def update_rag_config(form_data: ConfigUpdateForm, user=Depends(get_admin_
else app.state.config.PDF_EXTRACT_IMAGES
)
if form_data.file is not None:
app.state.config.FILE_MAX_SIZE = form_data.file.max_size
app.state.config.FILE_MAX_COUNT = form_data.file.max_count
if form_data.content_extraction is not None:
log.info(f"Updating text settings: {form_data.content_extraction}")
app.state.config.CONTENT_EXTRACTION_ENGINE = form_data.content_extraction.engine
@@ -521,6 +537,10 @@ async def update_rag_config(form_data: ConfigUpdateForm, user=Depends(get_admin_
return {
"status": True,
"pdf_extract_images": app.state.config.PDF_EXTRACT_IMAGES,
"file": {
"max_size": app.state.config.FILE_MAX_SIZE,
"max_count": app.state.config.FILE_MAX_COUNT,
},
"content_extraction": {
"engine": app.state.config.CONTENT_EXTRACTION_ENGINE,
"tika_server_url": app.state.config.TIKA_SERVER_URL,
@@ -592,6 +612,7 @@ async def update_query_settings(
app.state.config.ENABLE_RAG_HYBRID_SEARCH = (
form_data.hybrid if form_data.hybrid else False
)
return {
"status": True,
"template": app.state.config.RAG_TEMPLATE,
@@ -642,7 +663,7 @@ def query_doc_handler(
class QueryCollectionsForm(BaseModel):
collection_names: List[str]
collection_names: list[str]
query: str
k: Optional[int] = None
r: Optional[float] = None
@@ -1021,7 +1042,7 @@ class TikaLoader:
self.file_path = file_path
self.mime_type = mime_type
def load(self) -> List[Document]:
def load(self) -> list[Document]:
with open(self.file_path, "rb") as f:
data = f.read()
@@ -1185,7 +1206,7 @@ def store_doc(
f.close()
f = open(file_path, "rb")
if collection_name == None:
if collection_name is None:
collection_name = calculate_sha256(f)[:63]
f.close()
@@ -1238,7 +1259,7 @@ def process_doc(
f = open(file_path, "rb")
collection_name = form_data.collection_name
if collection_name == None:
if collection_name is None:
collection_name = calculate_sha256(f)[:63]
f.close()
@@ -1296,7 +1317,7 @@ def store_text(
):
collection_name = form_data.collection_name
if collection_name == None:
if collection_name is None:
collection_name = calculate_sha256_string(form_data.content)
result = store_text_in_vector_db(
@@ -1339,7 +1360,7 @@ def scan_docs_dir(user=Depends(get_admin_user)):
sanitized_filename = sanitize_filename(filename)
doc = Documents.get_doc_by_name(sanitized_filename)
if doc == None:
if doc is None:
doc = Documents.insert_new_doc(
user.id,
DocumentForm(
@@ -1375,12 +1396,12 @@ def scan_docs_dir(user=Depends(get_admin_user)):
return True
@app.get("/reset/db")
@app.post("/reset/db")
def reset_vector_db(user=Depends(get_admin_user)):
CHROMA_CLIENT.reset()
@app.get("/reset/uploads")
@app.post("/reset/uploads")
def reset_upload_dir(user=Depends(get_admin_user)) -> bool:
folder = f"{UPLOAD_DIR}"
try:
@@ -1404,7 +1425,7 @@ def reset_upload_dir(user=Depends(get_admin_user)) -> bool:
return True
@app.get("/reset")
@app.post("/reset")
def reset(user=Depends(get_admin_user)) -> bool:
folder = f"{UPLOAD_DIR}"
for filename in os.listdir(folder):
+2 -2
View File
@@ -1,5 +1,5 @@
import logging
from typing import List, Optional
from typing import Optional
import requests
from apps.rag.search.main import SearchResult, get_filtered_results
@@ -10,7 +10,7 @@ log.setLevel(SRC_LOG_LEVELS["RAG"])
def search_brave(
api_key: str, query: str, count: int, filter_list: Optional[List[str]] = None
api_key: str, query: str, count: int, filter_list: Optional[list[str]] = None
) -> list[SearchResult]:
"""Search using Brave's Search API and return the results as a list of SearchResult objects.
+3 -3
View File
@@ -1,5 +1,5 @@
import logging
from typing import List, Optional
from typing import Optional
from apps.rag.search.main import SearchResult, get_filtered_results
from duckduckgo_search import DDGS
from config import SRC_LOG_LEVELS
@@ -9,7 +9,7 @@ log.setLevel(SRC_LOG_LEVELS["RAG"])
def search_duckduckgo(
query: str, count: int, filter_list: Optional[List[str]] = None
query: str, count: int, filter_list: Optional[list[str]] = None
) -> list[SearchResult]:
"""
Search using DuckDuckGo's Search API and return the results as a list of SearchResult objects.
@@ -18,7 +18,7 @@ def search_duckduckgo(
count (int): The number of results to return
Returns:
List[SearchResult]: A list of search results
list[SearchResult]: A list of search results
"""
# Use the DDGS context manager to create a DDGS object
with DDGS() as ddgs:
+2 -2
View File
@@ -1,6 +1,6 @@
import json
import logging
from typing import List, Optional
from typing import Optional
import requests
from apps.rag.search.main import SearchResult, get_filtered_results
@@ -15,7 +15,7 @@ def search_google_pse(
search_engine_id: str,
query: str,
count: int,
filter_list: Optional[List[str]] = None,
filter_list: Optional[list[str]] = None,
) -> list[SearchResult]:
"""Search using Google's Programmable Search Engine API and return the results as a list of SearchResult objects.
+1 -1
View File
@@ -17,7 +17,7 @@ def search_jina(query: str, count: int) -> list[SearchResult]:
count (int): The number of results to return
Returns:
List[SearchResult]: A list of search results
list[SearchResult]: A list of search results
"""
jina_search_endpoint = "https://s.jina.ai/"
headers = {
+5 -5
View File
@@ -1,7 +1,7 @@
import logging
import requests
from typing import List, Optional
from typing import Optional
from apps.rag.search.main import SearchResult, get_filtered_results
from config import SRC_LOG_LEVELS
@@ -14,9 +14,9 @@ def search_searxng(
query_url: str,
query: str,
count: int,
filter_list: Optional[List[str]] = None,
filter_list: Optional[list[str]] = None,
**kwargs,
) -> List[SearchResult]:
) -> list[SearchResult]:
"""
Search a SearXNG instance for a given query and return the results as a list of SearchResult objects.
@@ -31,10 +31,10 @@ def search_searxng(
language (str): Language filter for the search results; e.g., "en-US". Defaults to an empty string.
safesearch (int): Safe search filter for safer web results; 0 = off, 1 = moderate, 2 = strict. Defaults to 1 (moderate).
time_range (str): Time range for filtering results by date; e.g., "2023-04-05..today" or "all-time". Defaults to ''.
categories: (Optional[List[str]]): Specific categories within which the search should be performed, defaulting to an empty string if not provided.
categories: (Optional[list[str]]): Specific categories within which the search should be performed, defaulting to an empty string if not provided.
Returns:
List[SearchResult]: A list of SearchResults sorted by relevance score in descending order.
list[SearchResult]: A list of SearchResults sorted by relevance score in descending order.
Raise:
requests.exceptions.RequestException: If a request error occurs during the search process.
+2 -2
View File
@@ -1,6 +1,6 @@
import json
import logging
from typing import List, Optional
from typing import Optional
import requests
from apps.rag.search.main import SearchResult, get_filtered_results
@@ -11,7 +11,7 @@ log.setLevel(SRC_LOG_LEVELS["RAG"])
def search_serper(
api_key: str, query: str, count: int, filter_list: Optional[List[str]] = None
api_key: str, query: str, count: int, filter_list: Optional[list[str]] = None
) -> list[SearchResult]:
"""Search using serper.dev's API and return the results as a list of SearchResult objects.
+2 -2
View File
@@ -1,6 +1,6 @@
import json
import logging
from typing import List, Optional
from typing import Optional
import requests
from urllib.parse import urlencode
@@ -19,7 +19,7 @@ def search_serply(
limit: int = 10,
device_type: str = "desktop",
proxy_location: str = "US",
filter_list: Optional[List[str]] = None,
filter_list: Optional[list[str]] = None,
) -> list[SearchResult]:
"""Search using serper.dev's API and return the results as a list of SearchResult objects.
+2 -2
View File
@@ -1,6 +1,6 @@
import json
import logging
from typing import List, Optional
from typing import Optional
import requests
from apps.rag.search.main import SearchResult, get_filtered_results
@@ -14,7 +14,7 @@ def search_serpstack(
api_key: str,
query: str,
count: int,
filter_list: Optional[List[str]] = None,
filter_list: Optional[list[str]] = None,
https_enabled: bool = True,
) -> list[SearchResult]:
"""Search using serpstack.com's and return the results as a list of SearchResult objects.
+1 -1
View File
@@ -17,7 +17,7 @@ def search_tavily(api_key: str, query: str, count: int) -> list[SearchResult]:
query (str): The query to search for
Returns:
List[SearchResult]: A list of search results
list[SearchResult]: A list of search results
"""
url = "https://api.tavily.com/search"
data = {"query": query, "api_key": api_key}
+19 -15
View File
@@ -2,7 +2,7 @@ import os
import logging
import requests
from typing import List, Union
from typing import Union
from apps.ollama.main import (
generate_ollama_embeddings,
@@ -142,28 +142,32 @@ def merge_and_sort_query_results(query_results, k, reverse=False):
def query_collection(
collection_names: List[str],
collection_names: list[str],
query: str,
embedding_function,
k: int,
):
results = []
for collection_name in collection_names:
try:
result = query_doc(
collection_name=collection_name,
query=query,
k=k,
embedding_function=embedding_function,
)
results.append(result)
except:
if collection_name:
try:
result = query_doc(
collection_name=collection_name,
query=query,
k=k,
embedding_function=embedding_function,
)
results.append(result)
except Exception:
pass
else:
pass
return merge_and_sort_query_results(results, k=k)
def query_collection_with_hybrid_search(
collection_names: List[str],
collection_names: list[str],
query: str,
embedding_function,
k: int,
@@ -182,7 +186,7 @@ def query_collection_with_hybrid_search(
r=r,
)
results.append(result)
except:
except Exception:
pass
return merge_and_sort_query_results(results, k=k, reverse=True)
@@ -257,7 +261,7 @@ def get_rag_context(
collection_names = (
file["collection_names"]
if file["type"] == "collection"
else [file["collection_name"]]
else [file["collection_name"]] if file["collection_name"] else []
)
collection_names = set(collection_names).difference(extracted_collections)
@@ -411,7 +415,7 @@ class ChromaRetriever(BaseRetriever):
query: str,
*,
run_manager: CallbackManagerForRetrieverRun,
) -> List[Document]:
) -> list[Document]:
query_embeddings = self.embedding_function(query)
results = self.collection.query(
+8 -21
View File
@@ -3,18 +3,19 @@ import logging
import json
from contextlib import contextmanager
from peewee_migrate import Router
from apps.webui.internal.wrappers import register_connection
from typing import Optional, Any
from typing_extensions import Self
from sqlalchemy import create_engine, types, Dialect
from sqlalchemy.sql.type_api import _T
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker, scoped_session
from sqlalchemy.sql.type_api import _T
from config import SRC_LOG_LEVELS, DATA_DIR, DATABASE_URL, BACKEND_DIR
from peewee_migrate import Router
from apps.webui.internal.wrappers import register_connection
from env import SRC_LOG_LEVELS, BACKEND_DIR, DATABASE_URL
log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["DB"])
@@ -42,34 +43,21 @@ class JSONField(types.TypeDecorator):
return json.loads(value)
# Check if the file exists
if os.path.exists(f"{DATA_DIR}/ollama.db"):
# Rename the file
os.rename(f"{DATA_DIR}/ollama.db", f"{DATA_DIR}/webui.db")
log.info("Database migrated from Ollama-WebUI successfully.")
else:
pass
# Workaround to handle the peewee migration
# This is required to ensure the peewee migration is handled before the alembic migration
def handle_peewee_migration(DATABASE_URL):
# db = None
try:
# Replace the postgresql:// with postgres:// and %40 with @ in the DATABASE_URL
db = register_connection(
DATABASE_URL.replace("postgresql://", "postgres://").replace("%40", "@")
)
# Replace the postgresql:// with postgres:// to handle the peewee migration
db = register_connection(DATABASE_URL.replace("postgresql://", "postgres://"))
migrate_dir = BACKEND_DIR / "apps" / "webui" / "internal" / "migrations"
router = Router(db, logger=log, migrate_dir=migrate_dir)
router.run()
db.close()
# check if db connection has been closed
except Exception as e:
log.error(f"Failed to initialize the database connection: {e}")
raise
finally:
# Properly closing the database connection
if db and not db.is_closed():
@@ -98,7 +86,6 @@ Base = declarative_base()
Session = scoped_session(SessionLocal)
# Dependency
def get_session():
db = SessionLocal()
try:
+4 -10
View File
@@ -6,7 +6,7 @@ import logging
from playhouse.db_url import connect, parse
from playhouse.shortcuts import ReconnectMixin
from config import SRC_LOG_LEVELS
from env import SRC_LOG_LEVELS
log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["DB"])
@@ -43,7 +43,7 @@ class ReconnectingPostgresqlDatabase(CustomReconnectMixin, PostgresqlDatabase):
def register_connection(db_url):
db = connect(db_url)
db = connect(db_url, unquote_password=True)
if isinstance(db, PostgresqlDatabase):
# Enable autoconnect for SQLite databases, managed by Peewee
db.autoconnect = True
@@ -51,16 +51,10 @@ def register_connection(db_url):
log.info("Connected to PostgreSQL database")
# Get the connection details
connection = parse(db_url)
connection = parse(db_url, unquote_password=True)
# Use our custom database class that supports reconnection
db = ReconnectingPostgresqlDatabase(
connection["database"],
user=connection["user"],
password=connection["password"],
host=connection["host"],
port=connection["port"],
)
db = ReconnectingPostgresqlDatabase(**connection)
db.connect(reuse_if_open=True)
elif isinstance(db, SqliteDatabase):
# Enable autoconnect for SQLite databases, managed by Peewee
+57 -35
View File
@@ -22,10 +22,11 @@ from apps.webui.utils import load_function_module_by_id
from utils.misc import (
openai_chat_chunk_message_template,
openai_chat_completion_message_template,
apply_model_params_to_body,
apply_model_params_to_body_openai,
apply_model_system_prompt_to_body,
)
from utils.tools import get_tools
from config import (
SHOW_ADMIN_DETAILS,
@@ -43,22 +44,26 @@ from config import (
JWT_EXPIRES_IN,
WEBUI_BANNERS,
ENABLE_COMMUNITY_SHARING,
ENABLE_MESSAGE_RATING,
AppConfig,
OAUTH_USERNAME_CLAIM,
OAUTH_PICTURE_CLAIM,
OAUTH_EMAIL_CLAIM,
CORS_ALLOW_ORIGIN,
)
from apps.socket.main import get_event_call, get_event_emitter
import inspect
import json
import logging
from typing import Iterator, Generator, AsyncGenerator
from pydantic import BaseModel
app = FastAPI()
origins = ["*"]
log = logging.getLogger(__name__)
app.state.config = AppConfig()
@@ -81,9 +86,11 @@ app.state.config.WEBHOOK_URL = WEBHOOK_URL
app.state.config.BANNERS = WEBUI_BANNERS
app.state.config.ENABLE_COMMUNITY_SHARING = ENABLE_COMMUNITY_SHARING
app.state.config.ENABLE_MESSAGE_RATING = ENABLE_MESSAGE_RATING
app.state.config.OAUTH_USERNAME_CLAIM = OAUTH_USERNAME_CLAIM
app.state.config.OAUTH_PICTURE_CLAIM = OAUTH_PICTURE_CLAIM
app.state.config.OAUTH_EMAIL_CLAIM = OAUTH_EMAIL_CLAIM
app.state.MODELS = {}
app.state.TOOLS = {}
@@ -91,7 +98,7 @@ app.state.FUNCTIONS = {}
app.add_middleware(
CORSMiddleware,
allow_origins=origins,
allow_origins=CORS_ALLOW_ORIGIN,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
@@ -239,40 +246,40 @@ def get_pipe_id(form_data: dict) -> str:
return pipe_id
def get_function_params(function_module, form_data, user, extra_params={}):
def get_function_params(function_module, form_data, user, extra_params=None):
if extra_params is None:
extra_params = {}
pipe_id = get_pipe_id(form_data)
# Get the signature of the function
sig = inspect.signature(function_module.pipe)
params = {"body": form_data}
for key, value in extra_params.items():
if key in sig.parameters:
params[key] = value
if "__user__" in sig.parameters:
__user__ = {
"id": user.id,
"email": user.email,
"name": user.name,
"role": user.role,
}
params = {"body": form_data} | {
k: v for k, v in extra_params.items() if k in sig.parameters
}
if "__user__" in params and hasattr(function_module, "UserValves"):
user_valves = Functions.get_user_valves_by_id_and_user_id(pipe_id, user.id)
try:
if hasattr(function_module, "UserValves"):
__user__["valves"] = function_module.UserValves(
**Functions.get_user_valves_by_id_and_user_id(pipe_id, user.id)
)
params["__user__"]["valves"] = function_module.UserValves(**user_valves)
except Exception as e:
print(e)
log.exception(e)
params["__user__"]["valves"] = function_module.UserValves()
params["__user__"] = __user__
return params
async def generate_function_chat_completion(form_data, user):
model_id = form_data.get("model")
model_info = Models.get_model_by_id(model_id)
metadata = form_data.pop("metadata", None)
metadata = form_data.pop("metadata", {})
files = metadata.get("files", [])
tool_ids = metadata.get("tool_ids", [])
# Check if tool_ids is None
if tool_ids is None:
tool_ids = []
__event_emitter__ = None
__event_call__ = None
@@ -284,28 +291,43 @@ async def generate_function_chat_completion(form_data, user):
__event_call__ = get_event_call(metadata)
__task__ = metadata.get("task", None)
extra_params = {
"__event_emitter__": __event_emitter__,
"__event_call__": __event_call__,
"__task__": __task__,
"__files__": files,
"__user__": {
"id": user.id,
"email": user.email,
"name": user.name,
"role": user.role,
},
}
extra_params["__tools__"] = get_tools(
app,
tool_ids,
user,
{
**extra_params,
"__model__": app.state.MODELS[form_data["model"]],
"__messages__": form_data["messages"],
"__files__": files,
},
)
if model_info:
if model_info.base_model_id:
form_data["model"] = model_info.base_model_id
params = model_info.params.model_dump()
form_data = apply_model_params_to_body(params, form_data)
form_data = apply_model_params_to_body_openai(params, form_data)
form_data = apply_model_system_prompt_to_body(params, form_data, user)
pipe_id = get_pipe_id(form_data)
function_module = get_function_module(pipe_id)
pipe = function_module.pipe
params = get_function_params(
function_module,
form_data,
user,
{
"__event_emitter__": __event_emitter__,
"__event_call__": __event_call__,
"__task__": __task__,
},
)
params = get_function_params(function_module, form_data, user, extra_params)
if form_data["stream"]:
+8 -8
View File
@@ -4,12 +4,12 @@ import uuid
import logging
from sqlalchemy import String, Column, Boolean, Text
from apps.webui.models.users import UserModel, Users
from utils.utils import verify_password
from apps.webui.models.users import UserModel, Users
from apps.webui.internal.db import Base, get_db
from config import SRC_LOG_LEVELS
from env import SRC_LOG_LEVELS
log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["MODELS"])
@@ -140,7 +140,7 @@ class AuthsTable:
return None
else:
return None
except:
except Exception:
return None
def authenticate_user_by_api_key(self, api_key: str) -> Optional[UserModel]:
@@ -152,7 +152,7 @@ class AuthsTable:
try:
user = Users.get_user_by_api_key(api_key)
return user if user else None
except:
except Exception:
return False
def authenticate_user_by_trusted_header(self, email: str) -> Optional[UserModel]:
@@ -163,7 +163,7 @@ class AuthsTable:
if auth:
user = Users.get_user_by_id(auth.id)
return user
except:
except Exception:
return None
def update_user_password_by_id(self, id: str, new_password: str) -> bool:
@@ -174,7 +174,7 @@ class AuthsTable:
)
db.commit()
return True if result == 1 else False
except:
except Exception:
return False
def update_email_by_id(self, id: str, email: str) -> bool:
@@ -183,7 +183,7 @@ class AuthsTable:
result = db.query(Auth).filter_by(id=id).update({"email": email})
db.commit()
return True if result == 1 else False
except:
except Exception:
return False
def delete_auth_by_id(self, id: str) -> bool:
@@ -200,7 +200,7 @@ class AuthsTable:
return True
else:
return False
except:
except Exception:
return False
+32 -29
View File
@@ -1,5 +1,5 @@
from pydantic import BaseModel, ConfigDict
from typing import List, Union, Optional
from typing import Union, Optional
import json
import uuid
@@ -164,7 +164,7 @@ class ChatTable:
db.refresh(chat)
return self.get_chat_by_id(chat.share_id)
except:
except Exception:
return None
def delete_shared_chat_by_chat_id(self, chat_id: str) -> bool:
@@ -175,7 +175,7 @@ class ChatTable:
db.commit()
return True
except:
except Exception:
return False
def update_chat_share_id_by_id(
@@ -189,7 +189,7 @@ class ChatTable:
db.commit()
db.refresh(chat)
return ChatModel.model_validate(chat)
except:
except Exception:
return None
def toggle_chat_archive_by_id(self, id: str) -> Optional[ChatModel]:
@@ -201,7 +201,7 @@ class ChatTable:
db.commit()
db.refresh(chat)
return ChatModel.model_validate(chat)
except:
except Exception:
return None
def archive_all_chats_by_user_id(self, user_id: str) -> bool:
@@ -210,12 +210,12 @@ class ChatTable:
db.query(Chat).filter_by(user_id=user_id).update({"archived": True})
db.commit()
return True
except:
except Exception:
return False
def get_archived_chat_list_by_user_id(
self, user_id: str, skip: int = 0, limit: int = 50
) -> List[ChatModel]:
) -> list[ChatModel]:
with get_db() as db:
all_chats = (
@@ -233,7 +233,7 @@ class ChatTable:
include_archived: bool = False,
skip: int = 0,
limit: int = 50,
) -> List[ChatModel]:
) -> list[ChatModel]:
with get_db() as db:
query = db.query(Chat).filter_by(user_id=user_id)
if not include_archived:
@@ -249,22 +249,25 @@ class ChatTable:
self,
user_id: str,
include_archived: bool = False,
skip: int = 0,
limit: int = -1,
) -> List[ChatTitleIdResponse]:
skip: Optional[int] = None,
limit: Optional[int] = None,
) -> list[ChatTitleIdResponse]:
with get_db() as db:
query = db.query(Chat).filter_by(user_id=user_id)
if not include_archived:
query = query.filter_by(archived=False)
all_chats = (
query.order_by(Chat.updated_at.desc())
# limit cols
.with_entities(Chat.id, Chat.title, Chat.updated_at, Chat.created_at)
.limit(limit)
.offset(skip)
.all()
query = query.order_by(Chat.updated_at.desc()).with_entities(
Chat.id, Chat.title, Chat.updated_at, Chat.created_at
)
if limit:
query = query.limit(limit)
if skip:
query = query.offset(skip)
all_chats = query.all()
# result has to be destrctured from sqlalchemy `row` and mapped to a dict since the `ChatModel`is not the returned dataclass.
return [
ChatTitleIdResponse.model_validate(
@@ -279,8 +282,8 @@ class ChatTable:
]
def get_chat_list_by_chat_ids(
self, chat_ids: List[str], skip: int = 0, limit: int = 50
) -> List[ChatModel]:
self, chat_ids: list[str], skip: int = 0, limit: int = 50
) -> list[ChatModel]:
with get_db() as db:
all_chats = (
db.query(Chat)
@@ -297,7 +300,7 @@ class ChatTable:
chat = db.get(Chat, id)
return ChatModel.model_validate(chat)
except:
except Exception:
return None
def get_chat_by_share_id(self, id: str) -> Optional[ChatModel]:
@@ -319,10 +322,10 @@ class ChatTable:
chat = db.query(Chat).filter_by(id=id, user_id=user_id).first()
return ChatModel.model_validate(chat)
except:
except Exception:
return None
def get_chats(self, skip: int = 0, limit: int = 50) -> List[ChatModel]:
def get_chats(self, skip: int = 0, limit: int = 50) -> list[ChatModel]:
with get_db() as db:
all_chats = (
@@ -332,7 +335,7 @@ class ChatTable:
)
return [ChatModel.model_validate(chat) for chat in all_chats]
def get_chats_by_user_id(self, user_id: str) -> List[ChatModel]:
def get_chats_by_user_id(self, user_id: str) -> list[ChatModel]:
with get_db() as db:
all_chats = (
@@ -342,7 +345,7 @@ class ChatTable:
)
return [ChatModel.model_validate(chat) for chat in all_chats]
def get_archived_chats_by_user_id(self, user_id: str) -> List[ChatModel]:
def get_archived_chats_by_user_id(self, user_id: str) -> list[ChatModel]:
with get_db() as db:
all_chats = (
@@ -360,7 +363,7 @@ class ChatTable:
db.commit()
return True and self.delete_shared_chat_by_chat_id(id)
except:
except Exception:
return False
def delete_chat_by_id_and_user_id(self, id: str, user_id: str) -> bool:
@@ -371,7 +374,7 @@ class ChatTable:
db.commit()
return True and self.delete_shared_chat_by_chat_id(id)
except:
except Exception:
return False
def delete_chats_by_user_id(self, user_id: str) -> bool:
@@ -385,7 +388,7 @@ class ChatTable:
db.commit()
return True
except:
except Exception:
return False
def delete_shared_chats_by_user_id(self, user_id: str) -> bool:
@@ -400,7 +403,7 @@ class ChatTable:
db.commit()
return True
except:
except Exception:
return False
+6 -6
View File
@@ -1,5 +1,5 @@
from pydantic import BaseModel, ConfigDict
from typing import List, Optional
from typing import Optional
import time
import logging
@@ -9,7 +9,7 @@ from apps.webui.internal.db import Base, get_db
import json
from config import SRC_LOG_LEVELS
from env import SRC_LOG_LEVELS
log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["MODELS"])
@@ -93,7 +93,7 @@ class DocumentsTable:
return DocumentModel.model_validate(result)
else:
return None
except:
except Exception:
return None
def get_doc_by_name(self, name: str) -> Optional[DocumentModel]:
@@ -102,10 +102,10 @@ class DocumentsTable:
document = db.query(Document).filter_by(name=name).first()
return DocumentModel.model_validate(document) if document else None
except:
except Exception:
return None
def get_docs(self) -> List[DocumentModel]:
def get_docs(self) -> list[DocumentModel]:
with get_db() as db:
return [
@@ -160,7 +160,7 @@ class DocumentsTable:
db.query(Document).filter_by(name=name).delete()
db.commit()
return True
except:
except Exception:
return False
+13 -6
View File
@@ -1,5 +1,5 @@
from pydantic import BaseModel, ConfigDict
from typing import List, Union, Optional
from typing import Union, Optional
import time
import logging
@@ -9,7 +9,7 @@ from apps.webui.internal.db import JSONField, Base, get_db
import json
from config import SRC_LOG_LEVELS
from env import SRC_LOG_LEVELS
log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["MODELS"])
@@ -90,14 +90,21 @@ class FilesTable:
try:
file = db.get(File, id)
return FileModel.model_validate(file)
except:
except Exception:
return None
def get_files(self) -> List[FileModel]:
def get_files(self) -> list[FileModel]:
with get_db() as db:
return [FileModel.model_validate(file) for file in db.query(File).all()]
def get_files_by_user_id(self, user_id: str) -> list[FileModel]:
with get_db() as db:
return [
FileModel.model_validate(file)
for file in db.query(File).filter_by(user_id=user_id).all()
]
def delete_file_by_id(self, id: str) -> bool:
with get_db() as db:
@@ -107,7 +114,7 @@ class FilesTable:
db.commit()
return True
except:
except Exception:
return False
def delete_all_files(self) -> bool:
@@ -119,7 +126,7 @@ class FilesTable:
db.commit()
return True
except:
except Exception:
return False
+11 -11
View File
@@ -1,5 +1,5 @@
from pydantic import BaseModel, ConfigDict
from typing import List, Union, Optional
from typing import Union, Optional
import time
import logging
@@ -12,7 +12,7 @@ import json
import copy
from config import SRC_LOG_LEVELS
from env import SRC_LOG_LEVELS
log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["MODELS"])
@@ -122,10 +122,10 @@ class FunctionsTable:
function = db.get(Function, id)
return FunctionModel.model_validate(function)
except:
except Exception:
return None
def get_functions(self, active_only=False) -> List[FunctionModel]:
def get_functions(self, active_only=False) -> list[FunctionModel]:
with get_db() as db:
if active_only:
@@ -141,7 +141,7 @@ class FunctionsTable:
def get_functions_by_type(
self, type: str, active_only=False
) -> List[FunctionModel]:
) -> list[FunctionModel]:
with get_db() as db:
if active_only:
@@ -157,7 +157,7 @@ class FunctionsTable:
for function in db.query(Function).filter_by(type=type).all()
]
def get_global_filter_functions(self) -> List[FunctionModel]:
def get_global_filter_functions(self) -> list[FunctionModel]:
with get_db() as db:
return [
@@ -167,7 +167,7 @@ class FunctionsTable:
.all()
]
def get_global_action_functions(self) -> List[FunctionModel]:
def get_global_action_functions(self) -> list[FunctionModel]:
with get_db() as db:
return [
FunctionModel.model_validate(function)
@@ -198,7 +198,7 @@ class FunctionsTable:
db.commit()
db.refresh(function)
return self.get_function_by_id(id)
except:
except Exception:
return None
def get_user_valves_by_id_and_user_id(
@@ -256,7 +256,7 @@ class FunctionsTable:
)
db.commit()
return self.get_function_by_id(id)
except:
except Exception:
return None
def deactivate_all_functions(self) -> Optional[bool]:
@@ -271,7 +271,7 @@ class FunctionsTable:
)
db.commit()
return True
except:
except Exception:
return None
def delete_function_by_id(self, id: str) -> bool:
@@ -281,7 +281,7 @@ class FunctionsTable:
db.commit()
return True
except:
except Exception:
return False
+10 -10
View File
@@ -1,5 +1,5 @@
from pydantic import BaseModel, ConfigDict
from typing import List, Union, Optional
from typing import Union, Optional
from sqlalchemy import Column, String, BigInteger, Text
@@ -80,25 +80,25 @@ class MemoriesTable:
)
db.commit()
return self.get_memory_by_id(id)
except:
except Exception:
return None
def get_memories(self) -> List[MemoryModel]:
def get_memories(self) -> list[MemoryModel]:
with get_db() as db:
try:
memories = db.query(Memory).all()
return [MemoryModel.model_validate(memory) for memory in memories]
except:
except Exception:
return None
def get_memories_by_user_id(self, user_id: str) -> List[MemoryModel]:
def get_memories_by_user_id(self, user_id: str) -> list[MemoryModel]:
with get_db() as db:
try:
memories = db.query(Memory).filter_by(user_id=user_id).all()
return [MemoryModel.model_validate(memory) for memory in memories]
except:
except Exception:
return None
def get_memory_by_id(self, id: str) -> Optional[MemoryModel]:
@@ -107,7 +107,7 @@ class MemoriesTable:
try:
memory = db.get(Memory, id)
return MemoryModel.model_validate(memory)
except:
except Exception:
return None
def delete_memory_by_id(self, id: str) -> bool:
@@ -119,7 +119,7 @@ class MemoriesTable:
return True
except:
except Exception:
return False
def delete_memories_by_user_id(self, user_id: str) -> bool:
@@ -130,7 +130,7 @@ class MemoriesTable:
db.commit()
return True
except:
except Exception:
return False
def delete_memory_by_id_and_user_id(self, id: str, user_id: str) -> bool:
@@ -141,7 +141,7 @@ class MemoriesTable:
db.commit()
return True
except:
except Exception:
return False
+4 -4
View File
@@ -6,7 +6,7 @@ from sqlalchemy import Column, BigInteger, Text
from apps.webui.internal.db import Base, JSONField, get_db
from config import SRC_LOG_LEVELS
from env import SRC_LOG_LEVELS
import time
@@ -137,7 +137,7 @@ class ModelsTable:
print(e)
return None
def get_all_models(self) -> List[ModelModel]:
def get_all_models(self) -> list[ModelModel]:
with get_db() as db:
return [ModelModel.model_validate(model) for model in db.query(Model).all()]
@@ -146,7 +146,7 @@ class ModelsTable:
with get_db() as db:
model = db.get(Model, id)
return ModelModel.model_validate(model)
except:
except Exception:
return None
def update_model_by_id(self, id: str, model: ModelForm) -> Optional[ModelModel]:
@@ -175,7 +175,7 @@ class ModelsTable:
db.commit()
return True
except:
except Exception:
return False
+5 -5
View File
@@ -1,5 +1,5 @@
from pydantic import BaseModel, ConfigDict
from typing import List, Optional
from typing import Optional
import time
from sqlalchemy import String, Column, BigInteger, Text
@@ -79,10 +79,10 @@ class PromptsTable:
prompt = db.query(Prompt).filter_by(command=command).first()
return PromptModel.model_validate(prompt)
except:
except Exception:
return None
def get_prompts(self) -> List[PromptModel]:
def get_prompts(self) -> list[PromptModel]:
with get_db() as db:
return [
@@ -101,7 +101,7 @@ class PromptsTable:
prompt.timestamp = int(time.time())
db.commit()
return PromptModel.model_validate(prompt)
except:
except Exception:
return None
def delete_prompt_by_command(self, command: str) -> bool:
@@ -112,7 +112,7 @@ class PromptsTable:
db.commit()
return True
except:
except Exception:
return False
+9 -9
View File
@@ -1,5 +1,5 @@
from pydantic import BaseModel, ConfigDict
from typing import List, Optional
from typing import Optional
import json
import uuid
@@ -10,7 +10,7 @@ from sqlalchemy import String, Column, BigInteger, Text
from apps.webui.internal.db import Base, get_db
from config import SRC_LOG_LEVELS
from env import SRC_LOG_LEVELS
log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["MODELS"])
@@ -69,11 +69,11 @@ class ChatIdTagForm(BaseModel):
class TagChatIdsResponse(BaseModel):
chat_ids: List[str]
chat_ids: list[str]
class ChatTagsResponse(BaseModel):
tags: List[str]
tags: list[str]
class TagTable:
@@ -109,7 +109,7 @@ class TagTable:
self, user_id: str, form_data: ChatIdTagForm
) -> Optional[ChatIdTagModel]:
tag = self.get_tag_by_name_and_user_id(form_data.tag_name, user_id)
if tag == None:
if tag is None:
tag = self.insert_new_tag(form_data.tag_name, user_id)
id = str(uuid.uuid4())
@@ -132,10 +132,10 @@ class TagTable:
return ChatIdTagModel.model_validate(result)
else:
return None
except:
except Exception:
return None
def get_tags_by_user_id(self, user_id: str) -> List[TagModel]:
def get_tags_by_user_id(self, user_id: str) -> list[TagModel]:
with get_db() as db:
tag_names = [
chat_id_tag.tag_name
@@ -159,7 +159,7 @@ class TagTable:
def get_tags_by_chat_id_and_user_id(
self, chat_id: str, user_id: str
) -> List[TagModel]:
) -> list[TagModel]:
with get_db() as db:
tag_names = [
@@ -184,7 +184,7 @@ class TagTable:
def get_chat_ids_by_tag_name_and_user_id(
self, tag_name: str, user_id: str
) -> List[ChatIdTagModel]:
) -> list[ChatIdTagModel]:
with get_db() as db:
return [
+9 -9
View File
@@ -1,5 +1,5 @@
from pydantic import BaseModel, ConfigDict
from typing import List, Optional
from typing import Optional
import time
import logging
from sqlalchemy import String, Column, BigInteger, Text
@@ -11,7 +11,7 @@ import json
import copy
from config import SRC_LOG_LEVELS
from env import SRC_LOG_LEVELS
log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["MODELS"])
@@ -45,7 +45,7 @@ class ToolModel(BaseModel):
user_id: str
name: str
content: str
specs: List[dict]
specs: list[dict]
meta: ToolMeta
updated_at: int # timestamp in epoch
created_at: int # timestamp in epoch
@@ -81,7 +81,7 @@ class ToolValves(BaseModel):
class ToolsTable:
def insert_new_tool(
self, user_id: str, form_data: ToolForm, specs: List[dict]
self, user_id: str, form_data: ToolForm, specs: list[dict]
) -> Optional[ToolModel]:
with get_db() as db:
@@ -115,10 +115,10 @@ class ToolsTable:
tool = db.get(Tool, id)
return ToolModel.model_validate(tool)
except:
except Exception:
return None
def get_tools(self) -> List[ToolModel]:
def get_tools(self) -> list[ToolModel]:
with get_db() as db:
return [ToolModel.model_validate(tool) for tool in db.query(Tool).all()]
@@ -141,7 +141,7 @@ class ToolsTable:
)
db.commit()
return self.get_tool_by_id(id)
except:
except Exception:
return None
def get_user_valves_by_id_and_user_id(
@@ -196,7 +196,7 @@ class ToolsTable:
tool = db.query(Tool).get(id)
db.refresh(tool)
return ToolModel.model_validate(tool)
except:
except Exception:
return None
def delete_tool_by_id(self, id: str) -> bool:
@@ -206,7 +206,7 @@ class ToolsTable:
db.commit()
return True
except:
except Exception:
return False
+14 -21
View File
@@ -1,12 +1,10 @@
from pydantic import BaseModel, ConfigDict, parse_obj_as
from typing import List, Union, Optional
from pydantic import BaseModel, ConfigDict
from typing import Optional
import time
from sqlalchemy import String, Column, BigInteger, Text
from utils.misc import get_gravatar_url
from apps.webui.internal.db import Base, JSONField, Session, get_db
from apps.webui.internal.db import Base, JSONField, get_db
from apps.webui.models.chats import Chats
####################
@@ -78,7 +76,6 @@ class UserUpdateForm(BaseModel):
class UsersTable:
def insert_new_user(
self,
id: str,
@@ -122,31 +119,28 @@ class UsersTable:
def get_user_by_api_key(self, api_key: str) -> Optional[UserModel]:
try:
with get_db() as db:
user = db.query(User).filter_by(api_key=api_key).first()
return UserModel.model_validate(user)
except:
except Exception:
return None
def get_user_by_email(self, email: str) -> Optional[UserModel]:
try:
with get_db() as db:
user = db.query(User).filter_by(email=email).first()
return UserModel.model_validate(user)
except:
except Exception:
return None
def get_user_by_oauth_sub(self, sub: str) -> Optional[UserModel]:
try:
with get_db() as db:
user = db.query(User).filter_by(oauth_sub=sub).first()
return UserModel.model_validate(user)
except:
except Exception:
return None
def get_users(self, skip: int = 0, limit: int = 50) -> List[UserModel]:
def get_users(self, skip: int = 0, limit: int = 50) -> list[UserModel]:
with get_db() as db:
users = (
db.query(User)
@@ -164,7 +158,7 @@ class UsersTable:
with get_db() as db:
user = db.query(User).order_by(User.created_at).first()
return UserModel.model_validate(user)
except:
except Exception:
return None
def update_user_role_by_id(self, id: str, role: str) -> Optional[UserModel]:
@@ -174,7 +168,7 @@ class UsersTable:
db.commit()
user = db.query(User).filter_by(id=id).first()
return UserModel.model_validate(user)
except:
except Exception:
return None
def update_user_profile_image_url_by_id(
@@ -189,13 +183,12 @@ class UsersTable:
user = db.query(User).filter_by(id=id).first()
return UserModel.model_validate(user)
except:
except Exception:
return None
def update_user_last_active_by_id(self, id: str) -> Optional[UserModel]:
try:
with get_db() as db:
db.query(User).filter_by(id=id).update(
{"last_active_at": int(time.time())}
)
@@ -203,7 +196,7 @@ class UsersTable:
user = db.query(User).filter_by(id=id).first()
return UserModel.model_validate(user)
except:
except Exception:
return None
def update_user_oauth_sub_by_id(
@@ -216,7 +209,7 @@ class UsersTable:
user = db.query(User).filter_by(id=id).first()
return UserModel.model_validate(user)
except:
except Exception:
return None
def update_user_by_id(self, id: str, updated: dict) -> Optional[UserModel]:
@@ -245,7 +238,7 @@ class UsersTable:
return True
else:
return False
except:
except Exception:
return False
def update_user_api_key_by_id(self, id: str, api_key: str) -> str:
@@ -254,7 +247,7 @@ class UsersTable:
result = db.query(User).filter_by(id=id).update({"api_key": api_key})
db.commit()
return True if result == 1 else False
except:
except Exception:
return False
def get_user_api_key_by_id(self, id: str) -> Optional[str]:
+9 -2
View File
@@ -195,7 +195,11 @@ async def signin(request: Request, response: Response, form_data: SigninForm):
@router.post("/signup", response_model=SigninResponse)
async def signup(request: Request, response: Response, form_data: SignupForm):
if not request.app.state.config.ENABLE_SIGNUP and WEBUI_AUTH:
if (
not request.app.state.config.ENABLE_SIGNUP
and request.app.state.config.ENABLE_LOGIN_FORM
and WEBUI_AUTH
):
raise HTTPException(
status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.ACCESS_PROHIBITED
)
@@ -228,7 +232,6 @@ async def signup(request: Request, response: Response, form_data: SignupForm):
data={"id": user.id},
expires_delta=parse_duration(request.app.state.config.JWT_EXPIRES_IN),
)
# response.set_cookie(key='token', value=token, httponly=True)
# Set the cookie token
response.set_cookie(
@@ -352,6 +355,7 @@ async def get_admin_config(request: Request, user=Depends(get_admin_user)):
"DEFAULT_USER_ROLE": request.app.state.config.DEFAULT_USER_ROLE,
"JWT_EXPIRES_IN": request.app.state.config.JWT_EXPIRES_IN,
"ENABLE_COMMUNITY_SHARING": request.app.state.config.ENABLE_COMMUNITY_SHARING,
"ENABLE_MESSAGE_RATING": request.app.state.config.ENABLE_MESSAGE_RATING,
}
@@ -361,6 +365,7 @@ class AdminConfig(BaseModel):
DEFAULT_USER_ROLE: str
JWT_EXPIRES_IN: str
ENABLE_COMMUNITY_SHARING: bool
ENABLE_MESSAGE_RATING: bool
@router.post("/admin/config")
@@ -382,6 +387,7 @@ async def update_admin_config(
request.app.state.config.ENABLE_COMMUNITY_SHARING = (
form_data.ENABLE_COMMUNITY_SHARING
)
request.app.state.config.ENABLE_MESSAGE_RATING = form_data.ENABLE_MESSAGE_RATING
return {
"SHOW_ADMIN_DETAILS": request.app.state.config.SHOW_ADMIN_DETAILS,
@@ -389,6 +395,7 @@ async def update_admin_config(
"DEFAULT_USER_ROLE": request.app.state.config.DEFAULT_USER_ROLE,
"JWT_EXPIRES_IN": request.app.state.config.JWT_EXPIRES_IN,
"ENABLE_COMMUNITY_SHARING": request.app.state.config.ENABLE_COMMUNITY_SHARING,
"ENABLE_MESSAGE_RATING": request.app.state.config.ENABLE_MESSAGE_RATING,
}
+11 -11
View File
@@ -1,6 +1,6 @@
from fastapi import Depends, Request, HTTPException, status
from datetime import datetime, timedelta
from typing import List, Union, Optional
from typing import Union, Optional
from utils.utils import get_verified_user, get_admin_user
from fastapi import APIRouter
from pydantic import BaseModel
@@ -40,8 +40,8 @@ router = APIRouter()
############################
@router.get("/", response_model=List[ChatTitleIdResponse])
@router.get("/list", response_model=List[ChatTitleIdResponse])
@router.get("/", response_model=list[ChatTitleIdResponse])
@router.get("/list", response_model=list[ChatTitleIdResponse])
async def get_session_user_chat_list(
user=Depends(get_verified_user), page: Optional[int] = None
):
@@ -80,7 +80,7 @@ async def delete_all_user_chats(request: Request, user=Depends(get_verified_user
############################
@router.get("/list/user/{user_id}", response_model=List[ChatTitleIdResponse])
@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),
@@ -119,7 +119,7 @@ async def create_new_chat(form_data: ChatForm, user=Depends(get_verified_user)):
############################
@router.get("/all", response_model=List[ChatResponse])
@router.get("/all", response_model=list[ChatResponse])
async def get_user_chats(user=Depends(get_verified_user)):
return [
ChatResponse(**{**chat.model_dump(), "chat": json.loads(chat.chat)})
@@ -132,7 +132,7 @@ async def get_user_chats(user=Depends(get_verified_user)):
############################
@router.get("/all/archived", response_model=List[ChatResponse])
@router.get("/all/archived", response_model=list[ChatResponse])
async def get_user_archived_chats(user=Depends(get_verified_user)):
return [
ChatResponse(**{**chat.model_dump(), "chat": json.loads(chat.chat)})
@@ -145,7 +145,7 @@ async def get_user_archived_chats(user=Depends(get_verified_user)):
############################
@router.get("/all/db", response_model=List[ChatResponse])
@router.get("/all/db", response_model=list[ChatResponse])
async def get_all_user_chats_in_db(user=Depends(get_admin_user)):
if not ENABLE_ADMIN_EXPORT:
raise HTTPException(
@@ -163,7 +163,7 @@ async def get_all_user_chats_in_db(user=Depends(get_admin_user)):
############################
@router.get("/archived", response_model=List[ChatTitleIdResponse])
@router.get("/archived", response_model=list[ChatTitleIdResponse])
async def get_archived_session_user_chat_list(
user=Depends(get_verified_user), skip: int = 0, limit: int = 50
):
@@ -216,7 +216,7 @@ class TagNameForm(BaseModel):
limit: Optional[int] = 50
@router.post("/tags", response_model=List[ChatTitleIdResponse])
@router.post("/tags", response_model=list[ChatTitleIdResponse])
async def get_user_chat_list_by_tag_name(
form_data: TagNameForm, user=Depends(get_verified_user)
):
@@ -241,7 +241,7 @@ async def get_user_chat_list_by_tag_name(
############################
@router.get("/tags/all", response_model=List[TagModel])
@router.get("/tags/all", response_model=list[TagModel])
async def get_all_tags(user=Depends(get_verified_user)):
try:
tags = Tags.get_tags_by_user_id(user.id)
@@ -417,7 +417,7 @@ async def delete_shared_chat_by_id(id: str, user=Depends(get_verified_user)):
############################
@router.get("/{id}/tags", response_model=List[TagModel])
@router.get("/{id}/tags", response_model=list[TagModel])
async def get_chat_tags_by_id(id: str, user=Depends(get_verified_user)):
tags = Tags.get_tags_by_chat_id_and_user_id(id, user.id)
+7 -7
View File
@@ -1,7 +1,7 @@
from fastapi import Response, Request
from fastapi import Depends, FastAPI, HTTPException, status
from datetime import datetime, timedelta
from typing import List, Union
from typing import Union
from fastapi import APIRouter
from pydantic import BaseModel
@@ -29,12 +29,12 @@ class SetDefaultModelsForm(BaseModel):
class PromptSuggestion(BaseModel):
title: List[str]
title: list[str]
content: str
class SetDefaultSuggestionsForm(BaseModel):
suggestions: List[PromptSuggestion]
suggestions: list[PromptSuggestion]
############################
@@ -50,7 +50,7 @@ async def set_global_default_models(
return request.app.state.config.DEFAULT_MODELS
@router.post("/default/suggestions", response_model=List[PromptSuggestion])
@router.post("/default/suggestions", response_model=list[PromptSuggestion])
async def set_global_default_suggestions(
request: Request,
form_data: SetDefaultSuggestionsForm,
@@ -67,10 +67,10 @@ async def set_global_default_suggestions(
class SetBannersForm(BaseModel):
banners: List[BannerModel]
banners: list[BannerModel]
@router.post("/banners", response_model=List[BannerModel])
@router.post("/banners", response_model=list[BannerModel])
async def set_banners(
request: Request,
form_data: SetBannersForm,
@@ -81,7 +81,7 @@ async def set_banners(
return request.app.state.config.BANNERS
@router.get("/banners", response_model=List[BannerModel])
@router.get("/banners", response_model=list[BannerModel])
async def get_banners(
request: Request,
user=Depends(get_verified_user),
+4 -4
View File
@@ -1,6 +1,6 @@
from fastapi import Depends, FastAPI, HTTPException, status
from datetime import datetime, timedelta
from typing import List, Union, Optional
from typing import Union, Optional
from fastapi import APIRouter
from pydantic import BaseModel
@@ -24,7 +24,7 @@ router = APIRouter()
############################
@router.get("/", response_model=List[DocumentResponse])
@router.get("/", response_model=list[DocumentResponse])
async def get_documents(user=Depends(get_verified_user)):
docs = [
DocumentResponse(
@@ -46,7 +46,7 @@ async def get_documents(user=Depends(get_verified_user)):
@router.post("/create", response_model=Optional[DocumentResponse])
async def create_new_doc(form_data: DocumentForm, user=Depends(get_admin_user)):
doc = Documents.get_doc_by_name(form_data.name)
if doc == None:
if doc is None:
doc = Documents.insert_new_doc(user.id, form_data)
if doc:
@@ -102,7 +102,7 @@ class TagItem(BaseModel):
class TagDocumentForm(BaseModel):
name: str
tags: List[dict]
tags: list[dict]
@router.post("/doc/tags", response_model=Optional[DocumentResponse])
+10 -8
View File
@@ -11,7 +11,7 @@ from fastapi import (
from datetime import datetime, timedelta
from typing import List, Union, Optional
from typing import Union, Optional
from pathlib import Path
from fastapi import APIRouter
@@ -104,9 +104,12 @@ def upload_file(file: UploadFile = File(...), user=Depends(get_verified_user)):
############################
@router.get("/", response_model=List[FileModel])
@router.get("/", response_model=list[FileModel])
async def list_files(user=Depends(get_verified_user)):
files = Files.get_files()
if user.role == "admin":
files = Files.get_files()
else:
files = Files.get_files_by_user_id(user.id)
return files
@@ -156,7 +159,7 @@ async def delete_all_files(user=Depends(get_admin_user)):
async def get_file_by_id(id: str, user=Depends(get_verified_user)):
file = Files.get_file_by_id(id)
if file:
if file and (file.user_id == user.id or user.role == "admin"):
return file
else:
raise HTTPException(
@@ -174,7 +177,7 @@ async def get_file_by_id(id: str, user=Depends(get_verified_user)):
async def get_file_content_by_id(id: str, user=Depends(get_verified_user)):
file = Files.get_file_by_id(id)
if file:
if file and (file.user_id == user.id or user.role == "admin"):
file_path = Path(file.meta["path"])
# Check if the file already exists in the cache
@@ -197,7 +200,7 @@ async def get_file_content_by_id(id: str, user=Depends(get_verified_user)):
async def get_file_content_by_id(id: str, user=Depends(get_verified_user)):
file = Files.get_file_by_id(id)
if file:
if file and (file.user_id == user.id or user.role == "admin"):
file_path = Path(file.meta["path"])
# Check if the file already exists in the cache
@@ -224,8 +227,7 @@ async def get_file_content_by_id(id: str, user=Depends(get_verified_user)):
@router.delete("/{id}")
async def delete_file_by_id(id: str, user=Depends(get_verified_user)):
file = Files.get_file_by_id(id)
if file:
if file and (file.user_id == user.id or user.role == "admin"):
result = Files.delete_file_by_id(id)
if result:
return {"message": "File deleted successfully"}
+5 -5
View File
@@ -1,6 +1,6 @@
from fastapi import Depends, FastAPI, HTTPException, status, Request
from datetime import datetime, timedelta
from typing import List, Union, Optional
from typing import Union, Optional
from fastapi import APIRouter
from pydantic import BaseModel
@@ -30,7 +30,7 @@ router = APIRouter()
############################
@router.get("/", response_model=List[FunctionResponse])
@router.get("/", response_model=list[FunctionResponse])
async def get_functions(user=Depends(get_verified_user)):
return Functions.get_functions()
@@ -40,7 +40,7 @@ async def get_functions(user=Depends(get_verified_user)):
############################
@router.get("/export", response_model=List[FunctionModel])
@router.get("/export", response_model=list[FunctionModel])
async def get_functions(user=Depends(get_admin_user)):
return Functions.get_functions()
@@ -63,7 +63,7 @@ async def create_new_function(
form_data.id = form_data.id.lower()
function = Functions.get_function_by_id(form_data.id)
if function == None:
if function is None:
function_path = os.path.join(FUNCTIONS_DIR, f"{form_data.id}.py")
try:
with open(function_path, "w") as function_file:
@@ -235,7 +235,7 @@ async def delete_function_by_id(
function_path = os.path.join(FUNCTIONS_DIR, f"{id}.py")
try:
os.remove(function_path)
except:
except Exception:
pass
return result
+72 -67
View File
@@ -1,7 +1,7 @@
from fastapi import Response, Request
from fastapi import Depends, FastAPI, HTTPException, status
from datetime import datetime, timedelta
from typing import List, Union, Optional
from typing import Union, Optional
from fastapi import APIRouter
from pydantic import BaseModel
@@ -30,7 +30,7 @@ async def get_embeddings(request: Request):
############################
@router.get("/", response_model=List[MemoryModel])
@router.get("/", response_model=list[MemoryModel])
async def get_memories(user=Depends(get_verified_user)):
return Memories.get_memories_by_user_id(user.id)
@@ -68,6 +68,76 @@ async def add_memory(
return memory
############################
# QueryMemory
############################
class QueryMemoryForm(BaseModel):
content: str
k: Optional[int] = 1
@router.post("/query")
async def query_memory(
request: Request, form_data: QueryMemoryForm, user=Depends(get_verified_user)
):
query_embedding = request.app.state.EMBEDDING_FUNCTION(form_data.content)
collection = CHROMA_CLIENT.get_or_create_collection(name=f"user-memory-{user.id}")
results = collection.query(
query_embeddings=[query_embedding],
n_results=form_data.k, # how many results to return
)
return results
############################
# ResetMemoryFromVectorDB
############################
@router.post("/reset", response_model=bool)
async def reset_memory_from_vector_db(
request: Request, user=Depends(get_verified_user)
):
CHROMA_CLIENT.delete_collection(f"user-memory-{user.id}")
collection = CHROMA_CLIENT.get_or_create_collection(name=f"user-memory-{user.id}")
memories = Memories.get_memories_by_user_id(user.id)
for memory in memories:
memory_embedding = request.app.state.EMBEDDING_FUNCTION(memory.content)
collection.upsert(
documents=[memory.content],
ids=[memory.id],
embeddings=[memory_embedding],
)
return True
############################
# DeleteMemoriesByUserId
############################
@router.delete("/delete/user", response_model=bool)
async def delete_memory_by_user_id(user=Depends(get_verified_user)):
result = Memories.delete_memories_by_user_id(user.id)
if result:
try:
CHROMA_CLIENT.delete_collection(f"user-memory-{user.id}")
except Exception as e:
log.error(e)
return True
return False
############################
# UpdateMemoryById
############################
@router.post("/{memory_id}/update", response_model=Optional[MemoryModel])
async def update_memory_by_id(
memory_id: str,
@@ -96,71 +166,6 @@ async def update_memory_by_id(
return memory
############################
# QueryMemory
############################
class QueryMemoryForm(BaseModel):
content: str
k: Optional[int] = 1
@router.post("/query")
async def query_memory(
request: Request, form_data: QueryMemoryForm, user=Depends(get_verified_user)
):
query_embedding = request.app.state.EMBEDDING_FUNCTION(form_data.content)
collection = CHROMA_CLIENT.get_or_create_collection(name=f"user-memory-{user.id}")
results = collection.query(
query_embeddings=[query_embedding],
n_results=form_data.k, # how many results to return
)
return results
############################
# ResetMemoryFromVectorDB
############################
@router.get("/reset", response_model=bool)
async def reset_memory_from_vector_db(
request: Request, user=Depends(get_verified_user)
):
CHROMA_CLIENT.delete_collection(f"user-memory-{user.id}")
collection = CHROMA_CLIENT.get_or_create_collection(name=f"user-memory-{user.id}")
memories = Memories.get_memories_by_user_id(user.id)
for memory in memories:
memory_embedding = request.app.state.EMBEDDING_FUNCTION(memory.content)
collection.upsert(
documents=[memory.content],
ids=[memory.id],
embeddings=[memory_embedding],
)
return True
############################
# DeleteMemoriesByUserId
############################
@router.delete("/user", response_model=bool)
async def delete_memory_by_user_id(user=Depends(get_verified_user)):
result = Memories.delete_memories_by_user_id(user.id)
if result:
try:
CHROMA_CLIENT.delete_collection(f"user-memory-{user.id}")
except Exception as e:
log.error(e)
return True
return False
############################
# DeleteMemoryById
############################
+2 -2
View File
@@ -1,6 +1,6 @@
from fastapi import Depends, FastAPI, HTTPException, status, Request
from datetime import datetime, timedelta
from typing import List, Union, Optional
from typing import Union, Optional
from fastapi import APIRouter
from pydantic import BaseModel
@@ -18,7 +18,7 @@ router = APIRouter()
###########################
@router.get("/", response_model=List[ModelResponse])
@router.get("/", response_model=list[ModelResponse])
async def get_models(user=Depends(get_verified_user)):
return Models.get_all_models()
+3 -3
View File
@@ -1,6 +1,6 @@
from fastapi import Depends, FastAPI, HTTPException, status
from datetime import datetime, timedelta
from typing import List, Union, Optional
from typing import Union, Optional
from fastapi import APIRouter
from pydantic import BaseModel
@@ -18,7 +18,7 @@ router = APIRouter()
############################
@router.get("/", response_model=List[PromptModel])
@router.get("/", response_model=list[PromptModel])
async def get_prompts(user=Depends(get_verified_user)):
return Prompts.get_prompts()
@@ -31,7 +31,7 @@ async def get_prompts(user=Depends(get_verified_user)):
@router.post("/create", response_model=Optional[PromptModel])
async def create_new_prompt(form_data: PromptForm, user=Depends(get_admin_user)):
prompt = Prompts.get_prompt_by_command(form_data.command)
if prompt == None:
if prompt is None:
prompt = Prompts.insert_new_prompt(user.id, form_data)
if prompt:
+3 -3
View File
@@ -1,5 +1,5 @@
from fastapi import Depends, HTTPException, status, Request
from typing import List, Optional
from typing import Optional
from fastapi import APIRouter
@@ -27,7 +27,7 @@ router = APIRouter()
############################
@router.get("/", response_model=List[ToolResponse])
@router.get("/", response_model=list[ToolResponse])
async def get_toolkits(user=Depends(get_verified_user)):
toolkits = [toolkit for toolkit in Tools.get_tools()]
return toolkits
@@ -38,7 +38,7 @@ async def get_toolkits(user=Depends(get_verified_user)):
############################
@router.get("/export", response_model=List[ToolModel])
@router.get("/export", response_model=list[ToolModel])
async def get_toolkits(user=Depends(get_admin_user)):
toolkits = [toolkit for toolkit in Tools.get_tools()]
return toolkits
+2 -2
View File
@@ -1,7 +1,7 @@
from fastapi import Response, Request
from fastapi import Depends, FastAPI, HTTPException, status
from datetime import datetime, timedelta
from typing import List, Union, Optional
from typing import Union, Optional
from fastapi import APIRouter
from pydantic import BaseModel
@@ -39,7 +39,7 @@ router = APIRouter()
############################
@router.get("/", response_model=List[UserModel])
@router.get("/", response_model=list[UserModel])
async def get_users(skip: int = 0, limit: int = 50, user=Depends(get_admin_user)):
return Users.get_users(skip, limit)
+4 -3
View File
@@ -17,7 +17,7 @@ from utils.misc import calculate_sha256, get_gravatar_url
from config import OLLAMA_BASE_URLS, DATA_DIR, UPLOAD_DIR, ENABLE_ADMIN_EXPORT
from constants import ERROR_MESSAGES
from typing import List
router = APIRouter()
@@ -57,7 +57,7 @@ async def get_html_from_markdown(
class ChatForm(BaseModel):
title: str
messages: List[dict]
messages: list[dict]
@router.post("/pdf")
@@ -85,9 +85,10 @@ async def download_chat_as_pdf(
pdf.add_font("NotoSans", "i", f"{FONTS_DIR}/NotoSans-Italic.ttf")
pdf.add_font("NotoSansKR", "", f"{FONTS_DIR}/NotoSansKR-Regular.ttf")
pdf.add_font("NotoSansJP", "", f"{FONTS_DIR}/NotoSansJP-Regular.ttf")
pdf.add_font("NotoSansSC", "", f"{FONTS_DIR}/NotoSansSC-Regular.ttf")
pdf.set_font("NotoSans", size=12)
pdf.set_fallback_fonts(["NotoSansKR", "NotoSansJP"])
pdf.set_fallback_fonts(["NotoSansKR", "NotoSansJP", "NotoSansSC"])
pdf.set_auto_page_break(auto=True, margin=15)
+34
View File
@@ -1,7 +1,12 @@
from importlib import util
import os
import re
import sys
import subprocess
from apps.webui.models.tools import Tools
from apps.webui.models.functions import Functions
from config import TOOLS_DIR, FUNCTIONS_DIR
@@ -47,11 +52,21 @@ def extract_frontmatter(file_path):
def load_toolkit_module_by_id(toolkit_id):
toolkit_path = os.path.join(TOOLS_DIR, f"{toolkit_id}.py")
if not os.path.exists(toolkit_path):
tool = Tools.get_tool_by_id(toolkit_id)
if tool:
with open(toolkit_path, "w") as file:
file.write(tool.content)
else:
raise Exception(f"Toolkit not found: {toolkit_id}")
spec = util.spec_from_file_location(toolkit_id, toolkit_path)
module = util.module_from_spec(spec)
frontmatter = extract_frontmatter(toolkit_path)
try:
install_frontmatter_requirements(frontmatter.get("requirements", ""))
spec.loader.exec_module(module)
print(f"Loaded module: {module.__name__}")
if hasattr(module, "Tools"):
@@ -68,11 +83,20 @@ def load_toolkit_module_by_id(toolkit_id):
def load_function_module_by_id(function_id):
function_path = os.path.join(FUNCTIONS_DIR, f"{function_id}.py")
if not os.path.exists(function_path):
function = Functions.get_function_by_id(function_id)
if function:
with open(function_path, "w") as file:
file.write(function.content)
else:
raise Exception(f"Function not found: {function_id}")
spec = util.spec_from_file_location(function_id, function_path)
module = util.module_from_spec(spec)
frontmatter = extract_frontmatter(function_path)
try:
install_frontmatter_requirements(frontmatter.get("requirements", ""))
spec.loader.exec_module(module)
print(f"Loaded module: {module.__name__}")
if hasattr(module, "Pipe"):
@@ -88,3 +112,13 @@ def load_function_module_by_id(function_id):
# Move the file to the error folder
os.rename(function_path, f"{function_path}.error")
raise e
def install_frontmatter_requirements(requirements):
if requirements:
req_list = [req.strip() for req in requirements.split(",")]
for req in req_list:
print(f"Installing requirement: {req}")
subprocess.check_call([sys.executable, "-m", "pip", "install", req])
else:
print("No requirements found in frontmatter.")
+375 -288
View File
@@ -1,11 +1,17 @@
from sqlalchemy import create_engine, Column, Integer, DateTime, JSON, func
from contextlib import contextmanager
import os
import sys
import logging
import importlib.metadata
import pkgutil
from urllib.parse import urlparse
from datetime import datetime
import chromadb
from chromadb import Settings
from bs4 import BeautifulSoup
from typing import TypeVar, Generic
from pydantic import BaseModel
from typing import Optional
@@ -14,68 +20,39 @@ from pathlib import Path
import json
import yaml
import markdown
import requests
import shutil
from apps.webui.internal.db import Base, get_db
from constants import ERROR_MESSAGES
####################################
# Load .env file
####################################
BACKEND_DIR = Path(__file__).parent # the path containing this file
BASE_DIR = BACKEND_DIR.parent # the path containing the backend/
print(BASE_DIR)
try:
from dotenv import load_dotenv, find_dotenv
load_dotenv(find_dotenv(str(BASE_DIR / ".env")))
except ImportError:
print("dotenv not installed, skipping...")
####################################
# LOGGING
####################################
log_levels = ["CRITICAL", "ERROR", "WARNING", "INFO", "DEBUG"]
GLOBAL_LOG_LEVEL = os.environ.get("GLOBAL_LOG_LEVEL", "").upper()
if GLOBAL_LOG_LEVEL in log_levels:
logging.basicConfig(stream=sys.stdout, level=GLOBAL_LOG_LEVEL, force=True)
else:
GLOBAL_LOG_LEVEL = "INFO"
log = logging.getLogger(__name__)
log.info(f"GLOBAL_LOG_LEVEL: {GLOBAL_LOG_LEVEL}")
log_sources = [
"AUDIO",
"COMFYUI",
"CONFIG",
"DB",
"IMAGES",
"MAIN",
"MODELS",
"OLLAMA",
"OPENAI",
"RAG",
"WEBHOOK",
]
SRC_LOG_LEVELS = {}
for source in log_sources:
log_env_var = source + "_LOG_LEVEL"
SRC_LOG_LEVELS[source] = os.environ.get(log_env_var, "").upper()
if SRC_LOG_LEVELS[source] not in log_levels:
SRC_LOG_LEVELS[source] = GLOBAL_LOG_LEVEL
log.info(f"{log_env_var}: {SRC_LOG_LEVELS[source]}")
log.setLevel(SRC_LOG_LEVELS["CONFIG"])
from env import (
ENV,
VERSION,
SAFE_MODE,
GLOBAL_LOG_LEVEL,
SRC_LOG_LEVELS,
BASE_DIR,
DATA_DIR,
BACKEND_DIR,
FRONTEND_BUILD_DIR,
WEBUI_NAME,
WEBUI_URL,
WEBUI_FAVICON_URL,
WEBUI_BUILD_HASH,
CONFIG_DATA,
DATABASE_URL,
CHANGELOG,
WEBUI_AUTH,
WEBUI_AUTH_TRUSTED_EMAIL_HEADER,
WEBUI_AUTH_TRUSTED_NAME_HEADER,
WEBUI_SECRET_KEY,
WEBUI_SESSION_COOKIE_SAME_SITE,
WEBUI_SESSION_COOKIE_SECURE,
log,
)
class EndpointFilter(logging.Filter):
@@ -86,136 +63,62 @@ class EndpointFilter(logging.Filter):
# Filter out /endpoint
logging.getLogger("uvicorn.access").addFilter(EndpointFilter())
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)
####################################
ENV = os.environ.get("ENV", "dev")
try:
PACKAGE_DATA = json.loads((BASE_DIR / "package.json").read_text())
except:
try:
PACKAGE_DATA = {"version": importlib.metadata.version("open-webui")}
except importlib.metadata.PackageNotFoundError:
PACKAGE_DATA = {"version": "0.0.0"}
VERSION = PACKAGE_DATA["version"]
# Function to parse each section
def parse_section(section):
items = []
for li in section.find_all("li"):
# Extract raw HTML string
raw_html = str(li)
# Extract text without HTML tags
text = li.get_text(separator=" ", strip=True)
# Split into title and content
parts = text.split(": ", 1)
title = parts[0].strip() if len(parts) > 1 else ""
content = parts[1].strip() if len(parts) > 1 else text
items.append({"title": title, "content": content, "raw": raw_html})
return items
try:
changelog_path = BASE_DIR / "CHANGELOG.md"
with open(str(changelog_path.absolute()), "r", encoding="utf8") as file:
changelog_content = file.read()
except:
changelog_content = (pkgutil.get_data("open_webui", "CHANGELOG.md") or b"").decode()
# Convert markdown content to HTML
html_content = markdown.markdown(changelog_content)
# Parse the HTML content
soup = BeautifulSoup(html_content, "html.parser")
# Initialize JSON structure
changelog_json = {}
# Iterate over each version
for version in soup.find_all("h2"):
version_number = version.get_text().strip().split(" - ")[0][1:-1] # Remove brackets
date = version.get_text().strip().split(" - ")[1]
version_data = {"date": date}
# Find the next sibling that is a h3 tag (section title)
current = version.find_next_sibling()
while current and current.name != "h2":
if current.name == "h3":
section_title = current.get_text().lower() # e.g., "added", "fixed"
section_items = parse_section(current.find_next_sibling("ul"))
version_data[section_title] = section_items
# Move to the next element
current = current.find_next_sibling()
changelog_json[version_number] = version_data
CHANGELOG = changelog_json
####################################
# SAFE_MODE
####################################
SAFE_MODE = os.environ.get("SAFE_MODE", "false").lower() == "true"
####################################
# WEBUI_BUILD_HASH
####################################
WEBUI_BUILD_HASH = os.environ.get("WEBUI_BUILD_HASH", "dev-build")
####################################
# DATA/FRONTEND BUILD DIR
####################################
DATA_DIR = Path(os.getenv("DATA_DIR", BACKEND_DIR / "data")).resolve()
FRONTEND_BUILD_DIR = Path(os.getenv("FRONTEND_BUILD_DIR", BASE_DIR / "build")).resolve()
RESET_CONFIG_ON_START = (
os.environ.get("RESET_CONFIG_ON_START", "False").lower() == "true"
)
if RESET_CONFIG_ON_START:
try:
os.remove(f"{DATA_DIR}/config.json")
with open(f"{DATA_DIR}/config.json", "w") as f:
f.write("{}")
except:
pass
try:
CONFIG_DATA = json.loads((DATA_DIR / "config.json").read_text())
except:
CONFIG_DATA = {}
####################################
# Config helpers
####################################
# Function to run the alembic migrations
def run_migrations():
print("Running migrations")
try:
from alembic.config import Config
from alembic import command
alembic_cfg = Config("alembic.ini")
command.upgrade(alembic_cfg, "head")
except Exception as e:
print(f"Error: {e}")
run_migrations()
class Config(Base):
__tablename__ = "config"
id = Column(Integer, primary_key=True)
data = Column(JSON, nullable=False)
version = Column(Integer, nullable=False, default=0)
created_at = Column(DateTime, nullable=False, server_default=func.now())
updated_at = Column(DateTime, nullable=True, onupdate=func.now())
def load_json_config():
with open(f"{DATA_DIR}/config.json", "r") as file:
return json.load(file)
def save_to_db(data):
with get_db() as db:
existing_config = db.query(Config).first()
if not existing_config:
new_config = Config(data=data, version=0)
db.add(new_config)
else:
existing_config.data = data
existing_config.updated_at = datetime.now()
db.add(existing_config)
db.commit()
# When initializing, check if config.json exists and migrate it to the database
if os.path.exists(f"{DATA_DIR}/config.json"):
data = load_json_config()
save_to_db(data)
os.rename(f"{DATA_DIR}/config.json", f"{DATA_DIR}/old_config.json")
def save_config():
try:
with open(f"{DATA_DIR}/config.json", "w") as f:
@@ -224,6 +127,68 @@ def save_config():
log.exception(e)
DEFAULT_CONFIG = {
"version": 0,
"ui": {
"default_locale": "",
"prompt_suggestions": [
{
"title": [
"Help me study",
"vocabulary for a college entrance exam",
],
"content": "Help me study vocabulary: write a sentence for me to fill in the blank, and I'll try to pick the correct option.",
},
{
"title": [
"Give me ideas",
"for what to do with my kids' art",
],
"content": "What are 5 creative things I could do with my kids' art? I don't want to throw them away, but it's also so much clutter.",
},
{
"title": ["Tell me a fun fact", "about the Roman Empire"],
"content": "Tell me a random fun fact about the Roman Empire",
},
{
"title": [
"Show me a code snippet",
"of a website's sticky header",
],
"content": "Show me a code snippet of a website's sticky header in CSS and JavaScript.",
},
{
"title": [
"Explain options trading",
"if I'm familiar with buying and selling stocks",
],
"content": "Explain options trading in simple terms if I'm familiar with buying and selling stocks.",
},
{
"title": ["Overcome procrastination", "give me tips"],
"content": "Could you start by asking me about instances when I procrastinate the most and then give me some suggestions to overcome it?",
},
{
"title": [
"Grammar check",
"rewrite it for better readability ",
],
"content": 'Check the following sentence for grammar and clarity: "[sentence]". Rewrite it for better readability while maintaining its original meaning.',
},
],
},
}
def get_config():
with get_db() as db:
config_entry = db.query(Config).order_by(Config.id.desc()).first()
return config_entry.data if config_entry else DEFAULT_CONFIG
CONFIG_DATA = get_config()
def get_config_value(config_path: str):
path_parts = config_path.split(".")
cur_config = CONFIG_DATA
@@ -245,7 +210,7 @@ class PersistentConfig(Generic[T]):
self.env_value = env_value
self.config_value = get_config_value(config_path)
if self.config_value is not None:
log.info(f"'{env_name}' loaded from config.json")
log.info(f"'{env_name}' loaded from the latest database entry")
self.value = self.config_value
else:
self.value = env_value
@@ -267,19 +232,15 @@ class PersistentConfig(Generic[T]):
return super().__getattribute__(item)
def save(self):
# Don't save if the value is the same as the env value and the config value
if self.env_value == self.value:
if self.config_value == self.value:
return
log.info(f"Saving '{self.env_name}' to config.json")
log.info(f"Saving '{self.env_name}' to the database")
path_parts = self.config_path.split(".")
config = CONFIG_DATA
sub_config = CONFIG_DATA
for key in path_parts[:-1]:
if key not in config:
config[key] = {}
config = config[key]
config[path_parts[-1]] = self.value
save_config()
if key not in sub_config:
sub_config[key] = {}
sub_config = sub_config[key]
sub_config[path_parts[-1]] = self.value
save_to_db(CONFIG_DATA)
self.config_value = self.value
@@ -304,11 +265,6 @@ class AppConfig:
# WEBUI_AUTH (Required for security)
####################################
WEBUI_AUTH = os.environ.get("WEBUI_AUTH", "True").lower() == "true"
WEBUI_AUTH_TRUSTED_EMAIL_HEADER = os.environ.get(
"WEBUI_AUTH_TRUSTED_EMAIL_HEADER", None
)
WEBUI_AUTH_TRUSTED_NAME_HEADER = os.environ.get("WEBUI_AUTH_TRUSTED_NAME_HEADER", None)
JWT_EXPIRES_IN = PersistentConfig(
"JWT_EXPIRES_IN", "auth.jwt_expiry", os.environ.get("JWT_EXPIRES_IN", "-1")
)
@@ -433,6 +389,12 @@ OAUTH_PICTURE_CLAIM = PersistentConfig(
os.environ.get("OAUTH_PICTURE_CLAIM", "picture"),
)
OAUTH_EMAIL_CLAIM = PersistentConfig(
"OAUTH_EMAIL_CLAIM",
"oauth.oidc.email_claim",
os.environ.get("OAUTH_EMAIL_CLAIM", "email"),
)
def load_oauth_providers():
OAUTH_PROVIDERS.clear()
@@ -641,7 +603,7 @@ if AIOHTTP_CLIENT_TIMEOUT == "":
else:
try:
AIOHTTP_CLIENT_TIMEOUT = int(AIOHTTP_CLIENT_TIMEOUT)
except:
except Exception:
AIOHTTP_CLIENT_TIMEOUT = 300
@@ -721,7 +683,7 @@ try:
OPENAI_API_KEY = OPENAI_API_KEYS.value[
OPENAI_API_BASE_URLS.value.index("https://api.openai.com/v1")
]
except:
except Exception:
pass
OPENAI_API_BASE_URL = "https://api.openai.com/v1"
@@ -800,10 +762,24 @@ USER_PERMISSIONS_CHAT_DELETION = (
os.environ.get("USER_PERMISSIONS_CHAT_DELETION", "True").lower() == "true"
)
USER_PERMISSIONS_CHAT_EDITING = (
os.environ.get("USER_PERMISSIONS_CHAT_EDITING", "True").lower() == "true"
)
USER_PERMISSIONS_CHAT_TEMPORARY = (
os.environ.get("USER_PERMISSIONS_CHAT_TEMPORARY", "True").lower() == "true"
)
USER_PERMISSIONS = PersistentConfig(
"USER_PERMISSIONS",
"ui.user_permissions",
{"chat": {"deletion": USER_PERMISSIONS_CHAT_DELETION}},
{
"chat": {
"deletion": USER_PERMISSIONS_CHAT_DELETION,
"editing": USER_PERMISSIONS_CHAT_EDITING,
"temporary": USER_PERMISSIONS_CHAT_TEMPORARY,
}
},
)
ENABLE_MODEL_FILTER = PersistentConfig(
@@ -834,6 +810,47 @@ ENABLE_COMMUNITY_SHARING = PersistentConfig(
os.environ.get("ENABLE_COMMUNITY_SHARING", "True").lower() == "true",
)
ENABLE_MESSAGE_RATING = PersistentConfig(
"ENABLE_MESSAGE_RATING",
"ui.enable_message_rating",
os.environ.get("ENABLE_MESSAGE_RATING", "True").lower() == "true",
)
def validate_cors_origins(origins):
for origin in origins:
if origin != "*":
validate_cors_origin(origin)
def validate_cors_origin(origin):
parsed_url = urlparse(origin)
# Check if the scheme is either http or https
if parsed_url.scheme not in ["http", "https"]:
raise ValueError(
f"Invalid scheme in CORS_ALLOW_ORIGIN: '{origin}'. Only 'http' and 'https' are allowed."
)
# Ensure that the netloc (domain + port) is present, indicating it's a valid URL
if not parsed_url.netloc:
raise ValueError(f"Invalid URL structure in CORS_ALLOW_ORIGIN: '{origin}'.")
# For production, you should only need one host as
# fastapi serves the svelte-kit built frontend and backend from the same host and port.
# To test CORS_ALLOW_ORIGIN locally, you can set something like
# CORS_ALLOW_ORIGIN=http://localhost:5173;http://localhost:8080
# in your .env file depending on your frontend port, 5173 in this case.
CORS_ALLOW_ORIGIN = os.environ.get("CORS_ALLOW_ORIGIN", "*").split(";")
if "*" in CORS_ALLOW_ORIGIN:
log.warning(
"\n\nWARNING: CORS_ALLOW_ORIGIN IS SET TO '*' - NOT RECOMMENDED FOR PRODUCTION DEPLOYMENTS.\n"
)
validate_cors_origins(CORS_ALLOW_ORIGIN)
class BannerModel(BaseModel):
id: str
@@ -889,10 +906,7 @@ TITLE_GENERATION_PROMPT_TEMPLATE = PersistentConfig(
"task.title.prompt_template",
os.environ.get(
"TITLE_GENERATION_PROMPT_TEMPLATE",
"""Here is the query:
{{prompt:middletruncate:8000}}
Create a concise, 3-5 word phrase with an emoji as a title for the previous query. Suitable Emojis for the summary can be used to enhance understanding but avoid quotation marks or special formatting. RESPOND ONLY WITH THE TITLE TEXT.
"""Create a concise, 3-5 word title with an emoji as a title for the prompt in the given language. Suitable Emojis for the summary can be used to enhance understanding but avoid quotation marks or special formatting. RESPOND ONLY WITH THE TITLE TEXT.
Examples of titles:
📉 Stock Market Trends
@@ -900,7 +914,9 @@ Examples of titles:
Evolution of Music Streaming
Remote Work Productivity Tips
Artificial Intelligence in Healthcare
🎮 Video Game Development Insights""",
🎮 Video Game Development Insights
Prompt: {{prompt:middletruncate:8000}}""",
),
)
@@ -933,36 +949,11 @@ TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE = PersistentConfig(
"task.tools.prompt_template",
os.environ.get(
"TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE",
"""Tools: {{TOOLS}}
If a function tool doesn't match the query, return an empty string. Else, pick a function tool, fill in the parameters from the function tool's schema, and return it in the format { "name": \"functionName\", "parameters": { "key": "value" } }. Only pick a function if the user asks. Only return the object. Do not return any other text.""",
"""Available Tools: {{TOOLS}}\nReturn an empty string if no tools match the query. If a function tool matches, construct and return a JSON object in the format {\"name\": \"functionName\", \"parameters\": {\"requiredFunctionParamKey\": \"requiredFunctionParamValue\"}} using the appropriate tool and its parameters. Only return the object and limit the response to the JSON object without additional text.""",
),
)
####################################
# WEBUI_SECRET_KEY
####################################
WEBUI_SECRET_KEY = os.environ.get(
"WEBUI_SECRET_KEY",
os.environ.get(
"WEBUI_JWT_SECRET_KEY", "t0p-s3cr3t"
), # DEPRECATED: remove at next major version
)
WEBUI_SESSION_COOKIE_SAME_SITE = os.environ.get(
"WEBUI_SESSION_COOKIE_SAME_SITE",
os.environ.get("WEBUI_SESSION_COOKIE_SAME_SITE", "lax"),
)
WEBUI_SESSION_COOKIE_SECURE = os.environ.get(
"WEBUI_SESSION_COOKIE_SECURE",
os.environ.get("WEBUI_SESSION_COOKIE_SECURE", "false").lower() == "true",
)
if WEBUI_AUTH and WEBUI_SECRET_KEY == "":
raise ValueError(ERROR_MESSAGES.ENV_VAR_NOT_FOUND)
####################################
# RAG document content extraction
####################################
@@ -1014,6 +1005,26 @@ ENABLE_RAG_HYBRID_SEARCH = PersistentConfig(
os.environ.get("ENABLE_RAG_HYBRID_SEARCH", "").lower() == "true",
)
RAG_FILE_MAX_COUNT = PersistentConfig(
"RAG_FILE_MAX_COUNT",
"rag.file.max_count",
(
int(os.environ.get("RAG_FILE_MAX_COUNT"))
if os.environ.get("RAG_FILE_MAX_COUNT")
else None
),
)
RAG_FILE_MAX_SIZE = PersistentConfig(
"RAG_FILE_MAX_SIZE",
"rag.file.max_size",
(
int(os.environ.get("RAG_FILE_MAX_SIZE"))
if os.environ.get("RAG_FILE_MAX_SIZE")
else None
),
)
ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION = PersistentConfig(
"ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION",
"rag.enable_web_loader_ssl_verification",
@@ -1037,7 +1048,7 @@ RAG_EMBEDDING_MODEL = PersistentConfig(
"rag.embedding_model",
os.environ.get("RAG_EMBEDDING_MODEL", "sentence-transformers/all-MiniLM-L6-v2"),
)
log.info(f"Embedding model set: {RAG_EMBEDDING_MODEL.value}"),
log.info(f"Embedding model set: {RAG_EMBEDDING_MODEL.value}")
RAG_EMBEDDING_MODEL_AUTO_UPDATE = (
os.environ.get("RAG_EMBEDDING_MODEL_AUTO_UPDATE", "").lower() == "true"
@@ -1050,7 +1061,7 @@ RAG_EMBEDDING_MODEL_TRUST_REMOTE_CODE = (
RAG_EMBEDDING_OPENAI_BATCH_SIZE = PersistentConfig(
"RAG_EMBEDDING_OPENAI_BATCH_SIZE",
"rag.embedding_openai_batch_size",
os.environ.get("RAG_EMBEDDING_OPENAI_BATCH_SIZE", 1),
int(os.environ.get("RAG_EMBEDDING_OPENAI_BATCH_SIZE", "1")),
)
RAG_RERANKING_MODEL = PersistentConfig(
@@ -1059,7 +1070,7 @@ RAG_RERANKING_MODEL = PersistentConfig(
os.environ.get("RAG_RERANKING_MODEL", ""),
)
if RAG_RERANKING_MODEL.value != "":
log.info(f"Reranking model set: {RAG_RERANKING_MODEL.value}"),
log.info(f"Reranking model set: {RAG_RERANKING_MODEL.value}")
RAG_RERANKING_MODEL_AUTO_UPDATE = (
os.environ.get("RAG_RERANKING_MODEL_AUTO_UPDATE", "").lower() == "true"
@@ -1257,7 +1268,7 @@ WHISPER_MODEL_AUTO_UPDATE = (
IMAGE_GENERATION_ENGINE = PersistentConfig(
"IMAGE_GENERATION_ENGINE",
"image_generation.engine",
os.getenv("IMAGE_GENERATION_ENGINE", ""),
os.getenv("IMAGE_GENERATION_ENGINE", "openai"),
)
ENABLE_IMAGE_GENERATION = PersistentConfig(
@@ -1282,46 +1293,127 @@ COMFYUI_BASE_URL = PersistentConfig(
os.getenv("COMFYUI_BASE_URL", ""),
)
COMFYUI_CFG_SCALE = PersistentConfig(
"COMFYUI_CFG_SCALE",
"image_generation.comfyui.cfg_scale",
os.getenv("COMFYUI_CFG_SCALE", ""),
COMFYUI_DEFAULT_WORKFLOW = """
{
"3": {
"inputs": {
"seed": 0,
"steps": 20,
"cfg": 8,
"sampler_name": "euler",
"scheduler": "normal",
"denoise": 1,
"model": [
"4",
0
],
"positive": [
"6",
0
],
"negative": [
"7",
0
],
"latent_image": [
"5",
0
]
},
"class_type": "KSampler",
"_meta": {
"title": "KSampler"
}
},
"4": {
"inputs": {
"ckpt_name": "model.safetensors"
},
"class_type": "CheckpointLoaderSimple",
"_meta": {
"title": "Load Checkpoint"
}
},
"5": {
"inputs": {
"width": 512,
"height": 512,
"batch_size": 1
},
"class_type": "EmptyLatentImage",
"_meta": {
"title": "Empty Latent Image"
}
},
"6": {
"inputs": {
"text": "Prompt",
"clip": [
"4",
1
]
},
"class_type": "CLIPTextEncode",
"_meta": {
"title": "CLIP Text Encode (Prompt)"
}
},
"7": {
"inputs": {
"text": "",
"clip": [
"4",
1
]
},
"class_type": "CLIPTextEncode",
"_meta": {
"title": "CLIP Text Encode (Prompt)"
}
},
"8": {
"inputs": {
"samples": [
"3",
0
],
"vae": [
"4",
2
]
},
"class_type": "VAEDecode",
"_meta": {
"title": "VAE Decode"
}
},
"9": {
"inputs": {
"filename_prefix": "ComfyUI",
"images": [
"8",
0
]
},
"class_type": "SaveImage",
"_meta": {
"title": "Save Image"
}
}
}
"""
COMFYUI_WORKFLOW = PersistentConfig(
"COMFYUI_WORKFLOW",
"image_generation.comfyui.workflow",
os.getenv("COMFYUI_WORKFLOW", COMFYUI_DEFAULT_WORKFLOW),
)
COMFYUI_SAMPLER = PersistentConfig(
"COMFYUI_SAMPLER",
"image_generation.comfyui.sampler",
os.getenv("COMFYUI_SAMPLER", ""),
)
COMFYUI_SCHEDULER = PersistentConfig(
"COMFYUI_SCHEDULER",
"image_generation.comfyui.scheduler",
os.getenv("COMFYUI_SCHEDULER", ""),
)
COMFYUI_SD3 = PersistentConfig(
"COMFYUI_SD3",
"image_generation.comfyui.sd3",
os.environ.get("COMFYUI_SD3", "").lower() == "true",
)
COMFYUI_FLUX = PersistentConfig(
"COMFYUI_FLUX",
"image_generation.comfyui.flux",
os.environ.get("COMFYUI_FLUX", "").lower() == "true",
)
COMFYUI_FLUX_WEIGHT_DTYPE = PersistentConfig(
"COMFYUI_FLUX_WEIGHT_DTYPE",
"image_generation.comfyui.flux_weight_dtype",
os.getenv("COMFYUI_FLUX_WEIGHT_DTYPE", ""),
)
COMFYUI_FLUX_FP8_CLIP = PersistentConfig(
"COMFYUI_FLUX_FP8_CLIP",
"image_generation.comfyui.flux_fp8_clip",
os.environ.get("COMFYUI_FLUX_FP8_CLIP", "").lower() == "true",
COMFYUI_WORKFLOW_NODES = PersistentConfig(
"COMFYUI_WORKFLOW",
"image_generation.comfyui.nodes",
[],
)
IMAGES_OPENAI_API_BASE_URL = PersistentConfig(
@@ -1404,22 +1496,17 @@ AUDIO_TTS_ENGINE = PersistentConfig(
AUDIO_TTS_MODEL = PersistentConfig(
"AUDIO_TTS_MODEL",
"audio.tts.model",
os.getenv("AUDIO_TTS_MODEL", "tts-1"),
os.getenv("AUDIO_TTS_MODEL", "tts-1"), # OpenAI default model
)
AUDIO_TTS_VOICE = PersistentConfig(
"AUDIO_TTS_VOICE",
"audio.tts.voice",
os.getenv("AUDIO_TTS_VOICE", "alloy"),
os.getenv("AUDIO_TTS_VOICE", "alloy"), # OpenAI default voice
)
####################################
# Database
####################################
DATABASE_URL = os.environ.get("DATABASE_URL", f"sqlite:///{DATA_DIR}/webui.db")
# Replace the postgres:// with postgresql://
if "postgres://" in DATABASE_URL:
DATABASE_URL = DATABASE_URL.replace("postgres://", "postgresql://")
AUDIO_TTS_SPLIT_ON = PersistentConfig(
"AUDIO_TTS_SPLIT_ON",
"audio.tts.split_on",
os.getenv("AUDIO_TTS_SPLIT_ON", "punctuation"),
)
+1
View File
@@ -100,3 +100,4 @@ class TASKS(str, Enum):
EMOJI_GENERATION = "emoji_generation"
QUERY_GENERATION = "query_generation"
FUNCTION_CALLING = "function_calling"
MOA_RESPONSE_GENERATION = "moa_response_generation"
-36
View File
@@ -1,36 +0,0 @@
{
"version": 0,
"ui": {
"default_locale": "",
"prompt_suggestions": [
{
"title": ["Help me study", "vocabulary for a college entrance exam"],
"content": "Help me study vocabulary: write a sentence for me to fill in the blank, and I'll try to pick the correct option."
},
{
"title": ["Give me ideas", "for what to do with my kids' art"],
"content": "What are 5 creative things I could do with my kids' art? I don't want to throw them away, but it's also so much clutter."
},
{
"title": ["Tell me a fun fact", "about the Roman Empire"],
"content": "Tell me a random fun fact about the Roman Empire"
},
{
"title": ["Show me a code snippet", "of a website's sticky header"],
"content": "Show me a code snippet of a website's sticky header in CSS and JavaScript."
},
{
"title": ["Explain options trading", "if I'm familiar with buying and selling stocks"],
"content": "Explain options trading in simple terms if I'm familiar with buying and selling stocks."
},
{
"title": ["Overcome procrastination", "give me tips"],
"content": "Could you start by asking me about instances when I procrastinate the most and then give me some suggestions to overcome it?"
},
{
"title": ["Grammar check", "rewrite it for better readability "],
"content": "Check the following sentence for grammar and clarity: \"[sentence]\". Rewrite it for better readability while maintaining its original meaning."
}
]
}
}
+252
View File
@@ -0,0 +1,252 @@
from pathlib import Path
import os
import logging
import sys
import json
import importlib.metadata
import pkgutil
from urllib.parse import urlparse
from datetime import datetime
import markdown
from bs4 import BeautifulSoup
from constants import ERROR_MESSAGES
####################################
# Load .env file
####################################
BACKEND_DIR = Path(__file__).parent # the path containing this file
BASE_DIR = BACKEND_DIR.parent # the path containing the backend/
print(BASE_DIR)
try:
from dotenv import load_dotenv, find_dotenv
load_dotenv(find_dotenv(str(BASE_DIR / ".env")))
except ImportError:
print("dotenv not installed, skipping...")
####################################
# LOGGING
####################################
log_levels = ["CRITICAL", "ERROR", "WARNING", "INFO", "DEBUG"]
GLOBAL_LOG_LEVEL = os.environ.get("GLOBAL_LOG_LEVEL", "").upper()
if GLOBAL_LOG_LEVEL in log_levels:
logging.basicConfig(stream=sys.stdout, level=GLOBAL_LOG_LEVEL, force=True)
else:
GLOBAL_LOG_LEVEL = "INFO"
log = logging.getLogger(__name__)
log.info(f"GLOBAL_LOG_LEVEL: {GLOBAL_LOG_LEVEL}")
log_sources = [
"AUDIO",
"COMFYUI",
"CONFIG",
"DB",
"IMAGES",
"MAIN",
"MODELS",
"OLLAMA",
"OPENAI",
"RAG",
"WEBHOOK",
]
SRC_LOG_LEVELS = {}
for source in log_sources:
log_env_var = source + "_LOG_LEVEL"
SRC_LOG_LEVELS[source] = os.environ.get(log_env_var, "").upper()
if SRC_LOG_LEVELS[source] not in log_levels:
SRC_LOG_LEVELS[source] = GLOBAL_LOG_LEVEL
log.info(f"{log_env_var}: {SRC_LOG_LEVELS[source]}")
log.setLevel(SRC_LOG_LEVELS["CONFIG"])
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)
####################################
ENV = os.environ.get("ENV", "dev")
try:
PACKAGE_DATA = json.loads((BASE_DIR / "package.json").read_text())
except Exception:
try:
PACKAGE_DATA = {"version": importlib.metadata.version("open-webui")}
except importlib.metadata.PackageNotFoundError:
PACKAGE_DATA = {"version": "0.0.0"}
VERSION = PACKAGE_DATA["version"]
# Function to parse each section
def parse_section(section):
items = []
for li in section.find_all("li"):
# Extract raw HTML string
raw_html = str(li)
# Extract text without HTML tags
text = li.get_text(separator=" ", strip=True)
# Split into title and content
parts = text.split(": ", 1)
title = parts[0].strip() if len(parts) > 1 else ""
content = parts[1].strip() if len(parts) > 1 else text
items.append({"title": title, "content": content, "raw": raw_html})
return items
try:
changelog_path = BASE_DIR / "CHANGELOG.md"
with open(str(changelog_path.absolute()), "r", encoding="utf8") as file:
changelog_content = file.read()
except Exception:
changelog_content = (pkgutil.get_data("open_webui", "CHANGELOG.md") or b"").decode()
# Convert markdown content to HTML
html_content = markdown.markdown(changelog_content)
# Parse the HTML content
soup = BeautifulSoup(html_content, "html.parser")
# Initialize JSON structure
changelog_json = {}
# Iterate over each version
for version in soup.find_all("h2"):
version_number = version.get_text().strip().split(" - ")[0][1:-1] # Remove brackets
date = version.get_text().strip().split(" - ")[1]
version_data = {"date": date}
# Find the next sibling that is a h3 tag (section title)
current = version.find_next_sibling()
while current and current.name != "h2":
if current.name == "h3":
section_title = current.get_text().lower() # e.g., "added", "fixed"
section_items = parse_section(current.find_next_sibling("ul"))
version_data[section_title] = section_items
# Move to the next element
current = current.find_next_sibling()
changelog_json[version_number] = version_data
CHANGELOG = changelog_json
####################################
# SAFE_MODE
####################################
SAFE_MODE = os.environ.get("SAFE_MODE", "false").lower() == "true"
####################################
# WEBUI_BUILD_HASH
####################################
WEBUI_BUILD_HASH = os.environ.get("WEBUI_BUILD_HASH", "dev-build")
####################################
# DATA/FRONTEND BUILD DIR
####################################
DATA_DIR = Path(os.getenv("DATA_DIR", BACKEND_DIR / "data")).resolve()
FRONTEND_BUILD_DIR = Path(os.getenv("FRONTEND_BUILD_DIR", BASE_DIR / "build")).resolve()
RESET_CONFIG_ON_START = (
os.environ.get("RESET_CONFIG_ON_START", "False").lower() == "true"
)
if RESET_CONFIG_ON_START:
try:
os.remove(f"{DATA_DIR}/config.json")
with open(f"{DATA_DIR}/config.json", "w") as f:
f.write("{}")
except Exception:
pass
try:
CONFIG_DATA = json.loads((DATA_DIR / "config.json").read_text())
except Exception:
CONFIG_DATA = {}
####################################
# Database
####################################
# Check if the file exists
if os.path.exists(f"{DATA_DIR}/ollama.db"):
# Rename the file
os.rename(f"{DATA_DIR}/ollama.db", f"{DATA_DIR}/webui.db")
log.info("Database migrated from Ollama-WebUI successfully.")
else:
pass
DATABASE_URL = os.environ.get("DATABASE_URL", f"sqlite:///{DATA_DIR}/webui.db")
# Replace the postgres:// with postgresql://
if "postgres://" in DATABASE_URL:
DATABASE_URL = DATABASE_URL.replace("postgres://", "postgresql://")
####################################
# WEBUI_AUTH (Required for security)
####################################
WEBUI_AUTH = os.environ.get("WEBUI_AUTH", "True").lower() == "true"
WEBUI_AUTH_TRUSTED_EMAIL_HEADER = os.environ.get(
"WEBUI_AUTH_TRUSTED_EMAIL_HEADER", None
)
WEBUI_AUTH_TRUSTED_NAME_HEADER = os.environ.get("WEBUI_AUTH_TRUSTED_NAME_HEADER", None)
####################################
# WEBUI_SECRET_KEY
####################################
WEBUI_SECRET_KEY = os.environ.get(
"WEBUI_SECRET_KEY",
os.environ.get(
"WEBUI_JWT_SECRET_KEY", "t0p-s3cr3t"
), # DEPRECATED: remove at next major version
)
WEBUI_SESSION_COOKIE_SAME_SITE = os.environ.get(
"WEBUI_SESSION_COOKIE_SAME_SITE",
os.environ.get("WEBUI_SESSION_COOKIE_SAME_SITE", "lax"),
)
WEBUI_SESSION_COOKIE_SECURE = os.environ.get(
"WEBUI_SESSION_COOKIE_SECURE",
os.environ.get("WEBUI_SESSION_COOKIE_SECURE", "false").lower() == "true",
)
if WEBUI_AUTH and WEBUI_SECRET_KEY == "":
raise ValueError(ERROR_MESSAGES.ENV_VAR_NOT_FOUND)
+471 -520
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -18,7 +18,7 @@ from apps.webui.models.users import User
from apps.webui.models.files import File
from apps.webui.models.functions import Function
from config import DATABASE_URL
from env import DATABASE_URL
# this is the Alembic Config object, which provides
# access to the values within the .ini file in use.
@@ -0,0 +1,43 @@
"""Add config table
Revision ID: ca81bd47c050
Revises: 7e5b5dc7342b
Create Date: 2024-08-25 15:26:35.241684
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
import apps.webui.internal.db
# revision identifiers, used by Alembic.
revision: str = "ca81bd47c050"
down_revision: Union[str, None] = "7e5b5dc7342b"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade():
op.create_table(
"config",
sa.Column("id", sa.Integer, primary_key=True),
sa.Column("data", sa.JSON(), nullable=False),
sa.Column("version", sa.Integer, nullable=False),
sa.Column(
"created_at", sa.DateTime(), nullable=False, server_default=sa.func.now()
),
sa.Column(
"updated_at",
sa.DateTime(),
nullable=True,
server_default=sa.func.now(),
onupdate=sa.func.now(),
),
)
def downgrade():
op.drop_table("config")
+15 -14
View File
@@ -1,5 +1,5 @@
fastapi==0.111.0
uvicorn[standard]==0.22.0
uvicorn[standard]==0.30.6
pydantic==2.8.2
python-multipart==0.0.9
@@ -11,19 +11,19 @@ python-jose==3.3.0
passlib[bcrypt]==1.7.4
requests==2.32.3
aiohttp==3.9.5
aiohttp==3.10.2
sqlalchemy==2.0.31
sqlalchemy==2.0.32
alembic==1.13.2
peewee==3.17.6
peewee-migrate==1.12.2
psycopg2-binary==2.9.9
PyMySQL==1.1.1
bcrypt==4.1.3
bcrypt==4.2.0
pymongo
redis
boto3==1.34.153
boto3==1.35.0
argon2-cffi==23.1.0
APScheduler==3.10.4
@@ -34,18 +34,19 @@ anthropic
google-generativeai==0.7.2
tiktoken
langchain==0.2.11
langchain-community==0.2.10
langchain==0.2.14
langchain-community==0.2.12
langchain-chroma==0.1.2
fake-useragent==1.5.1
chromadb==0.5.4
chromadb==0.5.5
sentence-transformers==3.0.1
pypdf==4.3.1
docx2txt==0.8
python-pptx==1.0.0
unstructured==0.15.0
Markdown==3.6
unstructured==0.15.7
nltk==3.9.1
Markdown==3.7
pypandoc==1.13
pandas==2.2.2
openpyxl==3.1.5
@@ -60,13 +61,13 @@ rapidocr-onnxruntime==1.3.24
fpdf2==2.7.9
rank-bm25==0.2.2
faster-whisper==1.0.2
faster-whisper==1.0.3
PyJWT[crypto]==2.8.0
PyJWT[crypto]==2.9.0
authlib==1.3.1
black==24.8.0
langfuse==2.39.2
langfuse==2.44.0
youtube-transcript-api==0.6.2
pytube==15.0.0
@@ -76,5 +77,5 @@ duckduckgo-search~=6.2.1
## Tests
docker~=7.1.0
pytest~=8.2.2
pytest~=8.3.2
pytest-docker~=3.1.1
-1
View File
@@ -30,7 +30,6 @@ 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
# Check if SPACE_ID is set, if so, configure for space
if [ -n "$SPACE_ID" ]; then
echo "Configuring for HuggingFace Space deployment"
Binary file not shown.
+56 -15
View File
@@ -2,14 +2,14 @@ from pathlib import Path
import hashlib
import re
from datetime import timedelta
from typing import Optional, List, Tuple
from typing import Optional, Callable
import uuid
import time
from utils.task import prompt_template
def get_last_user_message_item(messages: List[dict]) -> Optional[dict]:
def get_last_user_message_item(messages: list[dict]) -> Optional[dict]:
for message in reversed(messages):
if message["role"] == "user":
return message
@@ -26,7 +26,7 @@ def get_content_from_message(message: dict) -> Optional[str]:
return None
def get_last_user_message(messages: List[dict]) -> Optional[str]:
def get_last_user_message(messages: list[dict]) -> Optional[str]:
message = get_last_user_message_item(messages)
if message is None:
return None
@@ -34,31 +34,31 @@ def get_last_user_message(messages: List[dict]) -> Optional[str]:
return get_content_from_message(message)
def get_last_assistant_message(messages: List[dict]) -> Optional[str]:
def get_last_assistant_message(messages: list[dict]) -> Optional[str]:
for message in reversed(messages):
if message["role"] == "assistant":
return get_content_from_message(message)
return None
def get_system_message(messages: List[dict]) -> Optional[dict]:
def get_system_message(messages: list[dict]) -> Optional[dict]:
for message in messages:
if message["role"] == "system":
return message
return None
def remove_system_message(messages: List[dict]) -> List[dict]:
def remove_system_message(messages: list[dict]) -> list[dict]:
return [message for message in messages if message["role"] != "system"]
def pop_system_message(messages: List[dict]) -> Tuple[Optional[dict], List[dict]]:
def pop_system_message(messages: list[dict]) -> tuple[Optional[dict], list[dict]]:
return get_system_message(messages), remove_system_message(messages)
def prepend_to_first_user_message_content(
content: str, messages: List[dict]
) -> List[dict]:
content: str, messages: list[dict]
) -> list[dict]:
for message in messages:
if message["role"] == "user":
if isinstance(message["content"], list):
@@ -71,7 +71,7 @@ def prepend_to_first_user_message_content(
return messages
def add_or_update_system_message(content: str, messages: List[dict]):
def add_or_update_system_message(content: str, messages: list[dict]):
"""
Adds a new system message at the beginning of the messages list
or updates the existing system message at the beginning.
@@ -82,7 +82,7 @@ def add_or_update_system_message(content: str, messages: List[dict]):
"""
if messages and messages[0].get("role") == "system":
messages[0]["content"] += f"{content}\n{messages[0]['content']}"
messages[0]["content"] = f"{content}\n{messages[0]['content']}"
else:
# Insert at the beginning
messages.insert(0, {"role": "system", "content": content})
@@ -135,10 +135,21 @@ def apply_model_system_prompt_to_body(params: dict, form_data: dict, user) -> di
# inplace function: form_data is modified
def apply_model_params_to_body(params: dict, form_data: dict) -> dict:
def apply_model_params_to_body(
params: dict, form_data: dict, mappings: dict[str, Callable]
) -> dict:
if not params:
return form_data
for key, cast_func in mappings.items():
if (value := params.get(key)) is not None:
form_data[key] = cast_func(value)
return form_data
# inplace function: form_data is modified
def apply_model_params_to_body_openai(params: dict, form_data: dict) -> dict:
mappings = {
"temperature": float,
"top_p": int,
@@ -147,10 +158,40 @@ def apply_model_params_to_body(params: dict, form_data: dict) -> dict:
"seed": lambda x: x,
"stop": lambda x: [bytes(s, "utf-8").decode("unicode_escape") for s in x],
}
return apply_model_params_to_body(params, form_data, mappings)
for key, cast_func in mappings.items():
if (value := params.get(key)) is not None:
form_data[key] = cast_func(value)
def apply_model_params_to_body_ollama(params: dict, form_data: dict) -> dict:
opts = [
"temperature",
"top_p",
"seed",
"mirostat",
"mirostat_eta",
"mirostat_tau",
"num_ctx",
"num_batch",
"num_keep",
"repeat_last_n",
"tfs_z",
"top_k",
"min_p",
"use_mmap",
"use_mlock",
"num_thread",
"num_gpu",
]
mappings = {i: lambda x: x for i in opts}
form_data = apply_model_params_to_body(params, form_data, mappings)
name_differences = {
"max_tokens": "num_predict",
"frequency_penalty": "repeat_penalty",
}
for key, value in name_differences.items():
if (param := params.get(key, None)) is not None:
form_data[value] = param
return form_data
+104
View File
@@ -0,0 +1,104 @@
from pydantic import BaseModel, Field, create_model
from typing import Any, Optional, Type
def json_schema_to_model(tool_dict: dict[str, Any]) -> Type[BaseModel]:
"""
Converts a JSON schema to a Pydantic BaseModel class.
Args:
json_schema: The JSON schema to convert.
Returns:
A Pydantic BaseModel class.
"""
# Extract the model name from the schema title.
model_name = tool_dict["name"]
schema = tool_dict["parameters"]
# Extract the field definitions from the schema properties.
field_definitions = {
name: json_schema_to_pydantic_field(name, prop, schema.get("required", []))
for name, prop in schema.get("properties", {}).items()
}
# Create the BaseModel class using create_model().
return create_model(model_name, **field_definitions)
def json_schema_to_pydantic_field(
name: str, json_schema: dict[str, Any], required: list[str]
) -> Any:
"""
Converts a JSON schema property to a Pydantic field definition.
Args:
name: The field name.
json_schema: The JSON schema property.
Returns:
A Pydantic field definition.
"""
# Get the field type.
type_ = json_schema_to_pydantic_type(json_schema)
# Get the field description.
description = json_schema.get("description")
# Get the field examples.
examples = json_schema.get("examples")
# Create a Field object with the type, description, and examples.
# The 'required' flag will be set later when creating the model.
return (
type_,
Field(
description=description,
examples=examples,
default=... if name in required else None,
),
)
def json_schema_to_pydantic_type(json_schema: dict[str, Any]) -> Any:
"""
Converts a JSON schema type to a Pydantic type.
Args:
json_schema: The JSON schema to convert.
Returns:
A Pydantic type.
"""
type_ = json_schema.get("type")
if type_ == "string" or type_ == "str":
return str
elif type_ == "integer" or type_ == "int":
return int
elif type_ == "number" or type_ == "float":
return float
elif type_ == "boolean" or type_ == "bool":
return bool
elif type_ == "array":
items_schema = json_schema.get("items")
if items_schema:
item_type = json_schema_to_pydantic_type(items_schema)
return list[item_type]
else:
return list
elif type_ == "object":
# Handle nested models.
properties = json_schema.get("properties")
if properties:
nested_model = json_schema_to_model(json_schema)
return nested_model
else:
return dict
elif type_ == "null":
return Optional[Any] # Use Optional[Any] for nullable fields
else:
raise ValueError(f"Unsupported JSON schema type: {type_}")
+37
View File
@@ -121,6 +121,43 @@ def search_query_generation_template(
return template
def moa_response_generation_template(
template: str, prompt: str, responses: list[str]
) -> str:
def replacement_function(match):
full_match = match.group(0)
start_length = match.group(1)
end_length = match.group(2)
middle_length = match.group(3)
if full_match == "{{prompt}}":
return prompt
elif start_length is not None:
return prompt[: int(start_length)]
elif end_length is not None:
return prompt[-int(end_length) :]
elif middle_length is not None:
middle_length = int(middle_length)
if len(prompt) <= middle_length:
return prompt
start = prompt[: math.ceil(middle_length / 2)]
end = prompt[-math.floor(middle_length / 2) :]
return f"{start}...{end}"
return ""
template = re.sub(
r"{{prompt}}|{{prompt:start:(\d+)}}|{{prompt:end:(\d+)}}|{{prompt:middletruncate:(\d+)}}",
replacement_function,
template,
)
responses = [f'"""{response}"""' for response in responses]
responses = "\n\n".join(responses)
template = template.replace("{{responses}}", responses)
return template
def tools_function_calling_generation_template(template: str, tools_specs: str) -> str:
template = template.replace("{{TOOLS}}", tools_specs)
return template
+87 -2
View File
@@ -1,5 +1,90 @@
import inspect
from typing import get_type_hints, List, Dict, Any
import logging
from typing import Awaitable, Callable, get_type_hints
from apps.webui.models.tools import Tools
from apps.webui.models.users import UserModel
from apps.webui.utils import load_toolkit_module_by_id
from utils.schemas import json_schema_to_model
log = logging.getLogger(__name__)
def apply_extra_params_to_tool_function(
function: Callable, extra_params: dict
) -> Callable[..., Awaitable]:
sig = inspect.signature(function)
extra_params = {
key: value for key, value in extra_params.items() if key in sig.parameters
}
is_coroutine = inspect.iscoroutinefunction(function)
async def new_function(**kwargs):
extra_kwargs = kwargs | extra_params
if is_coroutine:
return await function(**extra_kwargs)
return function(**extra_kwargs)
return new_function
# Mutation on extra_params
def get_tools(
webui_app, tool_ids: list[str], user: UserModel, extra_params: dict
) -> dict[str, dict]:
tools = {}
for tool_id in tool_ids:
toolkit = Tools.get_tool_by_id(tool_id)
if toolkit is None:
continue
module = webui_app.state.TOOLS.get(tool_id, None)
if module is None:
module, _ = load_toolkit_module_by_id(tool_id)
webui_app.state.TOOLS[tool_id] = module
extra_params["__id__"] = tool_id
if hasattr(module, "valves") and hasattr(module, "Valves"):
valves = Tools.get_tool_valves_by_id(tool_id) or {}
module.valves = module.Valves(**valves)
if hasattr(module, "UserValves"):
extra_params["__user__"]["valves"] = module.UserValves( # type: ignore
**Tools.get_user_valves_by_id_and_user_id(tool_id, user.id)
)
for spec in toolkit.specs:
# TODO: Fix hack for OpenAI API
for val in spec.get("parameters", {}).get("properties", {}).values():
if val["type"] == "str":
val["type"] = "string"
function_name = spec["name"]
# convert to function that takes only model params and inserts custom params
original_func = getattr(module, function_name)
callable = apply_extra_params_to_tool_function(original_func, extra_params)
if hasattr(original_func, "__doc__"):
callable.__doc__ = original_func.__doc__
# TODO: This needs to be a pydantic model
tool_dict = {
"toolkit_id": tool_id,
"callable": callable,
"spec": spec,
"pydantic_model": json_schema_to_model(spec),
"file_handler": hasattr(module, "file_handler") and module.file_handler,
"citation": hasattr(module, "citation") and module.citation,
}
# TODO: if collision, prepend toolkit name
if function_name in tools:
log.warning(f"Tool {function_name} already exists in another toolkit!")
log.warning(f"Collision between {toolkit} and {tool_id}.")
log.warning(f"Discarding {toolkit}.{function_name}")
else:
tools[function_name] = tool_dict
return tools
def doc_to_dict(docstring):
@@ -16,7 +101,7 @@ def doc_to_dict(docstring):
return ret_dict
def get_tools_specs(tools) -> List[dict]:
def get_tools_specs(tools) -> list[dict]:
function_list = [
{"name": func, "function": getattr(tools, func)}
for func in dir(tools)
+4 -4
View File
@@ -6,16 +6,16 @@ from apps.webui.models.users import Users
from typing import Union, Optional
from constants import ERROR_MESSAGES
from passlib.context import CryptContext
from datetime import datetime, timedelta
from datetime import datetime, timedelta, UTC
import jwt
import uuid
import logging
import config
from env import WEBUI_SECRET_KEY
logging.getLogger("passlib").setLevel(logging.ERROR)
SESSION_SECRET = config.WEBUI_SECRET_KEY
SESSION_SECRET = WEBUI_SECRET_KEY
ALGORITHM = "HS256"
##############
@@ -40,7 +40,7 @@ def create_token(data: dict, expires_delta: Union[timedelta, None] = None) -> st
payload = data.copy()
if expires_delta:
expire = datetime.utcnow() + expires_delta
expire = datetime.now(UTC) + expires_delta
payload.update({"exp": expire})
encoded_jwt = jwt.encode(payload, SESSION_SECRET, algorithm=ALGORITHM)
+18 -13
View File
@@ -38,9 +38,10 @@ describe('Settings', () => {
// 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');
// .chat-assistant is created after the first token is received
cy.get('.chat-assistant', { timeout: 10_000 }).should('exist');
// Generation Info is created after the stop token is received
cy.get('div[aria-label="Generation Info"]', { timeout: 120_000 }).should('exist');
});
it('user can share chat', () => {
@@ -57,21 +58,24 @@ describe('Settings', () => {
// 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');
// .chat-assistant is created after the first token is received
cy.get('.chat-assistant', { timeout: 10_000 }).should('exist');
// Generation Info is created after the stop token is received
cy.get('div[aria-label="Generation Info"]', { timeout: 120_000 }).should('exist');
// spy on requests
const spy = cy.spy();
cy.intercept('GET', '/api/v1/chats/*', spy);
cy.intercept('POST', '/api/v1/chats/**/share', spy);
// Open context menu
cy.get('#chat-context-menu-button').click();
// Click share button
cy.get('#chat-share-button').click();
// Check if the share dialog is visible
cy.get('#copy-and-share-chat-button').should('exist');
cy.wrap({}, { timeout: 5000 }).should(() => {
// Check if the request was made twice (once for to replace chat object and once more due to change event)
expect(spy).to.be.callCount(2);
// Click the copy button
cy.get('#copy-and-share-chat-button').click();
cy.wrap({}, { timeout: 5_000 }).should(() => {
// Check if the share request was made
expect(spy).to.be.callCount(1);
});
});
@@ -89,9 +93,10 @@ describe('Settings', () => {
// 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');
// .chat-assistant is created after the first token is received
cy.get('.chat-assistant', { timeout: 10_000 }).should('exist');
// Generation Info is created after the stop token is received
cy.get('div[aria-label="Generation Info"]', { timeout: 120_000 }).should('exist');
// Click on the generate image button
cy.get('[aria-label="Generate Image"]').click();
// Wait for image to be visible
-1
View File
@@ -22,7 +22,6 @@ Noticed something off? Have an idea? Check our [Issues tab](https://github.com/o
> [!IMPORTANT]
>
> - **Template Compliance:** Please be aware that failure to follow the provided issue template, or not providing the requested information at all, will likely result in your issue being closed without further consideration. This approach is critical for maintaining the manageability and integrity of issue tracking.
>
> - **Detail is Key:** To ensure your issue is understood and can be effectively addressed, it's imperative to include comprehensive details. Descriptions should be clear, including steps to reproduce, expected outcomes, and actual results. Lack of sufficient detail may hinder our ability to resolve your issue.
### 🧭 Scope of Support
+10 -2
View File
@@ -9,10 +9,18 @@ Our primary goal is to ensure the protection and confidentiality of sensitive da
| main | :white_check_mark: |
| others | :x: |
## Zero Tolerance for External Platforms
Based on a precedent of an unacceptable degree of spamming and unsolicited communications from third-party platforms, we forcefully reaffirm our stance. **We refuse to engage with, join, or monitor any platforms outside of GitHub for vulnerability reporting.** Our reasons are not just procedural but are deep-seated in the ethos of our project, which champions transparency and direct community interaction inherent in the open-source culture. Any attempts to divert our processes to external platforms will be met with outright rejection. This policy is non-negotiable and understands no exceptions.
Any reports or solicitations arriving from sources other than our designated GitHub repository will be dismissed without consideration. Weve seen how external engagements can dilute and compromise the integrity of community-driven projects, and were not here to gamble with the security and privacy of our user community.
## Reporting a Vulnerability
We appreciate the community's interest in identifying potential vulnerabilities. However, effective immediately, we will **not** accept low-effort vulnerability reports. To ensure that submissions are constructive and actionable, please adhere to the following guidelines:
Reports not submitted through our designated GitHub repository will be disregarded, and we will categorically reject invitations to collaborate on external platforms. Our aggressive stance on this matter underscores our commitment to a secure, transparent, and open community where all operations are visible and contributors are accountable.
1. **No Vague Reports**: Submissions such as "I found a vulnerability" without any details will be treated as spam and will not be accepted.
2. **In-Depth Understanding Required**: Reports must reflect a clear understanding of the codebase and provide specific details about the vulnerability, including the affected components and potential impacts.
@@ -23,7 +31,7 @@ We appreciate the community's interest in identifying potential vulnerabilities.
5. **Streamlined Merging Process**: When vulnerability reports meet the above criteria, we can consider them for immediate merging, similar to regular pull requests. Well-structured and thorough submissions will expedite the process of enhancing our security.
Submissions that do not meet these criteria will be closed, and repeat offenders may face a ban from future submissions. We aim to create a respectful and constructive reporting environment, where high-quality submissions foster better security for everyone.
**Non-compliant submissions will be closed, and repeat violators may be banned.** Our goal is to foster a constructive reporting environment where quality submissions promote better security for all users.
## Product Security
@@ -33,4 +41,4 @@ For immediate concerns or detailed reports that meet our guidelines, please crea
---
_Last updated on **2024-08-06**._
_Last updated on **2024-08-19**._
+375 -604
View File
File diff suppressed because it is too large Load Diff
+22 -16
View File
@@ -1,6 +1,6 @@
{
"name": "open-webui",
"version": "0.3.12",
"version": "0.3.16",
"private": true,
"scripts": {
"dev": "npm run pyodide:fetch && vite dev --host",
@@ -20,30 +20,31 @@
"pyodide:fetch": "node scripts/prepare-pyodide.js"
},
"devDependencies": {
"@sveltejs/adapter-auto": "^2.0.0",
"@sveltejs/adapter-static": "^2.0.3",
"@sveltejs/kit": "^1.30.0",
"@tailwindcss/typography": "^0.5.10",
"@sveltejs/adapter-auto": "3.2.2",
"@sveltejs/adapter-static": "^3.0.2",
"@sveltejs/kit": "^2.5.20",
"@sveltejs/vite-plugin-svelte": "^3.1.1",
"@tailwindcss/typography": "^0.5.13",
"@types/bun": "latest",
"@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",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-cypress": "^3.4.0",
"eslint-plugin-svelte": "^2.43.0",
"i18next-parser": "^9.0.1",
"postcss": "^8.4.31",
"prettier": "^2.8.0",
"prettier-plugin-svelte": "^2.10.1",
"svelte": "^4.0.5",
"svelte-check": "^3.4.3",
"prettier": "^3.3.3",
"prettier-plugin-svelte": "^3.2.6",
"svelte": "^4.2.18",
"svelte-check": "^3.8.5",
"svelte-confetti": "^1.3.2",
"tailwindcss": "^3.3.3",
"tslib": "^2.4.1",
"typescript": "^5.0.0",
"vite": "^4.4.2",
"typescript": "^5.5.4",
"vite": "^5.3.5",
"vitest": "^1.6.0"
},
"type": "module",
@@ -52,12 +53,13 @@
"@codemirror/lang-python": "^6.1.6",
"@codemirror/theme-one-dark": "^6.1.2",
"@pyscript/core": "^0.4.32",
"@sveltejs/adapter-node": "^1.3.1",
"@sveltejs/adapter-node": "^2.0.0",
"async": "^3.2.5",
"bits-ui": "^0.19.7",
"codemirror": "^6.0.1",
"crc-32": "^1.2.2",
"dayjs": "^1.11.10",
"dompurify": "^3.1.6",
"eventsource-parser": "^1.1.2",
"file-saver": "^2.0.5",
"fuse.js": "^7.0.0",
@@ -77,5 +79,9 @@
"tippy.js": "^6.3.7",
"turndown": "^7.2.0",
"uuid": "^9.0.1"
},
"engines": {
"node": ">=18.13.0 <=21.x.x",
"npm": ">=6.0.0"
}
}
+15 -14
View File
@@ -1,13 +1,13 @@
[project]
name = "open-webui"
description = "Open WebUI (Formerly Ollama WebUI)"
description = "Open WebUI"
authors = [
{ name = "Timothy Jaeryang Baek", email = "tim@openwebui.com" }
]
license = { file = "LICENSE" }
dependencies = [
"fastapi==0.111.0",
"uvicorn[standard]==0.22.0",
"uvicorn[standard]==0.30.6",
"pydantic==2.8.2",
"python-multipart==0.0.9",
@@ -19,19 +19,19 @@ dependencies = [
"passlib[bcrypt]==1.7.4",
"requests==2.32.3",
"aiohttp==3.9.5",
"aiohttp==3.10.2",
"sqlalchemy==2.0.31",
"sqlalchemy==2.0.32",
"alembic==1.13.2",
"peewee==3.17.6",
"peewee-migrate==1.12.2",
"psycopg2-binary==2.9.9",
"PyMySQL==1.1.1",
"bcrypt==4.1.3",
"bcrypt==4.2.0",
"pymongo",
"redis",
"boto3==1.34.153",
"boto3==1.35.0",
"argon2-cffi==23.1.0",
"APScheduler==3.10.4",
@@ -41,18 +41,19 @@ dependencies = [
"google-generativeai==0.7.2",
"tiktoken",
"langchain==0.2.11",
"langchain-community==0.2.10",
"langchain==0.2.14",
"langchain-community==0.2.12",
"langchain-chroma==0.1.2",
"fake-useragent==1.5.1",
"chromadb==0.5.4",
"chromadb==0.5.5",
"sentence-transformers==3.0.1",
"pypdf==4.3.1",
"docx2txt==0.8",
"python-pptx==1.0.0",
"unstructured==0.15.0",
"Markdown==3.6",
"unstructured==0.15.7",
"nltk==3.9.1",
"Markdown==3.7",
"pypandoc==1.13",
"pandas==2.2.2",
"openpyxl==3.1.5",
@@ -67,13 +68,13 @@ dependencies = [
"fpdf2==2.7.9",
"rank-bm25==0.2.2",
"faster-whisper==1.0.2",
"faster-whisper==1.0.3",
"PyJWT[crypto]==2.8.0",
"PyJWT[crypto]==2.9.0",
"authlib==1.3.1",
"black==24.8.0",
"langfuse==2.39.2",
"langfuse==2.44.0",
"youtube-transcript-api==0.6.2",
"pytube==15.0.0",
-748
View File
@@ -1,748 +0,0 @@
# generated by rye
# use `rye lock` or `rye sync` to update this lockfile
#
# last locked with the following flags:
# pre: false
# features: []
# all-features: false
# with-sources: false
# generate-hashes: false
# universal: false
-e file:.
aiohttp==3.9.5
# via langchain
# via langchain-community
# via open-webui
aiosignal==1.3.1
# via aiohttp
alembic==1.13.2
# via open-webui
annotated-types==0.6.0
# via pydantic
anthropic==0.32.0
# via open-webui
anyio==4.4.0
# via anthropic
# via httpx
# via langfuse
# via openai
# via starlette
# via watchfiles
apscheduler==3.10.4
# via open-webui
argon2-cffi==23.1.0
# via open-webui
argon2-cffi-bindings==21.2.0
# via argon2-cffi
asgiref==3.8.1
# via opentelemetry-instrumentation-asgi
attrs==23.2.0
# via aiohttp
# via pytest-docker
authlib==1.3.1
# via open-webui
av==11.0.0
# via faster-whisper
backoff==2.2.1
# via langfuse
# via posthog
# via unstructured
bcrypt==4.1.3
# via chromadb
# via open-webui
# via passlib
beautifulsoup4==4.12.3
# via extract-msg
# via unstructured
bidict==0.23.1
# via python-socketio
black==24.8.0
# via open-webui
blinker==1.8.2
# via flask
boto3==1.34.153
# via open-webui
botocore==1.34.155
# via boto3
# via s3transfer
build==1.2.1
# via chromadb
cachetools==5.3.3
# via google-auth
certifi==2024.2.2
# via httpcore
# via httpx
# via kubernetes
# via requests
# via unstructured-client
cffi==1.16.0
# via argon2-cffi-bindings
# via cryptography
chardet==5.2.0
# via unstructured
charset-normalizer==3.3.2
# via requests
# via unstructured-client
chroma-hnswlib==0.7.5
# via chromadb
chromadb==0.5.4
# via langchain-chroma
# via open-webui
click==8.1.7
# via black
# via duckduckgo-search
# via flask
# via nltk
# via peewee-migrate
# via typer
# via uvicorn
colorclass==2.2.2
# via oletools
coloredlogs==15.0.1
# via onnxruntime
compressed-rtf==1.0.6
# via extract-msg
cryptography==42.0.7
# via authlib
# via msoffcrypto-tool
# via pyjwt
ctranslate2==4.2.1
# via faster-whisper
dataclasses-json==0.6.6
# via langchain-community
# via unstructured
# via unstructured-client
deepdiff==7.0.1
# via unstructured-client
defusedxml==0.7.1
# via fpdf2
deprecated==1.2.14
# via opentelemetry-api
# via opentelemetry-exporter-otlp-proto-grpc
distro==1.9.0
# via anthropic
# via openai
dnspython==2.6.1
# via email-validator
# via pymongo
docker==7.1.0
# via open-webui
docx2txt==0.8
# via open-webui
duckduckgo-search==6.2.6
# via open-webui
easygui==0.98.3
# via oletools
ebcdic==1.1.1
# via extract-msg
ecdsa==0.19.0
# via python-jose
email-validator==2.1.1
# via fastapi
emoji==2.11.1
# via unstructured
et-xmlfile==1.1.0
# via openpyxl
extract-msg==0.48.5
# via open-webui
fake-useragent==1.5.1
# via open-webui
fastapi==0.111.0
# via chromadb
# via langchain-chroma
# via open-webui
fastapi-cli==0.0.4
# via fastapi
faster-whisper==1.0.2
# via open-webui
filelock==3.14.0
# via huggingface-hub
# via torch
# via transformers
filetype==1.2.0
# via unstructured
flask==3.0.3
# via flask-cors
# via open-webui
flask-cors==4.0.1
# via open-webui
flatbuffers==24.3.25
# via onnxruntime
fonttools==4.51.0
# via fpdf2
fpdf2==2.7.9
# via open-webui
frozenlist==1.4.1
# via aiohttp
# via aiosignal
fsspec==2024.3.1
# via huggingface-hub
# via torch
google-ai-generativelanguage==0.6.6
# via google-generativeai
google-api-core==2.19.0
# via google-ai-generativelanguage
# via google-api-python-client
# via google-generativeai
google-api-python-client==2.129.0
# via google-generativeai
google-auth==2.29.0
# via google-ai-generativelanguage
# via google-api-core
# via google-api-python-client
# via google-auth-httplib2
# via google-generativeai
# via kubernetes
google-auth-httplib2==0.2.0
# via google-api-python-client
google-generativeai==0.7.2
# via open-webui
googleapis-common-protos==1.63.0
# via google-api-core
# via grpcio-status
# via opentelemetry-exporter-otlp-proto-grpc
grpcio==1.63.0
# via chromadb
# via google-api-core
# via grpcio-status
# via opentelemetry-exporter-otlp-proto-grpc
grpcio-status==1.62.2
# via google-api-core
h11==0.14.0
# via httpcore
# via uvicorn
# via wsproto
httpcore==1.0.5
# via httpx
httplib2==0.22.0
# via google-api-python-client
# via google-auth-httplib2
httptools==0.6.1
# via uvicorn
httpx==0.27.0
# via anthropic
# via chromadb
# via fastapi
# via langfuse
# via openai
huggingface-hub==0.23.0
# via faster-whisper
# via sentence-transformers
# via tokenizers
# via transformers
humanfriendly==10.0
# via coloredlogs
idna==3.7
# via anyio
# via email-validator
# via httpx
# via langfuse
# via requests
# via unstructured-client
# via yarl
importlib-metadata==7.0.0
# via opentelemetry-api
importlib-resources==6.4.0
# via chromadb
iniconfig==2.0.0
# via pytest
itsdangerous==2.2.0
# via flask
jinja2==3.1.4
# via fastapi
# via flask
# via torch
jiter==0.5.0
# via anthropic
jmespath==1.0.1
# via boto3
# via botocore
joblib==1.4.2
# via nltk
# via scikit-learn
jsonpatch==1.33
# via langchain-core
jsonpath-python==1.0.6
# via unstructured-client
jsonpointer==2.4
# via jsonpatch
kubernetes==29.0.0
# via chromadb
langchain==0.2.11
# via langchain-community
# via open-webui
langchain-chroma==0.1.2
# via open-webui
langchain-community==0.2.10
# via open-webui
langchain-core==0.2.28
# via langchain
# via langchain-chroma
# via langchain-community
# via langchain-text-splitters
langchain-text-splitters==0.2.0
# via langchain
langdetect==1.0.9
# via unstructured
langfuse==2.39.2
# via open-webui
langsmith==0.1.96
# via langchain
# via langchain-community
# via langchain-core
lark==1.1.8
# via rtfde
lxml==5.2.2
# via python-pptx
# via unstructured
mako==1.3.5
# via alembic
markdown==3.6
# via open-webui
markdown-it-py==3.0.0
# via rich
markupsafe==2.1.5
# via jinja2
# via mako
# via werkzeug
marshmallow==3.21.2
# via dataclasses-json
# via unstructured-client
mdurl==0.1.2
# via markdown-it-py
mmh3==4.1.0
# via chromadb
monotonic==1.6
# via posthog
mpmath==1.3.0
# via sympy
msoffcrypto-tool==5.4.1
# via oletools
multidict==6.0.5
# via aiohttp
# via yarl
mypy-extensions==1.0.0
# via black
# via typing-inspect
# via unstructured-client
networkx==3.3
# via torch
nltk==3.8.1
# via unstructured
numpy==1.26.4
# via chroma-hnswlib
# via chromadb
# via ctranslate2
# via langchain
# via langchain-chroma
# via langchain-community
# via onnxruntime
# via opencv-python
# via opencv-python-headless
# via pandas
# via rank-bm25
# via rapidocr-onnxruntime
# via scikit-learn
# via scipy
# via sentence-transformers
# via shapely
# via transformers
# via unstructured
oauthlib==3.2.2
# via kubernetes
# via requests-oauthlib
olefile==0.47
# via extract-msg
# via msoffcrypto-tool
# via oletools
oletools==0.60.1
# via pcodedmp
# via rtfde
onnxruntime==1.17.3
# via chromadb
# via faster-whisper
# via rapidocr-onnxruntime
openai==1.38.0
# via open-webui
opencv-python==4.9.0.80
# via rapidocr-onnxruntime
opencv-python-headless==4.10.0.84
# via open-webui
openpyxl==3.1.5
# via open-webui
opentelemetry-api==1.24.0
# via chromadb
# via opentelemetry-exporter-otlp-proto-grpc
# via opentelemetry-instrumentation
# via opentelemetry-instrumentation-asgi
# via opentelemetry-instrumentation-fastapi
# via opentelemetry-sdk
opentelemetry-exporter-otlp-proto-common==1.24.0
# via opentelemetry-exporter-otlp-proto-grpc
opentelemetry-exporter-otlp-proto-grpc==1.24.0
# via chromadb
opentelemetry-instrumentation==0.45b0
# via opentelemetry-instrumentation-asgi
# via opentelemetry-instrumentation-fastapi
opentelemetry-instrumentation-asgi==0.45b0
# via opentelemetry-instrumentation-fastapi
opentelemetry-instrumentation-fastapi==0.45b0
# via chromadb
opentelemetry-proto==1.24.0
# via opentelemetry-exporter-otlp-proto-common
# via opentelemetry-exporter-otlp-proto-grpc
opentelemetry-sdk==1.24.0
# via chromadb
# via opentelemetry-exporter-otlp-proto-grpc
opentelemetry-semantic-conventions==0.45b0
# via opentelemetry-instrumentation-asgi
# via opentelemetry-instrumentation-fastapi
# via opentelemetry-sdk
opentelemetry-util-http==0.45b0
# via opentelemetry-instrumentation-asgi
# via opentelemetry-instrumentation-fastapi
ordered-set==4.1.0
# via deepdiff
orjson==3.10.3
# via chromadb
# via fastapi
# via langsmith
overrides==7.7.0
# via chromadb
packaging==23.2
# via black
# via build
# via huggingface-hub
# via langchain-core
# via langfuse
# via marshmallow
# via onnxruntime
# via pytest
# via transformers
# via unstructured-client
pandas==2.2.2
# via open-webui
passlib==1.7.4
# via open-webui
pathspec==0.12.1
# via black
pcodedmp==1.2.6
# via oletools
peewee==3.17.6
# via open-webui
# via peewee-migrate
peewee-migrate==1.12.2
# via open-webui
pillow==10.3.0
# via fpdf2
# via python-pptx
# via rapidocr-onnxruntime
# via sentence-transformers
platformdirs==4.2.1
# via black
pluggy==1.5.0
# via pytest
posthog==3.5.0
# via chromadb
primp==0.5.5
# via duckduckgo-search
proto-plus==1.23.0
# via google-ai-generativelanguage
# via google-api-core
protobuf==4.25.3
# via google-ai-generativelanguage
# via google-api-core
# via google-generativeai
# via googleapis-common-protos
# via grpcio-status
# via onnxruntime
# via opentelemetry-proto
# via proto-plus
psutil==6.0.0
# via open-webui
# via unstructured
psycopg2-binary==2.9.9
# via open-webui
pyasn1==0.6.0
# via pyasn1-modules
# via python-jose
# via rsa
pyasn1-modules==0.4.0
# via google-auth
pyclipper==1.3.0.post5
# via rapidocr-onnxruntime
pycparser==2.22
# via cffi
pydantic==2.8.2
# via anthropic
# via chromadb
# via fastapi
# via google-generativeai
# via langchain
# via langchain-core
# via langfuse
# via langsmith
# via open-webui
# via openai
pydantic-core==2.20.1
# via pydantic
pydub==0.25.1
# via open-webui
pygments==2.18.0
# via rich
pyjwt==2.8.0
# via open-webui
pymongo==4.8.0
# via open-webui
pymysql==1.1.1
# via open-webui
pypandoc==1.13
# via open-webui
pyparsing==2.4.7
# via httplib2
# via oletools
pypdf==4.3.1
# via open-webui
# via unstructured-client
pypika==0.48.9
# via chromadb
pyproject-hooks==1.1.0
# via build
pytest==8.2.2
# via open-webui
# via pytest-docker
pytest-docker==3.1.1
# via open-webui
python-dateutil==2.9.0.post0
# via botocore
# via kubernetes
# via pandas
# via posthog
# via unstructured-client
python-dotenv==1.0.1
# via uvicorn
python-engineio==4.9.0
# via python-socketio
python-iso639==2024.4.27
# via unstructured
python-jose==3.3.0
# via open-webui
python-magic==0.4.27
# via unstructured
python-multipart==0.0.9
# via fastapi
# via open-webui
python-pptx==1.0.0
# via open-webui
python-socketio==5.11.3
# via open-webui
pytube==15.0.0
# via open-webui
pytz==2024.1
# via apscheduler
# via pandas
pyxlsb==1.0.10
# via open-webui
pyyaml==6.0.1
# via chromadb
# via ctranslate2
# via huggingface-hub
# via kubernetes
# via langchain
# via langchain-community
# via langchain-core
# via rapidocr-onnxruntime
# via transformers
# via uvicorn
rank-bm25==0.2.2
# via open-webui
rapidfuzz==3.9.0
# via unstructured
rapidocr-onnxruntime==1.3.24
# via open-webui
red-black-tree-mod==1.20
# via extract-msg
redis==5.0.8
# via open-webui
regex==2024.5.10
# via nltk
# via tiktoken
# via transformers
requests==2.32.3
# via docker
# via google-api-core
# via huggingface-hub
# via kubernetes
# via langchain
# via langchain-community
# via langsmith
# via open-webui
# via posthog
# via requests-oauthlib
# via tiktoken
# via transformers
# via unstructured
# via unstructured-client
# via youtube-transcript-api
requests-oauthlib==2.0.0
# via kubernetes
rich==13.7.1
# via typer
rsa==4.9
# via google-auth
# via python-jose
rtfde==0.1.1
# via extract-msg
s3transfer==0.10.1
# via boto3
safetensors==0.4.3
# via transformers
scikit-learn==1.4.2
# via sentence-transformers
scipy==1.13.0
# via scikit-learn
# via sentence-transformers
sentence-transformers==3.0.1
# via open-webui
setuptools==69.5.1
# via ctranslate2
# via opentelemetry-instrumentation
shapely==2.0.5
# via rapidocr-onnxruntime
shellingham==1.5.4
# via typer
simple-websocket==1.0.0
# via python-engineio
six==1.16.0
# via apscheduler
# via ecdsa
# via kubernetes
# via langdetect
# via posthog
# via python-dateutil
# via rapidocr-onnxruntime
# via unstructured-client
sniffio==1.3.1
# via anthropic
# via anyio
# via httpx
# via openai
soupsieve==2.5
# via beautifulsoup4
sqlalchemy==2.0.31
# via alembic
# via langchain
# via langchain-community
# via open-webui
starlette==0.37.2
# via fastapi
sympy==1.12
# via onnxruntime
# via torch
tabulate==0.9.0
# via unstructured
tenacity==8.3.0
# via chromadb
# via langchain
# via langchain-community
# via langchain-core
threadpoolctl==3.5.0
# via scikit-learn
tiktoken==0.7.0
# via open-webui
tokenizers==0.15.2
# via anthropic
# via chromadb
# via faster-whisper
# via transformers
torch==2.3.0
# via sentence-transformers
tqdm==4.66.4
# via chromadb
# via google-generativeai
# via huggingface-hub
# via nltk
# via openai
# via sentence-transformers
# via transformers
# via unstructured
transformers==4.39.3
# via sentence-transformers
typer==0.12.3
# via chromadb
# via fastapi-cli
typing-extensions==4.11.0
# via alembic
# via anthropic
# via chromadb
# via fastapi
# via google-generativeai
# via huggingface-hub
# via langchain-core
# via openai
# via opentelemetry-sdk
# via pydantic
# via pydantic-core
# via python-pptx
# via sqlalchemy
# via torch
# via typer
# via typing-inspect
# via unstructured
# via unstructured-client
typing-inspect==0.9.0
# via dataclasses-json
# via unstructured-client
tzdata==2024.1
# via pandas
tzlocal==5.2
# via apscheduler
# via extract-msg
ujson==5.10.0
# via fastapi
unstructured==0.15.0
# via open-webui
unstructured-client==0.22.0
# via unstructured
uritemplate==4.1.1
# via google-api-python-client
urllib3==2.2.1
# via botocore
# via docker
# via kubernetes
# via requests
# via unstructured-client
uvicorn==0.22.0
# via chromadb
# via fastapi
# via open-webui
uvloop==0.19.0
# via uvicorn
validators==0.33.0
# via open-webui
watchfiles==0.21.0
# via uvicorn
websocket-client==1.8.0
# via kubernetes
websockets==12.0
# via uvicorn
werkzeug==3.0.3
# via flask
wrapt==1.16.0
# via deprecated
# via langfuse
# via opentelemetry-instrumentation
# via unstructured
wsproto==1.2.0
# via simple-websocket
xlrd==2.0.1
# via open-webui
xlsxwriter==3.2.0
# via python-pptx
yarl==1.9.4
# via aiohttp
youtube-transcript-api==0.6.2
# via open-webui
zipp==3.18.1
# via importlib-metadata
-748
View File
@@ -1,748 +0,0 @@
# generated by rye
# use `rye lock` or `rye sync` to update this lockfile
#
# last locked with the following flags:
# pre: false
# features: []
# all-features: false
# with-sources: false
# generate-hashes: false
# universal: false
-e file:.
aiohttp==3.9.5
# via langchain
# via langchain-community
# via open-webui
aiosignal==1.3.1
# via aiohttp
alembic==1.13.2
# via open-webui
annotated-types==0.6.0
# via pydantic
anthropic==0.32.0
# via open-webui
anyio==4.4.0
# via anthropic
# via httpx
# via langfuse
# via openai
# via starlette
# via watchfiles
apscheduler==3.10.4
# via open-webui
argon2-cffi==23.1.0
# via open-webui
argon2-cffi-bindings==21.2.0
# via argon2-cffi
asgiref==3.8.1
# via opentelemetry-instrumentation-asgi
attrs==23.2.0
# via aiohttp
# via pytest-docker
authlib==1.3.1
# via open-webui
av==11.0.0
# via faster-whisper
backoff==2.2.1
# via langfuse
# via posthog
# via unstructured
bcrypt==4.1.3
# via chromadb
# via open-webui
# via passlib
beautifulsoup4==4.12.3
# via extract-msg
# via unstructured
bidict==0.23.1
# via python-socketio
black==24.8.0
# via open-webui
blinker==1.8.2
# via flask
boto3==1.34.153
# via open-webui
botocore==1.34.155
# via boto3
# via s3transfer
build==1.2.1
# via chromadb
cachetools==5.3.3
# via google-auth
certifi==2024.2.2
# via httpcore
# via httpx
# via kubernetes
# via requests
# via unstructured-client
cffi==1.16.0
# via argon2-cffi-bindings
# via cryptography
chardet==5.2.0
# via unstructured
charset-normalizer==3.3.2
# via requests
# via unstructured-client
chroma-hnswlib==0.7.5
# via chromadb
chromadb==0.5.4
# via langchain-chroma
# via open-webui
click==8.1.7
# via black
# via duckduckgo-search
# via flask
# via nltk
# via peewee-migrate
# via typer
# via uvicorn
colorclass==2.2.2
# via oletools
coloredlogs==15.0.1
# via onnxruntime
compressed-rtf==1.0.6
# via extract-msg
cryptography==42.0.7
# via authlib
# via msoffcrypto-tool
# via pyjwt
ctranslate2==4.2.1
# via faster-whisper
dataclasses-json==0.6.6
# via langchain-community
# via unstructured
# via unstructured-client
deepdiff==7.0.1
# via unstructured-client
defusedxml==0.7.1
# via fpdf2
deprecated==1.2.14
# via opentelemetry-api
# via opentelemetry-exporter-otlp-proto-grpc
distro==1.9.0
# via anthropic
# via openai
dnspython==2.6.1
# via email-validator
# via pymongo
docker==7.1.0
# via open-webui
docx2txt==0.8
# via open-webui
duckduckgo-search==6.2.6
# via open-webui
easygui==0.98.3
# via oletools
ebcdic==1.1.1
# via extract-msg
ecdsa==0.19.0
# via python-jose
email-validator==2.1.1
# via fastapi
emoji==2.11.1
# via unstructured
et-xmlfile==1.1.0
# via openpyxl
extract-msg==0.48.5
# via open-webui
fake-useragent==1.5.1
# via open-webui
fastapi==0.111.0
# via chromadb
# via langchain-chroma
# via open-webui
fastapi-cli==0.0.4
# via fastapi
faster-whisper==1.0.2
# via open-webui
filelock==3.14.0
# via huggingface-hub
# via torch
# via transformers
filetype==1.2.0
# via unstructured
flask==3.0.3
# via flask-cors
# via open-webui
flask-cors==4.0.1
# via open-webui
flatbuffers==24.3.25
# via onnxruntime
fonttools==4.51.0
# via fpdf2
fpdf2==2.7.9
# via open-webui
frozenlist==1.4.1
# via aiohttp
# via aiosignal
fsspec==2024.3.1
# via huggingface-hub
# via torch
google-ai-generativelanguage==0.6.6
# via google-generativeai
google-api-core==2.19.0
# via google-ai-generativelanguage
# via google-api-python-client
# via google-generativeai
google-api-python-client==2.129.0
# via google-generativeai
google-auth==2.29.0
# via google-ai-generativelanguage
# via google-api-core
# via google-api-python-client
# via google-auth-httplib2
# via google-generativeai
# via kubernetes
google-auth-httplib2==0.2.0
# via google-api-python-client
google-generativeai==0.7.2
# via open-webui
googleapis-common-protos==1.63.0
# via google-api-core
# via grpcio-status
# via opentelemetry-exporter-otlp-proto-grpc
grpcio==1.63.0
# via chromadb
# via google-api-core
# via grpcio-status
# via opentelemetry-exporter-otlp-proto-grpc
grpcio-status==1.62.2
# via google-api-core
h11==0.14.0
# via httpcore
# via uvicorn
# via wsproto
httpcore==1.0.5
# via httpx
httplib2==0.22.0
# via google-api-python-client
# via google-auth-httplib2
httptools==0.6.1
# via uvicorn
httpx==0.27.0
# via anthropic
# via chromadb
# via fastapi
# via langfuse
# via openai
huggingface-hub==0.23.0
# via faster-whisper
# via sentence-transformers
# via tokenizers
# via transformers
humanfriendly==10.0
# via coloredlogs
idna==3.7
# via anyio
# via email-validator
# via httpx
# via langfuse
# via requests
# via unstructured-client
# via yarl
importlib-metadata==7.0.0
# via opentelemetry-api
importlib-resources==6.4.0
# via chromadb
iniconfig==2.0.0
# via pytest
itsdangerous==2.2.0
# via flask
jinja2==3.1.4
# via fastapi
# via flask
# via torch
jiter==0.5.0
# via anthropic
jmespath==1.0.1
# via boto3
# via botocore
joblib==1.4.2
# via nltk
# via scikit-learn
jsonpatch==1.33
# via langchain-core
jsonpath-python==1.0.6
# via unstructured-client
jsonpointer==2.4
# via jsonpatch
kubernetes==29.0.0
# via chromadb
langchain==0.2.11
# via langchain-community
# via open-webui
langchain-chroma==0.1.2
# via open-webui
langchain-community==0.2.10
# via open-webui
langchain-core==0.2.28
# via langchain
# via langchain-chroma
# via langchain-community
# via langchain-text-splitters
langchain-text-splitters==0.2.0
# via langchain
langdetect==1.0.9
# via unstructured
langfuse==2.39.2
# via open-webui
langsmith==0.1.96
# via langchain
# via langchain-community
# via langchain-core
lark==1.1.8
# via rtfde
lxml==5.2.2
# via python-pptx
# via unstructured
mako==1.3.5
# via alembic
markdown==3.6
# via open-webui
markdown-it-py==3.0.0
# via rich
markupsafe==2.1.5
# via jinja2
# via mako
# via werkzeug
marshmallow==3.21.2
# via dataclasses-json
# via unstructured-client
mdurl==0.1.2
# via markdown-it-py
mmh3==4.1.0
# via chromadb
monotonic==1.6
# via posthog
mpmath==1.3.0
# via sympy
msoffcrypto-tool==5.4.1
# via oletools
multidict==6.0.5
# via aiohttp
# via yarl
mypy-extensions==1.0.0
# via black
# via typing-inspect
# via unstructured-client
networkx==3.3
# via torch
nltk==3.8.1
# via unstructured
numpy==1.26.4
# via chroma-hnswlib
# via chromadb
# via ctranslate2
# via langchain
# via langchain-chroma
# via langchain-community
# via onnxruntime
# via opencv-python
# via opencv-python-headless
# via pandas
# via rank-bm25
# via rapidocr-onnxruntime
# via scikit-learn
# via scipy
# via sentence-transformers
# via shapely
# via transformers
# via unstructured
oauthlib==3.2.2
# via kubernetes
# via requests-oauthlib
olefile==0.47
# via extract-msg
# via msoffcrypto-tool
# via oletools
oletools==0.60.1
# via pcodedmp
# via rtfde
onnxruntime==1.17.3
# via chromadb
# via faster-whisper
# via rapidocr-onnxruntime
openai==1.38.0
# via open-webui
opencv-python==4.9.0.80
# via rapidocr-onnxruntime
opencv-python-headless==4.10.0.84
# via open-webui
openpyxl==3.1.5
# via open-webui
opentelemetry-api==1.24.0
# via chromadb
# via opentelemetry-exporter-otlp-proto-grpc
# via opentelemetry-instrumentation
# via opentelemetry-instrumentation-asgi
# via opentelemetry-instrumentation-fastapi
# via opentelemetry-sdk
opentelemetry-exporter-otlp-proto-common==1.24.0
# via opentelemetry-exporter-otlp-proto-grpc
opentelemetry-exporter-otlp-proto-grpc==1.24.0
# via chromadb
opentelemetry-instrumentation==0.45b0
# via opentelemetry-instrumentation-asgi
# via opentelemetry-instrumentation-fastapi
opentelemetry-instrumentation-asgi==0.45b0
# via opentelemetry-instrumentation-fastapi
opentelemetry-instrumentation-fastapi==0.45b0
# via chromadb
opentelemetry-proto==1.24.0
# via opentelemetry-exporter-otlp-proto-common
# via opentelemetry-exporter-otlp-proto-grpc
opentelemetry-sdk==1.24.0
# via chromadb
# via opentelemetry-exporter-otlp-proto-grpc
opentelemetry-semantic-conventions==0.45b0
# via opentelemetry-instrumentation-asgi
# via opentelemetry-instrumentation-fastapi
# via opentelemetry-sdk
opentelemetry-util-http==0.45b0
# via opentelemetry-instrumentation-asgi
# via opentelemetry-instrumentation-fastapi
ordered-set==4.1.0
# via deepdiff
orjson==3.10.3
# via chromadb
# via fastapi
# via langsmith
overrides==7.7.0
# via chromadb
packaging==23.2
# via black
# via build
# via huggingface-hub
# via langchain-core
# via langfuse
# via marshmallow
# via onnxruntime
# via pytest
# via transformers
# via unstructured-client
pandas==2.2.2
# via open-webui
passlib==1.7.4
# via open-webui
pathspec==0.12.1
# via black
pcodedmp==1.2.6
# via oletools
peewee==3.17.6
# via open-webui
# via peewee-migrate
peewee-migrate==1.12.2
# via open-webui
pillow==10.3.0
# via fpdf2
# via python-pptx
# via rapidocr-onnxruntime
# via sentence-transformers
platformdirs==4.2.1
# via black
pluggy==1.5.0
# via pytest
posthog==3.5.0
# via chromadb
primp==0.5.5
# via duckduckgo-search
proto-plus==1.23.0
# via google-ai-generativelanguage
# via google-api-core
protobuf==4.25.3
# via google-ai-generativelanguage
# via google-api-core
# via google-generativeai
# via googleapis-common-protos
# via grpcio-status
# via onnxruntime
# via opentelemetry-proto
# via proto-plus
psutil==6.0.0
# via open-webui
# via unstructured
psycopg2-binary==2.9.9
# via open-webui
pyasn1==0.6.0
# via pyasn1-modules
# via python-jose
# via rsa
pyasn1-modules==0.4.0
# via google-auth
pyclipper==1.3.0.post5
# via rapidocr-onnxruntime
pycparser==2.22
# via cffi
pydantic==2.8.2
# via anthropic
# via chromadb
# via fastapi
# via google-generativeai
# via langchain
# via langchain-core
# via langfuse
# via langsmith
# via open-webui
# via openai
pydantic-core==2.20.1
# via pydantic
pydub==0.25.1
# via open-webui
pygments==2.18.0
# via rich
pyjwt==2.8.0
# via open-webui
pymongo==4.8.0
# via open-webui
pymysql==1.1.1
# via open-webui
pypandoc==1.13
# via open-webui
pyparsing==2.4.7
# via httplib2
# via oletools
pypdf==4.3.1
# via open-webui
# via unstructured-client
pypika==0.48.9
# via chromadb
pyproject-hooks==1.1.0
# via build
pytest==8.2.2
# via open-webui
# via pytest-docker
pytest-docker==3.1.1
# via open-webui
python-dateutil==2.9.0.post0
# via botocore
# via kubernetes
# via pandas
# via posthog
# via unstructured-client
python-dotenv==1.0.1
# via uvicorn
python-engineio==4.9.0
# via python-socketio
python-iso639==2024.4.27
# via unstructured
python-jose==3.3.0
# via open-webui
python-magic==0.4.27
# via unstructured
python-multipart==0.0.9
# via fastapi
# via open-webui
python-pptx==1.0.0
# via open-webui
python-socketio==5.11.3
# via open-webui
pytube==15.0.0
# via open-webui
pytz==2024.1
# via apscheduler
# via pandas
pyxlsb==1.0.10
# via open-webui
pyyaml==6.0.1
# via chromadb
# via ctranslate2
# via huggingface-hub
# via kubernetes
# via langchain
# via langchain-community
# via langchain-core
# via rapidocr-onnxruntime
# via transformers
# via uvicorn
rank-bm25==0.2.2
# via open-webui
rapidfuzz==3.9.0
# via unstructured
rapidocr-onnxruntime==1.3.24
# via open-webui
red-black-tree-mod==1.20
# via extract-msg
redis==5.0.8
# via open-webui
regex==2024.5.10
# via nltk
# via tiktoken
# via transformers
requests==2.32.3
# via docker
# via google-api-core
# via huggingface-hub
# via kubernetes
# via langchain
# via langchain-community
# via langsmith
# via open-webui
# via posthog
# via requests-oauthlib
# via tiktoken
# via transformers
# via unstructured
# via unstructured-client
# via youtube-transcript-api
requests-oauthlib==2.0.0
# via kubernetes
rich==13.7.1
# via typer
rsa==4.9
# via google-auth
# via python-jose
rtfde==0.1.1
# via extract-msg
s3transfer==0.10.1
# via boto3
safetensors==0.4.3
# via transformers
scikit-learn==1.4.2
# via sentence-transformers
scipy==1.13.0
# via scikit-learn
# via sentence-transformers
sentence-transformers==3.0.1
# via open-webui
setuptools==69.5.1
# via ctranslate2
# via opentelemetry-instrumentation
shapely==2.0.5
# via rapidocr-onnxruntime
shellingham==1.5.4
# via typer
simple-websocket==1.0.0
# via python-engineio
six==1.16.0
# via apscheduler
# via ecdsa
# via kubernetes
# via langdetect
# via posthog
# via python-dateutil
# via rapidocr-onnxruntime
# via unstructured-client
sniffio==1.3.1
# via anthropic
# via anyio
# via httpx
# via openai
soupsieve==2.5
# via beautifulsoup4
sqlalchemy==2.0.31
# via alembic
# via langchain
# via langchain-community
# via open-webui
starlette==0.37.2
# via fastapi
sympy==1.12
# via onnxruntime
# via torch
tabulate==0.9.0
# via unstructured
tenacity==8.3.0
# via chromadb
# via langchain
# via langchain-community
# via langchain-core
threadpoolctl==3.5.0
# via scikit-learn
tiktoken==0.7.0
# via open-webui
tokenizers==0.15.2
# via anthropic
# via chromadb
# via faster-whisper
# via transformers
torch==2.3.0
# via sentence-transformers
tqdm==4.66.4
# via chromadb
# via google-generativeai
# via huggingface-hub
# via nltk
# via openai
# via sentence-transformers
# via transformers
# via unstructured
transformers==4.39.3
# via sentence-transformers
typer==0.12.3
# via chromadb
# via fastapi-cli
typing-extensions==4.11.0
# via alembic
# via anthropic
# via chromadb
# via fastapi
# via google-generativeai
# via huggingface-hub
# via langchain-core
# via openai
# via opentelemetry-sdk
# via pydantic
# via pydantic-core
# via python-pptx
# via sqlalchemy
# via torch
# via typer
# via typing-inspect
# via unstructured
# via unstructured-client
typing-inspect==0.9.0
# via dataclasses-json
# via unstructured-client
tzdata==2024.1
# via pandas
tzlocal==5.2
# via apscheduler
# via extract-msg
ujson==5.10.0
# via fastapi
unstructured==0.15.0
# via open-webui
unstructured-client==0.22.0
# via unstructured
uritemplate==4.1.1
# via google-api-python-client
urllib3==2.2.1
# via botocore
# via docker
# via kubernetes
# via requests
# via unstructured-client
uvicorn==0.22.0
# via chromadb
# via fastapi
# via open-webui
uvloop==0.19.0
# via uvicorn
validators==0.33.0
# via open-webui
watchfiles==0.21.0
# via uvicorn
websocket-client==1.8.0
# via kubernetes
websockets==12.0
# via uvicorn
werkzeug==3.0.3
# via flask
wrapt==1.16.0
# via deprecated
# via langfuse
# via opentelemetry-instrumentation
# via unstructured
wsproto==1.2.0
# via simple-websocket
xlrd==2.0.1
# via open-webui
xlsxwriter==3.2.0
# via python-pptx
yarl==1.9.4
# via aiohttp
youtube-transcript-api==0.6.2
# via open-webui
zipp==3.18.1
# via importlib-metadata
+4
View File
@@ -34,6 +34,10 @@ math {
@apply rounded-lg;
}
.markdown-prose {
@apply prose dark:prose-invert prose-p:my-0 prose-img:my-1 prose-headings:my-1 prose-pre:my-0 prose-table:my-0 prose-blockquote:my-0 prose-ul:-my-0 prose-ol:-my-0 prose-li:-my-0 whitespace-pre-line;
}
.markdown a {
@apply underline;
}
+1 -1
View File
@@ -1,4 +1,4 @@
<!DOCTYPE html>
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
+5 -1
View File
@@ -132,7 +132,11 @@ export const synthesizeOpenAISpeech = async (
return res;
};
export const getModels = async (token: string = '') => {
interface AvailableModelsResponse {
models: { name: string; id: string }[] | { id: string }[];
}
export const getModels = async (token: string = ''): Promise<AvailableModelsResponse> => {
let error = null;
const res = await fetch(`${AUDIO_API_BASE_URL}/models`, {
+10 -252
View File
@@ -1,6 +1,6 @@
import { IMAGES_API_BASE_URL } from '$lib/constants';
export const getImageGenerationConfig = async (token: string = '') => {
export const getConfig = async (token: string = '') => {
let error = null;
const res = await fetch(`${IMAGES_API_BASE_URL}/config`, {
@@ -32,11 +32,7 @@ export const getImageGenerationConfig = async (token: string = '') => {
return res;
};
export const updateImageGenerationConfig = async (
token: string = '',
engine: string,
enabled: boolean
) => {
export const updateConfig = async (token: string = '', config: object) => {
let error = null;
const res = await fetch(`${IMAGES_API_BASE_URL}/config/update`, {
@@ -47,8 +43,7 @@ export const updateImageGenerationConfig = async (
...(token && { authorization: `Bearer ${token}` })
},
body: JSON.stringify({
engine,
enabled
...config
})
})
.then(async (res) => {
@@ -72,10 +67,10 @@ export const updateImageGenerationConfig = async (
return res;
};
export const getOpenAIConfig = async (token: string = '') => {
export const verifyConfigUrl = async (token: string = '') => {
let error = null;
const res = await fetch(`${IMAGES_API_BASE_URL}/openai/config`, {
const res = await fetch(`${IMAGES_API_BASE_URL}/config/url/verify`, {
method: 'GET',
headers: {
Accept: 'application/json',
@@ -104,46 +99,10 @@ export const getOpenAIConfig = async (token: string = '') => {
return res;
};
export const updateOpenAIConfig = async (token: string = '', url: string, key: string) => {
export const getImageGenerationConfig = async (token: string = '') => {
let error = null;
const res = await fetch(`${IMAGES_API_BASE_URL}/openai/config/update`, {
method: 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
...(token && { authorization: `Bearer ${token}` })
},
body: JSON.stringify({
url: url,
key: key
})
})
.then(async (res) => {
if (!res.ok) throw await res.json();
return res.json();
})
.catch((err) => {
console.log(err);
if ('detail' in err) {
error = err.detail;
} else {
error = 'Server connection failed';
}
return null;
});
if (error) {
throw error;
}
return res;
};
export const getImageGenerationEngineUrls = async (token: string = '') => {
let error = null;
const res = await fetch(`${IMAGES_API_BASE_URL}/url`, {
const res = await fetch(`${IMAGES_API_BASE_URL}/image/config`, {
method: 'GET',
headers: {
Accept: 'application/json',
@@ -172,19 +131,17 @@ export const getImageGenerationEngineUrls = async (token: string = '') => {
return res;
};
export const updateImageGenerationEngineUrls = async (token: string = '', urls: object = {}) => {
export const updateImageGenerationConfig = async (token: string = '', config: object) => {
let error = null;
const res = await fetch(`${IMAGES_API_BASE_URL}/url/update`, {
const res = await fetch(`${IMAGES_API_BASE_URL}/image/config/update`, {
method: 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
...(token && { authorization: `Bearer ${token}` })
},
body: JSON.stringify({
...urls
})
body: JSON.stringify({ ...config })
})
.then(async (res) => {
if (!res.ok) throw await res.json();
@@ -207,138 +164,6 @@ export const updateImageGenerationEngineUrls = async (token: string = '', urls:
return res;
};
export const getImageSize = async (token: string = '') => {
let error = null;
const res = await fetch(`${IMAGES_API_BASE_URL}/size`, {
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();
})
.catch((err) => {
console.log(err);
if ('detail' in err) {
error = err.detail;
} else {
error = 'Server connection failed';
}
return null;
});
if (error) {
throw error;
}
return res.IMAGE_SIZE;
};
export const updateImageSize = async (token: string = '', size: string) => {
let error = null;
const res = await fetch(`${IMAGES_API_BASE_URL}/size/update`, {
method: 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
...(token && { authorization: `Bearer ${token}` })
},
body: JSON.stringify({
size: size
})
})
.then(async (res) => {
if (!res.ok) throw await res.json();
return res.json();
})
.catch((err) => {
console.log(err);
if ('detail' in err) {
error = err.detail;
} else {
error = 'Server connection failed';
}
return null;
});
if (error) {
throw error;
}
return res.IMAGE_SIZE;
};
export const getImageSteps = async (token: string = '') => {
let error = null;
const res = await fetch(`${IMAGES_API_BASE_URL}/steps`, {
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();
})
.catch((err) => {
console.log(err);
if ('detail' in err) {
error = err.detail;
} else {
error = 'Server connection failed';
}
return null;
});
if (error) {
throw error;
}
return res.IMAGE_STEPS;
};
export const updateImageSteps = async (token: string = '', steps: number) => {
let error = null;
const res = await fetch(`${IMAGES_API_BASE_URL}/steps/update`, {
method: 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
...(token && { authorization: `Bearer ${token}` })
},
body: JSON.stringify({ steps })
})
.then(async (res) => {
if (!res.ok) throw await res.json();
return res.json();
})
.catch((err) => {
console.log(err);
if ('detail' in err) {
error = err.detail;
} else {
error = 'Server connection failed';
}
return null;
});
if (error) {
throw error;
}
return res.IMAGE_STEPS;
};
export const getImageGenerationModels = async (token: string = '') => {
let error = null;
@@ -371,73 +196,6 @@ export const getImageGenerationModels = async (token: string = '') => {
return res;
};
export const getDefaultImageGenerationModel = async (token: string = '') => {
let error = null;
const res = await fetch(`${IMAGES_API_BASE_URL}/models/default`, {
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();
})
.catch((err) => {
console.log(err);
if ('detail' in err) {
error = err.detail;
} else {
error = 'Server connection failed';
}
return null;
});
if (error) {
throw error;
}
return res.model;
};
export const updateDefaultImageGenerationModel = async (token: string = '', model: string) => {
let error = null;
const res = await fetch(`${IMAGES_API_BASE_URL}/models/default/update`, {
method: 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
...(token && { authorization: `Bearer ${token}` })
},
body: JSON.stringify({
model: model
})
})
.then(async (res) => {
if (!res.ok) throw await res.json();
return res.json();
})
.catch((err) => {
console.log(err);
if ('detail' in err) {
error = err.detail;
} else {
error = 'Server connection failed';
}
return null;
});
if (error) {
throw error;
}
return res.model;
};
export const imageGenerations = async (token: string = '', prompt: string) => {
let error = null;
+39
View File
@@ -69,6 +69,7 @@ type ChatCompletedForm = {
model: string;
messages: string[];
chat_id: string;
session_id: string;
};
export const chatCompleted = async (token: string, body: ChatCompletedForm) => {
@@ -332,6 +333,42 @@ export const generateSearchQuery = async (
return res?.choices[0]?.message?.content.replace(/["']/g, '') ?? prompt;
};
export const generateMoACompletion = async (
token: string = '',
model: string,
prompt: string,
responses: string[]
) => {
const controller = new AbortController();
let error = null;
const res = await fetch(`${WEBUI_BASE_URL}/api/task/moa/completions`, {
signal: controller.signal,
method: 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`
},
body: JSON.stringify({
model: model,
prompt: prompt,
responses: responses,
stream: true
})
}).catch((err) => {
console.log(err);
error = err;
return null;
});
if (error) {
throw error;
}
return [res, controller];
};
export const getPipelinesList = async (token: string = '') => {
let error = null;
@@ -628,6 +665,7 @@ export const getBackendConfig = async () => {
const res = await fetch(`${WEBUI_BASE_URL}/api/config`, {
method: 'GET',
credentials: 'include',
headers: {
'Content-Type': 'application/json'
}
@@ -912,6 +950,7 @@ export interface ModelConfig {
export interface ModelMeta {
description?: string;
capabilities?: object;
profile_image_url?: string;
}
export interface ModelParams {}
+1 -1
View File
@@ -156,7 +156,7 @@ export const deleteMemoryById = async (token: string, id: string) => {
export const deleteMemoriesByUserId = async (token: string) => {
let error = null;
const res = await fetch(`${WEBUI_API_BASE_URL}/memories/user`, {
const res = await fetch(`${WEBUI_API_BASE_URL}/memories/delete/user`, {
method: 'DELETE',
headers: {
Accept: 'application/json',
+1 -51
View File
@@ -1,5 +1,4 @@
import { OLLAMA_API_BASE_URL } from '$lib/constants';
import { titleGenerationTemplate } from '$lib/utils';
export const getOllamaConfig = async (token: string = '') => {
let error = null;
@@ -203,55 +202,6 @@ export const getOllamaModels = async (token: string = '') => {
});
};
// TODO: migrate to backend
export const generateTitle = async (
token: string = '',
template: string,
model: string,
prompt: string
) => {
let error = null;
template = titleGenerationTemplate(template, prompt);
console.log(template);
const res = await fetch(`${OLLAMA_API_BASE_URL}/api/generate`, {
method: 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`
},
body: JSON.stringify({
model: model,
prompt: template,
stream: false,
options: {
// Restrict the number of tokens generated to 50
num_predict: 50
}
})
})
.then(async (res) => {
if (!res.ok) throw await res.json();
return res.json();
})
.catch((err) => {
console.log(err);
if ('detail' in err) {
error = err.detail;
}
return null;
});
if (error) {
throw error;
}
return res?.response.replace(/["']/g, '') ?? 'New Chat';
};
export const generatePrompt = async (token: string = '', model: string, conversation: string) => {
let error = null;
@@ -446,7 +396,7 @@ export const deleteModel = async (token: string, tagName: string, urlIdx: string
return res;
};
export const pullModel = async (token: string, tagName: string, urlIdx: string | null = null) => {
export const pullModel = async (token: string, tagName: string, urlIdx: number | null = null) => {
let error = null;
const controller = new AbortController();
+1 -126
View File
@@ -1,6 +1,4 @@
import { OPENAI_API_BASE_URL } from '$lib/constants';
import { titleGenerationTemplate } from '$lib/utils';
import { type Model, models, settings } from '$lib/stores';
export const getOpenAIConfig = async (token: string = '') => {
let error = null;
@@ -260,7 +258,7 @@ export const getOpenAIModelsDirect = async (
throw error;
}
const models = Array.isArray(res) ? res : res?.data ?? null;
const models = Array.isArray(res) ? res : (res?.data ?? null);
return models
.map((model) => ({ id: model.id, name: model.name ?? model.id, external: true }))
@@ -330,126 +328,3 @@ export const synthesizeOpenAISpeech = async (
return res;
};
export const generateTitle = async (
token: string = '',
template: string,
model: string,
prompt: string,
chat_id?: string,
url: string = OPENAI_API_BASE_URL
) => {
let error = null;
template = titleGenerationTemplate(template, prompt);
console.log(template);
const res = await fetch(`${url}/chat/completions`, {
method: 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`
},
body: JSON.stringify({
model: model,
messages: [
{
role: 'user',
content: template
}
],
stream: false,
// Restricting the max tokens to 50 to avoid long titles
max_tokens: 50,
...(chat_id && { chat_id: chat_id }),
title: true
})
})
.then(async (res) => {
if (!res.ok) throw await res.json();
return res.json();
})
.catch((err) => {
console.log(err);
if ('detail' in err) {
error = err.detail;
}
return null;
});
if (error) {
throw error;
}
return res?.choices[0]?.message?.content.replace(/["']/g, '') ?? 'New Chat';
};
export const generateSearchQuery = async (
token: string = '',
model: string,
previousMessages: string[],
prompt: string,
url: string = OPENAI_API_BASE_URL
): Promise<string | undefined> => {
let error = null;
// TODO: Allow users to specify the prompt
// Get the current date in the format "January 20, 2024"
const currentDate = new Intl.DateTimeFormat('en-US', {
year: 'numeric',
month: 'long',
day: '2-digit'
}).format(new Date());
const res = await fetch(`${url}/chat/completions`, {
method: 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`
},
body: JSON.stringify({
model: model,
// Few shot prompting
messages: [
{
role: 'assistant',
content: `You are tasked with generating web search queries. Give me an appropriate query to answer my question for google search. Answer with only the query. Today is ${currentDate}.`
},
{
role: 'user',
content: prompt
}
// {
// role: 'user',
// content:
// (previousMessages.length > 0
// ? `Previous Questions:\n${previousMessages.join('\n')}\n\n`
// : '') + `Current Question: ${prompt}`
// }
],
stream: false,
// Restricting the max tokens to 30 to avoid long search queries
max_tokens: 30
})
})
.then(async (res) => {
if (!res.ok) throw await res.json();
return res.json();
})
.catch((err) => {
console.log(err);
if ('detail' in err) {
error = err.detail;
}
return undefined;
});
if (error) {
throw error;
}
return res?.choices[0]?.message?.content.replace(/["']/g, '') ?? undefined;
};
+2 -2
View File
@@ -400,7 +400,7 @@ export const resetUploadDir = async (token: string) => {
let error = null;
const res = await fetch(`${RAG_API_BASE_URL}/reset/uploads`, {
method: 'GET',
method: 'POST',
headers: {
Accept: 'application/json',
authorization: `Bearer ${token}`
@@ -426,7 +426,7 @@ export const resetVectorDB = async (token: string) => {
let error = null;
const res = await fetch(`${RAG_API_BASE_URL}/reset/db`, {
method: 'GET',
method: 'POST',
headers: {
Accept: 'application/json',
authorization: `Bearer ${token}`
+6 -6
View File
@@ -75,12 +75,12 @@
class="font-semibold uppercase text-xs {section === 'added'
? 'text-white bg-blue-600'
: section === 'fixed'
? 'text-white bg-green-600'
: section === 'changed'
? 'text-white bg-yellow-600'
: section === 'removed'
? 'text-white bg-red-600'
: ''} w-fit px-3 rounded-full my-2.5"
? 'text-white bg-green-600'
: section === 'changed'
? 'text-white bg-yellow-600'
: section === 'removed'
? 'text-white bg-red-600'
: ''} w-fit px-3 rounded-full my-2.5"
>
{section}
</div>
+8 -2
View File
@@ -336,8 +336,11 @@
<div class="flex-1 mt-3 lg:mt-0 overflow-y-scroll">
{#if selectedTab === 'general'}
<General
saveHandler={() => {
saveHandler={async () => {
toast.success($i18n.t('Settings saved successfully!'));
await tick();
await config.set(await getBackendConfig());
}}
/>
{:else if selectedTab === 'users'}
@@ -356,8 +359,11 @@
<Models />
{:else if selectedTab === 'documents'}
<Documents
saveHandler={() => {
on:save={async () => {
toast.success($i18n.t('Settings saved successfully!'));
await tick();
await config.set(await getBackendConfig());
}}
/>
{:else if selectedTab === 'web'}
+49 -16
View File
@@ -10,31 +10,36 @@
getModels as _getModels,
getVoices as _getVoices
} from '$lib/apis/audio';
import { user, settings, config } from '$lib/stores';
import { config } from '$lib/stores';
import SensitiveInput from '$lib/components/common/SensitiveInput.svelte';
const i18n = getContext('i18n');
import { TTS_RESPONSE_SPLIT } from '$lib/types';
export let saveHandler: Function;
import type { Writable } from 'svelte/store';
import type { i18n as i18nType } from 'i18next';
const i18n = getContext<Writable<i18nType>>('i18n');
export let saveHandler: () => void;
// Audio
let TTS_OPENAI_API_BASE_URL = '';
let TTS_OPENAI_API_KEY = '';
let TTS_API_KEY = '';
let TTS_ENGINE = '';
let TTS_MODEL = '';
let TTS_VOICE = '';
let TTS_SPLIT_ON: TTS_RESPONSE_SPLIT = TTS_RESPONSE_SPLIT.PUNCTUATION;
let STT_OPENAI_API_BASE_URL = '';
let STT_OPENAI_API_KEY = '';
let STT_ENGINE = '';
let STT_MODEL = '';
let voices = [];
let models = [];
let nonLocalVoices = false;
// eslint-disable-next-line no-undef
let voices: SpeechSynthesisVoice[] = [];
let models: Awaited<ReturnType<typeof _getModels>>['models'] = [];
const getModels = async () => {
if (TTS_ENGINE === '') {
@@ -53,8 +58,8 @@
const getVoices = async () => {
if (TTS_ENGINE === '') {
const getVoicesLoop = setInterval(async () => {
voices = await speechSynthesis.getVoices();
const getVoicesLoop = setInterval(() => {
voices = speechSynthesis.getVoices();
// do your loop
if (voices.length > 0) {
@@ -81,7 +86,8 @@
API_KEY: TTS_API_KEY,
ENGINE: TTS_ENGINE,
MODEL: TTS_MODEL,
VOICE: TTS_VOICE
VOICE: TTS_VOICE,
SPLIT_ON: TTS_SPLIT_ON
},
stt: {
OPENAI_API_BASE_URL: STT_OPENAI_API_BASE_URL,
@@ -92,9 +98,10 @@
});
if (res) {
toast.success($i18n.t('Audio settings updated successfully'));
config.set(await getBackendConfig());
saveHandler();
getBackendConfig()
.then(config.set)
.catch(() => {});
}
};
@@ -111,6 +118,8 @@
TTS_MODEL = res.tts.MODEL;
TTS_VOICE = res.tts.VOICE;
TTS_SPLIT_ON = res.tts.SPLIT_ON || TTS_RESPONSE_SPLIT.PUNCTUATION;
STT_OPENAI_API_BASE_URL = res.stt.OPENAI_API_BASE_URL;
STT_OPENAI_API_KEY = res.stt.OPENAI_API_KEY;
@@ -139,7 +148,7 @@
<div class=" self-center text-xs font-medium">{$i18n.t('Speech-to-Text Engine')}</div>
<div class="flex items-center relative">
<select
class="dark:bg-gray-900 w-fit pr-8 rounded px-2 p-1 text-xs bg-transparent outline-none text-right"
class="dark:bg-gray-900 cursor-pointer w-fit pr-8 rounded px-2 p-1 text-xs bg-transparent outline-none text-right"
bind:value={STT_ENGINE}
placeholder="Select an engine"
>
@@ -195,7 +204,7 @@
<div class=" self-center text-xs font-medium">{$i18n.t('Text-to-Speech Engine')}</div>
<div class="flex items-center relative">
<select
class=" dark:bg-gray-900 w-fit pr-8 rounded px-2 p-1 text-xs bg-transparent outline-none text-right"
class=" dark:bg-gray-900 w-fit pr-8 cursor-pointer rounded px-2 p-1 text-xs bg-transparent outline-none text-right"
bind:value={TTS_ENGINE}
placeholder="Select a mode"
on:change={async (e) => {
@@ -203,7 +212,7 @@
await getVoices();
await getModels();
if (e.target.value === 'openai') {
if (e.target?.value === 'openai') {
TTS_VOICE = 'alloy';
TTS_MODEL = 'tts-1';
} else {
@@ -351,6 +360,30 @@
</div>
</div>
{/if}
<hr class="dark:border-gray-850 my-2" />
<div class="pt-0.5 flex w-full justify-between">
<div class="self-center text-xs font-medium">{$i18n.t('Response splitting')}</div>
<div class="flex items-center relative">
<select
class="dark:bg-gray-900 w-fit pr-8 cursor-pointer rounded px-2 p-1 text-xs bg-transparent outline-none text-right"
aria-label="Select how to split message text for TTS requests"
bind:value={TTS_SPLIT_ON}
>
{#each Object.values(TTS_RESPONSE_SPLIT) as split}
<option value={split}
>{$i18n.t(split.charAt(0).toUpperCase() + split.slice(1))}</option
>
{/each}
</select>
</div>
</div>
<div class="mt-2 mb-1 text-xs text-gray-400 dark:text-gray-500">
{$i18n.t(
"Control how message text is split for TTS requests. 'Punctuation' splits into sentences, 'paragraphs' splits into paragraphs, and 'none' keeps the message as a single string."
)}
</div>
</div>
</div>
</div>
@@ -1,4 +1,8 @@
<script lang="ts">
import { onMount, getContext, createEventDispatcher } from 'svelte';
const dispatch = createEventDispatcher();
import { getDocs } from '$lib/apis/documents';
import { deleteAllFiles, deleteFileById } from '$lib/apis/files';
import {
@@ -18,14 +22,12 @@
import ResetVectorDBConfirmDialog from '$lib/components/common/ConfirmDialog.svelte';
import { documents, models } from '$lib/stores';
import { onMount, getContext } from 'svelte';
import { toast } from 'svelte-sonner';
import SensitiveInput from '$lib/components/common/SensitiveInput.svelte';
import Tooltip from '$lib/components/common/Tooltip.svelte';
const i18n = getContext('i18n');
export let saveHandler: Function;
let scanDirLoading = false;
let updateEmbeddingModelLoading = false;
let updateRerankingModelLoading = false;
@@ -37,6 +39,9 @@
let embeddingModel = '';
let rerankingModel = '';
let fileMaxSize = null;
let fileMaxCount = null;
let contentExtractionEngine = 'default';
let tikaServerUrl = '';
let showTikaServerUrl = false;
@@ -112,7 +117,7 @@
url: OpenAIUrl,
batch_size: OpenAIBatchSize
}
}
}
: {})
}).catch(async (error) => {
toast.error(error);
@@ -161,19 +166,22 @@
};
const submitHandler = async () => {
embeddingModelUpdateHandler();
await embeddingModelUpdateHandler();
if (querySettings.hybrid) {
rerankingModelUpdateHandler();
await rerankingModelUpdateHandler();
}
if (contentExtractionEngine === 'tika' && tikaServerUrl === '') {
toast.error($i18n.t('Tika Server URL required.'));
return;
}
const res = await updateRAGConfig(localStorage.token, {
pdf_extract_images: pdfExtractImages,
file: {
max_size: fileMaxSize === '' ? null : fileMaxSize,
max_count: fileMaxCount === '' ? null : fileMaxCount
},
chunk: {
chunk_overlap: chunkOverlap,
chunk_size: chunkSize
@@ -185,6 +193,8 @@
});
await updateQuerySettings(localStorage.token, querySettings);
dispatch('save');
};
const setEmbeddingConfig = async () => {
@@ -218,7 +228,6 @@
await setRerankingConfig();
querySettings = await getQuerySettings(localStorage.token);
const res = await getRAGConfig(localStorage.token);
if (res) {
@@ -230,6 +239,9 @@
contentExtractionEngine = res.content_extraction.engine;
tikaServerUrl = res.content_extraction.tika_server_url;
showTikaServerUrl = contentExtractionEngine === 'tika';
fileMaxSize = res?.file.max_size ?? '';
fileMaxCount = res?.file.max_count ?? '';
}
});
</script>
@@ -266,7 +278,6 @@
class="flex flex-col h-full justify-between space-y-3 text-sm"
on:submit|preventDefault={() => {
submitHandler();
saveHandler();
}}
>
<div class=" space-y-2.5 overflow-y-scroll scrollbar-hidden h-full pr-1.5">
@@ -610,6 +621,62 @@
</div>
{/if}
</div>
<hr class=" dark:border-gray-850" />
<div class="">
<div class="text-sm font-medium">{$i18n.t('Files')}</div>
<div class=" my-2 flex gap-1.5">
<div class="w-full">
<div class=" self-center text-xs font-medium min-w-fit mb-1">
{$i18n.t('Max Upload Size')}
</div>
<div class="self-center">
<Tooltip
content={$i18n.t(
'The maximum file size in MB. If the file size exceeds this limit, the file will not be uploaded.'
)}
placement="top-start"
>
<input
class="w-full rounded-lg py-1.5 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
type="number"
placeholder={$i18n.t('Leave empty for unlimited')}
bind:value={fileMaxSize}
autocomplete="off"
min="0"
/>
</Tooltip>
</div>
</div>
<div class=" w-full">
<div class="self-center text-xs font-medium min-w-fit mb-1">
{$i18n.t('Max Upload Count')}
</div>
<div class="self-center">
<Tooltip
content={$i18n.t(
'The maximum number of files that can be used at once in chat. If the number of files exceeds this limit, the files will not be uploaded.'
)}
placement="top-start"
>
<input
class=" w-full rounded-lg py-1.5 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
type="number"
placeholder={$i18n.t('Leave empty for unlimited')}
bind:value={fileMaxCount}
autocomplete="off"
min="0"
/>
</Tooltip>
</div>
</div>
</div>
</div>
<hr class=" dark:border-gray-850" />
<div class=" ">
@@ -1,22 +1,10 @@
<script lang="ts">
import {
getCommunitySharingEnabledStatus,
getWebhookUrl,
toggleCommunitySharingEnabledStatus,
updateWebhookUrl
} from '$lib/apis';
import {
getAdminConfig,
getDefaultUserRole,
getJWTExpiresDuration,
getSignUpEnabledStatus,
toggleSignUpEnabledStatus,
updateAdminConfig,
updateDefaultUserRole,
updateJWTExpiresDuration
} from '$lib/apis/auths';
import { getBackendConfig, getWebhookUrl, updateWebhookUrl } from '$lib/apis';
import { getAdminConfig, updateAdminConfig } from '$lib/apis/auths';
import Switch from '$lib/components/common/Switch.svelte';
import { config } from '$lib/stores';
import { onMount, getContext } from 'svelte';
import { toast } from 'svelte-sonner';
const i18n = getContext('i18n');
@@ -30,7 +18,7 @@
const res = await updateAdminConfig(localStorage.token, adminConfig);
if (res) {
toast.success(i18n.t('Settings updated successfully'));
saveHandler();
} else {
toast.error(i18n.t('Failed to update settings'));
}
@@ -51,9 +39,8 @@
<form
class="flex flex-col h-full justify-between space-y-3 text-sm"
on:submit|preventDefault={() => {
on:submit|preventDefault={async () => {
updateHandler();
saveHandler();
}}
>
<div class=" space-y-3 overflow-y-scroll scrollbar-hidden h-full">
@@ -98,6 +85,12 @@
<Switch bind:state={adminConfig.ENABLE_COMMUNITY_SHARING} />
</div>
<div class="my-3 flex w-full items-center justify-between pr-2">
<div class=" self-center text-xs font-medium">{$i18n.t('Enable Message Rating')}</div>
<Switch bind:state={adminConfig.ENABLE_MESSAGE_RATING} />
</div>
<hr class=" dark:border-gray-850 my-2" />
<div class=" w-full justify-between">
+470 -336
View File
@@ -2,152 +2,180 @@
import { toast } from 'svelte-sonner';
import { createEventDispatcher, onMount, getContext } from 'svelte';
import { config, user } from '$lib/stores';
import { config as backendConfig, user } from '$lib/stores';
import { getBackendConfig } from '$lib/apis';
import {
getImageGenerationModels,
getDefaultImageGenerationModel,
updateDefaultImageGenerationModel,
getImageSize,
getImageGenerationConfig,
updateImageGenerationConfig,
getImageGenerationEngineUrls,
updateImageGenerationEngineUrls,
updateImageSize,
getImageSteps,
updateImageSteps,
getOpenAIConfig,
updateOpenAIConfig
getConfig,
updateConfig,
verifyConfigUrl
} from '$lib/apis/images';
import { getBackendConfig } from '$lib/apis';
import SensitiveInput from '$lib/components/common/SensitiveInput.svelte';
import Switch from '$lib/components/common/Switch.svelte';
import Tooltip from '$lib/components/common/Tooltip.svelte';
const dispatch = createEventDispatcher();
const i18n = getContext('i18n');
let loading = false;
let imageGenerationEngine = '';
let enableImageGeneration = false;
let config = null;
let imageGenerationConfig = null;
let AUTOMATIC1111_BASE_URL = '';
let AUTOMATIC1111_API_AUTH = '';
let COMFYUI_BASE_URL = '';
let OPENAI_API_BASE_URL = '';
let OPENAI_API_KEY = '';
let selectedModel = '';
let models = null;
let imageSize = '';
let steps = 50;
let requiredWorkflowNodes = [
{
type: 'prompt',
key: 'text',
node_ids: ''
},
{
type: 'model',
key: 'ckpt_name',
node_ids: ''
},
{
type: 'width',
key: 'width',
node_ids: ''
},
{
type: 'height',
key: 'height',
node_ids: ''
},
{
type: 'steps',
key: 'steps',
node_ids: ''
},
{
type: 'seed',
key: 'seed',
node_ids: ''
}
];
const getModels = async () => {
models = await getImageGenerationModels(localStorage.token).catch((error) => {
toast.error(error);
return null;
});
selectedModel = await getDefaultImageGenerationModel(localStorage.token).catch((error) => {
return '';
});
};
const updateUrlHandler = async () => {
if (imageGenerationEngine === 'comfyui') {
const res = await updateImageGenerationEngineUrls(localStorage.token, {
COMFYUI_BASE_URL: COMFYUI_BASE_URL
}).catch((error) => {
toast.error(error);
console.log(error);
return null;
});
if (res) {
COMFYUI_BASE_URL = res.COMFYUI_BASE_URL;
await getModels();
if (models) {
toast.success($i18n.t('Server connection verified'));
}
} else {
({ COMFYUI_BASE_URL } = await getImageGenerationEngineUrls(localStorage.token));
}
} else {
const res = await updateImageGenerationEngineUrls(localStorage.token, {
AUTOMATIC1111_BASE_URL: AUTOMATIC1111_BASE_URL,
AUTOMATIC1111_API_AUTH: AUTOMATIC1111_API_AUTH
}).catch((error) => {
toast.error(error);
return null;
});
if (res) {
AUTOMATIC1111_BASE_URL = res.AUTOMATIC1111_BASE_URL;
AUTOMATIC1111_API_AUTH = res.AUTOMATIC1111_API_AUTH;
await getModels();
if (models) {
toast.success($i18n.t('Server connection verified'));
}
} else {
({ AUTOMATIC1111_BASE_URL, AUTOMATIC1111_API_AUTH } = await getImageGenerationEngineUrls(
localStorage.token
));
}
}
};
const updateImageGeneration = async () => {
const res = await updateImageGenerationConfig(
localStorage.token,
imageGenerationEngine,
enableImageGeneration
).catch((error) => {
const updateConfigHandler = async () => {
const res = await updateConfig(localStorage.token, config).catch((error) => {
toast.error(error);
return null;
});
if (res) {
imageGenerationEngine = res.engine;
enableImageGeneration = res.enabled;
config = res;
}
if (enableImageGeneration) {
config.set(await getBackendConfig(localStorage.token));
if (config.enabled) {
backendConfig.set(await getBackendConfig());
getModels();
}
};
const validateJSON = (json) => {
try {
const obj = JSON.parse(json);
if (obj && typeof obj === 'object') {
return true;
}
} catch (e) {}
return false;
};
const saveHandler = async () => {
loading = true;
if (config?.comfyui?.COMFYUI_WORKFLOW) {
if (!validateJSON(config.comfyui.COMFYUI_WORKFLOW)) {
toast.error('Invalid JSON format for ComfyUI Workflow.');
loading = false;
return;
}
}
if (config?.comfyui?.COMFYUI_WORKFLOW) {
config.comfyui.COMFYUI_WORKFLOW_NODES = requiredWorkflowNodes.map((node) => {
return {
type: node.type,
key: node.key,
node_ids:
node.node_ids.trim() === '' ? [] : node.node_ids.split(',').map((id) => id.trim())
};
});
}
await updateConfig(localStorage.token, config).catch((error) => {
toast.error(error);
loading = false;
return null;
});
await updateImageGenerationConfig(localStorage.token, imageGenerationConfig).catch((error) => {
toast.error(error);
loading = false;
return null;
});
getModels();
dispatch('save');
loading = false;
};
onMount(async () => {
if ($user.role === 'admin') {
const res = await getImageGenerationConfig(localStorage.token).catch((error) => {
const res = await getConfig(localStorage.token).catch((error) => {
toast.error(error);
return null;
});
if (res) {
imageGenerationEngine = res.engine;
enableImageGeneration = res.enabled;
config = res;
}
const URLS = await getImageGenerationEngineUrls(localStorage.token);
AUTOMATIC1111_BASE_URL = URLS.AUTOMATIC1111_BASE_URL;
AUTOMATIC1111_API_AUTH = URLS.AUTOMATIC1111_API_AUTH;
COMFYUI_BASE_URL = URLS.COMFYUI_BASE_URL;
const config = await getOpenAIConfig(localStorage.token);
OPENAI_API_KEY = config.OPENAI_API_KEY;
OPENAI_API_BASE_URL = config.OPENAI_API_BASE_URL;
imageSize = await getImageSize(localStorage.token);
steps = await getImageSteps(localStorage.token);
if (enableImageGeneration) {
if (config.enabled) {
getModels();
}
if (config.comfyui.COMFYUI_WORKFLOW) {
config.comfyui.COMFYUI_WORKFLOW = JSON.stringify(
JSON.parse(config.comfyui.COMFYUI_WORKFLOW),
null,
2
);
}
requiredWorkflowNodes = requiredWorkflowNodes.map((node) => {
const n = config.comfyui.COMFYUI_WORKFLOW_NODES.find((n) => n.type === node.type) ?? node;
console.log(n);
return {
type: n.type,
key: n.key,
node_ids: typeof n.node_ids === 'string' ? n.node_ids : n.node_ids.join(',')
};
});
const imageConfigRes = await getImageGenerationConfig(localStorage.token).catch((error) => {
toast.error(error);
return null;
});
if (imageConfigRes) {
imageGenerationConfig = imageConfigRes;
}
}
});
</script>
@@ -155,263 +183,369 @@
<form
class="flex flex-col h-full justify-between space-y-3 text-sm"
on:submit|preventDefault={async () => {
loading = true;
if (imageGenerationEngine === 'openai') {
await updateOpenAIConfig(localStorage.token, OPENAI_API_BASE_URL, OPENAI_API_KEY);
}
await updateDefaultImageGenerationModel(localStorage.token, selectedModel);
await updateImageSize(localStorage.token, imageSize).catch((error) => {
toast.error(error);
return null;
});
await updateImageSteps(localStorage.token, steps).catch((error) => {
toast.error(error);
return null;
});
dispatch('save');
loading = false;
saveHandler();
}}
>
<div class=" space-y-3 overflow-y-scroll scrollbar-hidden">
<div>
<div class=" mb-1 text-sm font-medium">{$i18n.t('Image Settings')}</div>
<div class=" py-0.5 flex w-full justify-between">
<div class=" self-center text-xs font-medium">{$i18n.t('Image Generation Engine')}</div>
<div class="flex items-center relative">
<select
class="w-fit pr-8 rounded px-2 p-1 text-xs bg-transparent outline-none text-right"
bind:value={imageGenerationEngine}
placeholder={$i18n.t('Select a mode')}
on:change={async () => {
await updateImageGeneration();
}}
>
<option value="">{$i18n.t('Default (Automatic1111)')}</option>
<option value="comfyui">{$i18n.t('ComfyUI')}</option>
<option value="openai">{$i18n.t('Open AI (Dall-E)')}</option>
</select>
</div>
</div>
<div class=" space-y-3 overflow-y-scroll scrollbar-hidden pr-2">
{#if config && imageGenerationConfig}
<div>
<div class=" py-0.5 flex w-full justify-between">
<div class=" self-center text-xs font-medium">
{$i18n.t('Image Generation (Experimental)')}
<div class=" mb-1 text-sm font-medium">{$i18n.t('Image Settings')}</div>
<div>
<div class=" py-0.5 flex w-full justify-between">
<div class=" self-center text-xs font-medium">
{$i18n.t('Image Generation (Experimental)')}
</div>
<div class="px-1">
<Switch
bind:state={config.enabled}
on:change={(e) => {
const enabled = e.detail;
if (enabled) {
if (
config.engine === 'automatic1111' &&
config.automatic1111.AUTOMATIC1111_BASE_URL === ''
) {
toast.error($i18n.t('AUTOMATIC1111 Base URL is required.'));
config.enabled = false;
} else if (
config.engine === 'comfyui' &&
config.comfyui.COMFYUI_BASE_URL === ''
) {
toast.error($i18n.t('ComfyUI Base URL is required.'));
config.enabled = false;
} else if (config.engine === 'openai' && config.openai.OPENAI_API_KEY === '') {
toast.error($i18n.t('OpenAI API Key is required.'));
config.enabled = false;
}
}
updateConfigHandler();
}}
/>
</div>
</div>
</div>
<button
class="p-1 px-3 text-xs flex rounded transition"
on:click={() => {
if (imageGenerationEngine === '' && AUTOMATIC1111_BASE_URL === '') {
toast.error($i18n.t('AUTOMATIC1111 Base URL is required.'));
enableImageGeneration = false;
} else if (imageGenerationEngine === 'comfyui' && COMFYUI_BASE_URL === '') {
toast.error($i18n.t('ComfyUI Base URL is required.'));
enableImageGeneration = false;
} else if (imageGenerationEngine === 'openai' && OPENAI_API_KEY === '') {
toast.error($i18n.t('OpenAI API Key is required.'));
enableImageGeneration = false;
} else {
enableImageGeneration = !enableImageGeneration;
}
updateImageGeneration();
}}
type="button"
>
{#if enableImageGeneration === true}
<span class="ml-2 self-center">{$i18n.t('On')}</span>
{:else}
<span class="ml-2 self-center">{$i18n.t('Off')}</span>
{/if}
</button>
<div class=" py-0.5 flex w-full justify-between">
<div class=" self-center text-xs font-medium">{$i18n.t('Image Generation Engine')}</div>
<div class="flex items-center relative">
<select
class="w-fit pr-8 rounded px-2 p-1 text-xs bg-transparent outline-none text-right"
bind:value={config.engine}
placeholder={$i18n.t('Select Engine')}
on:change={async () => {
updateConfigHandler();
}}
>
<option value="openai">{$i18n.t('Default (Open AI)')}</option>
<option value="comfyui">{$i18n.t('ComfyUI')}</option>
<option value="automatic1111">{$i18n.t('Automatic1111')}</option>
</select>
</div>
</div>
</div>
</div>
<hr class=" dark:border-gray-850" />
{#if imageGenerationEngine === ''}
<div class=" mb-2.5 text-sm font-medium">{$i18n.t('AUTOMATIC1111 Base URL')}</div>
<div class="flex w-full">
<div class="flex-1 mr-2">
<input
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
placeholder={$i18n.t('Enter URL (e.g. http://127.0.0.1:7860/)')}
bind:value={AUTOMATIC1111_BASE_URL}
/>
</div>
<button
class="px-2.5 bg-gray-50 hover:bg-gray-100 text-gray-800 dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-gray-100 rounded-lg transition"
type="button"
on:click={() => {
updateUrlHandler();
}}
>
<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="M15.312 11.424a5.5 5.5 0 01-9.201 2.466l-.312-.311h2.433a.75.75 0 000-1.5H3.989a.75.75 0 00-.75.75v4.242a.75.75 0 001.5 0v-2.43l.31.31a7 7 0 0011.712-3.138.75.75 0 00-1.449-.39zm1.23-3.723a.75.75 0 00.219-.53V2.929a.75.75 0 00-1.5 0V5.36l-.31-.31A7 7 0 003.239 8.188a.75.75 0 101.448.389A5.5 5.5 0 0113.89 6.11l.311.31h-2.432a.75.75 0 000 1.5h4.243a.75.75 0 00.53-.219z"
clip-rule="evenodd"
/>
</svg>
</button>
</div>
<div class="mt-2 text-xs text-gray-400 dark:text-gray-500">
{$i18n.t('Include `--api` flag when running stable-diffusion-webui')}
<a
class=" text-gray-300 font-medium"
href="https://github.com/AUTOMATIC1111/stable-diffusion-webui/discussions/3734"
target="_blank"
>
{$i18n.t('(e.g. `sh webui.sh --api`)')}
</a>
</div>
<div class=" mb-2.5 text-sm font-medium">{$i18n.t('AUTOMATIC1111 Api Auth String')}</div>
<SensitiveInput
placeholder={$i18n.t('Enter api auth string (e.g. username:password)')}
bind:value={AUTOMATIC1111_API_AUTH}
required={false}
/>
<div class="mt-2 text-xs text-gray-400 dark:text-gray-500">
{$i18n.t('Include `--api-auth` flag when running stable-diffusion-webui')}
<a
class=" text-gray-300 font-medium"
href="https://github.com/AUTOMATIC1111/stable-diffusion-webui/discussions/13993"
target="_blank"
>
{$i18n.t('(e.g. `sh webui.sh --api --api-auth username_password`)').replace('_', ':')}
</a>
</div>
{:else if imageGenerationEngine === 'comfyui'}
<div class=" mb-2.5 text-sm font-medium">{$i18n.t('ComfyUI Base URL')}</div>
<div class="flex w-full">
<div class="flex-1 mr-2">
<input
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
placeholder={$i18n.t('Enter URL (e.g. http://127.0.0.1:7860/)')}
bind:value={COMFYUI_BASE_URL}
/>
</div>
<button
class="px-2.5 bg-gray-50 hover:bg-gray-100 text-gray-800 dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-gray-100 rounded-lg transition"
type="button"
on:click={() => {
updateUrlHandler();
}}
>
<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="M15.312 11.424a5.5 5.5 0 01-9.201 2.466l-.312-.311h2.433a.75.75 0 000-1.5H3.989a.75.75 0 00-.75.75v4.242a.75.75 0 001.5 0v-2.43l.31.31a7 7 0 0011.712-3.138.75.75 0 00-1.449-.39zm1.23-3.723a.75.75 0 00.219-.53V2.929a.75.75 0 00-1.5 0V5.36l-.31-.31A7 7 0 003.239 8.188a.75.75 0 101.448.389A5.5 5.5 0 0113.89 6.11l.311.31h-2.432a.75.75 0 000 1.5h4.243a.75.75 0 00.53-.219z"
clip-rule="evenodd"
/>
</svg>
</button>
</div>
{:else if imageGenerationEngine === 'openai'}
<div>
<div class=" mb-1.5 text-sm font-medium">{$i18n.t('OpenAI API Config')}</div>
<div class="flex gap-2 mb-1">
<input
class="flex-1 w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
placeholder={$i18n.t('API Base URL')}
bind:value={OPENAI_API_BASE_URL}
required
/>
<SensitiveInput placeholder={$i18n.t('API Key')} bind:value={OPENAI_API_KEY} />
</div>
</div>
{/if}
{#if enableImageGeneration}
<hr class=" dark:border-gray-850" />
<div>
<div class=" mb-2.5 text-sm font-medium">{$i18n.t('Set Default Model')}</div>
<div class="flex w-full">
<div class="flex-1 mr-2">
{#if imageGenerationEngine === 'openai' && !OPENAI_API_BASE_URL.includes('https://api.openai.com')}
<div class="flex flex-col gap-2">
{#if (config?.engine ?? 'automatic1111') === 'automatic1111'}
<div>
<div class=" mb-2 text-sm font-medium">{$i18n.t('AUTOMATIC1111 Base URL')}</div>
<div class="flex w-full">
<div class="flex-1 mr-2">
<input
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
placeholder={$i18n.t('Enter URL (e.g. http://127.0.0.1:7860/)')}
bind:value={config.automatic1111.AUTOMATIC1111_BASE_URL}
/>
</div>
<button
class="px-2.5 bg-gray-50 hover:bg-gray-100 text-gray-800 dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-gray-100 rounded-lg transition"
type="button"
on:click={async () => {
await updateConfigHandler();
const res = await verifyConfigUrl(localStorage.token).catch((error) => {
toast.error(error);
return null;
});
if (res) {
toast.success($i18n.t('Server connection verified'));
}
}}
>
<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="M15.312 11.424a5.5 5.5 0 01-9.201 2.466l-.312-.311h2.433a.75.75 0 000-1.5H3.989a.75.75 0 00-.75.75v4.242a.75.75 0 001.5 0v-2.43l.31.31a7 7 0 0011.712-3.138.75.75 0 00-1.449-.39zm1.23-3.723a.75.75 0 00.219-.53V2.929a.75.75 0 00-1.5 0V5.36l-.31-.31A7 7 0 003.239 8.188a.75.75 0 101.448.389A5.5 5.5 0 0113.89 6.11l.311.31h-2.432a.75.75 0 000 1.5h4.243a.75.75 0 00.53-.219z"
clip-rule="evenodd"
/>
</svg>
</button>
</div>
<div class="mt-2 text-xs text-gray-400 dark:text-gray-500">
{$i18n.t('Include `--api` flag when running stable-diffusion-webui')}
<a
class=" text-gray-300 font-medium"
href="https://github.com/AUTOMATIC1111/stable-diffusion-webui/discussions/3734"
target="_blank"
>
{$i18n.t('(e.g. `sh webui.sh --api`)')}
</a>
</div>
</div>
<div>
<div class=" mb-2 text-sm font-medium">
{$i18n.t('AUTOMATIC1111 Api Auth String')}
</div>
<SensitiveInput
placeholder={$i18n.t('Enter api auth string (e.g. username:password)')}
bind:value={config.automatic1111.AUTOMATIC1111_API_AUTH}
required={false}
/>
<div class="mt-2 text-xs text-gray-400 dark:text-gray-500">
{$i18n.t('Include `--api-auth` flag when running stable-diffusion-webui')}
<a
class=" text-gray-300 font-medium"
href="https://github.com/AUTOMATIC1111/stable-diffusion-webui/discussions/13993"
target="_blank"
>
{$i18n
.t('(e.g. `sh webui.sh --api --api-auth username_password`)')
.replace('_', ':')}
</a>
</div>
</div>
{:else if config?.engine === 'comfyui'}
<div class="">
<div class=" mb-2 text-sm font-medium">{$i18n.t('ComfyUI Base URL')}</div>
<div class="flex w-full">
<div class="flex-1 mr-2">
<input
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
placeholder={$i18n.t('Enter URL (e.g. http://127.0.0.1:7860/)')}
bind:value={config.comfyui.COMFYUI_BASE_URL}
/>
</div>
<button
class="px-2.5 bg-gray-50 hover:bg-gray-100 text-gray-800 dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-gray-100 rounded-lg transition"
type="button"
on:click={async () => {
await updateConfigHandler();
const res = await verifyConfigUrl(localStorage.token).catch((error) => {
toast.error(error);
return null;
});
if (res) {
toast.success($i18n.t('Server connection verified'));
}
}}
>
<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="M15.312 11.424a5.5 5.5 0 01-9.201 2.466l-.312-.311h2.433a.75.75 0 000-1.5H3.989a.75.75 0 00-.75.75v4.242a.75.75 0 001.5 0v-2.43l.31.31a7 7 0 0011.712-3.138.75.75 0 00-1.449-.39zm1.23-3.723a.75.75 0 00.219-.53V2.929a.75.75 0 00-1.5 0V5.36l-.31-.31A7 7 0 003.239 8.188a.75.75 0 101.448.389A5.5 5.5 0 0113.89 6.11l.311.31h-2.432a.75.75 0 000 1.5h4.243a.75.75 0 00.53-.219z"
clip-rule="evenodd"
/>
</svg>
</button>
</div>
</div>
<div class="">
<div class=" mb-2 text-sm font-medium">{$i18n.t('ComfyUI Workflow')}</div>
{#if config.comfyui.COMFYUI_WORKFLOW}
<textarea
class="w-full rounded-lg mb-1 py-2 px-4 text-xs bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none disabled:text-gray-600 resize-none"
rows="10"
bind:value={config.comfyui.COMFYUI_WORKFLOW}
required
/>
{/if}
<div class="flex w-full">
<div class="flex-1">
<input
id="upload-comfyui-workflow-input"
hidden
type="file"
accept=".json"
on:change={(e) => {
const file = e.target.files[0];
const reader = new FileReader();
reader.onload = (e) => {
config.comfyui.COMFYUI_WORKFLOW = e.target.result;
e.target.value = null;
};
reader.readAsText(file);
}}
/>
<button
class="w-full text-sm font-medium py-2 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-comfyui-workflow-input')?.click();
}}
>
{$i18n.t('Click here to upload a workflow.json file.')}
</button>
</div>
</div>
<div class="mt-2 text-xs text-gray-400 dark:text-gray-500">
{$i18n.t('Make sure to export a workflow.json file as API format from ComfyUI.')}
</div>
</div>
{#if config.comfyui.COMFYUI_WORKFLOW}
<div class="">
<div class=" mb-2 text-sm font-medium">{$i18n.t('ComfyUI Workflow Nodes')}</div>
<div class="text-xs flex flex-col gap-1.5">
{#each requiredWorkflowNodes as node}
<div class="flex w-full items-center border dark:border-gray-850 rounded-lg">
<div class="flex-shrink-0">
<div
class=" capitalize line-clamp-1 font-medium px-3 py-1 w-20 text-center rounded-l-lg bg-green-500/10 text-green-700 dark:text-green-200"
>
{node.type}{node.type === 'prompt' ? '*' : ''}
</div>
</div>
<div class="">
<Tooltip content="Input Key (e.g. text, unet_name, steps)">
<input
class="py-1 px-3 w-24 text-xs text-center bg-transparent outline-none border-r dark:border-gray-850"
placeholder="Key"
bind:value={node.key}
required
/>
</Tooltip>
</div>
<div class="w-full">
<Tooltip
content="Comma separated Node Ids (e.g. 1 or 1,2)"
placement="top-start"
>
<input
class="w-full py-1 px-4 rounded-r-lg text-xs bg-transparent outline-none"
placeholder="Node Ids"
bind:value={node.node_ids}
/>
</Tooltip>
</div>
</div>
{/each}
</div>
<div class="mt-2 text-xs text-right text-gray-400 dark:text-gray-500">
{$i18n.t('*Prompt node ID(s) are required for image generation')}
</div>
</div>
{/if}
{:else if config?.engine === 'openai'}
<div>
<div class=" mb-1.5 text-sm font-medium">{$i18n.t('OpenAI API Config')}</div>
<div class="flex gap-2 mb-1">
<input
class="flex-1 w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
placeholder={$i18n.t('API Base URL')}
bind:value={config.openai.OPENAI_API_BASE_URL}
required
/>
<SensitiveInput
placeholder={$i18n.t('API Key')}
bind:value={config.openai.OPENAI_API_KEY}
/>
</div>
</div>
{/if}
</div>
{#if config?.enabled}
<hr class=" dark:border-gray-850" />
<div>
<div class=" mb-2.5 text-sm font-medium">{$i18n.t('Set Default Model')}</div>
<div class="flex w-full">
<div class="flex-1 mr-2">
<div class="flex w-full">
<div class="flex-1">
<input
list="model-list"
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
bind:value={selectedModel}
placeholder="Select a model"
/>
<Tooltip content={$i18n.t('Enter Model ID')} placement="top-start">
<input
list="model-list"
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
bind:value={imageGenerationConfig.MODEL}
placeholder="Select a model"
required
/>
<datalist id="model-list">
{#each models ?? [] as model}
<option value={model.id}>{model.name}</option>
{/each}
</datalist>
<datalist id="model-list">
{#each models ?? [] as model}
<option value={model.id}>{model.name}</option>
{/each}
</datalist>
</Tooltip>
</div>
</div>
{:else}
<select
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
bind:value={selectedModel}
placeholder={$i18n.t('Select a model')}
required
>
{#if !selectedModel}
<option value="" disabled selected>{$i18n.t('Select a model')}</option>
{/if}
{#each models ?? [] as model}
<option value={model.id} class="bg-gray-100 dark:bg-gray-700">{model.name}</option
>
{/each}
</select>
{/if}
</div>
</div>
</div>
</div>
<div>
<div class=" mb-2.5 text-sm font-medium">{$i18n.t('Set Image Size')}</div>
<div class="flex w-full">
<div class="flex-1 mr-2">
<input
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
placeholder={$i18n.t('Enter Image Size (e.g. 512x512)')}
bind:value={imageSize}
/>
<div>
<div class=" mb-2.5 text-sm font-medium">{$i18n.t('Set Image Size')}</div>
<div class="flex w-full">
<div class="flex-1 mr-2">
<Tooltip content={$i18n.t('Enter Image Size (e.g. 512x512)')} placement="top-start">
<input
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
placeholder={$i18n.t('Enter Image Size (e.g. 512x512)')}
bind:value={imageGenerationConfig.IMAGE_SIZE}
required
/>
</Tooltip>
</div>
</div>
</div>
</div>
<div>
<div class=" mb-2.5 text-sm font-medium">{$i18n.t('Set Steps')}</div>
<div class="flex w-full">
<div class="flex-1 mr-2">
<input
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
placeholder={$i18n.t('Enter Number of Steps (e.g. 50)')}
bind:value={steps}
/>
<div>
<div class=" mb-2.5 text-sm font-medium">{$i18n.t('Set Steps')}</div>
<div class="flex w-full">
<div class="flex-1 mr-2">
<Tooltip content={$i18n.t('Enter Number of Steps (e.g. 50)')} placement="top-start">
<input
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
placeholder={$i18n.t('Enter Number of Steps (e.g. 50)')}
bind:value={imageGenerationConfig.IMAGE_STEPS}
required
/>
</Tooltip>
</div>
</div>
</div>
</div>
{/if}
{/if}
</div>
@@ -37,7 +37,7 @@
let ollamaEnabled = null;
let OLLAMA_URLS = [];
let selectedOllamaUrlIdx: string | null = null;
let selectedOllamaUrlIdx: number | null = null;
let updateModelId = null;
let updateProgress = null;
+85 -1
View File
@@ -18,7 +18,9 @@
let whitelistModels = [''];
let permissions = {
chat: {
deletion: true
deletion: true,
edit: true,
temporary: true
}
};
@@ -92,6 +94,88 @@
{/if}
</button>
</div>
<div class=" flex w-full justify-between">
<div class=" self-center text-xs font-medium">{$i18n.t('Allow Chat Editing')}</div>
<button
class="p-1 px-3 text-xs flex rounded transition"
on:click={() => {
permissions.chat.editing = !(permissions?.chat?.editing ?? true);
}}
type="button"
>
{#if permissions?.chat?.editing ?? true}
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="currentColor"
class="w-4 h-4"
>
<path
d="M11.5 1A3.5 3.5 0 0 0 8 4.5V7H2.5A1.5 1.5 0 0 0 1 8.5v5A1.5 1.5 0 0 0 2.5 15h7a1.5 1.5 0 0 0 1.5-1.5v-5A1.5 1.5 0 0 0 9.5 7V4.5a2 2 0 1 1 4 0v1.75a.75.75 0 0 0 1.5 0V4.5A3.5 3.5 0 0 0 11.5 1Z"
/>
</svg>
<span class="ml-2 self-center">{$i18n.t('Allow')}</span>
{:else}
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="currentColor"
class="w-4 h-4"
>
<path
fill-rule="evenodd"
d="M8 1a3.5 3.5 0 0 0-3.5 3.5V7A1.5 1.5 0 0 0 3 8.5v5A1.5 1.5 0 0 0 4.5 15h7a1.5 1.5 0 0 0 1.5-1.5v-5A1.5 1.5 0 0 0 11.5 7V4.5A3.5 3.5 0 0 0 8 1Zm2 6V4.5a2 2 0 1 0-4 0V7h4Z"
clip-rule="evenodd"
/>
</svg>
<span class="ml-2 self-center">{$i18n.t("Don't Allow")}</span>
{/if}
</button>
</div>
<div class=" flex w-full justify-between">
<div class=" self-center text-xs font-medium">{$i18n.t('Allow Temporary Chat')}</div>
<button
class="p-1 px-3 text-xs flex rounded transition"
on:click={() => {
permissions.chat.temporary = !(permissions?.chat?.temporary ?? true);
}}
type="button"
>
{#if permissions?.chat?.temporary ?? true}
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="currentColor"
class="w-4 h-4"
>
<path
d="M11.5 1A3.5 3.5 0 0 0 8 4.5V7H2.5A1.5 1.5 0 0 0 1 8.5v5A1.5 1.5 0 0 0 2.5 15h7a1.5 1.5 0 0 0 1.5-1.5v-5A1.5 1.5 0 0 0 9.5 7V4.5a2 2 0 1 1 4 0v1.75a.75.75 0 0 0 1.5 0V4.5A3.5 3.5 0 0 0 11.5 1Z"
/>
</svg>
<span class="ml-2 self-center">{$i18n.t('Allow')}</span>
{:else}
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="currentColor"
class="w-4 h-4"
>
<path
fill-rule="evenodd"
d="M8 1a3.5 3.5 0 0 0-3.5 3.5V7A1.5 1.5 0 0 0 3 8.5v5A1.5 1.5 0 0 0 4.5 15h7a1.5 1.5 0 0 0 1.5-1.5v-5A1.5 1.5 0 0 0 11.5 7V4.5A3.5 3.5 0 0 0 8 1Zm2 6V4.5a2 2 0 1 0-4 0V7h4Z"
clip-rule="evenodd"
/>
</svg>
<span class="ml-2 self-center">{$i18n.t("Don't Allow")}</span>
{/if}
</button>
</div>
</div>
<hr class=" dark:border-gray-850 my-2" />
+312 -130
View File
@@ -3,13 +3,13 @@
import { toast } from 'svelte-sonner';
import mermaid from 'mermaid';
import { getContext, onMount, tick } from 'svelte';
import { getContext, onDestroy, onMount, tick } from 'svelte';
import { goto } from '$app/navigation';
import { page } from '$app/stores';
import type { Writable } from 'svelte/store';
import type { Unsubscriber, Writable } from 'svelte/store';
import type { i18n as i18nType } from 'i18next';
import { OLLAMA_API_BASE_URL, OPENAI_API_BASE_URL, WEBUI_BASE_URL } from '$lib/constants';
import { WEBUI_BASE_URL } from '$lib/constants';
import {
chatId,
@@ -19,30 +19,26 @@
models,
settings,
showSidebar,
tags as _tags,
WEBUI_NAME,
banners,
user,
socket,
showCallOverlay,
tools,
currentChatPage
currentChatPage,
temporaryChatEnabled
} from '$lib/stores';
import {
convertMessagesToHistory,
copyToClipboard,
getMessageContentParts,
extractSentencesForAudio,
getUserPosition,
promptTemplate,
splitStream
} from '$lib/utils';
import { generateChatCompletion } from '$lib/apis/ollama';
import {
addTagById,
createNewChat,
deleteTagById,
getAllChatTags,
getChatById,
getChatList,
getTagsById,
@@ -53,14 +49,18 @@
import { createOpenAITextStream } from '$lib/apis/streaming';
import { queryMemory } from '$lib/apis/memories';
import { getAndUpdateUserLocation, getUserSettings } from '$lib/apis/users';
import { chatCompleted, generateTitle, generateSearchQuery, chatAction } from '$lib/apis';
import {
chatCompleted,
generateTitle,
generateSearchQuery,
chatAction,
generateMoACompletion
} from '$lib/apis';
import Banner from '../common/Banner.svelte';
import MessageInput from '$lib/components/chat/MessageInput.svelte';
import Messages from '$lib/components/chat/Messages.svelte';
import Navbar from '$lib/components/layout/Navbar.svelte';
import CallOverlay from './MessageInput/CallOverlay.svelte';
import { error } from '@sveltejs/kit';
import ChatControls from './ChatControls.svelte';
import EventConfirmDialog from '../common/ConfirmDialog.svelte';
@@ -111,6 +111,8 @@
let params = {};
let chatIdUnsubscriber: Unsubscriber | undefined;
$: if (history.currentId !== null) {
let _messages = [];
@@ -166,6 +168,14 @@
message.content += data.content;
} else if (type === 'replace') {
message.content = data.content;
} else if (type === 'action') {
if (data.action === 'continue') {
const continueButton = document.getElementById('continue-response-button');
if (continueButton) {
continueButton.click();
}
}
} else if (type === 'confirmation') {
eventCallback = cb;
@@ -192,62 +202,66 @@
}
};
onMount(async () => {
const onMessageHandler = async (event) => {
if (event.origin === window.origin) {
// Replace with your iframe's origin
console.log('Message received from iframe:', event.data);
if (event.data.type === 'input:prompt') {
console.log(event.data.text);
const onMessageHandler = async (event: {
origin: string;
data: { type: string; text: string };
}) => {
if (event.origin !== window.origin) {
return;
}
const inputElement = document.getElementById('chat-textarea');
// Replace with your iframe's origin
if (event.data.type === 'input:prompt') {
console.debug(event.data.text);
if (inputElement) {
prompt = event.data.text;
inputElement.focus();
}
}
const inputElement = document.getElementById('chat-textarea');
if (event.data.type === 'action:submit') {
console.log(event.data.text);
if (prompt !== '') {
await tick();
submitPrompt(prompt);
}
}
if (event.data.type === 'input:prompt:submit') {
console.log(event.data.text);
if (prompt !== '') {
await tick();
submitPrompt(event.data.text);
}
}
if (inputElement) {
prompt = event.data.text;
inputElement.focus();
}
};
window.addEventListener('message', onMessageHandler);
}
$socket.on('chat-events', chatEventHandler);
if (event.data.type === 'action:submit') {
console.debug(event.data.text);
if (prompt !== '') {
await tick();
submitPrompt(prompt);
}
}
if (event.data.type === 'input:prompt:submit') {
console.debug(event.data.text);
if (prompt !== '') {
await tick();
submitPrompt(event.data.text);
}
}
};
onMount(async () => {
window.addEventListener('message', onMessageHandler);
$socket?.on('chat-events', chatEventHandler);
if (!$chatId) {
chatId.subscribe(async (value) => {
chatIdUnsubscriber = chatId.subscribe(async (value) => {
if (!value) {
await initNewChat();
}
});
} else {
if (!($settings.saveChatHistory ?? true)) {
if ($temporaryChatEnabled) {
await goto('/');
}
}
});
return () => {
window.removeEventListener('message', onMessageHandler);
$socket.off('chat-events');
};
onDestroy(() => {
chatIdUnsubscriber?.();
window.removeEventListener('message', onMessageHandler);
$socket?.off('chat-events');
});
//////////////////////////
@@ -255,9 +269,11 @@
//////////////////////////
const initNewChat = async () => {
window.history.replaceState(history.state, '', `/`);
await chatId.set('');
if ($page.url.pathname.includes('/c/')) {
window.history.replaceState(history.state, '', `/`);
}
await chatId.set('');
autoScroll = true;
title = '';
@@ -294,6 +310,10 @@
}
}
if ($page.url.searchParams.get('call') === 'true') {
showCallOverlay.set(true);
}
selectedModels = selectedModels.map((modelId) =>
$models.map((m) => m.id).includes(modelId) ? modelId : ''
);
@@ -414,7 +434,7 @@
}
if ($chatId == chatId) {
if ($settings.saveChatHistory ?? true) {
if (!$temporaryChatEnabled) {
chat = await updateChatById(localStorage.token, chatId, {
models: selectedModels,
messages: messages,
@@ -429,7 +449,7 @@
}
};
const chatActionHandler = async (chatId, actionId, modelId, responseMessageId) => {
const chatActionHandler = async (chatId, actionId, modelId, responseMessageId, event = null) => {
const res = await chatAction(localStorage.token, actionId, {
model: modelId,
messages: messages.map((m) => ({
@@ -439,6 +459,7 @@
info: m.info ? m.info : undefined,
timestamp: m.timestamp
})),
...(event ? { event: event } : {}),
chat_id: chatId,
session_id: $socket?.id,
id: responseMessageId
@@ -462,7 +483,7 @@
}
if ($chatId == chatId) {
if ($settings.saveChatHistory ?? true) {
if (!$temporaryChatEnabled) {
chat = await updateChatById(localStorage.token, chatId, {
models: selectedModels,
messages: messages,
@@ -521,6 +542,16 @@
`Oops! Hold tight! Your files are still in the processing oven. We're cooking them up to perfection. Please be patient and we'll let you know once they're ready.`
)
);
} else if (
($config?.file?.max_count ?? null) !== null &&
files.length + chatFiles.length > $config?.file?.max_count
) {
console.log(chatFiles.length, files.length);
toast.error(
$i18n.t(`You can only chat with a maximum of {{maxCount}} file(s) at a time.`, {
maxCount: $config?.file?.max_count
})
);
} else {
// Reset chat input textarea
const chatTextAreaElement = document.getElementById('chat-textarea');
@@ -552,7 +583,7 @@
content: userPrompt,
files: _files.length > 0 ? _files : undefined,
timestamp: Math.floor(Date.now() / 1000), // Unix epoch
models: selectedModels.filter((m, mIdx) => selectedModels.indexOf(m) === mIdx)
models: selectedModels
};
// Add message to history and Set currentId to messageId
@@ -572,19 +603,23 @@
return _responses;
};
const sendPrompt = async (prompt, parentId, { modelId = null, newChat = false } = {}) => {
let _responses = [];
const sendPrompt = async (
prompt: string,
parentId: string,
{ modelId = null, modelIdx = null, newChat = false } = {}
) => {
let _responses: string[] = [];
// If modelId is provided, use it, else use selected model
let selectedModelIds = modelId
? [modelId]
: atSelectedModel !== undefined
? [atSelectedModel.id]
: selectedModels;
? [atSelectedModel.id]
: selectedModels;
// Create response messages for each selected model
const responseMessageIds = {};
for (const modelId of selectedModelIds) {
const responseMessageIds: Record<PropertyKey, string> = {};
for (const [_modelIdx, modelId] of selectedModelIds.entries()) {
const model = $models.filter((m) => m.id === modelId).at(0);
if (model) {
@@ -597,6 +632,7 @@
content: '',
model: model.id,
modelName: model.name ?? model.id,
modelIdx: modelIdx ? modelIdx : _modelIdx,
userContext: null,
timestamp: Math.floor(Date.now() / 1000) // Unix epoch
};
@@ -613,14 +649,14 @@
];
}
responseMessageIds[modelId] = responseMessageId;
responseMessageIds[`${modelId}-${modelIdx ? modelIdx : _modelIdx}`] = responseMessageId;
}
}
await tick();
// Create new chat if only one message in messages
if (newChat && messages.length == 2) {
if ($settings.saveChatHistory ?? true) {
if (!$temporaryChatEnabled) {
chat = await createNewChat(localStorage.token, {
id: $chatId,
title: $i18n.t('New Chat'),
@@ -645,7 +681,7 @@
const _chatId = JSON.parse(JSON.stringify($chatId));
await Promise.all(
selectedModelIds.map(async (modelId) => {
selectedModelIds.map(async (modelId, _modelIdx) => {
console.log('modelId', modelId);
const model = $models.filter((m) => m.id === modelId).at(0);
@@ -663,7 +699,8 @@
);
}
let responseMessageId = responseMessageIds[modelId];
let responseMessageId =
responseMessageIds[`${modelId}-${modelIdx ? modelIdx : _modelIdx}`];
let responseMessage = history.messages[responseMessageId];
let userContext = null;
@@ -711,13 +748,13 @@
);
currentChatPage.set(1);
await chats.set(await getChatList(localStorage.token, $currentChatPage));
chats.set(await getChatList(localStorage.token, $currentChatPage));
return _responses;
};
const sendPromptOllama = async (model, userPrompt, responseMessageId, _chatId) => {
let _response = null;
let _response: string | null = null;
const responseMessage = history.messages[responseMessageId];
const userMessage = history.messages[responseMessage.parentId];
@@ -739,16 +776,16 @@
? await getAndUpdateUserLocation(localStorage.token)
: undefined
)}${
responseMessage?.userContext ?? null
(responseMessage?.userContext ?? null)
? `\n\nUser Context:\n${responseMessage?.userContext ?? ''}`
: ''
}`
}
}
: undefined,
...messages
]
.filter((message) => message?.content?.trim())
.map((message, idx, arr) => {
.map((message) => {
// Prepare the base message object
const baseMessage = {
role: message.role,
@@ -785,7 +822,18 @@
let files = JSON.parse(JSON.stringify(chatFiles));
if (model?.info?.meta?.knowledge ?? false) {
// Only initialize and add status if knowledge exists
responseMessage.statusHistory = [
{
action: 'knowledge_search',
description: $i18n.t(`Searching Knowledge for "{{searchQuery}}"`, {
searchQuery: userMessage.content
}),
done: false
}
];
files.push(...model.info.meta.knowledge);
messages = messages; // Trigger Svelte update
}
files.push(
...(userMessage?.files ?? []).filter((item) =>
@@ -794,6 +842,8 @@
...(responseMessage?.files ?? []).filter((item) => ['web_search_results'].includes(item.type))
);
scrollToBottom();
eventTarget.dispatchEvent(
new CustomEvent('chat:start', {
detail: {
@@ -811,10 +861,10 @@
options: {
...(params ?? $settings.params ?? {}),
stop:
params?.stop ?? $settings?.params?.stop ?? undefined
(params?.stop ?? $settings?.params?.stop ?? undefined)
? (params?.stop.split(',').map((token) => token.trim()) ?? $settings.params.stop).map(
(str) => decodeURIComponent(JSON.parse('"' + str.replace(/\"/g, '\\"') + '"'))
)
)
: undefined,
num_predict: params?.max_tokens ?? $settings?.params?.max_tokens ?? undefined,
repeat_penalty:
@@ -864,6 +914,12 @@
if ('citations' in data) {
responseMessage.citations = data.citations;
// Only remove status if it was initially set
if (model?.info?.meta?.knowledge ?? false) {
responseMessage.statusHistory = responseMessage.statusHistory.filter(
(status) => status.action !== 'knowledge_search'
);
}
continue;
}
@@ -877,18 +933,30 @@
} else {
responseMessage.content += data.message.content;
const sentences = extractSentencesForAudio(responseMessage.content);
sentences.pop();
if (navigator.vibrate && ($settings?.hapticFeedback ?? false)) {
navigator.vibrate(5);
}
const messageContentParts = getMessageContentParts(
responseMessage.content,
$config?.audio?.tts?.split_on ?? 'punctuation'
);
messageContentParts.pop();
// dispatch only last sentence and make sure it hasn't been dispatched before
if (
sentences.length > 0 &&
sentences[sentences.length - 1] !== responseMessage.lastSentence
messageContentParts.length > 0 &&
messageContentParts[messageContentParts.length - 1] !==
responseMessage.lastSentence
) {
responseMessage.lastSentence = sentences[sentences.length - 1];
responseMessage.lastSentence =
messageContentParts[messageContentParts.length - 1];
eventTarget.dispatchEvent(
new CustomEvent('chat', {
detail: { id: responseMessageId, content: sentences[sentences.length - 1] }
detail: {
id: responseMessageId,
content: messageContentParts[messageContentParts.length - 1]
}
})
);
}
@@ -985,20 +1053,32 @@
};
}
responseMessage.done = true;
if (responseMessage.statusHistory) {
responseMessage.statusHistory = responseMessage.statusHistory.filter(
(status) => status.action !== 'knowledge_search'
);
}
messages = messages;
}
stopResponseFlag = false;
await tick();
let lastSentence = extractSentencesForAudio(responseMessage.content)?.at(-1) ?? '';
if (lastSentence) {
let lastMessageContentPart =
getMessageContentParts(
responseMessage.content,
$config?.audio?.tts?.split_on ?? 'punctuation'
)?.at(-1) ?? '';
if (lastMessageContentPart) {
eventTarget.dispatchEvent(
new CustomEvent('chat', {
detail: { id: responseMessageId, content: lastSentence }
detail: { id: responseMessageId, content: lastMessageContentPart }
})
);
}
eventTarget.dispatchEvent(
new CustomEvent('chat:finish', {
detail: {
@@ -1029,7 +1109,18 @@
let files = JSON.parse(JSON.stringify(chatFiles));
if (model?.info?.meta?.knowledge ?? false) {
// Only initialize and add status if knowledge exists
responseMessage.statusHistory = [
{
action: 'knowledge_search',
description: $i18n.t(`Searching Knowledge for "{{searchQuery}}"`, {
searchQuery: userMessage.content
}),
done: false
}
];
files.push(...model.info.meta.knowledge);
messages = messages; // Trigger Svelte update
}
files.push(
...(userMessage?.files ?? []).filter((item) =>
@@ -1056,10 +1147,10 @@
stream: true,
model: model.id,
stream_options:
model.info?.meta?.capabilities?.usage ?? false
(model.info?.meta?.capabilities?.usage ?? false)
? {
include_usage: true
}
}
: undefined,
messages: [
params?.system || $settings.system || (responseMessage?.userContext ?? null)
@@ -1072,11 +1163,11 @@
? await getAndUpdateUserLocation(localStorage.token)
: undefined
)}${
responseMessage?.userContext ?? null
(responseMessage?.userContext ?? null)
? `\n\nUser Context:\n${responseMessage?.userContext ?? ''}`
: ''
}`
}
}
: undefined,
...messages
]
@@ -1092,7 +1183,7 @@
text:
arr.length - 1 !== idx
? message.content
: message?.raContent ?? message.content
: (message?.raContent ?? message.content)
},
...message.files
.filter((file) => file.type === 'image')
@@ -1103,20 +1194,20 @@
}
}))
]
}
}
: {
content:
arr.length - 1 !== idx
? message.content
: message?.raContent ?? message.content
})
: (message?.raContent ?? message.content)
})
})),
seed: params?.seed ?? $settings?.params?.seed ?? undefined,
stop:
params?.stop ?? $settings?.params?.stop ?? undefined
(params?.stop ?? $settings?.params?.stop ?? undefined)
? (params?.stop.split(',').map((token) => token.trim()) ?? $settings.params.stop).map(
(str) => decodeURIComponent(JSON.parse('"' + str.replace(/\"/g, '\\"') + '"'))
)
)
: undefined,
temperature: params?.temperature ?? $settings?.params?.temperature ?? undefined,
top_p: params?.top_p ?? $settings?.params?.top_p ?? undefined,
@@ -1169,6 +1260,12 @@
if (citations) {
responseMessage.citations = citations;
// Only remove status if it was initially set
if (model?.info?.meta?.knowledge ?? false) {
responseMessage.statusHistory = responseMessage.statusHistory.filter(
(status) => status.action !== 'knowledge_search'
);
}
continue;
}
@@ -1177,18 +1274,28 @@
} else {
responseMessage.content += value;
const sentences = extractSentencesForAudio(responseMessage.content);
sentences.pop();
if (navigator.vibrate && ($settings?.hapticFeedback ?? false)) {
navigator.vibrate(5);
}
const messageContentParts = getMessageContentParts(
responseMessage.content,
$config?.audio?.tts?.split_on ?? 'punctuation'
);
messageContentParts.pop();
// dispatch only last sentence and make sure it hasn't been dispatched before
if (
sentences.length > 0 &&
sentences[sentences.length - 1] !== responseMessage.lastSentence
messageContentParts.length > 0 &&
messageContentParts[messageContentParts.length - 1] !== responseMessage.lastSentence
) {
responseMessage.lastSentence = sentences[sentences.length - 1];
responseMessage.lastSentence = messageContentParts[messageContentParts.length - 1];
eventTarget.dispatchEvent(
new CustomEvent('chat', {
detail: { id: responseMessageId, content: sentences[sentences.length - 1] }
detail: {
id: responseMessageId,
content: messageContentParts[messageContentParts.length - 1]
}
})
);
}
@@ -1243,11 +1350,15 @@
stopResponseFlag = false;
await tick();
let lastSentence = extractSentencesForAudio(responseMessage.content)?.at(-1) ?? '';
if (lastSentence) {
let lastMessageContentPart =
getMessageContentParts(
responseMessage.content,
$config?.audio?.tts?.split_on ?? 'punctuation'
)?.at(-1) ?? '';
if (lastMessageContentPart) {
eventTarget.dispatchEvent(
new CustomEvent('chat', {
detail: { id: responseMessageId, content: lastSentence }
detail: { id: responseMessageId, content: lastMessageContentPart }
})
);
}
@@ -1311,6 +1422,12 @@
};
responseMessage.done = true;
if (responseMessage.statusHistory) {
responseMessage.statusHistory = responseMessage.statusHistory.filter(
(status) => status.action !== 'knowledge_search'
);
}
messages = messages;
};
@@ -1332,7 +1449,10 @@
} else {
// If there are multiple models selected, use the model of the response message for regeneration
// e.g. many model chat
await sendPrompt(userPrompt, userMessage.id, { modelId: message.model });
await sendPrompt(userPrompt, userMessage.id, {
modelId: message.model,
modelIdx: message.modelIdx
});
}
}
};
@@ -1392,7 +1512,7 @@
title = _title;
}
if ($settings.saveChatHistory ?? true) {
if (!$temporaryChatEnabled) {
chat = await updateChatById(localStorage.token, _chatId, { title: _title });
currentChatPage.set(1);
@@ -1484,6 +1604,69 @@
return [];
});
};
const saveChatHandler = async (_chatId) => {
if ($chatId == _chatId) {
if (!$temporaryChatEnabled) {
chat = await updateChatById(localStorage.token, _chatId, {
messages: messages,
history: history,
models: selectedModels,
params: params,
files: chatFiles
});
currentChatPage.set(1);
await chats.set(await getChatList(localStorage.token, $currentChatPage));
}
}
};
const mergeResponses = async (messageId, responses, _chatId) => {
console.log('mergeResponses', messageId, responses);
const message = history.messages[messageId];
const mergedResponse = {
status: true,
content: ''
};
message.merged = mergedResponse;
messages = messages;
try {
const [res, controller] = await generateMoACompletion(
localStorage.token,
message.model,
history.messages[message.parentId].content,
responses
);
if (res && res.ok && res.body) {
const textStream = await createOpenAITextStream(res.body, $settings.splitLargeChunks);
for await (const update of textStream) {
const { value, done, citations, error, usage } = update;
if (error || done) {
break;
}
if (mergedResponse.content == '' && value == '\n') {
continue;
} else {
mergedResponse.content += value;
messages = messages;
}
if (autoScroll) {
scrollToBottom();
}
}
await saveChatHandler(_chatId);
} else {
console.error(res);
}
} catch (e) {
console.error(e);
}
};
</script>
<svelte:head>
@@ -1515,17 +1698,6 @@
}}
/>
{#if $showCallOverlay}
<CallOverlay
{submitPrompt}
{stopResponse}
bind:files
modelId={selectedModelIds?.at(0) ?? null}
chatId={$chatId}
{eventTarget}
/>
{/if}
{#if !chatIdProp || (loaded && chatIdProp)}
<div
class="h-screen max-h-[100dvh] {$showSidebar
@@ -1610,6 +1782,7 @@
{sendPrompt}
{continueGeneration}
{regenerateResponse}
{mergeResponses}
{chatActionHandler}
/>
</div>
@@ -1635,21 +1808,30 @@
{messages}
{submitPrompt}
{stopResponse}
on:call={() => {
showControls = true;
}}
/>
</div>
</div>
<ChatControls
models={selectedModelIds.reduce((a, e, i, arr) => {
const model = $models.find((m) => m.id === e);
if (model) {
return [...a, model];
}
return a;
}, [])}
bind:show={showControls}
bind:chatFiles
bind:params
/>
</div>
{/if}
<ChatControls
models={selectedModelIds.reduce((a, e, i, arr) => {
const model = $models.find((m) => m.id === e);
if (model) {
return [...a, model];
}
return a;
}, [])}
bind:show={showControls}
bind:chatFiles
bind:params
bind:files
{submitPrompt}
{stopResponse}
modelId={selectedModelIds?.at(0) ?? null}
chatId={$chatId}
{eventTarget}
/>
+45 -8
View File
@@ -3,6 +3,8 @@
import Modal from '../common/Modal.svelte';
import Controls from './Controls/Controls.svelte';
import { onMount } from 'svelte';
import { mobile, showCallOverlay } from '$lib/stores';
import CallOverlay from './MessageInput/CallOverlay.svelte';
export let show = false;
@@ -12,6 +14,12 @@
export let chatFiles = [];
export let params = {};
export let eventTarget: EventTarget;
export let submitPrompt: Function;
export let stopResponse: Function;
export let files;
export let modelId;
let largeScreen = false;
onMount(() => {
// listen to resize 1024px
@@ -42,18 +50,47 @@
<div
class="w-full h-full px-5 py-4 bg-white dark:shadow-lg dark:bg-gray-850 border border-gray-50 dark:border-gray-800 rounded-xl z-50 pointer-events-auto overflow-y-auto scrollbar-hidden"
>
<Controls
on:close={() => {
show = false;
}}
{models}
bind:chatFiles
bind:params
/>
{#if $showCallOverlay}
<CallOverlay
bind:files
{submitPrompt}
{stopResponse}
{modelId}
{chatId}
{eventTarget}
/>
{:else}
<Controls
on:close={() => {
show = false;
}}
{models}
bind:chatFiles
bind:params
/>
{/if}
</div>
</div>
</div>
{/if}
{:else if $showCallOverlay}
<div class=" absolute w-full h-screen max-h-[100dvh] flex z-[999] overflow-hidden">
<div
class="absolute w-full h-screen max-h-[100dvh] bg-white text-gray-700 dark:bg-black dark:text-gray-300 flex justify-center"
>
<CallOverlay
bind:files
{submitPrompt}
{stopResponse}
{modelId}
{chatId}
{eventTarget}
on:close={() => {
show = false;
}}
/>
</div>
</div>
{:else}
<Modal bind:show>
<div class=" px-6 py-4 h-full">
@@ -1,4 +1,4 @@
<script>
<script lang="ts">
import { createEventDispatcher, getContext } from 'svelte';
const dispatch = createEventDispatcher();
const i18n = getContext('i18n');
@@ -9,8 +9,8 @@
import FileItem from '$lib/components/common/FileItem.svelte';
import Collapsible from '$lib/components/common/Collapsible.svelte';
import { user } from '$lib/stores';
export let models = [];
export let chatFiles = [];
export let params = {};
</script>
@@ -78,7 +78,7 @@
<Collapsible title={$i18n.t('Advanced Params')} open={true}>
<div class="text-sm mt-1.5" slot="content">
<div>
<AdvancedParams bind:params />
<AdvancedParams admin={$user?.role === 'admin'} bind:params />
</div>
</div>
</Collapsible>
+90 -169
View File
@@ -1,6 +1,8 @@
<script lang="ts">
import { toast } from 'svelte-sonner';
import { onMount, tick, getContext } from 'svelte';
import { onMount, tick, getContext, createEventDispatcher } from 'svelte';
const dispatch = createEventDispatcher();
import {
type Model,
mobile,
@@ -12,16 +14,12 @@
tools,
user as _user
} from '$lib/stores';
import { blobToFile, calculateSHA256, findWordIndices } from '$lib/utils';
import {
processDocToVectorDB,
uploadDocToVectorDB,
uploadWebToVectorDB,
uploadYoutubeTranscriptionToVectorDB
} from '$lib/apis/rag';
import { blobToFile, findWordIndices } from '$lib/utils';
import { transcribeAudio } from '$lib/apis/audio';
import { processDocToVectorDB } from '$lib/apis/rag';
import { uploadFile } from '$lib/apis/files';
import {
SUPPORTED_FILE_TYPE,
SUPPORTED_FILE_EXTENSIONS,
@@ -29,19 +27,14 @@
WEBUI_API_BASE_URL
} from '$lib/constants';
import Prompts from './MessageInput/PromptCommands.svelte';
import Suggestions from './MessageInput/Suggestions.svelte';
import AddFilesPlaceholder from '../AddFilesPlaceholder.svelte';
import Documents from './MessageInput/Documents.svelte';
import Models from './MessageInput/Models.svelte';
import Tooltip from '../common/Tooltip.svelte';
import XMark from '$lib/components/icons/XMark.svelte';
import InputMenu from './MessageInput/InputMenu.svelte';
import Headphone from '../icons/Headphone.svelte';
import VoiceRecording from './MessageInput/VoiceRecording.svelte';
import { transcribeAudio } from '$lib/apis/audio';
import FileItem from '../common/FileItem.svelte';
import FilesOverlay from './MessageInput/FilesOverlay.svelte';
import Commands from './MessageInput/Commands.svelte';
import XMark from '../icons/XMark.svelte';
const i18n = getContext('i18n');
@@ -50,7 +43,7 @@
export let submitPrompt: Function;
export let stopResponse: Function;
export let autoScroll = true;
export let autoScroll = false;
export let atSelectedModel: Model | undefined;
export let selectedModels: [''];
@@ -60,9 +53,7 @@
let chatTextAreaElement: HTMLTextAreaElement;
let filesInputElement;
let promptsElement;
let documentsElement;
let modelsElement;
let commandsElement;
let inputFiles;
let dragged = false;
@@ -93,7 +84,10 @@
const scrollToBottom = () => {
const element = document.getElementById('messages-container');
element.scrollTop = element.scrollHeight;
element.scrollTo({
top: element.scrollHeight,
behavior: 'smooth'
});
};
const uploadFileHandler = async (file) => {
@@ -177,60 +171,42 @@
}
};
const uploadWeb = async (url) => {
console.log(url);
const inputFilesHandler = async (inputFiles) => {
inputFiles.forEach((file) => {
console.log(file, file.name.split('.').at(-1));
const doc = {
type: 'doc',
name: url,
collection_name: '',
status: false,
url: url,
error: ''
};
try {
files = [...files, doc];
const res = await uploadWebToVectorDB(localStorage.token, '', url);
if (res) {
doc.status = 'processed';
doc.collection_name = res.collection_name;
files = files;
if (
($config?.file?.max_size ?? null) !== null &&
file.size > ($config?.file?.max_size ?? 0) * 1024 * 1024
) {
toast.error(
$i18n.t(`File size should not exceed {{maxSize}} MB.`, {
maxSize: $config?.file?.max_size
})
);
return;
}
} catch (e) {
// Remove the failed doc from the files array
files = files.filter((f) => f.name !== url);
toast.error(e);
}
};
const uploadYoutubeTranscription = async (url) => {
console.log(url);
const doc = {
type: 'doc',
name: url,
collection_name: '',
status: false,
url: url,
error: ''
};
try {
files = [...files, doc];
const res = await uploadYoutubeTranscriptionToVectorDB(localStorage.token, url);
if (res) {
doc.status = 'processed';
doc.collection_name = res.collection_name;
files = files;
if (['image/gif', 'image/webp', 'image/jpeg', 'image/png'].includes(file['type'])) {
if (visionCapableModels.length === 0) {
toast.error($i18n.t('Selected model(s) do not support image inputs'));
return;
}
let reader = new FileReader();
reader.onload = (event) => {
files = [
...files,
{
type: 'image',
url: `${event.target.result}`
}
];
};
reader.readAsDataURL(file);
} else {
uploadFileHandler(file);
}
} catch (e) {
// Remove the failed doc from the files array
files = files.filter((f) => f.name !== url);
toast.error(e);
}
});
};
onMount(() => {
@@ -260,30 +236,9 @@
if (e.dataTransfer?.files) {
const inputFiles = Array.from(e.dataTransfer?.files);
if (inputFiles && inputFiles.length > 0) {
inputFiles.forEach((file) => {
console.log(file, file.name.split('.').at(-1));
if (['image/gif', 'image/webp', 'image/jpeg', 'image/png'].includes(file['type'])) {
if (visionCapableModels.length === 0) {
toast.error($i18n.t('Selected model(s) do not support image inputs'));
return;
}
let reader = new FileReader();
reader.onload = (event) => {
files = [
...files,
{
type: 'image',
url: `${event.target.result}`
}
];
};
reader.readAsDataURL(file);
} else {
uploadFileHandler(file);
}
});
console.log(inputFiles);
inputFilesHandler(inputFiles);
} else {
toast.error($i18n.t(`File not found.`));
}
@@ -343,48 +298,9 @@
</div>
<div class="w-full relative">
{#if prompt.charAt(0) === '/'}
<Prompts bind:this={promptsElement} bind:prompt bind:files />
{:else if prompt.charAt(0) === '#'}
<Documents
bind:this={documentsElement}
bind:prompt
on:youtube={(e) => {
console.log(e);
uploadYoutubeTranscription(e.detail);
}}
on:url={(e) => {
console.log(e);
uploadWeb(e.detail);
}}
on:select={(e) => {
console.log(e);
files = [
...files,
{
type: e?.detail?.type ?? 'file',
...e.detail,
status: 'processed'
}
];
}}
/>
{/if}
<Models
bind:this={modelsElement}
bind:prompt
bind:chatInputPlaceholder
{messages}
on:select={(e) => {
atSelectedModel = e.detail;
chatTextAreaElement?.focus();
}}
/>
{#if atSelectedModel !== undefined}
<div
class="px-3 py-2.5 text-left w-full flex justify-between items-center absolute bottom-0 left-0 right-0 bg-gradient-to-t from-50% from-white dark:from-gray-900 z-50"
class="px-3 py-2.5 text-left w-full flex justify-between items-center absolute bottom-0.5 left-0 right-0 bg-gradient-to-t from-50% from-white dark:from-gray-900 z-10"
>
<div class="flex items-center gap-2 text-sm dark:text-gray-500">
<img
@@ -413,6 +329,21 @@
</div>
</div>
{/if}
<Commands
bind:this={commandsElement}
bind:prompt
bind:files
on:select={(e) => {
const data = e.detail;
if (data?.type === 'model') {
atSelectedModel = data.data;
}
chatTextAreaElement?.focus();
}}
/>
</div>
</div>
</div>
@@ -429,27 +360,7 @@
on:change={async () => {
if (inputFiles && inputFiles.length > 0) {
const _inputFiles = Array.from(inputFiles);
_inputFiles.forEach((file) => {
if (['image/gif', 'image/webp', 'image/jpeg', 'image/png'].includes(file['type'])) {
if (visionCapableModels.length === 0) {
toast.error($i18n.t('Selected model(s) do not support image inputs'));
return;
}
let reader = new FileReader();
reader.onload = (event) => {
files = [
...files,
{
type: 'image',
url: `${event.target.result}`
}
];
};
reader.readAsDataURL(file);
} else {
uploadFileHandler(file);
}
});
inputFilesHandler(_inputFiles);
} else {
toast.error($i18n.t(`File not found.`));
}
@@ -638,6 +549,7 @@
}}
on:keydown={async (e) => {
const isCtrlPressed = e.ctrlKey || e.metaKey; // metaKey is for Cmd key on Mac
const commandsContainerElement = document.getElementById('commands-container');
// Check if Ctrl + R is pressed
if (prompt === '' && isCtrlPressed && e.key.toLowerCase() === 'r') {
@@ -668,10 +580,9 @@
editButton?.click();
}
if (['/', '#', '@'].includes(prompt.charAt(0)) && e.key === 'ArrowUp') {
if (commandsContainerElement && e.key === 'ArrowUp') {
e.preventDefault();
(promptsElement || documentsElement || modelsElement).selectUp();
commandsElement.selectUp();
const commandOptionButton = [
...document.getElementsByClassName('selected-command-option-button')
@@ -679,10 +590,9 @@
commandOptionButton.scrollIntoView({ block: 'center' });
}
if (['/', '#', '@'].includes(prompt.charAt(0)) && e.key === 'ArrowDown') {
if (commandsContainerElement && e.key === 'ArrowDown') {
e.preventDefault();
(promptsElement || documentsElement || modelsElement).selectDown();
commandsElement.selectDown();
const commandOptionButton = [
...document.getElementsByClassName('selected-command-option-button')
@@ -690,7 +600,7 @@
commandOptionButton.scrollIntoView({ block: 'center' });
}
if (['/', '#', '@'].includes(prompt.charAt(0)) && e.key === 'Enter') {
if (commandsContainerElement && e.key === 'Enter') {
e.preventDefault();
const commandOptionButton = [
@@ -706,7 +616,7 @@
}
}
if (['/', '#', '@'].includes(prompt.charAt(0)) && e.key === 'Tab') {
if (commandsContainerElement && e.key === 'Tab') {
e.preventDefault();
const commandOptionButton = [
@@ -742,16 +652,16 @@
}
}}
rows="1"
on:input={(e) => {
on:input={async (e) => {
e.target.style.height = '';
e.target.style.height = Math.min(e.target.scrollHeight, 200) + 'px';
user = null;
}}
on:focus={(e) => {
on:focus={async (e) => {
e.target.style.height = '';
e.target.style.height = Math.min(e.target.scrollHeight, 200) + 'px';
}}
on:paste={(e) => {
on:paste={async (e) => {
const clipboardData = e.clipboardData || window.clipboardData;
if (clipboardData && clipboardData.items) {
@@ -786,7 +696,7 @@
type="button"
on:click={async () => {
try {
const res = await navigator.mediaDevices
let stream = await navigator.mediaDevices
.getUserMedia({ audio: true })
.catch(function (err) {
toast.error(
@@ -800,9 +710,12 @@
return null;
});
if (res) {
if (stream) {
recording = true;
const tracks = stream.getTracks();
tracks.forEach((track) => track.stop());
}
stream = null;
} catch {
toast.error($i18n.t('Permission denied when accessing microphone'));
}
@@ -849,10 +762,18 @@
}
// check if user has access to getUserMedia
try {
await navigator.mediaDevices.getUserMedia({ audio: true });
let stream = await navigator.mediaDevices.getUserMedia({ audio: true });
// If the user grants the permission, proceed to show the call overlay
if (stream) {
const tracks = stream.getTracks();
tracks.forEach((track) => track.stop());
}
stream = null;
showCallOverlay.set(true);
dispatch('call');
} catch (err) {
// If the user denies the permission or an error occurs, show an error message
toast.error($i18n.t('Permission denied when accessing media devices'));
@@ -1,6 +1,8 @@
<script lang="ts">
import { config, models, settings, showCallOverlay } from '$lib/stores';
import { onMount, tick, getContext } from 'svelte';
import { onMount, tick, getContext, onDestroy, createEventDispatcher } from 'svelte';
const dispatch = createEventDispatcher();
import {
blobToFile,
@@ -19,15 +21,14 @@
const i18n = getContext('i18n');
export let eventTarget: EventTarget;
export let submitPrompt: Function;
export let stopResponse: Function;
export let files;
export let chatId;
export let modelId;
let wakeLock = null;
let model = null;
let loading = false;
@@ -45,6 +46,7 @@
let rmsLevel = 0;
let hasStartedSpeaking = false;
let mediaRecorder;
let audioStream = null;
let audioChunks = [];
let videoInputDevices = [];
@@ -210,17 +212,23 @@
} else {
audioChunks = [];
mediaRecorder = false;
if (audioStream) {
const tracks = audioStream.getTracks();
tracks.forEach((track) => track.stop());
}
audioStream = null;
}
};
const startRecording = async () => {
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
mediaRecorder = new MediaRecorder(stream);
audioStream = await navigator.mediaDevices.getUserMedia({ audio: true });
mediaRecorder = new MediaRecorder(audioStream);
mediaRecorder.onstart = () => {
console.log('Recording started');
audioChunks = [];
analyseAudio(stream);
analyseAudio(audioStream);
};
mediaRecorder.ondataavailable = (event) => {
@@ -237,6 +245,15 @@
mediaRecorder.start();
};
const stopAudioStream = async () => {
if (audioStream) {
const tracks = audioStream.getTracks();
tracks.forEach((track) => track.stop());
}
audioStream = null;
};
// Function to calculate the RMS level from time domain data
const calculateRMS = (data: Uint8Array) => {
let sumSquares = 0;
@@ -509,6 +526,34 @@
};
onMount(async () => {
const setWakeLock = async () => {
try {
wakeLock = await navigator.wakeLock.request('screen');
} catch (err) {
// The Wake Lock request has failed - usually system related, such as battery.
console.log(err);
}
if (wakeLock) {
// Add a listener to release the wake lock when the page is unloaded
wakeLock.addEventListener('release', () => {
// the wake lock has been released
console.log('Wake Lock released');
});
}
};
if ('wakeLock' in navigator) {
await setWakeLock();
document.addEventListener('visibilitychange', async () => {
// Re-request the wake lock if the document becomes visible
if (wakeLock !== null && document.visibilityState === 'visible') {
await setWakeLock();
}
});
}
model = $models.find((m) => m.id === modelId);
startRecording();
@@ -585,302 +630,225 @@
await stopCamera();
};
});
onDestroy(async () => {
await stopAllAudio();
await stopRecordingCallback(false);
await stopCamera();
});
</script>
{#if $showCallOverlay}
<div class=" absolute w-full h-screen max-h-[100dvh] flex z-[999] overflow-hidden">
<div
class="absolute w-full h-screen max-h-[100dvh] bg-white text-gray-700 dark:bg-black dark:text-gray-300 flex justify-center"
>
<div class="max-w-lg w-full h-screen max-h-[100dvh] flex flex-col justify-between p-3 md:p-6">
{#if camera}
<button
type="button"
class="flex justify-center items-center w-full h-20 min-h-20"
on:click={() => {
if (assistantSpeaking) {
stopAllAudio();
}
}}
>
{#if emoji}
<div
class=" transition-all rounded-full"
style="font-size:{rmsLevel * 100 > 4
? '4.5'
: rmsLevel * 100 > 2
? '4.25'
: rmsLevel * 100 > 1
<div class="max-w-lg w-full h-full max-h-[100dvh] flex flex-col justify-between p-3 md:p-6">
{#if camera}
<button
type="button"
class="flex justify-center items-center w-full h-20 min-h-20"
on:click={() => {
if (assistantSpeaking) {
stopAllAudio();
}
}}
>
{#if emoji}
<div
class=" transition-all rounded-full"
style="font-size:{rmsLevel * 100 > 4
? '4.5'
: rmsLevel * 100 > 2
? '4.25'
: rmsLevel * 100 > 1
? '3.75'
: '3.5'}rem;width: 100%; text-align:center;"
>
{emoji}
</div>
{:else if loading || assistantSpeaking}
<svg
class="size-12 text-gray-900 dark:text-gray-400"
viewBox="0 0 24 24"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg"
><style>
.spinner_qM83 {
animation: spinner_8HQG 1.05s infinite;
}
.spinner_oXPr {
animation-delay: 0.1s;
}
.spinner_ZTLf {
animation-delay: 0.2s;
}
@keyframes spinner_8HQG {
0%,
57.14% {
animation-timing-function: cubic-bezier(0.33, 0.66, 0.66, 1);
transform: translate(0);
}
28.57% {
animation-timing-function: cubic-bezier(0.33, 0, 0.66, 0.33);
transform: translateY(-6px);
}
100% {
transform: translate(0);
}
}
</style><circle class="spinner_qM83" cx="4" cy="12" r="3" /><circle
class="spinner_qM83 spinner_oXPr"
cx="12"
cy="12"
r="3"
/><circle class="spinner_qM83 spinner_ZTLf" cx="20" cy="12" r="3" /></svg
>
{:else}
<div
class=" {rmsLevel * 100 > 4
? ' size-[4.5rem]'
: rmsLevel * 100 > 2
? ' size-16'
: rmsLevel * 100 > 1
>
{emoji}
</div>
{:else if loading || assistantSpeaking}
<svg
class="size-12 text-gray-900 dark:text-gray-400"
viewBox="0 0 24 24"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg"
><style>
.spinner_qM83 {
animation: spinner_8HQG 1.05s infinite;
}
.spinner_oXPr {
animation-delay: 0.1s;
}
.spinner_ZTLf {
animation-delay: 0.2s;
}
@keyframes spinner_8HQG {
0%,
57.14% {
animation-timing-function: cubic-bezier(0.33, 0.66, 0.66, 1);
transform: translate(0);
}
28.57% {
animation-timing-function: cubic-bezier(0.33, 0, 0.66, 0.33);
transform: translateY(-6px);
}
100% {
transform: translate(0);
}
}
</style><circle class="spinner_qM83" cx="4" cy="12" r="3" /><circle
class="spinner_qM83 spinner_oXPr"
cx="12"
cy="12"
r="3"
/><circle class="spinner_qM83 spinner_ZTLf" cx="20" cy="12" r="3" /></svg
>
{:else}
<div
class=" {rmsLevel * 100 > 4
? ' size-[4.5rem]'
: rmsLevel * 100 > 2
? ' size-16'
: rmsLevel * 100 > 1
? 'size-14'
: 'size-12'} transition-all rounded-full {(model?.info?.meta
?.profile_image_url ?? '/static/favicon.png') !== '/static/favicon.png'
? ' bg-cover bg-center bg-no-repeat'
: 'bg-black dark:bg-white'} bg-black dark:bg-white"
style={(model?.info?.meta?.profile_image_url ?? '/static/favicon.png') !==
'/static/favicon.png'
? `background-image: url('${model?.info?.meta?.profile_image_url}');`
: ''}
/>
{/if}
<!-- navbar -->
</button>
?.profile_image_url ?? '/static/favicon.png') !== '/static/favicon.png'
? ' bg-cover bg-center bg-no-repeat'
: 'bg-black dark:bg-white'} bg-black dark:bg-white"
style={(model?.info?.meta?.profile_image_url ?? '/static/favicon.png') !==
'/static/favicon.png'
? `background-image: url('${model?.info?.meta?.profile_image_url}');`
: ''}
/>
{/if}
<!-- navbar -->
</button>
{/if}
<div class="flex justify-center items-center flex-1 h-full w-full max-h-full">
{#if !camera}
<button
type="button"
on:click={() => {
if (assistantSpeaking) {
stopAllAudio();
}
}}
>
{#if emoji}
<div
class=" transition-all rounded-full"
style="font-size:{rmsLevel * 100 > 4
? '13'
: rmsLevel * 100 > 2
? '12'
: rmsLevel * 100 > 1
<div class="flex justify-center items-center flex-1 h-full w-full max-h-full">
{#if !camera}
<button
type="button"
on:click={() => {
if (assistantSpeaking) {
stopAllAudio();
}
}}
>
{#if emoji}
<div
class=" transition-all rounded-full"
style="font-size:{rmsLevel * 100 > 4
? '13'
: rmsLevel * 100 > 2
? '12'
: rmsLevel * 100 > 1
? '11.5'
: '11'}rem;width:100%;text-align:center;"
>
{emoji}
</div>
{:else if loading || assistantSpeaking}
<svg
class="size-44 text-gray-900 dark:text-gray-400"
viewBox="0 0 24 24"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg"
><style>
.spinner_qM83 {
animation: spinner_8HQG 1.05s infinite;
}
.spinner_oXPr {
animation-delay: 0.1s;
}
.spinner_ZTLf {
animation-delay: 0.2s;
}
@keyframes spinner_8HQG {
0%,
57.14% {
animation-timing-function: cubic-bezier(0.33, 0.66, 0.66, 1);
transform: translate(0);
}
28.57% {
animation-timing-function: cubic-bezier(0.33, 0, 0.66, 0.33);
transform: translateY(-6px);
}
100% {
transform: translate(0);
}
}
</style><circle class="spinner_qM83" cx="4" cy="12" r="3" /><circle
class="spinner_qM83 spinner_oXPr"
cx="12"
cy="12"
r="3"
/><circle class="spinner_qM83 spinner_ZTLf" cx="20" cy="12" r="3" /></svg
>
{:else}
<div
class=" {rmsLevel * 100 > 4
? ' size-52'
: rmsLevel * 100 > 2
? 'size-48'
: rmsLevel * 100 > 1
? 'size-[11.5rem]'
: 'size-44'} transition-all rounded-full {(model?.info?.meta
?.profile_image_url ?? '/static/favicon.png') !== '/static/favicon.png'
? ' bg-cover bg-center bg-no-repeat'
: 'bg-black dark:bg-white'} "
style={(model?.info?.meta?.profile_image_url ?? '/static/favicon.png') !==
'/static/favicon.png'
? `background-image: url('${model?.info?.meta?.profile_image_url}');`
: ''}
/>
{/if}
</button>
>
{emoji}
</div>
{:else if loading || assistantSpeaking}
<svg
class="size-44 text-gray-900 dark:text-gray-400"
viewBox="0 0 24 24"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg"
><style>
.spinner_qM83 {
animation: spinner_8HQG 1.05s infinite;
}
.spinner_oXPr {
animation-delay: 0.1s;
}
.spinner_ZTLf {
animation-delay: 0.2s;
}
@keyframes spinner_8HQG {
0%,
57.14% {
animation-timing-function: cubic-bezier(0.33, 0.66, 0.66, 1);
transform: translate(0);
}
28.57% {
animation-timing-function: cubic-bezier(0.33, 0, 0.66, 0.33);
transform: translateY(-6px);
}
100% {
transform: translate(0);
}
}
</style><circle class="spinner_qM83" cx="4" cy="12" r="3" /><circle
class="spinner_qM83 spinner_oXPr"
cx="12"
cy="12"
r="3"
/><circle class="spinner_qM83 spinner_ZTLf" cx="20" cy="12" r="3" /></svg
>
{:else}
<div
class="relative flex video-container w-full max-h-full pt-2 pb-4 md:py-6 px-2 h-full"
>
<video
id="camera-feed"
autoplay
class="rounded-2xl h-full min-w-full object-cover object-center"
playsinline
/>
<canvas id="camera-canvas" style="display:none;" />
<div class=" absolute top-4 md:top-8 left-4">
<button
type="button"
class="p-1.5 text-white cursor-pointer backdrop-blur-xl bg-black/10 rounded-full"
on:click={() => {
stopCamera();
}}
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="currentColor"
class="size-6"
>
<path
d="M5.28 4.22a.75.75 0 0 0-1.06 1.06L6.94 8l-2.72 2.72a.75.75 0 1 0 1.06 1.06L8 9.06l2.72 2.72a.75.75 0 1 0 1.06-1.06L9.06 8l2.72-2.72a.75.75 0 0 0-1.06-1.06L8 6.94 5.28 4.22Z"
/>
</svg>
</button>
</div>
</div>
class=" {rmsLevel * 100 > 4
? ' size-52'
: rmsLevel * 100 > 2
? 'size-48'
: rmsLevel * 100 > 1
? 'size-44'
: 'size-40'} transition-all rounded-full {(model?.info?.meta
?.profile_image_url ?? '/static/favicon.png') !== '/static/favicon.png'
? ' bg-cover bg-center bg-no-repeat'
: 'bg-black dark:bg-white'} "
style={(model?.info?.meta?.profile_image_url ?? '/static/favicon.png') !==
'/static/favicon.png'
? `background-image: url('${model?.info?.meta?.profile_image_url}');`
: ''}
/>
{/if}
</div>
</button>
{:else}
<div class="relative flex video-container w-full max-h-full pt-2 pb-4 md:py-6 px-2 h-full">
<video
id="camera-feed"
autoplay
class="rounded-2xl h-full min-w-full object-cover object-center"
playsinline
/>
<div class="flex justify-between items-center pb-2 w-full">
<div>
{#if camera}
<VideoInputMenu
devices={videoInputDevices}
on:change={async (e) => {
console.log(e.detail);
selectedVideoInputDeviceId = e.detail;
await stopVideoStream();
await startVideoStream();
}}
>
<button class=" p-3 rounded-full bg-gray-50 dark:bg-gray-900" type="button">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
class="size-5"
>
<path
fill-rule="evenodd"
d="M15.312 11.424a5.5 5.5 0 0 1-9.201 2.466l-.312-.311h2.433a.75.75 0 0 0 0-1.5H3.989a.75.75 0 0 0-.75.75v4.242a.75.75 0 0 0 1.5 0v-2.43l.31.31a7 7 0 0 0 11.712-3.138.75.75 0 0 0-1.449-.39Zm1.23-3.723a.75.75 0 0 0 .219-.53V2.929a.75.75 0 0 0-1.5 0V5.36l-.31-.31A7 7 0 0 0 3.239 8.188a.75.75 0 1 0 1.448.389A5.5 5.5 0 0 1 13.89 6.11l.311.31h-2.432a.75.75 0 0 0 0 1.5h4.243a.75.75 0 0 0 .53-.219Z"
clip-rule="evenodd"
/>
</svg>
</button>
</VideoInputMenu>
{:else}
<Tooltip content={$i18n.t('Camera')}>
<button
class=" p-3 rounded-full bg-gray-50 dark:bg-gray-900"
type="button"
on:click={async () => {
await navigator.mediaDevices.getUserMedia({ video: true });
startCamera();
}}
>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="size-5"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M6.827 6.175A2.31 2.31 0 0 1 5.186 7.23c-.38.054-.757.112-1.134.175C2.999 7.58 2.25 8.507 2.25 9.574V18a2.25 2.25 0 0 0 2.25 2.25h15A2.25 2.25 0 0 0 21.75 18V9.574c0-1.067-.75-1.994-1.802-2.169a47.865 47.865 0 0 0-1.134-.175 2.31 2.31 0 0 1-1.64-1.055l-.822-1.316a2.192 2.192 0 0 0-1.736-1.039 48.774 48.774 0 0 0-5.232 0 2.192 2.192 0 0 0-1.736 1.039l-.821 1.316Z"
/>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M16.5 12.75a4.5 4.5 0 1 1-9 0 4.5 4.5 0 0 1 9 0ZM18.75 10.5h.008v.008h-.008V10.5Z"
/>
</svg>
</button>
</Tooltip>
{/if}
</div>
<canvas id="camera-canvas" style="display:none;" />
<div>
<div class=" absolute top-4 md:top-8 left-4">
<button
type="button"
class="p-1.5 text-white cursor-pointer backdrop-blur-xl bg-black/10 rounded-full"
on:click={() => {
if (assistantSpeaking) {
stopAllAudio();
}
stopCamera();
}}
>
<div class=" line-clamp-1 text-sm font-medium">
{#if loading}
{$i18n.t('Thinking...')}
{:else if assistantSpeaking}
{$i18n.t('Tap to interrupt')}
{:else}
{$i18n.t('Listening...')}
{/if}
</div>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="currentColor"
class="size-6"
>
<path
d="M5.28 4.22a.75.75 0 0 0-1.06 1.06L6.94 8l-2.72 2.72a.75.75 0 1 0 1.06 1.06L8 9.06l2.72 2.72a.75.75 0 1 0 1.06-1.06L9.06 8l2.72-2.72a.75.75 0 0 0-1.06-1.06L8 6.94 5.28 4.22Z"
/>
</svg>
</button>
</div>
</div>
{/if}
</div>
<div>
<button
class=" p-3 rounded-full bg-gray-50 dark:bg-gray-900"
on:click={async () => {
showCallOverlay.set(false);
}}
type="button"
>
<div class="flex justify-between items-center pb-2 w-full">
<div>
{#if camera}
<VideoInputMenu
devices={videoInputDevices}
on:change={async (e) => {
console.log(e.detail);
selectedVideoInputDeviceId = e.detail;
await stopVideoStream();
await startVideoStream();
}}
>
<button class=" p-3 rounded-full bg-gray-50 dark:bg-gray-900" type="button">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
@@ -888,12 +856,90 @@
class="size-5"
>
<path
d="M6.28 5.22a.75.75 0 0 0-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 1 0 1.06 1.06L10 11.06l3.72 3.72a.75.75 0 1 0 1.06-1.06L11.06 10l3.72-3.72a.75.75 0 0 0-1.06-1.06L10 8.94 6.28 5.22Z"
fill-rule="evenodd"
d="M15.312 11.424a5.5 5.5 0 0 1-9.201 2.466l-.312-.311h2.433a.75.75 0 0 0 0-1.5H3.989a.75.75 0 0 0-.75.75v4.242a.75.75 0 0 0 1.5 0v-2.43l.31.31a7 7 0 0 0 11.712-3.138.75.75 0 0 0-1.449-.39Zm1.23-3.723a.75.75 0 0 0 .219-.53V2.929a.75.75 0 0 0-1.5 0V5.36l-.31-.31A7 7 0 0 0 3.239 8.188a.75.75 0 1 0 1.448.389A5.5 5.5 0 0 1 13.89 6.11l.311.31h-2.432a.75.75 0 0 0 0 1.5h4.243a.75.75 0 0 0 .53-.219Z"
clip-rule="evenodd"
/>
</svg>
</button>
</VideoInputMenu>
{:else}
<Tooltip content={$i18n.t('Camera')}>
<button
class=" p-3 rounded-full bg-gray-50 dark:bg-gray-900"
type="button"
on:click={async () => {
await navigator.mediaDevices.getUserMedia({ video: true });
startCamera();
}}
>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="size-5"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M6.827 6.175A2.31 2.31 0 0 1 5.186 7.23c-.38.054-.757.112-1.134.175C2.999 7.58 2.25 8.507 2.25 9.574V18a2.25 2.25 0 0 0 2.25 2.25h15A2.25 2.25 0 0 0 21.75 18V9.574c0-1.067-.75-1.994-1.802-2.169a47.865 47.865 0 0 0-1.134-.175 2.31 2.31 0 0 1-1.64-1.055l-.822-1.316a2.192 2.192 0 0 0-1.736-1.039 48.774 48.774 0 0 0-5.232 0 2.192 2.192 0 0 0-1.736 1.039l-.821 1.316Z"
/>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M16.5 12.75a4.5 4.5 0 1 1-9 0 4.5 4.5 0 0 1 9 0ZM18.75 10.5h.008v.008h-.008V10.5Z"
/>
</svg>
</button>
</Tooltip>
{/if}
</div>
<div>
<button
type="button"
on:click={() => {
if (assistantSpeaking) {
stopAllAudio();
}
}}
>
<div class=" line-clamp-1 text-sm font-medium">
{#if loading}
{$i18n.t('Thinking...')}
{:else if assistantSpeaking}
{$i18n.t('Tap to interrupt')}
{:else}
{$i18n.t('Listening...')}
{/if}
</div>
</div>
</button>
</div>
<div>
<button
class=" p-3 rounded-full bg-gray-50 dark:bg-gray-900"
on:click={async () => {
stopAudioStream();
stopVideoStream();
showCallOverlay.set(false);
dispatch('close');
}}
type="button"
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
class="size-5"
>
<path
d="M6.28 5.22a.75.75 0 0 0-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 1 0 1.06 1.06L10 11.06l3.72 3.72a.75.75 0 1 0 1.06-1.06L11.06 10l3.72-3.72a.75.75 0 0 0-1.06-1.06L10 8.94 6.28 5.22Z"
/>
</svg>
</button>
</div>
</div>
</div>
@@ -0,0 +1,131 @@
<script>
import { createEventDispatcher } from 'svelte';
import { toast } from 'svelte-sonner';
const dispatch = createEventDispatcher();
import Prompts from './Commands/Prompts.svelte';
import Documents from './Commands/Documents.svelte';
import Models from './Commands/Models.svelte';
import { removeLastWordFromString } from '$lib/utils';
import { uploadWebToVectorDB, uploadYoutubeTranscriptionToVectorDB } from '$lib/apis/rag';
export let prompt = '';
export let files = [];
let commandElement = null;
export const selectUp = () => {
commandElement?.selectUp();
};
export const selectDown = () => {
commandElement?.selectDown();
};
let command = '';
$: command = (prompt?.trim() ?? '').split(' ')?.at(-1) ?? '';
const uploadWeb = async (url) => {
console.log(url);
const doc = {
type: 'doc',
name: url,
collection_name: '',
status: false,
url: url,
error: ''
};
try {
files = [...files, doc];
const res = await uploadWebToVectorDB(localStorage.token, '', url);
if (res) {
doc.status = 'processed';
doc.collection_name = res.collection_name;
files = files;
}
} catch (e) {
// Remove the failed doc from the files array
files = files.filter((f) => f.name !== url);
toast.error(e);
}
};
const uploadYoutubeTranscription = async (url) => {
console.log(url);
const doc = {
type: 'doc',
name: url,
collection_name: '',
status: false,
url: url,
error: ''
};
try {
files = [...files, doc];
const res = await uploadYoutubeTranscriptionToVectorDB(localStorage.token, url);
if (res) {
doc.status = 'processed';
doc.collection_name = res.collection_name;
files = files;
}
} catch (e) {
// Remove the failed doc from the files array
files = files.filter((f) => f.name !== url);
toast.error(e);
}
};
</script>
{#if ['/', '#', '@'].includes(command?.charAt(0))}
{#if command?.charAt(0) === '/'}
<Prompts bind:this={commandElement} bind:prompt bind:files {command} />
{:else if command?.charAt(0) === '#'}
<Documents
bind:this={commandElement}
bind:prompt
{command}
on:youtube={(e) => {
console.log(e);
uploadYoutubeTranscription(e.detail);
}}
on:url={(e) => {
console.log(e);
uploadWeb(e.detail);
}}
on:select={(e) => {
console.log(e);
files = [
...files,
{
type: e?.detail?.type ?? 'file',
...e.detail,
status: 'processed'
}
];
dispatch('select');
}}
/>
{:else if command?.charAt(0) === '@'}
<Models
bind:this={commandElement}
{command}
on:select={(e) => {
prompt = removeLastWordFromString(prompt, command);
dispatch('select', {
type: 'model',
data: e.detail
});
}}
/>
{/if}
{/if}
@@ -2,13 +2,14 @@
import { createEventDispatcher } from 'svelte';
import { documents } from '$lib/stores';
import { removeFirstHashWord, isValidHttpUrl } from '$lib/utils';
import { removeLastWordFromString, isValidHttpUrl } from '$lib/utils';
import { tick, getContext } from 'svelte';
import { toast } from 'svelte-sonner';
const i18n = getContext('i18n');
export let prompt = '';
export let command = '';
const dispatch = createEventDispatcher();
let selectedIdx = 0;
@@ -27,7 +28,7 @@
title: $i18n.t('All Documents'),
collection_names: $documents.map((doc) => doc.collection_name)
}
]
]
: []),
...$documents
.reduce((a, e, i, arr) => {
@@ -43,16 +44,16 @@
];
$: filteredCollections = collections
.filter((collection) => findByName(collection, prompt))
.filter((collection) => findByName(collection, command))
.sort((a, b) => a.name.localeCompare(b.name));
$: filteredDocs = $documents
.filter((doc) => findByName(doc, prompt))
.filter((doc) => findByName(doc, command))
.sort((a, b) => a.title.localeCompare(b.title));
$: filteredItems = [...filteredCollections, ...filteredDocs];
$: if (prompt) {
$: if (command) {
selectedIdx = 0;
console.log(filteredCollections);
@@ -62,9 +63,9 @@
name: string;
};
const findByName = (obj: ObjectWithName, prompt: string) => {
const findByName = (obj: ObjectWithName, command: string) => {
const name = obj.name.toLowerCase();
return name.includes(prompt.toLowerCase().split(' ')?.at(0)?.substring(1) ?? '');
return name.includes(command.toLowerCase().split(' ')?.at(0)?.substring(1) ?? '');
};
export const selectUp = () => {
@@ -78,7 +79,7 @@
const confirmSelect = async (doc) => {
dispatch('select', doc);
prompt = removeFirstHashWord(prompt);
prompt = removeLastWordFromString(prompt, command);
const chatInputElement = document.getElementById('chat-textarea');
await tick();
@@ -89,7 +90,7 @@
const confirmSelectWeb = async (url) => {
dispatch('url', url);
prompt = removeFirstHashWord(prompt);
prompt = removeLastWordFromString(prompt, command);
const chatInputElement = document.getElementById('chat-textarea');
await tick();
@@ -100,7 +101,7 @@
const confirmSelectYoutube = async (url) => {
dispatch('youtube', url);
prompt = removeFirstHashWord(prompt);
prompt = removeLastWordFromString(prompt, command);
const chatInputElement = document.getElementById('chat-textarea');
await tick();
@@ -110,7 +111,10 @@
</script>
{#if filteredItems.length > 0 || prompt.split(' ')?.at(0)?.substring(1).startsWith('http')}
<div class="pl-1 pr-12 mb-3 text-left w-full absolute bottom-0 left-0 right-0 z-10">
<div
id="commands-container"
class="pl-1 pr-12 mb-3 text-left w-full absolute bottom-0 left-0 right-0 z-10"
>
<div class="flex w-full dark:border dark:border-gray-850 rounded-lg">
<div class=" bg-gray-50 dark:bg-gray-850 w-10 rounded-l-lg text-center">
<div class=" text-lg font-semibold mt-2">#</div>
@@ -0,0 +1,90 @@
<script lang="ts">
import { createEventDispatcher, onMount } from 'svelte';
import { tick, getContext } from 'svelte';
import { models } from '$lib/stores';
const i18n = getContext('i18n');
const dispatch = createEventDispatcher();
export let command = '';
let selectedIdx = 0;
let filteredModels = [];
$: filteredModels = $models
.filter((p) =>
p.name.toLowerCase().includes(command.toLowerCase().split(' ')?.at(0)?.substring(1) ?? '')
)
.sort((a, b) => a.name.localeCompare(b.name));
$: if (command) {
selectedIdx = 0;
}
export const selectUp = () => {
selectedIdx = Math.max(0, selectedIdx - 1);
};
export const selectDown = () => {
selectedIdx = Math.min(selectedIdx + 1, filteredModels.length - 1);
};
const confirmSelect = async (model) => {
command = '';
dispatch('select', model);
};
onMount(async () => {
await tick();
const chatInputElement = document.getElementById('chat-textarea');
await tick();
chatInputElement?.focus();
await tick();
});
</script>
{#if filteredModels.length > 0}
<div
id="commands-container"
class="pl-1 pr-12 mb-3 text-left w-full absolute bottom-0 left-0 right-0 z-10"
>
<div class="flex w-full dark:border dark:border-gray-850 rounded-lg">
<div class=" bg-gray-50 dark:bg-gray-850 w-10 rounded-l-lg text-center">
<div class=" text-lg font-semibold mt-2">@</div>
</div>
<div
class="max-h-60 flex flex-col w-full rounded-r-lg bg-white dark:bg-gray-900 dark:text-gray-100"
>
<div class="m-1 overflow-y-auto p-1 rounded-r-lg space-y-0.5 scrollbar-hidden">
{#each filteredModels as model, modelIdx}
<button
class="px-3 py-1.5 rounded-xl w-full text-left {modelIdx === selectedIdx
? 'bg-gray-50 dark:bg-gray-850 selected-command-option-button'
: ''}"
type="button"
on:click={() => {
confirmSelect(model);
}}
on:mousemove={() => {
selectedIdx = modelIdx;
}}
on:focus={() => {}}
>
<div class="flex font-medium text-black dark:text-gray-100 line-clamp-1">
<img
src={model?.info?.meta?.profile_image_url ?? '/static/favicon.png'}
alt={model?.name ?? model.id}
class="rounded-full size-6 items-center mr-2"
/>
{model.name}
</div>
</button>
{/each}
</div>
</div>
</div>
</div>
{/if}
@@ -7,27 +7,30 @@
const i18n = getContext('i18n');
export let files;
export let prompt = '';
let selectedCommandIdx = 0;
let filteredPromptCommands = [];
$: filteredPromptCommands = $prompts
.filter((p) => p.command.toLowerCase().includes(prompt.toLowerCase()))
export let prompt = '';
export let command = '';
let selectedPromptIdx = 0;
let filteredPrompts = [];
$: filteredPrompts = $prompts
.filter((p) => p.command.toLowerCase().includes(command.toLowerCase()))
.sort((a, b) => a.title.localeCompare(b.title));
$: if (prompt) {
selectedCommandIdx = 0;
$: if (command) {
selectedPromptIdx = 0;
}
export const selectUp = () => {
selectedCommandIdx = Math.max(0, selectedCommandIdx - 1);
selectedPromptIdx = Math.max(0, selectedPromptIdx - 1);
};
export const selectDown = () => {
selectedCommandIdx = Math.min(selectedCommandIdx + 1, filteredPromptCommands.length - 1);
selectedPromptIdx = Math.min(selectedPromptIdx + 1, filteredPrompts.length - 1);
};
const confirmCommand = async (command) => {
const confirmPrompt = async (command) => {
let text = command.content;
if (command.content.includes('{{CLIPBOARD}}')) {
@@ -79,7 +82,6 @@
await tick();
const words = findWordIndices(prompt);
if (words.length > 0) {
const word = words.at(0);
chatInputElement.setSelectionRange(word?.startIndex, word.endIndex + 1);
@@ -87,8 +89,11 @@
};
</script>
{#if filteredPromptCommands.length > 0}
<div class="pl-1 pr-12 mb-3 text-left w-full absolute bottom-0 left-0 right-0 z-10">
{#if filteredPrompts.length > 0}
<div
id="commands-container"
class="pl-1 pr-12 mb-3 text-left w-full absolute bottom-0 left-0 right-0 z-10"
>
<div class="flex w-full dark:border dark:border-gray-850 rounded-lg">
<div class=" bg-gray-50 dark:bg-gray-850 w-10 rounded-l-lg text-center">
<div class=" text-lg font-semibold mt-2">/</div>
@@ -98,26 +103,26 @@
class="max-h-60 flex flex-col w-full rounded-r-lg bg-white dark:bg-gray-900 dark:text-gray-100"
>
<div class="m-1 overflow-y-auto p-1 rounded-r-lg space-y-0.5 scrollbar-hidden">
{#each filteredPromptCommands as command, commandIdx}
{#each filteredPrompts as prompt, promptIdx}
<button
class=" px-3 py-1.5 rounded-xl w-full text-left {commandIdx === selectedCommandIdx
class=" px-3 py-1.5 rounded-xl w-full text-left {promptIdx === selectedPromptIdx
? ' bg-gray-50 dark:bg-gray-850 selected-command-option-button'
: ''}"
type="button"
on:click={() => {
confirmCommand(command);
confirmPrompt(prompt);
}}
on:mousemove={() => {
selectedCommandIdx = commandIdx;
selectedPromptIdx = promptIdx;
}}
on:focus={() => {}}
>
<div class=" font-medium text-black dark:text-gray-100">
{command.command}
{prompt.command}
</div>
<div class=" text-xs text-gray-600 dark:text-gray-100">
{command.title}
{prompt.title}
</div>
</button>
{/each}
@@ -1,181 +0,0 @@
<script lang="ts">
import { createEventDispatcher } from 'svelte';
import { generatePrompt } from '$lib/apis/ollama';
import { models } from '$lib/stores';
import { splitStream } from '$lib/utils';
import { tick, getContext } from 'svelte';
import { toast } from 'svelte-sonner';
const i18n = getContext('i18n');
const dispatch = createEventDispatcher();
export let prompt = '';
export let user = null;
export let chatInputPlaceholder = '';
export let messages = [];
let selectedIdx = 0;
let filteredModels = [];
$: filteredModels = $models
.filter((p) =>
p.name.toLowerCase().includes(prompt.toLowerCase().split(' ')?.at(0)?.substring(1) ?? '')
)
.sort((a, b) => a.name.localeCompare(b.name));
$: if (prompt) {
selectedIdx = 0;
}
export const selectUp = () => {
selectedIdx = Math.max(0, selectedIdx - 1);
};
export const selectDown = () => {
selectedIdx = Math.min(selectedIdx + 1, filteredModels.length - 1);
};
const confirmSelect = async (model) => {
prompt = '';
dispatch('select', model);
};
const confirmSelectCollaborativeChat = async (model) => {
// dispatch('select', model);
prompt = '';
user = JSON.parse(JSON.stringify(model.name));
await tick();
chatInputPlaceholder = $i18n.t('{{modelName}} is thinking...', { modelName: model.name });
const chatInputElement = document.getElementById('chat-textarea');
await tick();
chatInputElement?.focus();
await tick();
const convoText = messages.reduce((a, message, i, arr) => {
return `${a}### ${message.role.toUpperCase()}\n${message.content}\n\n`;
}, '');
const res = await generatePrompt(localStorage.token, model.name, convoText);
if (res && res.ok) {
const reader = res.body
.pipeThrough(new TextDecoderStream())
.pipeThrough(splitStream('\n'))
.getReader();
while (true) {
const { value, done } = await reader.read();
if (done) {
break;
}
try {
let lines = value.split('\n');
for (const line of lines) {
if (line !== '') {
console.log(line);
let data = JSON.parse(line);
if ('detail' in data) {
throw data;
}
if ('id' in data) {
console.log(data);
} else {
if (data.done == false) {
if (prompt == '' && data.response == '\n') {
continue;
} else {
prompt += data.response;
console.log(data.response);
chatInputElement.scrollTop = chatInputElement.scrollHeight;
await tick();
}
}
}
}
}
} catch (error) {
console.log(error);
if ('detail' in error) {
toast.error(error.detail);
}
break;
}
}
} else {
if (res !== null) {
const error = await res.json();
console.log(error);
if ('detail' in error) {
toast.error(error.detail);
} else {
toast.error(error.error);
}
} else {
toast.error(
$i18n.t('Uh-oh! There was an issue connecting to {{provider}}.', { provider: 'llama' })
);
}
}
chatInputPlaceholder = '';
console.log(user);
};
</script>
{#if prompt.charAt(0) === '@'}
{#if filteredModels.length > 0}
<div class="pl-1 pr-12 mb-3 text-left w-full absolute bottom-0 left-0 right-0 z-10">
<div class="flex w-full dark:border dark:border-gray-850 rounded-lg">
<div class=" bg-gray-50 dark:bg-gray-850 w-10 rounded-l-lg text-center">
<div class=" text-lg font-semibold mt-2">@</div>
</div>
<div
class="max-h-60 flex flex-col w-full rounded-r-lg bg-white dark:bg-gray-900 dark:text-gray-100"
>
<div class="m-1 overflow-y-auto p-1 rounded-r-lg space-y-0.5 scrollbar-hidden">
{#each filteredModels as model, modelIdx}
<button
class="px-3 py-1.5 rounded-xl w-full text-left {modelIdx === selectedIdx
? 'bg-gray-50 dark:bg-gray-850 selected-command-option-button'
: ''}"
type="button"
on:click={() => {
confirmSelect(model);
}}
on:mousemove={() => {
selectedIdx = modelIdx;
}}
on:focus={() => {}}
>
<div class="flex font-medium text-black dark:text-gray-100 line-clamp-1">
<img
src={model?.info?.meta?.profile_image_url ?? '/static/favicon.png'}
alt={model?.name ?? model.id}
class="rounded-full size-6 items-center mr-2"
/>
{model.name}
</div>
<!-- <div class=" text-xs text-gray-600 line-clamp-1">
{doc.title}
</div> -->
</button>
{/each}
</div>
</div>
</div>
</div>
{/if}
{/if}
+60 -36
View File
@@ -5,15 +5,12 @@
import { toast } from 'svelte-sonner';
import { getChatList, updateChatById } from '$lib/apis/chats';
import { copyToClipboard, findWordIndices } from '$lib/utils';
import UserMessage from './Messages/UserMessage.svelte';
import ResponseMessage from './Messages/ResponseMessage.svelte';
import Placeholder from './Messages/Placeholder.svelte';
import Spinner from '../common/Spinner.svelte';
import { imageGenerations } from '$lib/apis/images';
import { copyToClipboard, findWordIndices } from '$lib/utils';
import CompareMessages from './Messages/CompareMessages.svelte';
import { stringify } from 'postcss';
import MultiResponseMessages from './Messages/MultiResponseMessages.svelte';
const i18n = getContext('i18n');
@@ -22,6 +19,7 @@
export let sendPrompt: Function;
export let continueGeneration: Function;
export let regenerateResponse: Function;
export let mergeResponses: Function;
export let chatActionHandler: Function;
export let user = $_user;
@@ -53,34 +51,43 @@
}
};
const confirmEditMessage = async (messageId, content) => {
let userPrompt = content;
let userMessageId = uuidv4();
const confirmEditMessage = async (messageId, content, submit = true) => {
if (submit) {
let userPrompt = content;
let userMessageId = uuidv4();
let userMessage = {
id: userMessageId,
parentId: history.messages[messageId].parentId,
childrenIds: [],
role: 'user',
content: userPrompt,
...(history.messages[messageId].files && { files: history.messages[messageId].files }),
models: selectedModels.filter((m, mIdx) => selectedModels.indexOf(m) === mIdx)
};
let userMessage = {
id: userMessageId,
parentId: history.messages[messageId].parentId,
childrenIds: [],
role: 'user',
content: userPrompt,
...(history.messages[messageId].files && { files: history.messages[messageId].files }),
models: selectedModels
};
let messageParentId = history.messages[messageId].parentId;
let messageParentId = history.messages[messageId].parentId;
if (messageParentId !== null) {
history.messages[messageParentId].childrenIds = [
...history.messages[messageParentId].childrenIds,
userMessageId
];
if (messageParentId !== null) {
history.messages[messageParentId].childrenIds = [
...history.messages[messageParentId].childrenIds,
userMessageId
];
}
history.messages[userMessageId] = userMessage;
history.currentId = userMessageId;
await tick();
await sendPrompt(userPrompt, userMessageId);
} else {
history.messages[messageId].content = content;
await tick();
await updateChatById(localStorage.token, chatId, {
messages: messages,
history: history
});
}
history.messages[userMessageId] = userMessage;
history.currentId = userMessageId;
await tick();
await sendPrompt(userPrompt, userMessageId);
};
const updateChatMessages = async () => {
@@ -305,7 +312,7 @@
{#each messages as message, messageIdx}
<div class=" w-full {messageIdx === messages.length - 1 ? ' pb-12' : ''}">
<div
class="flex flex-col justify-between px-5 mb-3 {$settings?.widescreenMode ?? null
class="flex flex-col justify-between px-5 mb-3 {($settings?.widescreenMode ?? null)
? 'max-w-full'
: 'max-w-5xl'} mx-auto rounded-lg group"
>
@@ -317,16 +324,16 @@
{message}
isFirstMessage={messageIdx === 0}
siblings={message.parentId !== null
? history.messages[message.parentId]?.childrenIds ?? []
: Object.values(history.messages)
? (history.messages[message.parentId]?.childrenIds ?? [])
: (Object.values(history.messages)
.filter((message) => message.parentId === null)
.map((message) => message.id) ?? []}
.map((message) => message.id) ?? [])}
{confirmEditMessage}
{showPreviousMessage}
{showNextMessage}
copyToClipboard={copyToClipboardWithToast}
/>
{:else if $mobile || (history.messages[message.parentId]?.models?.length ?? 1) === 1}
{:else if (history.messages[message.parentId]?.models?.length ?? 1) === 1}
{#key message.id && history.currentId}
<ResponseMessage
{message}
@@ -342,7 +349,13 @@
{continueGeneration}
{regenerateResponse}
on:action={async (e) => {
await chatActionHandler(chatId, e.detail, message.model, message.id);
console.log('action', e);
if (typeof e.detail === 'string') {
await chatActionHandler(chatId, e.detail, message.model, message.id);
} else {
const { id, event } = e.detail;
await chatActionHandler(chatId, id, message.model, message.id, event);
}
}}
on:save={async (e) => {
console.log('save', e);
@@ -358,8 +371,9 @@
{/key}
{:else}
{#key message.parentId}
<CompareMessages
<MultiResponseMessages
bind:history
isLastMessage={messageIdx + 1 === messages.length}
{messages}
{readOnly}
{chatId}
@@ -370,7 +384,17 @@
{rateMessage}
copyToClipboard={copyToClipboardWithToast}
{continueGeneration}
{mergeResponses}
{regenerateResponse}
on:action={async (e) => {
console.log('action', e);
if (typeof e.detail === 'string') {
await chatActionHandler(chatId, e.detail, message.model, message.id);
} else {
const { id, event } = e.detail;
await chatActionHandler(chatId, id, message.model, message.id, event);
}
}}
on:change={async () => {
await updateChatById(localStorage.token, chatId, {
messages: messages,
@@ -0,0 +1,56 @@
<script lang="ts">
import CitationsModal from './CitationsModal.svelte';
export let citations = [];
let showCitationModal = false;
let selectedCitation = null;
</script>
<CitationsModal bind:show={showCitationModal} citation={selectedCitation} />
<div class="mt-1 mb-2 w-full flex gap-1 items-center flex-wrap">
{#each citations.reduce((acc, citation) => {
citation.document.forEach((document, index) => {
const metadata = citation.metadata?.[index];
const id = metadata?.source ?? 'N/A';
let source = citation?.source;
if (metadata?.name) {
source = { ...source, name: metadata.name };
}
// Check if ID looks like a URL
if (id.startsWith('http://') || id.startsWith('https://')) {
source = { name: id };
}
const existingSource = acc.find((item) => item.id === id);
if (existingSource) {
existingSource.document.push(document);
existingSource.metadata.push(metadata);
} else {
acc.push( { id: id, source: 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="flex-1 mx-2 line-clamp-1">
{citation.source.name}
</div>
</button>
</div>
{/each}
</div>

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