Compare commits

...

388 Commits

Author SHA1 Message Date
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
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
Timothy Jaeryang Baek c869652ef4 Merge pull request #4322 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.12
2024-08-07 15:22:04 +02:00
Timothy J. Baek 240a30147d chore: requirements bump 2024-08-07 15:07:47 +02:00
Timothy J. Baek 91fdb86fcc chore: update pyproject.toml 2024-08-07 15:06:59 +02:00
Timothy J. Baek 0e5a56e2cf doc: changelog 2024-08-07 15:04:11 +02:00
Timothy J. Baek 40ecc2563a chore: format 2024-08-07 14:51:07 +02:00
Timothy J. Baek 7e473f194d fix: modal not closing in many model chat 2024-08-07 14:47:43 +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
Timothy J. Baek 0c231fd387 fix: styling 2024-08-07 12:22:35 +02:00
Timothy Jaeryang Baek 7f3ebcaa91 Merge pull request #4410 from silentoplayz/silentoplayz-patch-1
docs: Update bug_report.md
2024-08-07 12:17:53 +02:00
root 1f8d08eaa2 SSRF Fix 2024-08-07 03:30:21 -04:00
Timothy J. Baek 5904ef86f2 refac 2024-08-07 02:18:29 +02:00
Timothy J. Baek d692649bac refac 2024-08-07 02:11:37 +02:00
Timothy J. Baek 0d019a00c9 refac 2024-08-07 02:06:57 +02:00
Timothy J. Baek e6b3de310b refac 2024-08-07 01:55:37 +02:00
silentoplayz d19b96d0c2 Update bug_report.md 2024-08-06 23:10:04 +00:00
Timothy J. Baek 7cbc94592e fix: styling 2024-08-06 23:48:10 +02:00
Timothy J. Baek 9747f1e841 revert: markdown rendering 2024-08-06 23:34:51 +02:00
Timothy Jaeryang Baek 42bc24a646 Merge pull request #4404 from justinh-rahb/security-policy
Update SECURITY.md to Improve Vulnerability Reporting Process
2024-08-06 21:38:21 +02:00
Justin Hayes 35115957d8 Update SECURITY.md 2024-08-06 15:08:37 -04:00
Justin Hayes b193eb1d82 Update SECURITY.md 2024-08-06 14:57:07 -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
Timothy J. Baek ff90b125ee Revert "refac"
This reverts commit a140d319fe.
2024-08-06 11:43:47 +02:00
Michael Poluektov 831fe9f509 cleanup 2024-08-06 10:15:29 +01:00
Timothy J. Baek a140d319fe refac 2024-08-06 10:29:29 +02:00
Timothy J. Baek 9581bf5dde refac: styling 2024-08-05 19:59:10 +02:00
Timothy J. Baek 76dcabdce6 fix 2024-08-05 19:54:32 +02:00
Timothy J. Baek dc262a5f79 refac: styling 2024-08-05 19:43:51 +02:00
Timothy J. Baek db5830dc28 refac: prose styling 2024-08-05 19:40:46 +02:00
Timothy J. Baek ecf3d567cf chore: pyproject.toml 2024-08-05 19:29:32 +02:00
Timothy J. Baek e8375e9acd refac 2024-08-05 19:16:57 +02:00
Timothy J. Baek 8c84c74197 refac 2024-08-05 18:58:57 +02:00
Timothy J. Baek f7d1225c23 enh: codespan styling 2024-08-05 18:57:28 +02:00
Timothy Jaeryang Baek 3dc90b854c Merge pull request #4389 from open-webui/dev-markdown
refac: markdown
2024-08-05 18:01:26 +02:00
Timothy J. Baek ac3c657315 refac 2024-08-05 18:00:55 +02:00
Timothy J. Baek 37b117a84c fix: styling 2024-08-05 18:00:04 +02:00
Timothy J. Baek ab6346ea1c refac: markdown rendering
Co-Authored-By: Jun Siang Cheah <me@jscheah.me>
2024-08-05 17:47:18 +02:00
Candido Sales Gomes b289eef523 typo 2024-08-05 11:16:28 -04:00
Timothy J. Baek 6e6f9862e6 fix 2024-08-05 17:15:07 +02:00
Timothy J. Baek 7d42a79177 refac: token rendering 2024-08-05 17:12:41 +02: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
Timothy Jaeryang Baek 05bbca5b07 Merge pull request #4369 from open-webui/dependabot/pip/backend/dev/black-24.8.0
chore(deps): bump black from 24.4.2 to 24.8.0 in /backend
2024-08-05 11:07:15 +02:00
Timothy Jaeryang Baek 5afe0be5b4 Merge pull request #4368 from open-webui/dependabot/pip/backend/dev/validators-0.33.0
chore(deps): bump validators from 0.28.1 to 0.33.0 in /backend
2024-08-05 11:07:04 +02:00
Timothy Jaeryang Baek d8aa2cc05f Merge pull request #4367 from open-webui/dependabot/pip/backend/dev/python-pptx-1.0.0
chore(deps): bump python-pptx from 0.6.23 to 1.0.0 in /backend
2024-08-05 11:06:56 +02:00
Timothy Jaeryang Baek 7b071d403f Merge pull request #4366 from open-webui/dependabot/pip/backend/dev/pypdf-4.3.1
chore(deps): bump pypdf from 4.2.0 to 4.3.1 in /backend
2024-08-05 11:06:44 +02:00
Timothy Jaeryang Baek d5b1e5e0c0 Merge pull request #4365 from open-webui/dependabot/pip/backend/dev/boto3-1.34.153
chore(deps): bump boto3 from 1.34.110 to 1.34.153 in /backend
2024-08-05 11:06:35 +02:00
Jan-Timo Hesse cec5fdd144 Added EXTRA_MODULES argument 2024-08-05 09:42:16 +02:00
dependabot[bot] d8498aa2b0 chore(deps): bump black from 24.4.2 to 24.8.0 in /backend
Bumps [black](https://github.com/psf/black) from 24.4.2 to 24.8.0.
- [Release notes](https://github.com/psf/black/releases)
- [Changelog](https://github.com/psf/black/blob/main/CHANGES.md)
- [Commits](https://github.com/psf/black/compare/24.4.2...24.8.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-08-05 02:52:14 +00:00
dependabot[bot] e593d3aee3 chore(deps): bump validators from 0.28.1 to 0.33.0 in /backend
Bumps [validators](https://github.com/python-validators/validators) from 0.28.1 to 0.33.0.
- [Release notes](https://github.com/python-validators/validators/releases)
- [Changelog](https://github.com/python-validators/validators/blob/master/CHANGES.md)
- [Commits](https://github.com/python-validators/validators/compare/0.28.1...0.33.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-08-05 02:52:10 +00:00
dependabot[bot] 323cfaf2d7 chore(deps): bump python-pptx from 0.6.23 to 1.0.0 in /backend
Bumps [python-pptx](https://github.com/scanny/python-pptx) from 0.6.23 to 1.0.0.
- [Changelog](https://github.com/scanny/python-pptx/blob/master/HISTORY.rst)
- [Commits](https://github.com/scanny/python-pptx/compare/v0.6.23...v1.0.0)

---
updated-dependencies:
- dependency-name: python-pptx
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-08-05 02:52:07 +00:00
dependabot[bot] 637f0c6fb2 chore(deps): bump pypdf from 4.2.0 to 4.3.1 in /backend
Bumps [pypdf](https://github.com/py-pdf/pypdf) from 4.2.0 to 4.3.1.
- [Release notes](https://github.com/py-pdf/pypdf/releases)
- [Changelog](https://github.com/py-pdf/pypdf/blob/main/CHANGELOG.md)
- [Commits](https://github.com/py-pdf/pypdf/compare/4.2.0...4.3.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-08-05 02:52:06 +00:00
dependabot[bot] 2756403102 chore(deps): bump boto3 from 1.34.110 to 1.34.153 in /backend
Bumps [boto3](https://github.com/boto/boto3) from 1.34.110 to 1.34.153.
- [Release notes](https://github.com/boto/boto3/releases)
- [Commits](https://github.com/boto/boto3/compare/1.34.110...1.34.153)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-08-05 02:52:03 +00:00
Timothy Jaeryang Baek 92861d095a Merge pull request #4362 from PeterDaveHello/Update-locale-zh-TW
i18n: Update and improve zh-TW Traditional Chinese translation
2024-08-05 01:05:10 +02:00
Timothy J. Baek bf6c6627e8 revert 2024-08-05 01:03:22 +02:00
Peter Dave Hello b217cb76f0 i18n: Update and improve zh-TW Traditional Chinese translation 2024-08-05 03:29:12 +08:00
Timothy J. Baek b414fde6ca fix 2024-08-04 18:34:29 +02:00
Timothy J. Baek d8c39569be fix: infinite scroll stuck on loading issue 2024-08-04 18:20:59 +02:00
Timothy J. Baek 5ce15bb0df refac 2024-08-04 18:16:00 +02:00
Timothy J. Baek 88086935af feat: markdown image preview 2024-08-04 18:14:17 +02:00
Timothy J. Baek ace3552e15 chore: format 2024-08-04 17:42:16 +02:00
Timothy Jaeryang Baek 003ceff7e4 Merge pull request #4349 from open-webui/dev-scroll
feat: Sidebar infinite scroll (pagination)
2024-08-04 17:36:18 +02:00
Timothy J. Baek 209ccdf668 refac 2024-08-04 17:35:26 +02:00
Timothy J. Baek e5dd7e65d4 refac 2024-08-04 17:31:41 +02:00
Timothy J. Baek b2999ad590 refac 2024-08-04 17:11:41 +02:00
Timothy Jaeryang Baek 1871a7a2fa Merge pull request #4354 from cheahjs/fix/admin-shared-chat-when-disabled
fix: admins shared chats with ENABLE_ADMIN_CHAT_ACCESS set to false
2024-08-04 17:08:33 +02:00
Timothy J. Baek 49677e9c9d refac 2024-08-04 17:04:15 +02:00
Jun Siang Cheah 73954f4a63 fix: admins viewing shared chats with ENABLE_ADMIN_CHAT_ACCESS set to false 2024-08-04 16:00:52 +01:00
Timothy J. Baek a084938d9c refac: chatlist skip, limit -> page 2024-08-04 16:58:08 +02:00
Timothy Jaeryang Baek 1bf042ac84 Merge pull request #4351 from cheahjs/feat/disable-admin-chat-view
feat: add ENABLE_ADMIN_CHAT_ACCESS to control admin access to user chats
2024-08-04 16:37:30 +02:00
Timothy J. Baek a2f9f7c975 refac 2024-08-04 16:36:44 +02:00
Jun Siang Cheah 565f40c642 feat: add ENABLE_ADMIN_CHAT_ACCESS to control admin access to user chats 2024-08-04 15:16:14 +01:00
Timothy J. Baek 4441338574 refac: onScroll -> IntersectionObserver for infinite scroll 2024-08-04 15:58:36 +02:00
Timothy Jaeryang Baek 389d650ee3 Merge pull request #4266 from thearyadev/sidebar-pagination
feat: Sidebar infinite scroll (pagination)
2024-08-04 15:33:09 +02:00
Timothy J. Baek 080d45239a fix: chat control "stop token" param issue 2024-08-04 14:27:18 +02:00
Timothy Jaeryang Baek 91851114e4 Merge pull request #4295 from michaelpoluektov/refactor-tools
refactor: Refactor OpenAI API to use helper functions, silence LSP/linter warnings
2024-08-04 14:17:52 +02:00
Timothy Jaeryang Baek 5b11099a0b Merge pull request #4335 from 5E-324/toggle-scrolling
feat: Add option to toggle scrolling to bottom when switching between different branches of the conversation
2024-08-04 14:11:03 +02:00
Timothy Jaeryang Baek 2996d1e096 Merge pull request #4332 from cheahjs/fix/integration-test-no-space
ci: prune docker cache earlier in integration tests
2024-08-04 14:09:39 +02:00
Timothy Jaeryang Baek be82175f86 Merge pull request #4338 from KarlLee830/translate
i18n: Update Chinese Translation
2024-08-04 14:09:25 +02:00
Timothy Jaeryang Baek 05977bd1c7 Merge pull request #4342 from michaelpoluektov/fix-non-streaming-function-text
fix: function returning "str"
2024-08-04 14:09:12 +02:00
Timothy Jaeryang Baek 8074289a8f Merge pull request #4333 from JohnTheNerd/comfyui-fix
fix: made the COMFYUI_FLUX_FP8_CLIP environment variable bool instead of str
2024-08-04 14:08:44 +02:00
Michael Poluektov f8ba0334e8 fix: non streaming functions 2024-08-04 12:10:02 +01:00
Karl Lee 7c9bed83d2 i18n: Update Chinese Translation 2024-08-04 16:00:42 +08:00
Zhuoran 482a7723b2 Default to true 2024-08-04 11:33:20 +08:00
Zhuoran 948d29b676 Add option to toggle scrolling to bottom when switching between branches 2024-08-04 11:15:20 +08:00
Aryan Kothari 3fa6c41303 chore: remove debug ui elements 2024-08-03 20:17:03 -04:00
Aryan Kothari 220a4bb535 add: loading animation 2024-08-03 20:13:59 -04:00
Aryan Kothari 287559e834 fix: tag deletion rehydration bug
fixes a bug that caused the deletion of a tag, when filtering by tag, to rehydrate all chats instead of the filtered list.
2024-08-03 19:42:45 -04:00
Aryan Kothari cdac0cd1df refactor: disable pagination moved to a function
reduces repeated code
2024-08-03 19:40:31 -04:00
Michael Poluektov 3653126179 refac: undo raw split, remove gpt-4-vision-preview 2024-08-04 00:34:24 +01:00
Jun Siang Cheah 2eb662df85 fix: prune docker cache earlier in integration tests 2024-08-03 20:53:31 +01:00
John Karabudak cdcf214455 made the COMFYUI_FLUX_FP8_CLIP environment variable bool instead of str
this should fix #4328
2024-08-03 16:58:03 -02:30
Timothy Jaeryang Baek a75a9c953a Merge pull request #4329 from cheahjs/fix/missing-openai-usage-info
fix: missing openai usage information
2024-08-03 20:51:03 +02:00
Timothy Jaeryang Baek 800ba206f7 Merge pull request #4326 from amirsubhi/dev
i18n : Updated ms-MY Malay (Bahasa Malaysia) Language
2024-08-03 20:50:27 +02:00
Timothy Jaeryang Baek 8d5aa98297 Merge pull request #4325 from Yanyutin753/model_bug
💄 Fixed BUG that could not be deleted after @model was selected
2024-08-03 20:48:13 +02:00
Jun Siang Cheah 283234d51d fix: missing openai usage information 2024-08-03 19:06:19 +01:00
Aryan Kothari f9e1a933a9 fix: bug in chat deletion pagination interact
change 1: when selecting a tag to filter the `tagView` store is set to disable paginated loading. This is so all tagged items can be loaded at once.  deleting a tag when in the filtered view returns to the unfiltered view. this change now sets the `tagView` store to `false` so pagination can continue

change 2: formatting
2024-08-03 11:53:02 -04:00
Clivia ebd8ef1a9e 💄 Fixed BUG that could not be deleted after @model was selected 2024-08-03 23:36:30 +08:00
amirsubhi ed691e26a6 Updated ms-MY Malay (Bahasa Malaysia) Language
Added ms-MY Malay (Bahasa Malaysia) Translation
2024-08-03 23:09:54 +08:00
Aryan Kothari 067d76fece fix: dynamically determine page size
- larger screens where chat list (35px*pageSize) is smaller than window.InnerHeight, will not be able to scroll.
- performance can dynamically scale, allowing mobile devices to load only what they need.
2024-08-03 10:35:13 -04:00
Timothy Jaeryang Baek 534c18c94c Merge pull request #4307 from Yanyutin753/@model_image
💄 Support @Model display model pictures
2024-08-03 16:01:45 +02:00
Timothy J. Baek af9e0cc33a fix 2024-08-03 16:01:25 +02:00
Aryan Kothari 6847c2fc8c Merge branch 'origin/dev' into sidebar-pagination [skip ci] 2024-08-03 09:57:54 -04:00
Clivia 028eb7b351 Better Practice
 Better Practice
2024-08-03 21:37:45 +08:00
Michael Poluektov 12c21fac22 refac: apps/openai/main.py and utils 2024-08-03 14:24:26 +01:00
Timothy J. Baek 774defd184 fix: image preview element styling issue 2024-08-03 14:42:06 +02:00
Timothy Jaeryang Baek 6657f28694 Merge pull request #4309 from aleixdorca/dev
i18n: Update catalan translation.json
2024-08-03 14:18:09 +02:00
Aleix Dorca 9d7507236f i18n: Update catalan translation.json 2024-08-03 06:46:08 +02:00
Clivia 8f49429228 Show only models that are not be hidden 2024-08-03 10:58:12 +08:00
Clivia f21e9dbd9a 💄 Support @Model display model pictures 2024-08-03 10:48:54 +08:00
Timothy Jaeryang Baek d3146d20ad Merge pull request #4304 from open-webui/dev
fix: requirements
2024-08-03 00:48:57 +02:00
Timothy J. Baek 24a177a149 chore: requirements 2024-08-03 00:47:05 +02:00
Timothy Jaeryang Baek a58dfccb7d Merge pull request #4273 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.11
2024-08-03 00:03:15 +02:00
Timothy J. Baek b35293228e refac 2024-08-03 00:01:54 +02:00
Timothy J. Baek 963e250654 refac 2024-08-02 23:54:18 +02:00
Timothy J. Baek ca8b766c09 refac 2024-08-02 23:48:49 +02:00
Timothy J. Baek 21eca7f1c1 doc: changelog 2024-08-02 23:47:00 +02:00
Timothy J. Baek 8062866973 chore: format 2024-08-02 22:35:02 +02:00
Timothy Jaeryang Baek 99530358fd Merge pull request #4300 from JohnTheNerd/flux-image-gen
feat: added support for the new Flux image gen model using ComfyUI
2024-08-02 22:32:22 +02:00
Timothy J. Baek 621393ca62 chore: format 2024-08-02 22:31:37 +02:00
Timothy J. Baek 0c6284be02 fix: user voice should update when global voice updates 2024-08-02 22:28:39 +02:00
Timothy Jaeryang Baek 9d5e3e2a91 Merge pull request #4299 from open-webui/dev-elevenlabs
feat: Fetch ElevenLabs voice ID by name
2024-08-02 19:29:24 +02:00
Timothy J. Baek 7f260938db refac 2024-08-02 19:29:03 +02:00
Timothy J. Baek c416444e24 fix 2024-08-02 19:26:34 +02:00
Timothy J. Baek b559bc84a7 refac 2024-08-02 19:24:47 +02:00
Timothy Jaeryang Baek 3f53abb233 Merge pull request #4018 from justinh-rahb/elevenlabs-voice-names
feat: Fetch ElevenLabs voice ID by name
2024-08-02 18:44:48 +02:00
Timothy J. Baek 67efd0dd39 enh: enable tools with url search param 2024-08-02 18:32:51 +02:00
Timothy J. Baek dcd32faa83 fix: styling 2024-08-02 18:12:54 +02:00
Timothy J. Baek 8fcb5cee66 enh: min_p 2024-08-02 18:11:20 +02:00
Timothy J. Baek 13b757d847 enh: min_p added to params 2024-08-02 18:10:12 +02:00
Timothy J. Baek c6d7f24542 fix: block users from /workspace and /admin 2024-08-02 18:08:23 +02:00
Timothy Jaeryang Baek 4c1a9380e6 Merge pull request #3998 from ScribblerCoder/patch-1
fix: Dockerfile Healthcheck unexpected exit code
2024-08-02 17:56:27 +02:00
John Karabudak ad6e8edcd3 added support for the new Flux image gen model using ComfyUI
this commit adds three environment variables:

- COMFYUI_FLUX: determines whether Flux is used, the workflow is completely different so this is necessary.
- COMFYUI_FLUX_WEIGHT_DTYPE: sets the weight precision for Flux. you will probably want to set this to "fp8_e4m3fn" as the fp16 weights take up about 24GB of VRAM. optional, defaults to "default".
- COMFYUI_FLUX_FP8_CLIP: Flux requires two CLIP models downloaded, one of which is available in fp8 and fp16. set to true if you are using the fp8 CLIP weights.
2024-08-02 13:23:20 -02:30
Timothy J. Baek 95c742ba9b chore: format 2024-08-02 17:47:43 +02:00
Timothy J. Baek 546d442626 refac 2024-08-02 17:45:30 +02:00
Timothy J. Baek ed8c0b23a0 refac 2024-08-02 17:41:15 +02:00
Timothy J. Baek c8f44b73f1 refac: user valves save handler 2024-08-02 17:36:16 +02:00
Timothy J. Baek bf6b149b8b refac: user valves 2024-08-02 17:19:52 +02:00
Timothy Jaeryang Baek f334c834e5 Merge pull request #4274 from Yanyutin753/display_size
💄Feat show the size of file and prepare for subsequent upload restrictions
2024-08-02 14:27:42 +02:00
Timothy Jaeryang Baek 7c850bb78a Merge pull request #4289 from OriginalSimon/dev
i18n: Update of the Ukrainian translation
2024-08-02 14:27:11 +02:00
Simon a280fed4be Merge branch 'open-webui:dev' into dev 2024-08-02 14:07:04 +02:00
Timothy Jaeryang Baek cf9b5241ae Merge pull request #4282 from laurentiu-miu/dev
i18n: Added Romanian translation
2024-08-02 13:31:33 +02:00
laurentium dfa25af271 i18n: Romanian translation fix pull request deleted extra space 2024-08-02 13:14:50 +03:00
laurentium b538c5a206 i18n: Romanian translation fix one-to-one equivalent with the en-US file 2024-08-02 12:58:11 +03:00
laurentium 4fa3fcbb3b i18n: Romanian translation 2024-08-02 12:03:00 +03:00
Clivia b049ae269b 💄Feat show the size of file and prepare for subsequent upload restrictions 2024-08-02 09:46:16 +08:00
Timothy J. Baek e5bf27e716 enh: include desc in fuzzy search 2024-08-02 01:53:53 +02:00
Timothy Jaeryang Baek a9a6ed8b71 Merge pull request #4237 from michaelpoluektov/refactor-webui-main
refactor: Simplify functions
2024-08-02 01:47:06 +02:00
Timothy J. Baek e6c64282fc refac 2024-08-02 01:45:50 +02:00
Timothy J. Baek 64b41655bb chore: format 2024-08-01 22:50:59 +02:00
Timothy J. Baek 6ac40552b4 feat: model selector fuzzy search
Co-Authored-By: Aryan Kothari <87589047+thearyadev@users.noreply.github.com>
2024-08-01 22:47:53 +02:00
Timothy Jaeryang Baek 039c5c540b Merge pull request #4225 from Yanyutin753/pref_file_upload
Initialize fileItem first to speed up file display
2024-08-01 22:09:59 +02:00
Timothy J. Baek c9ed934d0b refac: file should not have 'uploaded' status 2024-08-01 22:09:29 +02:00
Aryan Kothari 06a64219bc fix: bool eval order 2024-08-01 15:54:12 -04:00
Timothy Jaeryang Baek ef36b21684 Merge pull request #4253 from erickgamer787/fix-SensitiveInput-for-admin-pages
fix: Fix the SensitiveInput  the browser thought it was a password field
2024-08-01 21:43:42 +02:00
Timothy J. Baek c44e51ae58 refac 2024-08-01 21:43:13 +02:00
Aryan Kothari 2c4bc7a2b2 refactor: uses of chats.set(...) support pagi sidebar 2024-08-01 15:20:36 -04:00
Aryan Kothari 62dc486c85 add: add paginated scroll handler 2024-08-01 15:19:14 -04:00
Aryan Kothari d11961626c add: use skip and limit in api call 2024-08-01 15:15:49 -04:00
Aryan Kothari 519375b4c0 add: skip and limit use in query
- limit default changed to -1
2024-08-01 15:13:45 -04:00
Timothy Jaeryang Baek 6ecb7c80f1 Merge pull request #4262 from michaelpoluektov/fix-redirect-uri
fix: Fix custom redirect URI for OAuth behind reverse proxy
2024-08-01 20:57:27 +02:00
Timothy Jaeryang Baek 380cb93e46 Merge pull request #4257 from thearyadev/fix/pdf-gen-static-path
fix: pdf gen static path resolution
2024-08-01 20:57:00 +02:00
Timothy Jaeryang Baek 36e7662706 Merge pull request #4263 from mr-raw/dev
i18n: Translated missing keys. Minor changes to existing translations.
2024-08-01 20:41:00 +02:00
Aryan Kothari 49199819db add: stores for pagination state 2024-08-01 12:24:05 -04:00
Simon aa8d26496c Merge branch 'open-webui:dev' into dev 2024-08-01 15:52:58 +02:00
Michael Poluektov 4619e32b6a undo changelog changes 2024-08-01 12:59:39 +01:00
Michael Poluektov 0352e956b6 fix: redirect uri 2024-08-01 12:54:06 +01:00
Michael Poluektov f999956997 Merge remote-tracking branch 'oauth/main' into dev 2024-08-01 12:53:36 +01:00
Erik Raae bfd268c5e6 Translated missing keys. Minor changes to existing translations. 2024-08-01 13:46:25 +02:00
Aryan Kothari b7ad47017d fix: static dir path resolution
when running in different environments, the static_path is different.
This path is now 'determined' at runtime
2024-07-31 23:56:47 -04:00
Michael Poluektov 502b722902 Merge branch 'dev' of https://github.com/open-webui/open-webui into refactor-webui-main 2024-07-31 22:09:36 +01:00
Michael Poluektov c89b34fd75 flatten job() 2024-07-31 22:05:37 +01:00
Michael Poluektov b9b1fdd1a1 refac: rename message_template 2024-07-31 22:01:22 +01:00
Michael Poluektov 2e0fa1c6a0 refac: rename stream_message_template 2024-07-31 22:00:00 +01:00
Michael Poluektov f8726719ef refac: rename whole_message_template, silence lsp 2024-07-31 21:58:40 +01:00
Erick Joseph 3569fe9c73 the fix for the issue where chrome thought the SensitiveInput input was a password field 2024-07-31 16:15:00 -03:00
Timothy Jaeryang Baek 4ffcabfb3f Merge pull request #4250 from erickgamer787/i18n-correction-to-pt-br
i18n: redone and updated the entire pt br translation
2024-07-31 21:01:42 +02:00
Erick Joseph a7ed74744f correct and update all pt br translation 2024-07-31 15:53:02 -03:00
Michael Poluektov ae0bb8f1eb Merge branch 'dev' of https://github.com/open-webui/open-webui into refactor-webui-main 2024-07-31 17:42:16 +01:00
Michael Poluektov 034411e47e fix: type not manifold 2024-07-31 17:24:00 +01:00
Michael Poluektov baf58ef396 refac: use add_or_update_system_message 2024-07-31 17:16:07 +01:00
Michael Poluektov 006fc3495e fix: stream_message_template 2024-07-31 16:45:47 +01:00
Michael Poluektov 29a3b82336 refac: reuse stream_message_template 2024-07-31 15:26:26 +01:00
Michael Poluektov 22a5e196c9 simplify main.py 2024-07-31 14:01:40 +01:00
Michael Poluektov deec41d29a fix: function early returns 2024-07-31 13:51:25 +01:00
Michael Poluektov 3978efd710 refac: Refactor functions 2024-07-31 13:35:02 +01:00
Timothy J. Baek 7a80e732df refac: styling 2024-07-31 12:37:49 +01:00
Clivia 6681df29d2 Initialize fileItem first to speed up file display 2024-07-31 07:25:53 +08:00
Timothy J. Baek 9d58bb1c66 fix: styling 2024-07-30 12:34:30 +01:00
Timothy J. Baek acbdb05bd0 fix: styling 2024-07-30 11:44:32 +01:00
Simon 25f5259e84 Update translation.json 2024-07-29 14:16:13 +02:00
Timothy J. Baek 0ced153f86 refac 2024-07-28 23:17:49 +01:00
Timothy J. Baek faca8c8b53 feat: multiple action support 2024-07-28 22:02:23 +01:00
Timothy Jaeryang Baek 4ad8181088 Merge pull request #4166 from que-nguyen/dev
Update Vietnamese translations
2024-07-28 21:08:39 +02:00
Que Nguyen 4f3ff4a69d Update Vietnamese translations 2024-07-28 21:44:25 +07:00
Timothy J. Baek fc0f2f2b3a chore: format 2024-07-28 13:13:33 +01:00
Timothy J. Baek b4cd084117 feat: action selector 2024-07-28 13:06:47 +01:00
Timothy J. Baek c1fd55bb04 fix/refac: actions 2024-07-28 13:00:58 +01:00
Timothy J. Baek 77b2d2dbee refac: styling 2024-07-27 23:34:29 +01:00
Timothy J. Baek f92aed1b91 fix 2024-07-27 23:07:58 +01:00
Timothy Jaeryang Baek 90a6be8001 Merge pull request #4138 from ther0bster/fix-socket-io-url
fix: socket.io client url
2024-07-27 16:17:36 +02:00
Timothy Jaeryang Baek 2e4de209fe Merge pull request #4149 from arsaboo/openrouter
chore: Add HTTP-Referer and X-Title headers for OpenRouter
2024-07-27 16:06:17 +02:00
Alok Saboo 287147687e Add HTTP-Referer and X-Title headers for OpenRouter 2024-07-27 09:43:03 -04:00
ther0bster a53f5d01aa fix: don't break dev tests 2024-07-26 19:01:36 +02:00
Timothy J. Baek 1aaa2e8219 fix: ollama rag issue workaround 2024-07-26 12:22:13 +01:00
ther0bster 02b104f56b fix: socket.io client url
* fixes backend crash with IPv6 and some IPv4 setups
* instead of passing empty url, let socket.io set default url
  (defaults to window.location.host)
2024-07-25 21:23:28 +02:00
Timothy J. Baek e691839771 refac: styling 2024-07-25 12:16:19 +01:00
Timothy J. Baek 4c22d130d9 refac: styling 2024-07-25 12:08:47 +01:00
Timothy Jaeryang Baek 065d23d0b1 Merge pull request #4068 from Louden7/main
feat: Added environment variable to hide email and password sign in elements
2024-07-25 11:21:49 +02:00
Timothy Jaeryang Baek 4b0a232ae1 Merge pull request #4059 from thearyadev/feat/model-selection-with-arrow-keys
feat: select model with arrow keys
2024-07-25 11:13:08 +02:00
Timothy Jaeryang Baek 81130c205c Merge pull request #4096 from open-webui/dependabot/pip/pip-b4bdd6eb86
chore(deps): bump the pip group across 2 directories with 2 updates
2024-07-25 11:12:47 +02:00
Dillon 36b94ca5f5 updated environment variable to suggested ENABLE_LOGIN_FORM 2024-07-24 21:44:40 -04:00
Aryan Kothari 9e4326a582 fix: unintentional import from previous commit 2024-07-24 19:03:31 -04:00
dependabot[bot] 8193c2f847 chore(deps): bump the pip group across 2 directories with 2 updates
Bumps the pip group with 2 updates in the / directory: [pymysql](https://github.com/PyMySQL/PyMySQL) and [langchain-community](https://github.com/langchain-ai/langchain).
Bumps the pip group with 1 update in the /backend directory: [langchain-community](https://github.com/langchain-ai/langchain).


Updates `pymysql` from 1.1.0 to 1.1.1
- [Release notes](https://github.com/PyMySQL/PyMySQL/releases)
- [Changelog](https://github.com/PyMySQL/PyMySQL/blob/main/CHANGELOG.md)
- [Commits](https://github.com/PyMySQL/PyMySQL/compare/v1.1.0...v1.1.1)

Updates `langchain-community` from 0.2.0 to 0.2.9
- [Release notes](https://github.com/langchain-ai/langchain/releases)
- [Commits](https://github.com/langchain-ai/langchain/compare/langchain-community==0.2.0...langchain-community==0.2.9)

Updates `langchain-community` from 0.2.6 to 0.2.9
- [Release notes](https://github.com/langchain-ai/langchain/releases)
- [Commits](https://github.com/langchain-ai/langchain/compare/langchain-community==0.2.0...langchain-community==0.2.9)

---
updated-dependencies:
- dependency-name: pymysql
  dependency-type: direct:production
  dependency-group: pip
- dependency-name: langchain-community
  dependency-type: direct:production
  dependency-group: pip
- dependency-name: langchain-community
  dependency-type: direct:production
  dependency-group: pip
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-07-24 17:41:24 +00:00
Aryan Kothari fbef731a04 fix: check filteredItems length before unsafe index access 2024-07-24 09:06:21 -04:00
Aryan Kothari dbf88a2eca refactor: rename pseudoSelectedIndex to selectedModelIdx 2024-07-24 09:04:56 -04:00
Timothy J. Baek 413a9031eb chore: format 2024-07-24 11:36:41 +01:00
Timothy J. Baek 23e69bcdb4 enh: AsyncGenerator support 2024-07-24 11:29:57 +01:00
Timothy J. Baek edff071cd2 refac 2024-07-24 11:25:07 +01:00
Timothy Jaeryang Baek 3b487cfae9 Merge pull request #4046 from thearyadev/chat-list-optimization
perf: optimize query for chat list
2024-07-24 12:21:04 +02:00
Timothy J. Baek a8d2072e9f refac 2024-07-24 11:19:42 +01:00
Timothy Jaeryang Baek 260e42a691 Merge pull request #4036 from open-webui/dependabot/pip/backend/dev/google-generativeai-0.7.2
chore(deps): bump google-generativeai from 0.5.4 to 0.7.2 in /backend
2024-07-24 12:17:31 +02:00
Timothy Jaeryang Baek 596a9c60d7 Merge pull request #4039 from open-webui/dependabot/pip/backend/dev/rapidocr-onnxruntime-1.3.24
chore(deps): bump rapidocr-onnxruntime from 1.3.22 to 1.3.24 in /backend
2024-07-24 12:17:24 +02:00
Timothy Jaeryang Baek a47e4a1148 Merge pull request #4047 from Peter-De-Ath/confirmDialog-add-inputValue
feat: add eventConfirmationInputValue handling in Chat.svelte
2024-07-24 12:16:45 +02:00
Timothy Jaeryang Baek 312c8b9e7a Merge pull request #4042 from Yanyutin753/model_image
💄 Add a model image to the model selector
2024-07-24 12:15:33 +02:00
Timothy Jaeryang Baek 1d99508943 Merge pull request #4055 from que-nguyen/dev
Update Vietnamese translations
2024-07-24 12:11:46 +02:00
Timothy Jaeryang Baek b42fda0971 Merge pull request #4040 from open-webui/dependabot/pip/backend/dev/duckduckgo-search-approx-eq-6.2.1
chore(deps): update duckduckgo-search requirement from ~=6.1.12 to ~=6.2.1 in /backend
2024-07-24 12:11:10 +02:00
Timothy Jaeryang Baek cf377d2e1c Merge pull request #4038 from open-webui/dependabot/pip/backend/dev/unstructured-0.15.0
chore(deps): bump unstructured from 0.14.10 to 0.15.0 in /backend
2024-07-24 12:07:50 +02:00
Timothy Jaeryang Baek ec9d0b8a6d Merge pull request #4037 from open-webui/dependabot/pip/backend/dev/langfuse-2.39.2
chore(deps): bump langfuse from 2.38.0 to 2.39.2 in /backend
2024-07-24 12:07:41 +02:00
Timothy Jaeryang Baek 14cac8ed22 Merge pull request #4030 from songritk/main
i18n: Thai translation
2024-07-24 12:02:43 +02:00
Dillon 4ecf9dd62d Created if blocks to show or hide username, password, signin and or sections depending on new enable_username_password_login variable 2024-07-23 22:25:29 -04:00
Dillon b56dcf155c Added enable_username_password_login to the Config object 2024-07-23 22:23:37 -04:00
Dillon 63ffdb38aa Added and set enable_username_password_login to the get_app_config function 2024-07-23 22:22:09 -04:00
Dillon f9289d3079 Created new PersistentConfig for new environment variable ENABLE_USERNAME_PASSWORD_LOGIN 2024-07-23 22:20:45 -04:00
Dillon 709b56bc40 imported and added new ENABLE_USERNAME_PASSWORD_LOGIN environment variable 2024-07-23 22:19:41 -04:00
Aryan Kothari eb38d382ba fix: pseudoSelectedIndex is reset when modal is closed. 2024-07-23 11:31:17 -04:00
Aryan Kothari 5beaa5d892 feat: select model with arrow keys (+enter) 2024-07-23 11:05:11 -04:00
Que Nguyen 01b24f6282 Update Vietnamese translations 2024-07-23 17:33:22 +07:00
Peter De-Ath cbe3694b81 refac: add eventConfirmationInputValue handling in Chat.svelte 2024-07-22 20:43:31 +01:00
Aryan Kothari f531a51e91 chore: formatting 2024-07-22 14:45:47 -04:00
Aryan Kothari a0667dfd1b change /chats/ and /chats/list to utilize new function 2024-07-22 14:09:22 -04:00
Aryan Kothari 2b78e613a4 add func to get chat list with more specific sql query 2024-07-22 14:08:15 -04:00
Clivia b34ab10b5e 💄 Add a model image to the model selector 2024-07-22 20:30:27 +08:00
dependabot[bot] f07cc37939 chore(deps): update duckduckgo-search requirement in /backend
Updates the requirements on [duckduckgo-search](https://github.com/deedy5/duckduckgo_search) to permit the latest version.
- [Release notes](https://github.com/deedy5/duckduckgo_search/releases)
- [Commits](https://github.com/deedy5/duckduckgo_search/compare/v6.1.12...v6.2.1)

---
updated-dependencies:
- dependency-name: duckduckgo-search
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-07-22 02:44:43 +00:00
dependabot[bot] 957b0ecd15 chore(deps): bump rapidocr-onnxruntime from 1.3.22 to 1.3.24 in /backend
Bumps [rapidocr-onnxruntime](https://github.com/RapidAI/RapidOCR) from 1.3.22 to 1.3.24.
- [Release notes](https://github.com/RapidAI/RapidOCR/releases)
- [Commits](https://github.com/RapidAI/RapidOCR/compare/v1.3.22...v1.3.24)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-07-22 02:44:40 +00:00
dependabot[bot] 659bc246c9 chore(deps): bump unstructured from 0.14.10 to 0.15.0 in /backend
Bumps [unstructured](https://github.com/Unstructured-IO/unstructured) from 0.14.10 to 0.15.0.
- [Release notes](https://github.com/Unstructured-IO/unstructured/releases)
- [Changelog](https://github.com/Unstructured-IO/unstructured/blob/main/CHANGELOG.md)
- [Commits](https://github.com/Unstructured-IO/unstructured/compare/0.14.10...0.15.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-07-22 02:44:37 +00:00
dependabot[bot] 1502af7e94 chore(deps): bump langfuse from 2.38.0 to 2.39.2 in /backend
Bumps [langfuse](https://github.com/langfuse/langfuse) from 2.38.0 to 2.39.2.
- [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-07-22 02:44:33 +00:00
dependabot[bot] 26c5a533ec chore(deps): bump google-generativeai from 0.5.4 to 0.7.2 in /backend
Bumps [google-generativeai](https://github.com/google/generative-ai-python) from 0.5.4 to 0.7.2.
- [Release notes](https://github.com/google/generative-ai-python/releases)
- [Changelog](https://github.com/google-gemini/generative-ai-python/blob/main/RELEASE.md)
- [Commits](https://github.com/google/generative-ai-python/compare/v0.5.4...v0.7.2)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-07-22 02:44:27 +00:00
Songrit Kitisriworapan 4274016491 i18n: Thai translation 2024-07-21 21:06:49 +07:00
Justin Hayes c6c298b412 Fetch didn't happen 2024-07-20 11:02:59 -04:00
Timothy Jaeryang Baek 63eda0fe42 Merge pull request #4024 from Seth-Rothschild/fix-seed-0
fix: seed and temperature can be 0 in models
2024-07-20 14:01:34 +02:00
Seth Rothschild c3693c91b3 fix: allow temperature and seed to be 0 2024-07-20 06:53:52 -04:00
Timothy J. Baek e426067e46 fix: latex
Co-Authored-By: Clivia <132346501+Yanyutin753@users.noreply.github.com>
2024-07-20 11:33:27 +02:00
Timothy J. Baek 2e482eca6d refac 2024-07-20 11:29:44 +02:00
Timothy J. Baek fdea1afc7f refac: latex should not use \\begin and \\end as delimiters 2024-07-20 11:24:57 +02:00
Timothy J. Baek 41fecb800d enh: latex 2024-07-20 11:17:24 +02:00
Timothy Jaeryang Baek 52cf06b36c Merge pull request #4021 from KarlLee830/translate
i18n: Update Chinese Translation
2024-07-20 11:05:27 +02:00
Timothy J. Baek ebce006974 refac: latex handling 2024-07-20 10:58:29 +02:00
Karl Lee ff60695a0f i18n: Update Chinese translation 2024-07-20 16:45:13 +08:00
Karl Lee 3aa9685f5a i18n: Update Chinese translation 2024-07-20 16:41:33 +08:00
Justin Hayes 6cecb964a9 name 2024-07-20 03:07:50 -04:00
Justin Hayes 0216ee101c enh: ElevenLabs voice name -> ID 2024-07-20 02:56:00 -04:00
Timothy J. Baek d3c6bfb764 enh: latex 2024-07-19 16:54:28 +02:00
Timothy J. Baek ea6a5121b3 chore: format 2024-07-19 13:58:56 +02:00
Timothy J. Baek d6a4fe00b3 refac: styling 2024-07-19 13:57:35 +02:00
Timothy Jaeryang Baek 36318e8a1b Merge pull request #3992 from thearyadev/feat/enter-to-select-model-indicator
feat: add indicator for enter-triggered model selection
2024-07-19 13:41:43 +02:00
Timothy J. Baek 79562a2531 fix: elevenlabs tts 2024-07-19 13:30:36 +02:00
Timothy Jaeryang Baek d2664ab2a6 Merge pull request #3999 from ricky-davis/importDocumentMappingTags
fix: importing document mapping now adds tags
2024-07-19 12:43:53 +02:00
Timothy J. Baek e664a429a1 feat: elevenlabs tts support 2024-07-19 10:35:05 +02:00
Timothy J. Baek b09bd1ed34 refac: styling 2024-07-19 10:17:59 +02:00
Lukas f83c80aaec add changelog 2024-07-19 09:08:46 +02:00
Lukas bbc8adca94 support custom redirect url in OAuth
closes #3727 #3945
2024-07-19 09:03:41 +02:00
rdavis 20c71d127b Added content field to Document mapping import to ensure tags get re-mapped 2024-07-18 20:53:06 -05:00
Aryan Kothari e35df904a0 feat: add indicator for enter-triggered model selection 2024-07-18 20:48:14 -04:00
Timothy Jaeryang Baek 62682a8c4c Merge pull request #3990 from josx/josx/ext
chore RAG: adding languages known extensions for erlang, elixir, haskell and jsx/tsx
2024-07-19 02:10:15 +02:00
Timothy Jaeryang Baek 253c02cc3c Merge pull request #3982 from jonathan-rohde/fix/missing-tags-add-doc-modal
test: Add tests to verify basic document list features
2024-07-19 02:10:01 +02:00
ScribblerCoder 175d1cc8f2 patch Healtcheck
- Add optional PORT
- added `-n` to jq to return expected exit code when input is empty/null
2024-07-19 03:06:15 +03:00
José Luis Di Biase 23c9122458 chore RAG: adding languages known extension for erlang, elixir, haskell and jsx/tsx
Signed-off-by: José Luis Di Biase <josx@interorganic.com.ar>
2024-07-18 17:48:39 -03:00
Jonathan Rohde b42d2886a2 enh: add missing file 2024-07-18 15:19:30 +02:00
Jonathan Rohde c3f90ba347 enh: add translation keys 2024-07-18 15:09:02 +02:00
Jonathan Rohde c6eba8c0a1 enh: add e2e tests for document list 2024-07-18 15:09:02 +02:00
Timothy Jaeryang Baek 51bdb4cbea Merge pull request #3962 from thearyadev/feat/enter-to-select-model
feat: enter selects first model in list
2024-07-18 13:16:35 +02:00
Timothy Jaeryang Baek 376b2d0297 Merge pull request #3972 from jonathan-rohde/fix/missing-tags-add-doc-modal
fix: missing tags add doc modal
2024-07-18 13:15:22 +02:00
Jonathan Rohde 7e03624408 fix: send tags to upload doc handler in modal dialog 2024-07-18 12:38:23 +02:00
Aryan Kothari 133f223df6 feat: enter selects first model in list 2024-07-17 17:47:17 -04:00
Timothy J. Baek 8dc73fdbdb enh: remove /health logging 2024-07-17 22:24:34 +02:00
Timothy J. Baek 8ec3ed1830 refac: file handling 2024-07-17 22:01:39 +02:00
Timothy J. Baek 8b71ce23bb refac: styling 2024-07-17 22:01:32 +02:00
Timothy Jaeryang Baek 82079e644a Merge pull request #3952 from open-webui/dev
fix
2024-07-17 19:04:40 +02:00
Timothy J. Baek 1e7a364c68 fix 2024-07-17 19:04:18 +02:00
173 changed files with 8675 additions and 4846 deletions
+24 -21
View File
@@ -8,36 +8,43 @@ assignees: ''
# Bug Report
## Description
## Installation Method
**Bug Summary:**
[Provide a brief but clear summary of the bug]
**Steps to Reproduce:**
[Outline the steps to reproduce the bug. Be as detailed as possible.]
**Expected Behavior:**
[Describe what you expected to happen.]
**Actual Behavior:**
[Describe what actually happened.]
[Describe the method you used to install the project, e.g., git clone, Docker, pip, etc.]
## Environment
- **Open WebUI Version:** [e.g., 0.1.120]
- **Ollama (if applicable):** [e.g., 0.1.30, 0.1.32-rc1]
- **Open WebUI Version:** [e.g., v0.3.11]
- **Ollama (if applicable):** [e.g., v0.2.0, v0.1.32-rc1]
- **Operating System:** [e.g., Windows 10, macOS Big Sur, Ubuntu 20.04]
- **Browser (if applicable):** [e.g., Chrome 100.0, Firefox 98.0]
## Reproduction Details
**Confirmation:**
- [ ] I have read and followed all the instructions provided in the README.md.
- [ ] I am on the latest version of both Open WebUI and Ollama.
- [ ] I have included the browser console logs.
- [ ] I have included the Docker container logs.
- [ ] I have provided the exact steps to reproduce the bug in the "Steps to Reproduce" section below.
## Expected Behavior:
[Describe what you expected to happen.]
## Actual Behavior:
[Describe what actually happened.]
## Description
**Bug Summary:**
[Provide a brief but clear summary of the bug]
## Reproduction Details
**Steps to Reproduce:**
[Outline the steps to reproduce the bug. Be as detailed as possible.]
## Logs and Screenshots
@@ -47,13 +54,9 @@ assignees: ''
**Docker Container Logs:**
[Include relevant Docker container logs, if applicable]
**Screenshots (if applicable):**
**Screenshots/Screen Recordings (if applicable):**
[Attach any relevant screenshots to help illustrate the issue]
## Installation Method
[Describe the method you used to install the project, e.g., manual installation, Docker, package manager, etc.]
## Additional Information
[Include any additional details that may help in understanding and reproducing the issue. This could include specific configurations, error messages, or anything else relevant to the bug.]
+11 -4
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
@@ -26,6 +33,10 @@ jobs:
--file docker-compose.a1111-test.yaml \
up --detach --build
- name: Delete Docker build cache
run: |
docker builder prune --all --force
- name: Wait for Ollama to be up
timeout-minutes: 5
run: |
@@ -35,10 +46,6 @@ jobs:
done
echo "Service is up!"
- name: Delete Docker build cache
run: |
docker builder prune --all --force
- name: Preload Ollama model
run: |
docker exec ollama ollama pull qwen:0.5b-chat-v1.5-q2_K
+80
View File
@@ -5,6 +5,86 @@ 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.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
- **🔄 Sidebar Infinite Scroll**: Added an infinite scroll feature in the sidebar for more efficient chat navigation, reducing load times and enhancing user experience.
- **🚀 Enhanced Markdown Rendering**: Support for rendering all code blocks and making images clickable for preview; codespan styling is also enhanced to improve readability and user interaction.
- **🔒 Admin Shared Chat Visibility**: Admins no longer have default visibility over shared chats when ENABLE_ADMIN_CHAT_ACCESS is set to false, tightening security and privacy settings for users.
- **🌍 Language Updates**: Added Malay (Bahasa Malaysia) translation and updated Catalan and Traditional Chinese translations to improve accessibility for more users.
### Fixed
- **📊 Markdown Rendering Issues**: Resolved issues with markdown rendering to ensure consistent and correct display across components.
- **🛠️ Styling Issues**: Multiple fixes applied to styling throughout the application, improving the overall visual experience and interface consistency.
- **🗃️ Modal Handling**: Fixed an issue where modals were not closing correctly in various model chat scenarios, enhancing usability and interface reliability.
- **📄 Missing OpenAI Usage Information**: Resolved issues where usage statistics for OpenAI services were not being correctly displayed, ensuring users have access to crucial data for managing and monitoring their API consumption.
- **🔧 Non-Streaming Support for Functions Plugin**: Fixed a functionality issue with the Functions plugin where non-streaming operations were not functioning as intended, restoring full capabilities for async and sync integration within the platform.
- **🔄 Environment Variable Type Correction (COMFYUI_FLUX_FP8_CLIP)**: Corrected the data type of the 'COMFYUI_FLUX_FP8_CLIP' environment variable from string to boolean, ensuring environment settings apply correctly and enhance configuration management.
### Changed
- **🔧 Backend Dependency Updates**: Updated several backend dependencies such as boto3, pypdf, python-pptx, validators, and black, ensuring up-to-date security and performance optimizations.
## [0.3.11] - 2024-08-02
### Added
- **📊 Model Information Display**: Added visuals for model selection, including images next to model names for more intuitive navigation.
- **🗣 ElevenLabs Voice Adaptations**: Voice enhancements including support for ElevenLabs voice ID by name for personalized vocal interactions.
- **⌨️ Arrow Keys Model Selection**: Users can now use arrow keys for quicker model selection, enhancing accessibility.
- **🔍 Fuzzy Search in Model Selector**: Enhanced model selector with fuzzy search to locate models swiftly, including descriptions.
- **🕹️ ComfyUI Flux Image Generation**: Added support for the new Flux image gen model; introduces environment controls like weight precision and CLIP model options in Settings.
- **💾 Display File Size for Uploads**: Enhanced file interface now displays file size, preparing for upcoming upload restrictions.
- **🎚️ Advanced Params "Min P"**: Added 'Min P' parameter in the advanced settings for customized model precision control.
- **🔒 Enhanced OAuth**: Introduced custom redirect URI support for OAuth behind reverse proxies, enabling safer authentication processes.
- **🖥 Enhanced Latex Rendering**: Adjustments made to latex rendering processes, now accurately detecting and presenting latex inputs from text.
- **🌐 Internationalization**: Enhanced with new Romanian and updated Vietnamese and Ukrainian translations, helping broaden accessibility for international users.
### Fixed
- **🔧 Tags Handling in Document Upload**: Tags are now properly sent to the upload document handler, resolving issues with missing metadata.
- **🖥️ Sensitive Input Fields**: Corrected browser misinterpretation of secure input fields, preventing misclassification as password fields.
- **📂 Static Path Resolution in PDF Generation**: Fixed static paths that adjust dynamically to prevent issues across various environments.
### Changed
- **🎨 UI/UX Styling Enhancements**: Multiple minor styling updates for a cleaner and more intuitive user interface.
- **🚧 Refactoring Various Components**: Numerous refactoring changes across styling, file handling, and function simplifications for clarity and performance.
- **🎛️ User Valves Management**: Moved user valves from settings to direct chat controls for more user-friendly access during interactions.
### Removed
- **⚙️ Health Check Logging**: Removed verbose logging from the health checking processes to declutter logs and improve backend performance.
## [0.3.10] - 2024-07-17
### Fixed
+1 -1
View File
@@ -151,7 +151,7 @@ COPY --chown=$UID:$GID ./backend .
EXPOSE 8080
HEALTHCHECK CMD curl --silent --fail http://localhost:8080/health | jq -e '.status == true' || exit 1
HEALTHCHECK CMD curl --silent --fail http://localhost:${PORT:-8080}/health | jq -ne 'input.status == true' || exit 1
USER $UID:$GID
+173 -44
View File
@@ -10,12 +10,12 @@ from fastapi import (
File,
Form,
)
from fastapi.responses import StreamingResponse, JSONResponse, FileResponse
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel
import uuid
import requests
import hashlib
@@ -31,6 +31,7 @@ from utils.utils import (
)
from utils.misc import calculate_sha256
from config import (
SRC_LOG_LEVELS,
CACHE_DIR,
@@ -43,6 +44,7 @@ from config import (
AUDIO_STT_OPENAI_API_KEY,
AUDIO_TTS_OPENAI_API_BASE_URL,
AUDIO_TTS_OPENAI_API_KEY,
AUDIO_TTS_API_KEY,
AUDIO_STT_ENGINE,
AUDIO_STT_MODEL,
AUDIO_TTS_ENGINE,
@@ -75,6 +77,7 @@ app.state.config.TTS_OPENAI_API_KEY = AUDIO_TTS_OPENAI_API_KEY
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
# setting device type for whisper model
whisper_device_type = DEVICE_TYPE if DEVICE_TYPE and DEVICE_TYPE == "cuda" else "cpu"
@@ -87,6 +90,7 @@ SPEECH_CACHE_DIR.mkdir(parents=True, exist_ok=True)
class TTSConfigForm(BaseModel):
OPENAI_API_BASE_URL: str
OPENAI_API_KEY: str
API_KEY: str
ENGINE: str
MODEL: str
VOICE: str
@@ -137,6 +141,7 @@ async def get_audio_config(user=Depends(get_admin_user)):
"tts": {
"OPENAI_API_BASE_URL": app.state.config.TTS_OPENAI_API_BASE_URL,
"OPENAI_API_KEY": app.state.config.TTS_OPENAI_API_KEY,
"API_KEY": app.state.config.TTS_API_KEY,
"ENGINE": app.state.config.TTS_ENGINE,
"MODEL": app.state.config.TTS_MODEL,
"VOICE": app.state.config.TTS_VOICE,
@@ -156,6 +161,7 @@ async def update_audio_config(
):
app.state.config.TTS_OPENAI_API_BASE_URL = form_data.tts.OPENAI_API_BASE_URL
app.state.config.TTS_OPENAI_API_KEY = form_data.tts.OPENAI_API_KEY
app.state.config.TTS_API_KEY = form_data.tts.API_KEY
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
@@ -169,6 +175,7 @@ async def update_audio_config(
"tts": {
"OPENAI_API_BASE_URL": app.state.config.TTS_OPENAI_API_BASE_URL,
"OPENAI_API_KEY": app.state.config.TTS_OPENAI_API_KEY,
"API_KEY": app.state.config.TTS_API_KEY,
"ENGINE": app.state.config.TTS_ENGINE,
"MODEL": app.state.config.TTS_MODEL,
"VOICE": app.state.config.TTS_VOICE,
@@ -194,55 +201,111 @@ async def speech(request: Request, user=Depends(get_verified_user)):
if file_path.is_file():
return FileResponse(file_path)
headers = {}
headers["Authorization"] = f"Bearer {app.state.config.TTS_OPENAI_API_KEY}"
headers["Content-Type"] = "application/json"
if app.state.config.TTS_ENGINE == "openai":
headers = {}
headers["Authorization"] = f"Bearer {app.state.config.TTS_OPENAI_API_KEY}"
headers["Content-Type"] = "application/json"
try:
body = body.decode("utf-8")
body = json.loads(body)
body["model"] = app.state.config.TTS_MODEL
body = json.dumps(body).encode("utf-8")
except Exception as e:
pass
try:
body = body.decode("utf-8")
body = json.loads(body)
body["model"] = app.state.config.TTS_MODEL
body = json.dumps(body).encode("utf-8")
except Exception as e:
pass
r = None
try:
r = requests.post(
url=f"{app.state.config.TTS_OPENAI_API_BASE_URL}/audio/speech",
data=body,
headers=headers,
stream=True,
)
r = None
try:
r = requests.post(
url=f"{app.state.config.TTS_OPENAI_API_BASE_URL}/audio/speech",
data=body,
headers=headers,
stream=True,
)
r.raise_for_status()
r.raise_for_status()
# Save the streaming content to a file
with open(file_path, "wb") as f:
for chunk in r.iter_content(chunk_size=8192):
f.write(chunk)
# Save the streaming content to a file
with open(file_path, "wb") as f:
for chunk in r.iter_content(chunk_size=8192):
f.write(chunk)
with open(file_body_path, "w") as f:
json.dump(json.loads(body.decode("utf-8")), f)
with open(file_body_path, "w") as f:
json.dump(json.loads(body.decode("utf-8")), f)
# Return the saved file
return FileResponse(file_path)
# Return the saved file
return FileResponse(file_path)
except Exception as e:
log.exception(e)
error_detail = "Open WebUI: Server Connection Error"
if r is not None:
try:
res = r.json()
if "error" in res:
error_detail = f"External: {res['error']['message']}"
except:
error_detail = f"External: {e}"
except Exception as e:
log.exception(e)
error_detail = "Open WebUI: Server Connection Error"
if r is not None:
try:
res = r.json()
if "error" in res:
error_detail = f"External: {res['error']['message']}"
except Exception:
error_detail = f"External: {e}"
raise HTTPException(
status_code=r.status_code if r != None else 500,
detail=error_detail,
)
raise HTTPException(
status_code=r.status_code if r != None else 500,
detail=error_detail,
)
elif app.state.config.TTS_ENGINE == "elevenlabs":
payload = None
try:
payload = json.loads(body.decode("utf-8"))
except Exception as e:
log.exception(e)
raise HTTPException(status_code=400, detail="Invalid JSON payload")
voice_id = payload.get("voice", "")
url = f"https://api.elevenlabs.io/v1/text-to-speech/{voice_id}"
headers = {
"Accept": "audio/mpeg",
"Content-Type": "application/json",
"xi-api-key": app.state.config.TTS_API_KEY,
}
data = {
"text": payload["input"],
"model_id": app.state.config.TTS_MODEL,
"voice_settings": {"stability": 0.5, "similarity_boost": 0.5},
}
try:
r = requests.post(url, json=data, headers=headers)
r.raise_for_status()
# Save the streaming content to a file
with open(file_path, "wb") as f:
for chunk in r.iter_content(chunk_size=8192):
f.write(chunk)
with open(file_body_path, "w") as f:
json.dump(json.loads(body.decode("utf-8")), f)
# Return the saved file
return FileResponse(file_path)
except Exception as e:
log.exception(e)
error_detail = "Open WebUI: Server Connection Error"
if r is not None:
try:
res = r.json()
if "error" in res:
error_detail = f"External: {res['error']['message']}"
except Exception:
error_detail = f"External: {e}"
raise HTTPException(
status_code=r.status_code if r != None else 500,
detail=error_detail,
)
@app.post("/transcriptions")
@@ -290,7 +353,7 @@ def transcribe(
try:
model = WhisperModel(**whisper_kwargs)
except:
except Exception:
log.warning(
"WhisperModel initialization failed, attempting download with local_files_only=False"
)
@@ -358,7 +421,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(
@@ -373,3 +436,69 @@ def transcribe(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.DEFAULT(e),
)
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":
headers = {
"xi-api-key": app.state.config.TTS_API_KEY,
"Content-Type": "application/json",
}
try:
response = requests.get(
"https://api.elevenlabs.io/v1/models", headers=headers
)
response.raise_for_status()
models = response.json()
return [
{"name": model["name"], "id": model["model_id"]} for model in models
]
except requests.RequestException as e:
log.error(f"Error fetching voices: {str(e)}")
return []
@app.get("/models")
async def get_models(user=Depends(get_verified_user)):
return {"models": get_available_models()}
def get_available_voices() -> list[dict]:
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",
}
try:
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.append({"name": voice["name"], "id": voice["voice_id"]})
return voices
except requests.RequestException as e:
log.error(f"Error fetching voices: {str(e)}")
return []
@app.get("/voices")
async def get_voices(user=Depends(get_verified_user)):
return {"voices": get_available_voices()}
+24 -7
View File
@@ -42,6 +42,9 @@ from config import (
COMFYUI_SAMPLER,
COMFYUI_SCHEDULER,
COMFYUI_SD3,
COMFYUI_FLUX,
COMFYUI_FLUX_WEIGHT_DTYPE,
COMFYUI_FLUX_FP8_CLIP,
IMAGES_OPENAI_API_BASE_URL,
IMAGES_OPENAI_API_KEY,
IMAGE_GENERATION_MODEL,
@@ -85,10 +88,13 @@ 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
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")
@@ -139,28 +145,30 @@ async def get_engine_url(user=Depends(get_admin_user)):
async def update_engine_url(
form_data: EngineUrlUpdateForm, user=Depends(get_admin_user)
):
if form_data.AUTOMATIC1111_BASE_URL == None:
if form_data.AUTOMATIC1111_BASE_URL is None:
app.state.config.AUTOMATIC1111_BASE_URL = AUTOMATIC1111_BASE_URL
else:
url = form_data.AUTOMATIC1111_BASE_URL.strip("/")
try:
r = requests.head(url)
r.raise_for_status()
app.state.config.AUTOMATIC1111_BASE_URL = url
except Exception as e:
raise HTTPException(status_code=400, detail=ERROR_MESSAGES.DEFAULT(e))
raise HTTPException(status_code=400, detail=ERROR_MESSAGES.INVALID_URL)
if form_data.COMFYUI_BASE_URL == None:
if form_data.COMFYUI_BASE_URL is None:
app.state.config.COMFYUI_BASE_URL = COMFYUI_BASE_URL
else:
url = form_data.COMFYUI_BASE_URL.strip("/")
try:
r = requests.head(url)
r.raise_for_status()
app.state.config.COMFYUI_BASE_URL = url
except Exception as e:
raise HTTPException(status_code=400, detail=ERROR_MESSAGES.DEFAULT(e))
raise HTTPException(status_code=400, detail=ERROR_MESSAGES.INVALID_URL)
if form_data.AUTOMATIC1111_API_AUTH == None:
if form_data.AUTOMATIC1111_API_AUTH is None:
app.state.config.AUTOMATIC1111_API_AUTH = AUTOMATIC1111_API_AUTH
else:
app.state.config.AUTOMATIC1111_API_AUTH = form_data.AUTOMATIC1111_API_AUTH
@@ -497,9 +505,18 @@ async def image_generations(
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(
res = await comfyui_generate_image(
app.state.config.MODEL,
data,
user.id,
+170 -11
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
@@ -125,6 +125,135 @@ COMFYUI_DEFAULT_PROMPT = """
}
"""
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.safetensors"
},
"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.safetensors",
"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")
@@ -194,9 +323,12 @@ class ImageGenerationPayload(BaseModel):
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
def comfyui_generate_image(
async def comfyui_generate_image(
model: str, payload: ImageGenerationPayload, client_id, base_url
):
ws_url = base_url.replace("http://", "ws://").replace("https://", "wss://")
@@ -215,21 +347,46 @@ def comfyui_generate_image(
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
comfyui_prompt["7"]["inputs"]["text"] = payload.negative_prompt
if payload.steps:
comfyui_prompt["3"]["inputs"]["steps"] = payload.steps
comfyui_prompt["3"]["inputs"]["seed"] = (
payload.seed if payload.seed else random.randint(0, 18446744073709551614)
)
try:
ws = websocket.WebSocket()
@@ -240,7 +397,9 @@ def comfyui_generate_image(
return None
try:
images = get_images(ws, comfyui_prompt, client_id, base_url)
images = await asyncio.to_thread(
get_images, ws, comfyui_prompt, client_id, base_url
)
except Exception as e:
log.exception(f"Error while receiving images: {e}")
images = None
+90 -309
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,
@@ -53,7 +42,12 @@ from config import (
UPLOAD_DIR,
AppConfig,
)
from utils.misc import calculate_sha256, add_or_update_system_message
from utils.misc import (
calculate_sha256,
apply_model_params_to_body_ollama,
apply_model_params_to_body_openai,
apply_model_system_prompt_to_body,
)
log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["OLLAMA"])
@@ -120,7 +114,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")
@@ -183,7 +177,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 +232,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 +263,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 +276,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 +316,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 +339,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 +358,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 +408,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 +419,13 @@ 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",
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 +439,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 +455,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 +467,12 @@ 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",
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 +486,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 +507,12 @@ 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",
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 +524,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 +547,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 +564,12 @@ 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",
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 +581,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 +594,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 +613,12 @@ 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",
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 +628,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 +637,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 +664,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 +689,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 +702,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,12 +721,7 @@ 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()
)
)
log.debug(f"{form_data.model_dump_json(exclude_none=True).encode()}=")
payload = {
**form_data.model_dump(exclude_none=True, exclude=["metadata"]),
@@ -740,179 +736,21 @@ async def generate_chat_completion(
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)
and payload["options"].get("temperature") is None
):
payload["options"]["temperature"] = model_info.params.get(
"temperature", None
)
if (
model_info.params.get("seed", 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("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)
@@ -934,7 +772,7 @@ class OpenAIChatMessage(BaseModel):
class OpenAIChatCompletionForm(BaseModel):
model: str
messages: List[OpenAIChatMessage]
messages: list[OpenAIChatMessage]
model_config = ConfigDict(extra="allow")
@@ -946,83 +784,28 @@ 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
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(
@@ -1038,7 +821,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:
@@ -1093,7 +876,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(
@@ -1119,7 +902,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
@@ -1184,7 +966,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):
@@ -1193,7 +974,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]
@@ -1216,7 +997,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]
+78 -138
View File
@@ -1,6 +1,6 @@
from fastapi import FastAPI, Request, Response, HTTPException, Depends
from fastapi import FastAPI, Request, HTTPException, Depends
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import StreamingResponse, JSONResponse, FileResponse
from fastapi.responses import StreamingResponse, FileResponse
import requests
import aiohttp
@@ -12,16 +12,15 @@ from pydantic import BaseModel
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_verified_user,
get_verified_user,
get_admin_user,
)
from utils.task import prompt_template
from utils.misc import add_or_update_system_message
from utils.misc import (
apply_model_params_to_body_openai,
apply_model_system_prompt_to_body,
)
from config import (
SRC_LOG_LEVELS,
@@ -34,7 +33,7 @@ from config import (
MODEL_FILTER_LIST,
AppConfig,
)
from typing import List, Optional
from typing import Optional, Literal, overload
import hashlib
@@ -69,8 +68,6 @@ app.state.MODELS = {}
async def check_url(request: Request, call_next):
if len(app.state.MODELS) == 0:
await get_all_models()
else:
pass
response = await call_next(request)
return response
@@ -92,11 +89,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")
@@ -175,7 +172,7 @@ async def speech(request: Request, user=Depends(get_verified_user)):
res = r.json()
if "error" in res:
error_detail = f"External: {res['error']}"
except:
except Exception:
error_detail = f"External: {e}"
raise HTTPException(
@@ -234,64 +231,68 @@ def merge_models_lists(model_lists):
return merged_list
async def get_all_models(raw: bool = False):
def is_openai_api_disabled():
api_keys = app.state.config.OPENAI_API_KEYS
no_keys = len(api_keys) == 1 and api_keys[0] == ""
return no_keys or not app.state.config.ENABLE_OPENAI_API
async def get_all_models_raw() -> list:
if is_openai_api_disabled():
return []
# Check if API KEYS length is same than API URLS length
num_urls = len(app.state.config.OPENAI_API_BASE_URLS)
num_keys = len(app.state.config.OPENAI_API_KEYS)
if num_keys != num_urls:
# if there are more keys than urls, remove the extra keys
if num_keys > num_urls:
new_keys = app.state.config.OPENAI_API_KEYS[:num_urls]
app.state.config.OPENAI_API_KEYS = new_keys
# if there are more urls than keys, add empty keys
else:
app.state.config.OPENAI_API_KEYS += [""] * (num_urls - num_keys)
tasks = [
fetch_url(f"{url}/models", app.state.config.OPENAI_API_KEYS[idx])
for idx, url in enumerate(app.state.config.OPENAI_API_BASE_URLS)
]
responses = await asyncio.gather(*tasks)
log.debug(f"get_all_models:responses() {responses}")
return responses
@overload
async def get_all_models(raw: Literal[True]) -> list: ...
@overload
async def get_all_models(raw: Literal[False] = False) -> dict[str, list]: ...
async def get_all_models(raw=False) -> dict[str, list] | list:
log.info("get_all_models()")
if is_openai_api_disabled():
return [] if raw else {"data": []}
if (
len(app.state.config.OPENAI_API_KEYS) == 1
and app.state.config.OPENAI_API_KEYS[0] == ""
) or not app.state.config.ENABLE_OPENAI_API:
models = {"data": []}
else:
# Check if API KEYS length is same than API URLS length
if len(app.state.config.OPENAI_API_KEYS) != len(
app.state.config.OPENAI_API_BASE_URLS
):
# if there are more keys than urls, remove the extra keys
if len(app.state.config.OPENAI_API_KEYS) > len(
app.state.config.OPENAI_API_BASE_URLS
):
app.state.config.OPENAI_API_KEYS = app.state.config.OPENAI_API_KEYS[
: len(app.state.config.OPENAI_API_BASE_URLS)
]
# if there are more urls than keys, add empty keys
else:
app.state.config.OPENAI_API_KEYS += [
""
for _ in range(
len(app.state.config.OPENAI_API_BASE_URLS)
- len(app.state.config.OPENAI_API_KEYS)
)
]
responses = await get_all_models_raw()
if raw:
return responses
tasks = [
fetch_url(f"{url}/models", app.state.config.OPENAI_API_KEYS[idx])
for idx, url in enumerate(app.state.config.OPENAI_API_BASE_URLS)
]
def extract_data(response):
if response and "data" in response:
return response["data"]
if isinstance(response, list):
return response
return None
responses = await asyncio.gather(*tasks)
log.debug(f"get_all_models:responses() {responses}")
models = {"data": merge_models_lists(map(extract_data, responses))}
if raw:
return responses
models = {
"data": merge_models_lists(
list(
map(
lambda response: (
response["data"]
if (response and "data" in response)
else (response if isinstance(response, list) else None)
),
responses,
)
)
)
}
log.debug(f"models: {models}")
app.state.MODELS = {model["id"]: model for model in models["data"]}
log.debug(f"models: {models}")
app.state.MODELS = {model["id"]: model for model in models["data"]}
return models
@@ -299,7 +300,7 @@ async def get_all_models(raw: bool = False):
@app.get("/models")
@app.get("/models/{url_idx}")
async def get_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:
if user.role == "user":
@@ -340,7 +341,7 @@ async def get_models(url_idx: Optional[int] = None, user=Depends(get_verified_us
res = r.json()
if "error" in res:
error_detail = f"External: {res['error']}"
except:
except Exception:
error_detail = f"External: {e}"
raise HTTPException(
@@ -358,6 +359,7 @@ async def generate_chat_completion(
):
idx = 0
payload = {**form_data}
if "metadata" in payload:
del payload["metadata"]
@@ -368,67 +370,9 @@ async def generate_chat_completion(
if model_info.base_model_id:
payload["model"] = model_info.base_model_id
model_info.params = model_info.params.model_dump()
if model_info.params:
if (
model_info.params.get("temperature", None)
and payload.get("temperature") is None
):
payload["temperature"] = float(model_info.params.get("temperature"))
if model_info.params.get("top_p", None) and payload.get("top_p") is None:
payload["top_p"] = int(model_info.params.get("top_p", None))
if (
model_info.params.get("max_tokens", None)
and payload.get("max_tokens") is None
):
payload["max_tokens"] = int(model_info.params.get("max_tokens", None))
if (
model_info.params.get("frequency_penalty", None)
and payload.get("frequency_penalty") is None
):
payload["frequency_penalty"] = int(
model_info.params.get("frequency_penalty", None)
)
if model_info.params.get("seed", None) and payload.get("seed") is None:
payload["seed"] = model_info.params.get("seed", None)
if model_info.params.get("stop", None) and payload.get("stop") is 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
)
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 {}
),
)
if payload.get("messages"):
payload["messages"] = add_or_update_system_message(
system, payload["messages"]
)
else:
pass
params = model_info.params.model_dump()
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")]
idx = model["urlIdx"]
@@ -441,13 +385,6 @@ async def generate_chat_completion(
"role": user.role,
}
# Check if the model is "gpt-4-vision-preview" and set "max_tokens" to 4000
# This is a workaround until OpenAI fixes the issue with this model
if payload.get("model") == "gpt-4-vision-preview":
if "max_tokens" not in payload:
payload["max_tokens"] = 4000
log.debug("Modified payload:", payload)
# Convert the modified body back to JSON
payload = json.dumps(payload)
@@ -459,6 +396,9 @@ async def generate_chat_completion(
headers = {}
headers["Authorization"] = f"Bearer {key}"
headers["Content-Type"] = "application/json"
if "openrouter.ai" in app.state.config.OPENAI_API_BASE_URLS[idx]:
headers["HTTP-Referer"] = "https://openwebui.com/"
headers["X-Title"] = "Open WebUI"
r = None
session = None
@@ -500,7 +440,7 @@ async def generate_chat_completion(
print(res)
if "error" in res:
error_detail = f"External: {res['error']['message'] if 'message' in res['error'] else res['error']}"
except:
except Exception:
error_detail = f"External: {e}"
raise HTTPException(status_code=r.status if r else 500, detail=error_detail)
finally:
@@ -563,7 +503,7 @@ async def proxy(path: str, request: Request, user=Depends(get_verified_user)):
print(res)
if "error" in res:
error_detail = f"External: {res['error']['message'] if 'message' in res['error'] else res['error']}"
except:
except Exception:
error_detail = f"External: {e}"
raise HTTPException(status_code=r.status if r else 500, detail=error_detail)
finally:
+16 -9
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
@@ -376,7 +376,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,
@@ -439,7 +439,7 @@ class ChunkParamUpdateForm(BaseModel):
class YoutubeLoaderConfig(BaseModel):
language: List[str]
language: list[str]
translation: Optional[str] = None
@@ -642,7 +642,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 +1021,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()
@@ -1099,6 +1099,13 @@ def get_loader(filename: str, file_content_type: str, file_path: str):
"vue",
"svelte",
"msg",
"ex",
"exs",
"erl",
"tsx",
"jsx",
"hs",
"lhs",
]
if (
@@ -1178,7 +1185,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()
@@ -1231,7 +1238,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()
@@ -1289,7 +1296,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(
@@ -1332,7 +1339,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(
+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}
+6 -6
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,7 +142,7 @@ 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,
@@ -157,13 +157,13 @@ def query_collection(
embedding_function=embedding_function,
)
results.append(result)
except:
except Exception:
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 +182,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)
@@ -411,7 +411,7 @@ class ChromaRetriever(BaseRetriever):
query: str,
*,
run_manager: CallbackManagerForRetrieverRun,
) -> List[Document]:
) -> list[Document]:
query_embeddings = self.embedding_function(query)
results = self.collection.query(
+17 -16
View File
@@ -44,24 +44,26 @@ async def user_join(sid, data):
print("user-join", sid, data)
auth = data["auth"] if "auth" in data else None
if not auth or "token" not in auth:
return
if auth and "token" in auth:
data = decode_token(auth["token"])
data = decode_token(auth["token"])
if data is None or "id" not in data:
return
if data is not None and "id" in data:
user = Users.get_user_by_id(data["id"])
user = Users.get_user_by_id(data["id"])
if not user:
return
if user:
SESSION_POOL[sid] = user.id
if user.id in USER_POOL:
USER_POOL[user.id].append(sid)
else:
USER_POOL[user.id] = [sid]
SESSION_POOL[sid] = user.id
if user.id in USER_POOL:
USER_POOL[user.id].append(sid)
else:
USER_POOL[user.id] = [sid]
print(f"user {user.name}({user.id}) connected with session ID {sid}")
print(f"user {user.name}({user.id}) connected with session ID {sid}")
await sio.emit("user-count", {"count": len(set(USER_POOL))})
await sio.emit("user-count", {"count": len(set(USER_POOL))})
@sio.on("user-count")
@@ -80,7 +82,6 @@ def get_models_in_use():
@sio.on("usage")
async def usage(sid, data):
model_id = data["model"]
# Cancel previous callback if there is one
@@ -139,7 +140,7 @@ async def disconnect(sid):
print(f"Unknown session ID {sid} disconnected")
async def get_event_emitter(request_info):
def get_event_emitter(request_info):
async def __event_emitter__(event_data):
await sio.emit(
"chat-events",
@@ -154,7 +155,7 @@ async def get_event_emitter(request_info):
return __event_emitter__
async def get_event_call(request_info):
def get_event_call(request_info):
async def __event_call__(event_data):
response = await sio.call(
"chat-events",
+192 -284
View File
@@ -1,9 +1,6 @@
from fastapi import FastAPI, Depends
from fastapi.routing import APIRoute
from fastapi import FastAPI
from fastapi.responses import StreamingResponse
from fastapi.middleware.cors import CORSMiddleware
from starlette.middleware.sessions import SessionMiddleware
from sqlalchemy.orm import Session
from apps.webui.routers import (
auths,
users,
@@ -22,12 +19,15 @@ from apps.webui.models.functions import Functions
from apps.webui.models.models import Models
from apps.webui.utils import load_function_module_by_id
from utils.misc import stream_message_template
from utils.task import prompt_template
from utils.misc import (
openai_chat_chunk_message_template,
openai_chat_completion_message_template,
apply_model_params_to_body_openai,
apply_model_system_prompt_to_body,
)
from config import (
WEBUI_BUILD_HASH,
SHOW_ADMIN_DETAILS,
ADMIN_EMAIL,
WEBUI_AUTH,
@@ -35,6 +35,7 @@ from config import (
DEFAULT_PROMPT_SUGGESTIONS,
DEFAULT_USER_ROLE,
ENABLE_SIGNUP,
ENABLE_LOGIN_FORM,
USER_PERMISSIONS,
WEBHOOK_URL,
WEBUI_AUTH_TRUSTED_EMAIL_HEADER,
@@ -45,16 +46,15 @@ from config import (
AppConfig,
OAUTH_USERNAME_CLAIM,
OAUTH_PICTURE_CLAIM,
OAUTH_EMAIL_CLAIM,
)
from apps.socket.main import get_event_call, get_event_emitter
import inspect
import uuid
import time
import json
from typing import Iterator, Generator, Optional
from typing import Iterator, Generator, AsyncGenerator
from pydantic import BaseModel
app = FastAPI()
@@ -64,6 +64,7 @@ origins = ["*"]
app.state.config = AppConfig()
app.state.config.ENABLE_SIGNUP = ENABLE_SIGNUP
app.state.config.ENABLE_LOGIN_FORM = ENABLE_LOGIN_FORM
app.state.config.JWT_EXPIRES_IN = JWT_EXPIRES_IN
app.state.AUTH_TRUSTED_EMAIL_HEADER = WEBUI_AUTH_TRUSTED_EMAIL_HEADER
app.state.AUTH_TRUSTED_NAME_HEADER = WEBUI_AUTH_TRUSTED_NAME_HEADER
@@ -84,6 +85,7 @@ app.state.config.ENABLE_COMMUNITY_SHARING = ENABLE_COMMUNITY_SHARING
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 = {}
@@ -125,60 +127,58 @@ async def get_status():
}
def get_function_module(pipe_id: str):
# Check if function is already loaded
if pipe_id not in app.state.FUNCTIONS:
function_module, _, _ = load_function_module_by_id(pipe_id)
app.state.FUNCTIONS[pipe_id] = function_module
else:
function_module = app.state.FUNCTIONS[pipe_id]
if hasattr(function_module, "valves") and hasattr(function_module, "Valves"):
valves = Functions.get_function_valves_by_id(pipe_id)
function_module.valves = function_module.Valves(**(valves if valves else {}))
return function_module
async def get_pipe_models():
pipes = Functions.get_functions_by_type("pipe", active_only=True)
pipe_models = []
for pipe in pipes:
# Check if function is already loaded
if pipe.id not in app.state.FUNCTIONS:
function_module, function_type, frontmatter = load_function_module_by_id(
pipe.id
)
app.state.FUNCTIONS[pipe.id] = function_module
else:
function_module = app.state.FUNCTIONS[pipe.id]
if hasattr(function_module, "valves") and hasattr(function_module, "Valves"):
valves = Functions.get_function_valves_by_id(pipe.id)
function_module.valves = function_module.Valves(
**(valves if valves else {})
)
function_module = get_function_module(pipe.id)
# Check if function is a manifold
if hasattr(function_module, "type"):
if function_module.type == "manifold":
manifold_pipes = []
if hasattr(function_module, "pipes"):
manifold_pipes = []
# Check if pipes is a function or a list
if callable(function_module.pipes):
manifold_pipes = function_module.pipes()
else:
manifold_pipes = function_module.pipes
# Check if pipes is a function or a list
if callable(function_module.pipes):
manifold_pipes = function_module.pipes()
else:
manifold_pipes = function_module.pipes
for p in manifold_pipes:
manifold_pipe_id = f'{pipe.id}.{p["id"]}'
manifold_pipe_name = p["name"]
for p in manifold_pipes:
manifold_pipe_id = f'{pipe.id}.{p["id"]}'
manifold_pipe_name = p["name"]
if hasattr(function_module, "name"):
manifold_pipe_name = (
f"{function_module.name}{manifold_pipe_name}"
)
if hasattr(function_module, "name"):
manifold_pipe_name = f"{function_module.name}{manifold_pipe_name}"
pipe_flag = {"type": pipe.type}
if hasattr(function_module, "ChatValves"):
pipe_flag["valves_spec"] = function_module.ChatValves.schema()
pipe_flag = {"type": pipe.type}
if hasattr(function_module, "ChatValves"):
pipe_flag["valves_spec"] = function_module.ChatValves.schema()
pipe_models.append(
{
"id": manifold_pipe_id,
"name": manifold_pipe_name,
"object": "model",
"created": pipe.created_at,
"owned_by": "openai",
"pipe": pipe_flag,
}
)
pipe_models.append(
{
"id": manifold_pipe_id,
"name": manifold_pipe_name,
"object": "model",
"created": pipe.created_at,
"owned_by": "openai",
"pipe": pipe_flag,
}
)
else:
pipe_flag = {"type": "pipe"}
if hasattr(function_module, "ChatValves"):
@@ -198,262 +198,170 @@ async def get_pipe_models():
return pipe_models
async def execute_pipe(pipe, params):
if inspect.iscoroutinefunction(pipe):
return await pipe(**params)
else:
return pipe(**params)
async def get_message_content(res: str | Generator | AsyncGenerator) -> str:
if isinstance(res, str):
return res
if isinstance(res, Generator):
return "".join(map(str, res))
if isinstance(res, AsyncGenerator):
return "".join([str(stream) async for stream in res])
def process_line(form_data: dict, line):
if isinstance(line, BaseModel):
line = line.model_dump_json()
line = f"data: {line}"
if isinstance(line, dict):
line = f"data: {json.dumps(line)}"
try:
line = line.decode("utf-8")
except Exception:
pass
if line.startswith("data:"):
return f"{line}\n\n"
else:
line = openai_chat_chunk_message_template(form_data["model"], line)
return f"data: {json.dumps(line)}\n\n"
def get_pipe_id(form_data: dict) -> str:
pipe_id = form_data["model"]
if "." in pipe_id:
pipe_id, _ = pipe_id.split(".", 1)
print(pipe_id)
return pipe_id
def get_function_params(function_module, form_data, user, 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,
}
try:
if hasattr(function_module, "UserValves"):
__user__["valves"] = function_module.UserValves(
**Functions.get_user_valves_by_id_and_user_id(pipe_id, user.id)
)
except Exception as e:
print(e)
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 = None
if "metadata" in form_data:
metadata = form_data["metadata"]
del form_data["metadata"]
metadata = form_data.pop("metadata", None)
__event_emitter__ = None
__event_call__ = None
__task__ = None
if metadata:
if (
metadata.get("session_id")
and metadata.get("chat_id")
and metadata.get("message_id")
):
__event_emitter__ = await get_event_emitter(metadata)
__event_call__ = await get_event_call(metadata)
if metadata.get("task"):
__task__ = metadata.get("task")
if all(k in metadata for k in ("session_id", "chat_id", "message_id")):
__event_emitter__ = get_event_emitter(metadata)
__event_call__ = get_event_call(metadata)
__task__ = metadata.get("task", None)
if model_info:
if model_info.base_model_id:
form_data["model"] = model_info.base_model_id
model_info.params = model_info.params.model_dump()
params = model_info.params.model_dump()
form_data = apply_model_params_to_body_openai(params, form_data)
form_data = apply_model_system_prompt_to_body(params, form_data, user)
if model_info.params:
if model_info.params.get("temperature", None) is not None:
form_data["temperature"] = float(model_info.params.get("temperature"))
pipe_id = get_pipe_id(form_data)
function_module = get_function_module(pipe_id)
if model_info.params.get("top_p", None):
form_data["top_p"] = int(model_info.params.get("top_p", None))
pipe = function_module.pipe
params = get_function_params(
function_module,
form_data,
user,
{
"__event_emitter__": __event_emitter__,
"__event_call__": __event_call__,
"__task__": __task__,
},
)
if model_info.params.get("max_tokens", None):
form_data["max_tokens"] = int(model_info.params.get("max_tokens", None))
if model_info.params.get("frequency_penalty", None):
form_data["frequency_penalty"] = int(
model_info.params.get("frequency_penalty", None)
)
if model_info.params.get("seed", None):
form_data["seed"] = model_info.params.get("seed", None)
if model_info.params.get("stop", None):
form_data["stop"] = (
[
bytes(stop, "utf-8").decode("unicode_escape")
for stop in model_info.params["stop"]
]
if model_info.params.get("stop", None)
else 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 {}
),
)
# Check if the payload already has a system message
# If not, add a system message to the payload
if form_data.get("messages"):
for message in form_data["messages"]:
if message.get("role") == "system":
message["content"] = system + message["content"]
break
else:
form_data["messages"].insert(
0,
{
"role": "system",
"content": system,
},
)
else:
pass
async def job():
pipe_id = form_data["model"]
if "." in pipe_id:
pipe_id, sub_pipe_id = pipe_id.split(".", 1)
print(pipe_id)
# Check if function is already loaded
if pipe_id not in app.state.FUNCTIONS:
function_module, function_type, frontmatter = load_function_module_by_id(
pipe_id
)
app.state.FUNCTIONS[pipe_id] = function_module
else:
function_module = app.state.FUNCTIONS[pipe_id]
if hasattr(function_module, "valves") and hasattr(function_module, "Valves"):
valves = Functions.get_function_valves_by_id(pipe_id)
function_module.valves = function_module.Valves(
**(valves if valves else {})
)
pipe = function_module.pipe
# Get the signature of the function
sig = inspect.signature(pipe)
params = {"body": form_data}
if "__user__" in sig.parameters:
__user__ = {
"id": user.id,
"email": user.email,
"name": user.name,
"role": user.role,
}
if form_data["stream"]:
async def stream_content():
try:
if hasattr(function_module, "UserValves"):
__user__["valves"] = function_module.UserValves(
**Functions.get_user_valves_by_id_and_user_id(pipe_id, user.id)
)
except Exception as e:
print(e)
res = await execute_pipe(pipe, params)
params = {**params, "__user__": __user__}
if "__event_emitter__" in sig.parameters:
params = {**params, "__event_emitter__": __event_emitter__}
if "__event_call__" in sig.parameters:
params = {**params, "__event_call__": __event_call__}
if "__task__" in sig.parameters:
params = {**params, "__task__": __task__}
if form_data["stream"]:
async def stream_content():
try:
if inspect.iscoroutinefunction(pipe):
res = await pipe(**params)
else:
res = pipe(**params)
# Directly return if the response is a StreamingResponse
if isinstance(res, StreamingResponse):
async for data in res.body_iterator:
yield data
return
if isinstance(res, dict):
yield f"data: {json.dumps(res)}\n\n"
return
except Exception as e:
print(f"Error: {e}")
yield f"data: {json.dumps({'error': {'detail':str(e)}})}\n\n"
# Directly return if the response is a StreamingResponse
if isinstance(res, StreamingResponse):
async for data in res.body_iterator:
yield data
return
if isinstance(res, dict):
yield f"data: {json.dumps(res)}\n\n"
return
if isinstance(res, str):
message = stream_message_template(form_data["model"], res)
yield f"data: {json.dumps(message)}\n\n"
if isinstance(res, Iterator):
for line in res:
if isinstance(line, BaseModel):
line = line.model_dump_json()
line = f"data: {line}"
if isinstance(line, dict):
line = f"data: {json.dumps(line)}"
try:
line = line.decode("utf-8")
except:
pass
if line.startswith("data:"):
yield f"{line}\n\n"
else:
line = stream_message_template(form_data["model"], line)
yield f"data: {json.dumps(line)}\n\n"
if isinstance(res, str) or isinstance(res, Generator):
finish_message = {
"id": f"{form_data['model']}-{str(uuid.uuid4())}",
"object": "chat.completion.chunk",
"created": int(time.time()),
"model": form_data["model"],
"choices": [
{
"index": 0,
"delta": {},
"logprobs": None,
"finish_reason": "stop",
}
],
}
yield f"data: {json.dumps(finish_message)}\n\n"
yield f"data: [DONE]"
return StreamingResponse(stream_content(), media_type="text/event-stream")
else:
try:
if inspect.iscoroutinefunction(pipe):
res = await pipe(**params)
else:
res = pipe(**params)
if isinstance(res, StreamingResponse):
return res
except Exception as e:
print(f"Error: {e}")
return {"error": {"detail": str(e)}}
yield f"data: {json.dumps({'error': {'detail':str(e)}})}\n\n"
return
if isinstance(res, dict):
return res
elif isinstance(res, BaseModel):
return res.model_dump()
else:
message = ""
if isinstance(res, str):
message = res
if isinstance(res, Generator):
for stream in res:
message = f"{message}{stream}"
if isinstance(res, str):
message = openai_chat_chunk_message_template(form_data["model"], res)
yield f"data: {json.dumps(message)}\n\n"
return {
"id": f"{form_data['model']}-{str(uuid.uuid4())}",
"object": "chat.completion",
"created": int(time.time()),
"model": form_data["model"],
"choices": [
{
"index": 0,
"message": {
"role": "assistant",
"content": message,
},
"logprobs": None,
"finish_reason": "stop",
}
],
}
if isinstance(res, Iterator):
for line in res:
yield process_line(form_data, line)
return await job()
if isinstance(res, AsyncGenerator):
async for line in res:
yield process_line(form_data, line)
if isinstance(res, str) or isinstance(res, Generator):
finish_message = openai_chat_chunk_message_template(
form_data["model"], ""
)
finish_message["choices"][0]["finish_reason"] = "stop"
yield f"data: {json.dumps(finish_message)}\n\n"
yield "data: [DONE]"
return StreamingResponse(stream_content(), media_type="text/event-stream")
else:
try:
res = await execute_pipe(pipe, params)
except Exception as e:
print(f"Error: {e}")
return {"error": {"detail": str(e)}}
if isinstance(res, StreamingResponse) or isinstance(res, dict):
return res
if isinstance(res, BaseModel):
return res.model_dump()
message = await get_message_content(res)
return openai_chat_completion_message_template(form_data["model"], message)
+6 -6
View File
@@ -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
+52 -19
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:
@@ -245,9 +245,42 @@ class ChatTable:
)
return [ChatModel.model_validate(chat) for chat in all_chats]
def get_chat_title_id_list_by_user_id(
self,
user_id: str,
include_archived: bool = False,
skip: int = 0,
limit: int = -1,
) -> 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()
)
# 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(
{
"id": chat[0],
"title": chat[1],
"updated_at": chat[2],
"created_at": chat[3],
}
)
for chat in all_chats
]
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)
@@ -264,7 +297,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]:
@@ -286,10 +319,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 = (
@@ -299,7 +332,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 = (
@@ -309,7 +342,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 = (
@@ -327,7 +360,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:
@@ -338,7 +371,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:
@@ -352,7 +385,7 @@ class ChatTable:
db.commit()
return True
except:
except Exception:
return False
def delete_shared_chats_by_user_id(self, user_id: str) -> bool:
@@ -367,7 +400,7 @@ class ChatTable:
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
import logging
@@ -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
+5 -5
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
@@ -90,10 +90,10 @@ 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()]
@@ -107,7 +107,7 @@ class FilesTable:
db.commit()
return True
except:
except Exception:
return False
def delete_all_files(self) -> bool:
@@ -119,7 +119,7 @@ class FilesTable:
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
import time
import logging
@@ -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
+5 -13
View File
@@ -1,13 +1,11 @@
import json
import logging
from typing import Optional
from typing import Optional, List
from pydantic import BaseModel, ConfigDict
from sqlalchemy import String, Column, BigInteger, Text
from sqlalchemy import Column, BigInteger, Text
from apps.webui.internal.db import Base, JSONField, get_db
from typing import List, Union, Optional
from config import SRC_LOG_LEVELS
import time
@@ -113,7 +111,6 @@ class ModelForm(BaseModel):
class ModelsTable:
def insert_new_model(
self, form_data: ModelForm, user_id: str
) -> Optional[ModelModel]:
@@ -126,9 +123,7 @@ class ModelsTable:
}
)
try:
with get_db() as db:
result = Model(**model.model_dump())
db.add(result)
db.commit()
@@ -142,18 +137,16 @@ 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()]
def get_model_by_id(self, id: str) -> Optional[ModelModel]:
try:
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]:
@@ -178,12 +171,11 @@ class ModelsTable:
def delete_model_by_id(self, id: str) -> bool:
try:
with get_db() as db:
db.query(Model).filter_by(id=id).delete()
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
+8 -8
View File
@@ -1,5 +1,5 @@
from pydantic import BaseModel, ConfigDict
from typing import List, Optional
from typing import Optional
import json
import uuid
@@ -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 [
+8 -8
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
@@ -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
+12 -12
View File
@@ -1,5 +1,5 @@
from pydantic import BaseModel, ConfigDict, parse_obj_as
from typing import List, Union, Optional
from typing import Union, Optional
import time
from sqlalchemy import String, Column, BigInteger, Text
@@ -125,7 +125,7 @@ class UsersTable:
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]:
@@ -134,7 +134,7 @@ class UsersTable:
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]:
@@ -143,10 +143,10 @@ class UsersTable:
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 +164,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 +174,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,7 +189,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_last_active_by_id(self, id: str) -> Optional[UserModel]:
@@ -203,7 +203,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 +216,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 +245,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 +254,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]:
+27 -16
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
@@ -28,7 +28,7 @@ from apps.webui.models.tags import (
from constants import ERROR_MESSAGES
from config import SRC_LOG_LEVELS, ENABLE_ADMIN_EXPORT
from config import SRC_LOG_LEVELS, ENABLE_ADMIN_EXPORT, ENABLE_ADMIN_CHAT_ACCESS
log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["MODELS"])
@@ -40,12 +40,18 @@ 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), skip: int = 0, limit: int = 50
user=Depends(get_verified_user), page: Optional[int] = None
):
return Chats.get_chat_list_by_user_id(user.id, skip, limit)
if page is not None:
limit = 60
skip = (page - 1) * limit
return Chats.get_chat_title_id_list_by_user_id(user.id, skip=skip, limit=limit)
else:
return Chats.get_chat_title_id_list_by_user_id(user.id)
############################
@@ -74,13 +80,18 @@ 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),
skip: int = 0,
limit: int = 50,
):
if not ENABLE_ADMIN_CHAT_ACCESS:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
)
return Chats.get_chat_list_by_user_id(
user_id, include_archived=True, skip=skip, limit=limit
)
@@ -108,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)})
@@ -121,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)})
@@ -134,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(
@@ -152,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
):
@@ -181,9 +192,9 @@ async def get_shared_chat_by_id(share_id: str, user=Depends(get_verified_user)):
status_code=status.HTTP_401_UNAUTHORIZED, detail=ERROR_MESSAGES.NOT_FOUND
)
if user.role == "user":
if user.role == "user" or (user.role == "admin" and not ENABLE_ADMIN_CHAT_ACCESS):
chat = Chats.get_chat_by_share_id(share_id)
elif user.role == "admin":
elif user.role == "admin" and ENABLE_ADMIN_CHAT_ACCESS:
chat = Chats.get_chat_by_id(share_id)
if chat:
@@ -205,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)
):
@@ -230,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)
@@ -406,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])
+2 -2
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,7 +104,7 @@ 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()
return files
+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
+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
@@ -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)
+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:
+15 -20
View File
@@ -1,12 +1,8 @@
from fastapi import Depends, FastAPI, HTTPException, status, Request
from datetime import datetime, timedelta
from typing import List, Union, Optional
from fastapi import Depends, HTTPException, status, Request
from typing import Optional
from fastapi import APIRouter
from pydantic import BaseModel
import json
from apps.webui.models.users import Users
from apps.webui.models.tools import Tools, ToolForm, ToolModel, ToolResponse
from apps.webui.utils import load_toolkit_module_by_id
@@ -14,7 +10,6 @@ from utils.utils import get_admin_user, get_verified_user
from utils.tools import get_tools_specs
from constants import ERROR_MESSAGES
from importlib import util
import os
from pathlib import Path
@@ -32,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
@@ -43,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
@@ -69,7 +64,7 @@ async def create_new_toolkit(
form_data.id = form_data.id.lower()
toolkit = Tools.get_tool_by_id(form_data.id)
if toolkit == None:
if toolkit is None:
toolkit_path = os.path.join(TOOLS_DIR, f"{form_data.id}.py")
try:
with open(toolkit_path, "w") as tool_file:
@@ -98,7 +93,7 @@ async def create_new_toolkit(
print(e)
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.DEFAULT(e),
detail=ERROR_MESSAGES.DEFAULT(str(e)),
)
else:
raise HTTPException(
@@ -170,7 +165,7 @@ async def update_toolkit_by_id(
except Exception as e:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.DEFAULT(e),
detail=ERROR_MESSAGES.DEFAULT(str(e)),
)
@@ -210,7 +205,7 @@ async def get_toolkit_valves_by_id(id: str, user=Depends(get_admin_user)):
except Exception as e:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.DEFAULT(e),
detail=ERROR_MESSAGES.DEFAULT(str(e)),
)
else:
raise HTTPException(
@@ -233,7 +228,7 @@ async def get_toolkit_valves_spec_by_id(
if id in request.app.state.TOOLS:
toolkit_module = request.app.state.TOOLS[id]
else:
toolkit_module, frontmatter = load_toolkit_module_by_id(id)
toolkit_module, _ = load_toolkit_module_by_id(id)
request.app.state.TOOLS[id] = toolkit_module
if hasattr(toolkit_module, "Valves"):
@@ -261,7 +256,7 @@ async def update_toolkit_valves_by_id(
if id in request.app.state.TOOLS:
toolkit_module = request.app.state.TOOLS[id]
else:
toolkit_module, frontmatter = load_toolkit_module_by_id(id)
toolkit_module, _ = load_toolkit_module_by_id(id)
request.app.state.TOOLS[id] = toolkit_module
if hasattr(toolkit_module, "Valves"):
@@ -276,7 +271,7 @@ async def update_toolkit_valves_by_id(
print(e)
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.DEFAULT(e),
detail=ERROR_MESSAGES.DEFAULT(str(e)),
)
else:
raise HTTPException(
@@ -306,7 +301,7 @@ async def get_toolkit_user_valves_by_id(id: str, user=Depends(get_verified_user)
except Exception as e:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.DEFAULT(e),
detail=ERROR_MESSAGES.DEFAULT(str(e)),
)
else:
raise HTTPException(
@@ -324,7 +319,7 @@ async def get_toolkit_user_valves_spec_by_id(
if id in request.app.state.TOOLS:
toolkit_module = request.app.state.TOOLS[id]
else:
toolkit_module, frontmatter = load_toolkit_module_by_id(id)
toolkit_module, _ = load_toolkit_module_by_id(id)
request.app.state.TOOLS[id] = toolkit_module
if hasattr(toolkit_module, "UserValves"):
@@ -348,7 +343,7 @@ async def update_toolkit_user_valves_by_id(
if id in request.app.state.TOOLS:
toolkit_module = request.app.state.TOOLS[id]
else:
toolkit_module, frontmatter = load_toolkit_module_by_id(id)
toolkit_module, _ = load_toolkit_module_by_id(id)
request.app.state.TOOLS[id] = toolkit_module
if hasattr(toolkit_module, "UserValves"):
@@ -365,7 +360,7 @@ async def update_toolkit_user_valves_by_id(
print(e)
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.DEFAULT(e),
detail=ERROR_MESSAGES.DEFAULT(str(e)),
)
else:
raise HTTPException(
+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)
+17 -4
View File
@@ -1,3 +1,6 @@
from pathlib import Path
import site
from fastapi import APIRouter, UploadFile, File, Response
from fastapi import Depends, HTTPException, status
from starlette.responses import StreamingResponse, FileResponse
@@ -14,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()
@@ -54,7 +57,7 @@ async def get_html_from_markdown(
class ChatForm(BaseModel):
title: str
messages: List[dict]
messages: list[dict]
@router.post("/pdf")
@@ -64,8 +67,18 @@ async def download_chat_as_pdf(
pdf = FPDF()
pdf.add_page()
STATIC_DIR = "./static"
FONTS_DIR = f"{STATIC_DIR}/fonts"
# When running in docker, workdir is /app/backend, so fonts is in /app/backend/static/fonts
FONTS_DIR = Path("./static/fonts")
# Non Docker Installation
# When running using `pip install` the static directory is in the site packages.
if not FONTS_DIR.exists():
FONTS_DIR = Path(site.getsitepackages()[0]) / "static/fonts"
# When running using `pip install -e .` the static directory is in the site packages.
# This path only works if `open-webui serve` is run from the root of this project.
if not FONTS_DIR.exists():
FONTS_DIR = Path("./backend/static/fonts")
pdf.add_font("NotoSans", "", f"{FONTS_DIR}/NotoSans-Regular.ttf")
pdf.add_font("NotoSans", "b", f"{FONTS_DIR}/NotoSans-Bold.ttf")
+14
View File
@@ -1,6 +1,8 @@
from importlib import util
import os
import re
import sys
import subprocess
from config import TOOLS_DIR, FUNCTIONS_DIR
@@ -52,6 +54,7 @@ def load_toolkit_module_by_id(toolkit_id):
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"):
@@ -73,6 +76,7 @@ def load_function_module_by_id(function_id):
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 +92,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.")
+78 -8
View File
@@ -77,6 +77,16 @@ for source in log_sources:
log.setLevel(SRC_LOG_LEVELS["CONFIG"])
class EndpointFilter(logging.Filter):
def filter(self, record: logging.LogRecord) -> bool:
return record.getMessage().find("/health") == -1
# 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)"
@@ -94,7 +104,7 @@ ENV = os.environ.get("ENV", "dev")
try:
PACKAGE_DATA = json.loads((BASE_DIR / "package.json").read_text())
except:
except Exception:
try:
PACKAGE_DATA = {"version": importlib.metadata.version("open-webui")}
except importlib.metadata.PackageNotFoundError:
@@ -127,7 +137,7 @@ try:
with open(str(changelog_path.absolute()), "r", encoding="utf8") as file:
changelog_content = file.read()
except:
except Exception:
changelog_content = (pkgutil.get_data("open_webui", "CHANGELOG.md") or b"").decode()
@@ -192,12 +202,12 @@ if RESET_CONFIG_ON_START:
os.remove(f"{DATA_DIR}/config.json")
with open(f"{DATA_DIR}/config.json", "w") as f:
f.write("{}")
except:
except Exception:
pass
try:
CONFIG_DATA = json.loads((DATA_DIR / "config.json").read_text())
except:
except Exception:
CONFIG_DATA = {}
@@ -339,6 +349,12 @@ GOOGLE_OAUTH_SCOPE = PersistentConfig(
os.environ.get("GOOGLE_OAUTH_SCOPE", "openid email profile"),
)
GOOGLE_REDIRECT_URI = PersistentConfig(
"GOOGLE_REDIRECT_URI",
"oauth.google.redirect_uri",
os.environ.get("GOOGLE_REDIRECT_URI", ""),
)
MICROSOFT_CLIENT_ID = PersistentConfig(
"MICROSOFT_CLIENT_ID",
"oauth.microsoft.client_id",
@@ -363,6 +379,12 @@ MICROSOFT_OAUTH_SCOPE = PersistentConfig(
os.environ.get("MICROSOFT_OAUTH_SCOPE", "openid email profile"),
)
MICROSOFT_REDIRECT_URI = PersistentConfig(
"MICROSOFT_REDIRECT_URI",
"oauth.microsoft.redirect_uri",
os.environ.get("MICROSOFT_REDIRECT_URI", ""),
)
OAUTH_CLIENT_ID = PersistentConfig(
"OAUTH_CLIENT_ID",
"oauth.oidc.client_id",
@@ -381,6 +403,12 @@ OPENID_PROVIDER_URL = PersistentConfig(
os.environ.get("OPENID_PROVIDER_URL", ""),
)
OPENID_REDIRECT_URI = PersistentConfig(
"OPENID_REDIRECT_URI",
"oauth.oidc.redirect_uri",
os.environ.get("OPENID_REDIRECT_URI", ""),
)
OAUTH_SCOPES = PersistentConfig(
"OAUTH_SCOPES",
"oauth.oidc.scopes",
@@ -405,6 +433,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()
@@ -414,6 +448,7 @@ def load_oauth_providers():
"client_secret": GOOGLE_CLIENT_SECRET.value,
"server_metadata_url": "https://accounts.google.com/.well-known/openid-configuration",
"scope": GOOGLE_OAUTH_SCOPE.value,
"redirect_uri": GOOGLE_REDIRECT_URI.value,
}
if (
@@ -426,6 +461,7 @@ def load_oauth_providers():
"client_secret": MICROSOFT_CLIENT_SECRET.value,
"server_metadata_url": f"https://login.microsoftonline.com/{MICROSOFT_CLIENT_TENANT_ID.value}/v2.0/.well-known/openid-configuration",
"scope": MICROSOFT_OAUTH_SCOPE.value,
"redirect_uri": MICROSOFT_REDIRECT_URI.value,
}
if (
@@ -439,6 +475,7 @@ def load_oauth_providers():
"server_metadata_url": OPENID_PROVIDER_URL.value,
"scope": OAUTH_SCOPES.value,
"name": OAUTH_PROVIDER_NAME.value,
"redirect_uri": OPENID_REDIRECT_URI.value,
}
@@ -610,7 +647,7 @@ if AIOHTTP_CLIENT_TIMEOUT == "":
else:
try:
AIOHTTP_CLIENT_TIMEOUT = int(AIOHTTP_CLIENT_TIMEOUT)
except:
except Exception:
AIOHTTP_CLIENT_TIMEOUT = 300
@@ -690,7 +727,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"
@@ -709,6 +746,12 @@ ENABLE_SIGNUP = PersistentConfig(
),
)
ENABLE_LOGIN_FORM = PersistentConfig(
"ENABLE_LOGIN_FORM",
"ui.ENABLE_LOGIN_FORM",
os.environ.get("ENABLE_LOGIN_FORM", "True").lower() == "true",
)
DEFAULT_LOCALE = PersistentConfig(
"DEFAULT_LOCALE",
"ui.default_locale",
@@ -787,6 +830,10 @@ WEBHOOK_URL = PersistentConfig(
ENABLE_ADMIN_EXPORT = os.environ.get("ENABLE_ADMIN_EXPORT", "True").lower() == "true"
ENABLE_ADMIN_CHAT_ACCESS = (
os.environ.get("ENABLE_ADMIN_CHAT_ACCESS", "True").lower() == "true"
)
ENABLE_COMMUNITY_SHARING = PersistentConfig(
"ENABLE_COMMUNITY_SHARING",
"ui.enable_community_sharing",
@@ -996,7 +1043,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"
@@ -1018,7 +1065,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"
@@ -1265,6 +1312,24 @@ COMFYUI_SD3 = PersistentConfig(
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",
)
IMAGES_OPENAI_API_BASE_URL = PersistentConfig(
"IMAGES_OPENAI_API_BASE_URL",
"image_generation.openai.api_base_url",
@@ -1329,6 +1394,11 @@ AUDIO_TTS_OPENAI_API_KEY = PersistentConfig(
os.getenv("AUDIO_TTS_OPENAI_API_KEY", OPENAI_API_KEY),
)
AUDIO_TTS_API_KEY = PersistentConfig(
"AUDIO_TTS_API_KEY",
"audio.tts.api_key",
os.getenv("AUDIO_TTS_API_KEY", ""),
)
AUDIO_TTS_ENGINE = PersistentConfig(
"AUDIO_TTS_ENGINE",
+116 -112
View File
@@ -13,8 +13,6 @@ import aiohttp
import requests
import mimetypes
import shutil
import os
import uuid
import inspect
from fastapi import FastAPI, Request, Depends, status, UploadFile, File, Form
@@ -29,7 +27,7 @@ from starlette.middleware.sessions import SessionMiddleware
from starlette.responses import StreamingResponse, Response, RedirectResponse
from apps.socket.main import sio, app as socket_app, get_event_emitter, get_event_call
from apps.socket.main import app as socket_app, get_event_emitter, get_event_call
from apps.ollama.main import (
app as ollama_app,
get_all_models as get_ollama_models,
@@ -53,7 +51,7 @@ from apps.webui.internal.db import Session
from pydantic import BaseModel
from typing import List, Optional
from typing import Optional
from apps.webui.models.auths import Auths
from apps.webui.models.models import Models
@@ -79,6 +77,7 @@ from utils.task import (
from utils.misc import (
get_last_user_message,
add_or_update_system_message,
prepend_to_first_user_message_content,
parse_duration,
)
@@ -117,6 +116,7 @@ from config import (
WEBUI_SECRET_KEY,
WEBUI_SESSION_COOKIE_SAME_SITE,
WEBUI_SESSION_COOKIE_SECURE,
ENABLE_ADMIN_CHAT_ACCESS,
AppConfig,
)
@@ -618,32 +618,15 @@ class ChatCompletionMiddleware(BaseHTTPMiddleware):
content={"detail": str(e)},
)
# Extract valves from the request body
valves = None
if "valves" in body:
valves = body["valves"]
del body["valves"]
metadata = {
"chat_id": body.pop("chat_id", None),
"message_id": body.pop("id", None),
"session_id": body.pop("session_id", None),
"valves": body.pop("valves", None),
}
# Extract session_id, chat_id and message_id from the request body
session_id = None
if "session_id" in body:
session_id = body["session_id"]
del body["session_id"]
chat_id = None
if "chat_id" in body:
chat_id = body["chat_id"]
del body["chat_id"]
message_id = None
if "id" in body:
message_id = body["id"]
del body["id"]
__event_emitter__ = await get_event_emitter(
{"chat_id": chat_id, "message_id": message_id, "session_id": session_id}
)
__event_call__ = await get_event_call(
{"chat_id": chat_id, "message_id": message_id, "session_id": session_id}
)
__event_emitter__ = get_event_emitter(metadata)
__event_call__ = get_event_call(metadata)
# Initialize data_items to store additional data to be sent to the client
data_items = []
@@ -686,24 +669,29 @@ class ChatCompletionMiddleware(BaseHTTPMiddleware):
if len(contexts) > 0:
context_string = "/n".join(contexts).strip()
prompt = get_last_user_message(body["messages"])
body["messages"] = add_or_update_system_message(
rag_template(
rag_app.state.config.RAG_TEMPLATE, context_string, prompt
),
body["messages"],
)
# Workaround for Ollama 2.0+ system prompt issue
# TODO: replace with add_or_update_system_message
if model["owned_by"] == "ollama":
body["messages"] = prepend_to_first_user_message_content(
rag_template(
rag_app.state.config.RAG_TEMPLATE, context_string, prompt
),
body["messages"],
)
else:
body["messages"] = add_or_update_system_message(
rag_template(
rag_app.state.config.RAG_TEMPLATE, context_string, prompt
),
body["messages"],
)
# If there are citations, add them to the data_items
if len(citations) > 0:
data_items.append({"citations": citations})
body["metadata"] = {
"session_id": session_id,
"chat_id": chat_id,
"message_id": message_id,
"valves": valves,
}
body["metadata"] = metadata
modified_body_bytes = json.dumps(body).encode("utf-8")
# Replace the request body with the modified one
request._body = modified_body_bytes
@@ -970,7 +958,7 @@ async def get_all_models():
custom_models = Models.get_all_models()
for custom_model in custom_models:
if custom_model.base_model_id == None:
if custom_model.base_model_id is None:
for model in models:
if (
custom_model.id == model["id"]
@@ -979,32 +967,15 @@ async def get_all_models():
model["name"] = custom_model.name
model["info"] = custom_model.model_dump()
action_ids = [] + global_action_ids
action_ids = []
if "info" in model and "meta" in model["info"]:
action_ids.extend(model["info"]["meta"].get("actionIds", []))
action_ids = list(set(action_ids))
action_ids = [
action_id
for action_id in action_ids
if action_id in enabled_action_ids
]
model["actions"] = []
for action_id in action_ids:
action = Functions.get_function_by_id(action_id)
model["actions"].append(
{
"id": action_id,
"name": action.name,
"description": action.meta.description,
"icon_url": action.meta.manifest.get("icon_url", None),
}
)
model["action_ids"] = action_ids
else:
owned_by = "openai"
pipe = None
actions = []
action_ids = []
for model in models:
if (
@@ -1015,26 +986,8 @@ async def get_all_models():
if "pipe" in model:
pipe = model["pipe"]
action_ids = [] + global_action_ids
if "info" in model and "meta" in model["info"]:
action_ids.extend(model["info"]["meta"].get("actionIds", []))
action_ids = list(set(action_ids))
action_ids = [
action_id
for action_id in action_ids
if action_id in enabled_action_ids
]
actions = [
{
"id": action_id,
"name": Functions.get_function_by_id(action_id).name,
"description": Functions.get_function_by_id(
action_id
).meta.description,
}
for action_id in action_ids
]
break
models.append(
@@ -1047,10 +1000,59 @@ async def get_all_models():
"info": custom_model.model_dump(),
"preset": True,
**({"pipe": pipe} if pipe is not None else {}),
"actions": actions,
"action_ids": action_ids,
}
)
for model in models:
action_ids = []
if "action_ids" in model:
action_ids = model["action_ids"]
del model["action_ids"]
action_ids = action_ids + global_action_ids
action_ids = list(set(action_ids))
action_ids = [
action_id for action_id in action_ids if action_id in enabled_action_ids
]
model["actions"] = []
for action_id in action_ids:
action = Functions.get_function_by_id(action_id)
if action_id in webui_app.state.FUNCTIONS:
function_module = webui_app.state.FUNCTIONS[action_id]
else:
function_module, _, _ = load_function_module_by_id(action_id)
webui_app.state.FUNCTIONS[action_id] = function_module
if hasattr(function_module, "actions"):
actions = function_module.actions
model["actions"].extend(
[
{
"id": f"{action_id}.{_action['id']}",
"name": _action.get(
"name", f"{action.name} ({_action['id']})"
),
"description": action.meta.description,
"icon_url": _action.get(
"icon_url", action.meta.manifest.get("icon_url", None)
),
}
for _action in actions
]
)
else:
model["actions"].append(
{
"id": action_id,
"name": action.name,
"description": action.meta.description,
"icon_url": action.meta.manifest.get("icon_url", None),
}
)
app.state.MODELS = {model["id"]: model for model in models}
webui_app.state.MODELS = app.state.MODELS
@@ -1165,13 +1167,13 @@ async def chat_completed(form_data: dict, user=Depends(get_verified_user)):
status_code=r.status_code,
content=res,
)
except:
except Exception:
pass
else:
pass
__event_emitter__ = await get_event_emitter(
__event_emitter__ = get_event_emitter(
{
"chat_id": data["chat_id"],
"message_id": data["id"],
@@ -1179,7 +1181,7 @@ async def chat_completed(form_data: dict, user=Depends(get_verified_user)):
}
)
__event_call__ = await get_event_call(
__event_call__ = get_event_call(
{
"chat_id": data["chat_id"],
"message_id": data["id"],
@@ -1284,9 +1286,12 @@ async def chat_completed(form_data: dict, user=Depends(get_verified_user)):
@app.post("/api/chat/actions/{action_id}")
async def chat_completed(
action_id: str, form_data: dict, user=Depends(get_verified_user)
):
async def chat_action(action_id: str, form_data: dict, user=Depends(get_verified_user)):
if "." in action_id:
action_id, sub_action_id = action_id.split(".")
else:
sub_action_id = None
action = Functions.get_function_by_id(action_id)
if not action:
raise HTTPException(
@@ -1303,14 +1308,14 @@ async def chat_completed(
)
model = app.state.MODELS[model_id]
__event_emitter__ = await get_event_emitter(
__event_emitter__ = get_event_emitter(
{
"chat_id": data["chat_id"],
"message_id": data["id"],
"session_id": data["session_id"],
}
)
__event_call__ = await get_event_call(
__event_call__ = get_event_call(
{
"chat_id": data["chat_id"],
"message_id": data["id"],
@@ -1339,7 +1344,7 @@ async def chat_completed(
# Extra parameters to be passed to the function
extra_params = {
"__model__": model,
"__id__": action_id,
"__id__": sub_action_id if sub_action_id is not None else action_id,
"__event_emitter__": __event_emitter__,
"__event_call__": __event_call__,
}
@@ -1658,7 +1663,7 @@ async def get_pipelines_list(user=Depends(get_admin_user)):
urlIdxs = [
idx
for idx, response in enumerate(responses)
if response != None and "pipelines" in response
if response is not None and "pipelines" in response
]
return {
@@ -1719,7 +1724,7 @@ async def upload_pipeline(
res = r.json()
if "detail" in res:
detail = res["detail"]
except:
except Exception:
pass
raise HTTPException(
@@ -1739,7 +1744,6 @@ class AddPipelineForm(BaseModel):
@app.post("/api/pipelines/add")
async def add_pipeline(form_data: AddPipelineForm, user=Depends(get_admin_user)):
r = None
try:
urlIdx = form_data.urlIdx
@@ -1766,7 +1770,7 @@ async def add_pipeline(form_data: AddPipelineForm, user=Depends(get_admin_user))
res = r.json()
if "detail" in res:
detail = res["detail"]
except:
except Exception:
pass
raise HTTPException(
@@ -1782,7 +1786,6 @@ class DeletePipelineForm(BaseModel):
@app.delete("/api/pipelines/delete")
async def delete_pipeline(form_data: DeletePipelineForm, user=Depends(get_admin_user)):
r = None
try:
urlIdx = form_data.urlIdx
@@ -1809,7 +1812,7 @@ async def delete_pipeline(form_data: DeletePipelineForm, user=Depends(get_admin_
res = r.json()
if "detail" in res:
detail = res["detail"]
except:
except Exception:
pass
raise HTTPException(
@@ -1842,7 +1845,7 @@ async def get_pipelines(urlIdx: Optional[int] = None, user=Depends(get_admin_use
res = r.json()
if "detail" in res:
detail = res["detail"]
except:
except Exception:
pass
raise HTTPException(
@@ -1857,10 +1860,8 @@ async def get_pipeline_valves(
pipeline_id: str,
user=Depends(get_admin_user),
):
models = await get_all_models()
r = None
try:
url = openai_app.state.config.OPENAI_API_BASE_URLS[urlIdx]
key = openai_app.state.config.OPENAI_API_KEYS[urlIdx]
@@ -1882,7 +1883,7 @@ async def get_pipeline_valves(
res = r.json()
if "detail" in res:
detail = res["detail"]
except:
except Exception:
pass
raise HTTPException(
@@ -1897,8 +1898,6 @@ async def get_pipeline_valves_spec(
pipeline_id: str,
user=Depends(get_admin_user),
):
models = await get_all_models()
r = None
try:
url = openai_app.state.config.OPENAI_API_BASE_URLS[urlIdx]
@@ -1921,7 +1920,7 @@ async def get_pipeline_valves_spec(
res = r.json()
if "detail" in res:
detail = res["detail"]
except:
except Exception:
pass
raise HTTPException(
@@ -1937,8 +1936,6 @@ async def update_pipeline_valves(
form_data: dict,
user=Depends(get_admin_user),
):
models = await get_all_models()
r = None
try:
url = openai_app.state.config.OPENAI_API_BASE_URLS[urlIdx]
@@ -1966,7 +1963,7 @@ async def update_pipeline_valves(
res = r.json()
if "detail" in res:
detail = res["detail"]
except:
except Exception:
pass
raise HTTPException(
@@ -1995,10 +1992,12 @@ async def get_app_config():
"auth": WEBUI_AUTH,
"auth_trusted_header": bool(webui_app.state.AUTH_TRUSTED_EMAIL_HEADER),
"enable_signup": webui_app.state.config.ENABLE_SIGNUP,
"enable_login_form": webui_app.state.config.ENABLE_LOGIN_FORM,
"enable_web_search": rag_app.state.config.ENABLE_RAG_WEB_SEARCH,
"enable_image_generation": images_app.state.config.ENABLED,
"enable_community_sharing": webui_app.state.config.ENABLE_COMMUNITY_SHARING,
"enable_admin_export": ENABLE_ADMIN_EXPORT,
"enable_admin_chat_access": ENABLE_ADMIN_CHAT_ACCESS,
},
"audio": {
"tts": {
@@ -2028,7 +2027,7 @@ async def get_model_filter_config(user=Depends(get_admin_user)):
class ModelFilterConfigForm(BaseModel):
enabled: bool
models: List[str]
models: list[str]
@app.post("/api/config/model/filter")
@@ -2066,7 +2065,7 @@ async def update_webhook_url(form_data: UrlForm, user=Depends(get_admin_user)):
@app.get("/api/version")
async def get_app_config():
async def get_app_version():
return {
"version": VERSION,
}
@@ -2089,7 +2088,7 @@ async def get_app_latest_release_version():
latest_version = data["tag_name"]
return {"current": VERSION, "latest": latest_version[1:]}
except aiohttp.ClientError as e:
except aiohttp.ClientError:
raise HTTPException(
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
detail=ERROR_MESSAGES.RATE_LIMIT_EXCEEDED,
@@ -2111,6 +2110,7 @@ for provider_name, provider_config in OAUTH_PROVIDERS.items():
client_kwargs={
"scope": provider_config["scope"],
},
redirect_uri=provider_config["redirect_uri"],
)
# SessionMiddleware is used by authlib for oauth
@@ -2128,7 +2128,10 @@ if len(OAUTH_PROVIDERS) > 0:
async def oauth_login(provider: str, request: Request):
if provider not in OAUTH_PROVIDERS:
raise HTTPException(404)
redirect_uri = request.url_for("oauth_callback", provider=provider)
# If the provider has a custom redirect URL, use that, otherwise automatically generate one
redirect_uri = OAUTH_PROVIDERS[provider].get("redirect_uri") or request.url_for(
"oauth_callback", provider=provider
)
return await oauth.create_client(provider).authorize_redirect(request, redirect_uri)
@@ -2155,7 +2158,8 @@ async def oauth_callback(provider: str, request: Request, response: Response):
log.warning(f"OAuth callback failed, sub is missing: {user_data}")
raise HTTPException(400, detail=ERROR_MESSAGES.INVALID_CRED)
provider_sub = f"{provider}@{sub}"
email = user_data.get("email", "").lower()
email_claim = webui_app.state.config.OAUTH_EMAIL_CLAIM
email = user_data.get(email_claim, "").lower()
# We currently mandate that email addresses are provided
if not email:
log.warning(f"OAuth callback failed, email is missing: {user_data}")
+18 -17
View File
@@ -11,7 +11,8 @@ 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
alembic==1.13.2
peewee==3.17.6
@@ -19,10 +20,10 @@ peewee-migrate==1.12.2
psycopg2-binary==2.9.9
PyMySQL==1.1.1
bcrypt==4.1.3
SQLAlchemy
pymongo
redis
boto3==1.34.110
boto3==1.34.153
argon2-cffi==23.1.0
APScheduler==3.10.4
@@ -30,50 +31,50 @@ APScheduler==3.10.4
# AI libraries
openai
anthropic
google-generativeai==0.5.4
google-generativeai==0.7.2
tiktoken
langchain==0.2.6
langchain-community==0.2.6
langchain==0.2.12
langchain-community==0.2.10
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.2.0
pypdf==4.3.1
docx2txt==0.8
python-pptx==0.6.23
unstructured==0.14.10
python-pptx==1.0.0
unstructured==0.15.0
Markdown==3.6
pypandoc==1.13
pandas==2.2.2
openpyxl==3.1.5
pyxlsb==1.0.10
xlrd==2.0.1
validators==0.28.1
validators==0.33.0
psutil
opencv-python-headless==4.10.0.84
rapidocr-onnxruntime==1.3.22
rapidocr-onnxruntime==1.3.24
fpdf2==2.7.9
rank-bm25==0.2.2
faster-whisper==1.0.2
PyJWT[crypto]==2.8.0
PyJWT[crypto]==2.9.0
authlib==1.3.1
black==24.4.2
langfuse==2.38.0
black==24.8.0
langfuse==2.43.3
youtube-transcript-api==0.6.2
pytube==15.0.0
extract_msg
pydub
duckduckgo-search~=6.1.12
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"
+142 -35
View File
@@ -1,59 +1,77 @@
from pathlib import Path
import hashlib
import json
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]) -> str:
def get_last_user_message_item(messages: list[dict]) -> Optional[dict]:
for message in reversed(messages):
if message["role"] == "user":
return message
return None
def get_last_user_message(messages: List[dict]) -> str:
message = get_last_user_message_item(messages)
if message is not None:
if isinstance(message["content"], list):
for item in message["content"]:
if item["type"] == "text":
return item["text"]
def get_content_from_message(message: dict) -> Optional[str]:
if isinstance(message["content"], list):
for item in message["content"]:
if item["type"] == "text":
return item["text"]
else:
return message["content"]
return None
def get_last_assistant_message(messages: List[dict]) -> str:
def get_last_user_message(messages: list[dict]) -> Optional[str]:
message = get_last_user_message_item(messages)
if message is None:
return None
return get_content_from_message(message)
def get_last_assistant_message(messages: list[dict]) -> Optional[str]:
for message in reversed(messages):
if message["role"] == "assistant":
if isinstance(message["content"], list):
for item in message["content"]:
if item["type"] == "text":
return item["text"]
return message["content"]
return get_content_from_message(message)
return None
def get_system_message(messages: List[dict]) -> 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[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 add_or_update_system_message(content: str, messages: List[dict]):
def prepend_to_first_user_message_content(
content: str, messages: list[dict]
) -> list[dict]:
for message in messages:
if message["role"] == "user":
if isinstance(message["content"], list):
for item in message["content"]:
if item["type"] == "text":
item["text"] = f"{content}\n{item['text']}"
else:
message["content"] = f"{content}\n{message['content']}"
break
return messages
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.
@@ -72,23 +90,112 @@ def add_or_update_system_message(content: str, messages: List[dict]):
return messages
def stream_message_template(model: str, message: str):
def openai_chat_message_template(model: str):
return {
"id": f"{model}-{str(uuid.uuid4())}",
"object": "chat.completion.chunk",
"created": int(time.time()),
"model": model,
"choices": [
{
"index": 0,
"delta": {"content": message},
"logprobs": None,
"finish_reason": None,
}
],
"choices": [{"index": 0, "logprobs": None, "finish_reason": None}],
}
def openai_chat_chunk_message_template(model: str, message: str) -> dict:
template = openai_chat_message_template(model)
template["object"] = "chat.completion.chunk"
template["choices"][0]["delta"] = {"content": message}
return template
def openai_chat_completion_message_template(model: str, message: str) -> dict:
template = openai_chat_message_template(model)
template["object"] = "chat.completion"
template["choices"][0]["message"] = {"content": message, "role": "assistant"}
template["choices"][0]["finish_reason"] = "stop"
return template
# inplace function: form_data is modified
def apply_model_system_prompt_to_body(params: dict, form_data: dict, user) -> dict:
system = params.get("system", None)
if not system:
return form_data
if user:
template_params = {
"user_name": user.name,
"user_location": user.info.get("location") if user.info else None,
}
else:
template_params = {}
system = prompt_template(system, **template_params)
form_data["messages"] = add_or_update_system_message(
system, form_data.get("messages", [])
)
return form_data
# inplace function: form_data is modified
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,
"max_tokens": int,
"frequency_penalty": int,
"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)
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
def get_gravatar_url(email):
# Trim leading and trailing whitespace from
# an email address and force all characters
@@ -159,7 +266,7 @@ def extract_folders_after_data_docs(path):
tags = []
folders = parts[index_docs:-1]
for idx, part in enumerate(folders):
for idx, _ in enumerate(folders):
tags.append("/".join(folders[: idx + 1]))
return tags
@@ -255,11 +362,11 @@ def parse_ollama_modelfile(model_text):
value = param_match.group(1)
try:
if param_type == int:
if param_type is int:
value = int(value)
elif param_type == float:
elif param_type is float:
value = float(value)
elif param_type == bool:
elif param_type is bool:
value = value.lower() == "true"
except Exception as e:
print(e)
+1 -2
View File
@@ -6,7 +6,7 @@ from typing import Optional
def prompt_template(
template: str, user_name: str = None, user_location: str = None
template: str, user_name: Optional[str] = None, user_location: Optional[str] = None
) -> str:
# Get the current date
current_date = datetime.now()
@@ -83,7 +83,6 @@ def title_generation_template(
def search_query_generation_template(
template: str, prompt: str, user: Optional[dict] = None
) -> str:
def replacement_function(match):
full_match = match.group(0)
start_length = match.group(1)
+2 -2
View File
@@ -1,5 +1,5 @@
import inspect
from typing import get_type_hints, List, Dict, Any
from typing import get_type_hints
def doc_to_dict(docstring):
@@ -16,7 +16,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)
+3 -6
View File
@@ -1,15 +1,12 @@
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from fastapi import HTTPException, status, Depends, Request
from sqlalchemy.orm import Session
from apps.webui.models.users import Users
from pydantic import BaseModel
from typing import Union, Optional
from constants import ERROR_MESSAGES
from passlib.context import CryptContext
from datetime import datetime, timedelta
import requests
import jwt
import uuid
import logging
@@ -54,7 +51,7 @@ def decode_token(token: str) -> Optional[dict]:
try:
decoded = jwt.decode(token, SESSION_SECRET, algorithms=[ALGORITHM])
return decoded
except Exception as e:
except Exception:
return None
@@ -71,7 +68,7 @@ def get_http_authorization_cred(auth_header: str):
try:
scheme, credentials = auth_header.split(" ")
return HTTPAuthorizationCredentials(scheme=scheme, credentials=credentials)
except:
except Exception:
raise ValueError(ERROR_MESSAGES.INVALID_TOKEN)
@@ -96,7 +93,7 @@ def get_current_user(
# auth by jwt token
data = decode_token(token)
if data != None and "id" in data:
if data is not None and "id" in data:
user = Users.get_user_by_id(data["id"])
if user is None:
raise HTTPException(
+9
View File
@@ -0,0 +1,9 @@
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Pellentesque elit eget gravida cum sociis natoque. Morbi tristique senectus et netus et malesuada. Sapien nec sagittis aliquam malesuada bibendum. Amet consectetur adipiscing elit duis tristique sollicitudin. Non pulvinar neque laoreet suspendisse interdum consectetur libero. Arcu cursus vitae congue mauris rhoncus aenean vel elit scelerisque. Nec feugiat nisl pretium fusce id velit. Imperdiet proin fermentum leo vel. Arcu dui vivamus arcu felis bibendum ut tristique et egestas. Pellentesque sit amet porttitor eget dolor morbi non arcu risus. Egestas tellus rutrum tellus pellentesque eu tincidunt tortor aliquam. Et ultrices neque ornare aenean euismod.
Enim nulla aliquet porttitor lacus luctus accumsan tortor posuere ac. Viverra nibh cras pulvinar mattis nunc. Lacinia at quis risus sed vulputate. Ac tortor vitae purus faucibus ornare suspendisse sed nisi lacus. Bibendum arcu vitae elementum curabitur vitae nunc. Consectetur adipiscing elit duis tristique sollicitudin nibh sit amet commodo. Velit egestas dui id ornare arcu odio ut. Et malesuada fames ac turpis egestas integer eget aliquet. Lacus suspendisse faucibus interdum posuere lorem ipsum dolor sit. Morbi tristique senectus et netus. Pretium viverra suspendisse potenti nullam ac tortor vitae. Parturient montes nascetur ridiculus mus mauris vitae. Quis viverra nibh cras pulvinar mattis nunc sed blandit libero. Euismod nisi porta lorem mollis aliquam ut porttitor leo. Mauris in aliquam sem fringilla ut morbi. Faucibus pulvinar elementum integer enim neque. Neque sodales ut etiam sit. Consectetur a erat nam at.
Sed nisi lacus sed viverra tellus in hac habitasse. Proin sagittis nisl rhoncus mattis rhoncus. Risus commodo viverra maecenas accumsan lacus. Morbi quis commodo odio aenean sed adipiscing. Mollis nunc sed id semper risus in. Ultricies mi eget mauris pharetra et ultrices neque. Amet luctus venenatis lectus magna fringilla urna porttitor rhoncus. Eget magna fermentum iaculis eu non diam phasellus. Id diam maecenas ultricies mi eget mauris pharetra et ultrices. Id donec ultrices tincidunt arcu non sodales. Sed cras ornare arcu dui vivamus arcu felis bibendum ut. Urna duis convallis convallis tellus id interdum velit. Rhoncus mattis rhoncus urna neque viverra justo nec. Purus semper eget duis at tellus at urna condimentum. Et odio pellentesque diam volutpat commodo sed egestas. Blandit volutpat maecenas volutpat blandit. In egestas erat imperdiet sed euismod nisi porta lorem mollis. Est ullamcorper eget nulla facilisi etiam dignissim.
Justo nec ultrices dui sapien eget mi proin sed. Purus gravida quis blandit turpis cursus in hac. Placerat orci nulla pellentesque dignissim enim sit. Morbi tristique senectus et netus et malesuada fames ac. Consequat mauris nunc congue nisi. Eu lobortis elementum nibh tellus molestie nunc non blandit. Viverra justo nec ultrices dui. Morbi non arcu risus quis. Elementum sagittis vitae et leo duis. Lectus mauris ultrices eros in cursus. Neque laoreet suspendisse interdum consectetur.
Facilisis gravida neque convallis a cras. Nisl rhoncus mattis rhoncus urna neque viverra justo. Faucibus purus in massa tempor. Lacus laoreet non curabitur gravida arcu ac tortor. Tincidunt eget nullam non nisi est sit amet. Ornare lectus sit amet est placerat in egestas. Sollicitudin tempor id eu nisl nunc mi. Scelerisque viverra mauris in aliquam sem fringilla ut. Ullamcorper sit amet risus nullam. Mauris rhoncus aenean vel elit scelerisque mauris pellentesque pulvinar. Velit euismod in pellentesque massa placerat duis ultricies lacus. Pharetra magna ac placerat vestibulum lectus mauris ultrices eros in. Lorem ipsum dolor sit amet. Sit amet mauris commodo quis imperdiet. Quam pellentesque nec nam aliquam sem et tortor. Amet nisl purus in mollis nunc. Sed risus pretium quam vulputate dignissim suspendisse in est. Nisl condimentum id venenatis a condimentum. Velit euismod in pellentesque massa. Quam id leo in vitae turpis massa sed.
+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
+46
View File
@@ -0,0 +1,46 @@
// eslint-disable-next-line @typescript-eslint/triple-slash-reference
/// <reference path="../support/index.d.ts" />
describe('Documents', () => {
const timestamp = Date.now();
before(() => {
cy.uploadTestDocument(timestamp);
});
after(() => {
cy.deleteTestDocument(timestamp);
});
context('Admin', () => {
beforeEach(() => {
// Login as the admin user
cy.loginAdmin();
// Visit the home page
cy.visit('/workspace/documents');
cy.get('button').contains('#cypress-test').click();
});
it('can see documents', () => {
cy.get('div').contains(`document-test-initial-${timestamp}.txt`).should('have.length', 1);
});
it('can see edit button', () => {
cy.get('div')
.contains(`document-test-initial-${timestamp}.txt`)
.get("button[aria-label='Edit Doc']")
.should('exist');
});
it('can see delete button', () => {
cy.get('div')
.contains(`document-test-initial-${timestamp}.txt`)
.get("button[aria-label='Delete Doc']")
.should('exist');
});
it('can see upload button', () => {
cy.get("button[aria-label='Add Docs']").should('exist');
});
});
});
+49
View File
@@ -1,4 +1,6 @@
/// <reference types="cypress" />
// eslint-disable-next-line @typescript-eslint/triple-slash-reference
/// <reference path="../support/index.d.ts" />
export const adminUser = {
name: 'Admin User',
@@ -10,6 +12,9 @@ const login = (email: string, password: string) => {
return cy.session(
email,
() => {
// Make sure to test against us english to have stable tests,
// regardless on local language preferences
localStorage.setItem('locale', 'en-US');
// Visit auth page
cy.visit('/auth');
// Fill out the form
@@ -68,6 +73,50 @@ Cypress.Commands.add('register', (name, email, password) => register(name, email
Cypress.Commands.add('registerAdmin', () => registerAdmin());
Cypress.Commands.add('loginAdmin', () => loginAdmin());
Cypress.Commands.add('uploadTestDocument', (suffix: any) => {
// Login as admin
cy.loginAdmin();
// upload example document
cy.visit('/workspace/documents');
// Create a document
cy.get("button[aria-label='Add Docs']").click();
cy.readFile('cypress/data/example-doc.txt').then((text) => {
// select file
cy.get('#upload-doc-input').selectFile(
{
contents: Cypress.Buffer.from(text + Date.now()),
fileName: `document-test-initial-${suffix}.txt`,
mimeType: 'text/plain',
lastModified: Date.now()
},
{
force: true
}
);
// open tag input
cy.get("button[aria-label='Add Tag']").click();
cy.get("input[placeholder='Add a tag']").type('cypress-test');
cy.get("button[aria-label='Save Tag']").click();
// submit to upload
cy.get("button[type='submit']").click();
// wait for upload to finish
cy.get('button').contains('#cypress-test').should('exist');
cy.get('div').contains(`document-test-initial-${suffix}.txt`).should('exist');
});
});
Cypress.Commands.add('deleteTestDocument', (suffix: any) => {
cy.loginAdmin();
cy.visit('/workspace/documents');
// clean up uploaded documents
cy.get('div')
.contains(`document-test-initial-${suffix}.txt`)
.find("button[aria-label='Delete Doc']")
.click();
});
before(() => {
cy.registerAdmin();
});
+2
View File
@@ -7,5 +7,7 @@ declare namespace Cypress {
register(name: string, email: string, password: string): Chainable<Element>;
registerAdmin(): Chainable<Element>;
loginAdmin(): Chainable<Element>;
uploadTestDocument(suffix: any): Chainable<Element>;
deleteTestDocument(suffix: any): Chainable<Element>;
}
}
-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
+19 -3
View File
@@ -11,10 +11,26 @@ Our primary goal is to ensure the protection and confidentiality of sensitive da
## Reporting a Vulnerability
If you discover a security issue within our system, please notify us immediately via a pull request or contact us on discord.
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:
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.
3. **Proof of Concept (PoC) is Mandatory**: Each submission must include a well-documented proof of concept (PoC) that demonstrates the vulnerability. If confidentiality is a concern, reporters are encouraged to create a private fork of the repository and share access with the maintainers. Reports lacking valid evidence will be disregarded.
4. **Required Patch Submission**: Along with the PoC, reporters must provide a patch or actionable steps to remediate the identified vulnerability. This helps us evaluate and implement fixes rapidly.
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.
## Product Security
We regularly audit our internal processes and system's architecture for vulnerabilities using a combination of automated and manual testing techniques.
We regularly audit our internal processes and system architecture for vulnerabilities using a combination of automated and manual testing techniques. We are also planning to implement SAST and SCA scans in our project soon.
We are planning on implementing SAST and SCA scans in our project soon.
For immediate concerns or detailed reports that meet our guidelines, please create an issue in our [issue tracker](/open-webui/open-webui/issues) or contact us on [Discord](https://discord.gg/5rJgQTnV4s).
---
_Last updated on **2024-08-06**._
+394 -597
View File
File diff suppressed because it is too large Load Diff
+23 -16
View File
@@ -1,6 +1,6 @@
{
"name": "open-webui",
"version": "0.3.10",
"version": "0.3.13",
"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,7 +53,7 @@
"@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",
@@ -60,6 +61,7 @@
"dayjs": "^1.11.10",
"eventsource-parser": "^1.1.2",
"file-saver": "^2.0.5",
"fuse.js": "^7.0.0",
"highlight.js": "^11.9.0",
"i18next": "^23.10.0",
"i18next-browser-languagedetector": "^7.2.0",
@@ -68,6 +70,7 @@
"js-sha256": "^0.10.1",
"katex": "^0.16.9",
"marked": "^9.1.0",
"marked-katex-extension": "^5.1.1",
"mermaid": "^10.9.1",
"pyodide": "^0.26.1",
"socket.io-client": "^4.2.0",
@@ -76,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"
}
}
+39 -24
View File
@@ -1,6 +1,6 @@
[project]
name = "open-webui"
description = "Open WebUI (Formerly Ollama WebUI)"
description = "Open WebUI"
authors = [
{ name = "Timothy Jaeryang Baek", email = "tim@openwebui.com" }
]
@@ -8,67 +8,82 @@ license = { file = "LICENSE" }
dependencies = [
"fastapi==0.111.0",
"uvicorn[standard]==0.22.0",
"pydantic==2.7.1",
"pydantic==2.8.2",
"python-multipart==0.0.9",
"Flask==3.0.3",
"Flask-Cors==4.0.1",
"python-socketio==5.11.2",
"python-socketio==5.11.3",
"python-jose==3.3.0",
"passlib[bcrypt]==1.7.4",
"requests==2.32.2",
"aiohttp==3.9.5",
"peewee==3.17.5",
"requests==2.32.3",
"aiohttp==3.10.2",
"sqlalchemy==2.0.31",
"alembic==1.13.2",
"peewee==3.17.6",
"peewee-migrate==1.12.2",
"psycopg2-binary==2.9.9",
"PyMySQL==1.1.0",
"PyMySQL==1.1.1",
"bcrypt==4.1.3",
"boto3==1.34.110",
"pymongo",
"redis",
"boto3==1.34.153",
"argon2-cffi==23.1.0",
"APScheduler==3.10.4",
"google-generativeai==0.5.4",
"langchain==0.2.0",
"langchain-community==0.2.0",
"langchain-chroma==0.1.1",
"openai",
"anthropic",
"google-generativeai==0.7.2",
"tiktoken",
"langchain==0.2.12",
"langchain-community==0.2.10",
"langchain-chroma==0.1.2",
"fake-useragent==1.5.1",
"chromadb==0.5.0",
"sentence-transformers==2.7.0",
"pypdf==4.2.0",
"chromadb==0.5.5",
"sentence-transformers==3.0.1",
"pypdf==4.3.1",
"docx2txt==0.8",
"unstructured==0.14.0",
"python-pptx==1.0.0",
"unstructured==0.15.0",
"Markdown==3.6",
"pypandoc==1.13",
"pandas==2.2.2",
"openpyxl==3.1.2",
"openpyxl==3.1.5",
"pyxlsb==1.0.10",
"xlrd==2.0.1",
"validators==0.28.1",
"validators==0.33.0",
"psutil",
"opencv-python-headless==4.9.0.80",
"rapidocr-onnxruntime==1.3.22",
"opencv-python-headless==4.10.0.84",
"rapidocr-onnxruntime==1.3.24",
"fpdf2==2.7.9",
"rank-bm25==0.2.2",
"faster-whisper==1.0.2",
"PyJWT[crypto]==2.8.0",
"PyJWT[crypto]==2.9.0",
"authlib==1.3.1",
"black==24.4.2",
"langfuse==2.33.0",
"black==24.8.0",
"langfuse==2.43.3",
"youtube-transcript-api==0.6.2",
"pytube==15.0.0",
"extract_msg",
"pydub",
"duckduckgo-search~=6.1.5"
"duckduckgo-search~=6.2.1",
"docker~=7.1.0",
"pytest~=8.2.2",
"pytest-docker~=3.1.1"
]
readme = "README.md"
requires-python = ">= 3.11, < 3.12.0a1"
+103 -37
View File
@@ -10,16 +10,25 @@
# universal: false
-e file:.
aiohttp==3.9.5
aiohappyeyeballs==2.3.5
# via aiohttp
aiohttp==3.10.2
# 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
anyio==4.3.0
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
@@ -32,6 +41,7 @@ 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
@@ -49,13 +59,13 @@ beautifulsoup4==4.12.3
# via unstructured
bidict==0.23.1
# via python-socketio
black==24.4.2
black==24.8.0
# via open-webui
blinker==1.8.2
# via flask
boto3==1.34.110
boto3==1.34.153
# via open-webui
botocore==1.34.110
botocore==1.34.155
# via boto3
# via s3transfer
build==1.2.1
@@ -76,9 +86,9 @@ chardet==5.2.0
charset-normalizer==3.3.2
# via requests
# via unstructured-client
chroma-hnswlib==0.7.3
chroma-hnswlib==0.7.6
# via chromadb
chromadb==0.5.0
chromadb==0.5.5
# via langchain-chroma
# via open-webui
click==8.1.7
@@ -102,7 +112,6 @@ cryptography==42.0.7
ctranslate2==4.2.1
# via faster-whisper
dataclasses-json==0.6.6
# via langchain
# via langchain-community
# via unstructured
# via unstructured-client
@@ -113,11 +122,17 @@ defusedxml==0.7.1
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.1.5
duckduckgo-search==6.2.6
# via open-webui
easygui==0.98.3
# via oletools
@@ -166,7 +181,7 @@ frozenlist==1.4.1
fsspec==2024.3.1
# via huggingface-hub
# via torch
google-ai-generativelanguage==0.6.4
google-ai-generativelanguage==0.6.6
# via google-generativeai
google-api-core==2.19.0
# via google-ai-generativelanguage
@@ -183,7 +198,7 @@ google-auth==2.29.0
# via kubernetes
google-auth-httplib2==0.2.0
# via google-api-python-client
google-generativeai==0.5.4
google-generativeai==0.7.2
# via open-webui
googleapis-common-protos==1.63.0
# via google-api-core
@@ -208,8 +223,11 @@ httplib2==0.22.0
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
@@ -229,12 +247,16 @@ 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
@@ -249,14 +271,14 @@ jsonpointer==2.4
# via jsonpatch
kubernetes==29.0.0
# via chromadb
langchain==0.2.0
langchain==0.2.12
# via langchain-community
# via open-webui
langchain-chroma==0.1.1
langchain-chroma==0.1.2
# via open-webui
langchain-community==0.2.0
langchain-community==0.2.10
# via open-webui
langchain-core==0.2.1
langchain-core==0.2.28
# via langchain
# via langchain-chroma
# via langchain-community
@@ -265,22 +287,26 @@ langchain-text-splitters==0.2.0
# via langchain
langdetect==1.0.9
# via unstructured
langfuse==2.33.0
langfuse==2.43.3
# via open-webui
langsmith==0.1.57
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
@@ -339,11 +365,13 @@ 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.9.0.80
opencv-python-headless==4.10.0.84
# via open-webui
openpyxl==3.1.2
openpyxl==3.1.5
# via open-webui
opentelemetry-api==1.24.0
# via chromadb
@@ -380,7 +408,6 @@ ordered-set==4.1.0
# via deepdiff
orjson==3.10.3
# via chromadb
# via duckduckgo-search
# via fastapi
# via langsmith
overrides==7.7.0
@@ -393,6 +420,7 @@ packaging==23.2
# via langfuse
# via marshmallow
# via onnxruntime
# via pytest
# via transformers
# via unstructured-client
pandas==2.2.2
@@ -403,19 +431,24 @@ pathspec==0.12.1
# via black
pcodedmp==1.2.6
# via oletools
peewee==3.17.5
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
@@ -428,6 +461,9 @@ protobuf==4.25.3
# 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
@@ -440,7 +476,8 @@ pyclipper==1.3.0.post5
# via rapidocr-onnxruntime
pycparser==2.22
# via cffi
pydantic==2.7.1
pydantic==2.8.2
# via anthropic
# via chromadb
# via fastapi
# via google-generativeai
@@ -449,30 +486,36 @@ pydantic==2.7.1
# via langfuse
# via langsmith
# via open-webui
pydantic-core==2.18.2
# 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
pyjwt==2.9.0
# via open-webui
pymysql==1.1.0
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.2.0
pypdf==4.3.1
# via open-webui
# via unstructured-client
pypika==0.48.9
# via chromadb
pyproject-hooks==1.1.0
# via build
pyreqwest-impersonate==0.4.7
# via duckduckgo-search
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
@@ -492,7 +535,9 @@ python-magic==0.4.27
python-multipart==0.0.9
# via fastapi
# via open-webui
python-socketio==5.11.2
python-pptx==1.0.0
# via open-webui
python-socketio==5.11.3
# via open-webui
pytube==15.0.0
# via open-webui
@@ -516,15 +561,18 @@ rank-bm25==0.2.2
# via open-webui
rapidfuzz==3.9.0
# via unstructured
rapidocr-onnxruntime==1.3.22
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.2
# via chromadb
requests==2.32.3
# via docker
# via google-api-core
# via huggingface-hub
# via kubernetes
@@ -534,6 +582,7 @@ requests==2.32.2
# via open-webui
# via posthog
# via requests-oauthlib
# via tiktoken
# via transformers
# via unstructured
# via unstructured-client
@@ -556,12 +605,12 @@ scikit-learn==1.4.2
scipy==1.13.0
# via scikit-learn
# via sentence-transformers
sentence-transformers==2.7.0
sentence-transformers==3.0.1
# via open-webui
setuptools==69.5.1
# via ctranslate2
# via opentelemetry-instrumentation
shapely==2.0.4
shapely==2.0.5
# via rapidocr-onnxruntime
shellingham==1.5.4
# via typer
@@ -577,13 +626,17 @@ six==1.16.0
# 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.30
sqlalchemy==2.0.31
# via alembic
# via langchain
# via langchain-community
# via open-webui
starlette==0.37.2
# via fastapi
sympy==1.12
@@ -598,7 +651,10 @@ tenacity==8.3.0
# 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
@@ -609,21 +665,28 @@ tqdm==4.66.4
# 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
@@ -640,7 +703,7 @@ tzlocal==5.2
# via extract-msg
ujson==5.10.0
# via fastapi
unstructured==0.14.0
unstructured==0.15.0
# via open-webui
unstructured-client==0.22.0
# via unstructured
@@ -648,6 +711,7 @@ uritemplate==4.1.1
# via google-api-python-client
urllib3==2.2.1
# via botocore
# via docker
# via kubernetes
# via requests
# via unstructured-client
@@ -657,7 +721,7 @@ uvicorn==0.22.0
# via open-webui
uvloop==0.19.0
# via uvicorn
validators==0.28.1
validators==0.33.0
# via open-webui
watchfiles==0.21.0
# via uvicorn
@@ -676,6 +740,8 @@ 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
+103 -37
View File
@@ -10,16 +10,25 @@
# universal: false
-e file:.
aiohttp==3.9.5
aiohappyeyeballs==2.3.5
# via aiohttp
aiohttp==3.10.2
# 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
anyio==4.3.0
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
@@ -32,6 +41,7 @@ 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
@@ -49,13 +59,13 @@ beautifulsoup4==4.12.3
# via unstructured
bidict==0.23.1
# via python-socketio
black==24.4.2
black==24.8.0
# via open-webui
blinker==1.8.2
# via flask
boto3==1.34.110
boto3==1.34.153
# via open-webui
botocore==1.34.110
botocore==1.34.155
# via boto3
# via s3transfer
build==1.2.1
@@ -76,9 +86,9 @@ chardet==5.2.0
charset-normalizer==3.3.2
# via requests
# via unstructured-client
chroma-hnswlib==0.7.3
chroma-hnswlib==0.7.6
# via chromadb
chromadb==0.5.0
chromadb==0.5.5
# via langchain-chroma
# via open-webui
click==8.1.7
@@ -102,7 +112,6 @@ cryptography==42.0.7
ctranslate2==4.2.1
# via faster-whisper
dataclasses-json==0.6.6
# via langchain
# via langchain-community
# via unstructured
# via unstructured-client
@@ -113,11 +122,17 @@ defusedxml==0.7.1
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.1.5
duckduckgo-search==6.2.6
# via open-webui
easygui==0.98.3
# via oletools
@@ -166,7 +181,7 @@ frozenlist==1.4.1
fsspec==2024.3.1
# via huggingface-hub
# via torch
google-ai-generativelanguage==0.6.4
google-ai-generativelanguage==0.6.6
# via google-generativeai
google-api-core==2.19.0
# via google-ai-generativelanguage
@@ -183,7 +198,7 @@ google-auth==2.29.0
# via kubernetes
google-auth-httplib2==0.2.0
# via google-api-python-client
google-generativeai==0.5.4
google-generativeai==0.7.2
# via open-webui
googleapis-common-protos==1.63.0
# via google-api-core
@@ -208,8 +223,11 @@ httplib2==0.22.0
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
@@ -229,12 +247,16 @@ 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
@@ -249,14 +271,14 @@ jsonpointer==2.4
# via jsonpatch
kubernetes==29.0.0
# via chromadb
langchain==0.2.0
langchain==0.2.12
# via langchain-community
# via open-webui
langchain-chroma==0.1.1
langchain-chroma==0.1.2
# via open-webui
langchain-community==0.2.0
langchain-community==0.2.10
# via open-webui
langchain-core==0.2.1
langchain-core==0.2.28
# via langchain
# via langchain-chroma
# via langchain-community
@@ -265,22 +287,26 @@ langchain-text-splitters==0.2.0
# via langchain
langdetect==1.0.9
# via unstructured
langfuse==2.33.0
langfuse==2.43.3
# via open-webui
langsmith==0.1.57
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
@@ -339,11 +365,13 @@ 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.9.0.80
opencv-python-headless==4.10.0.84
# via open-webui
openpyxl==3.1.2
openpyxl==3.1.5
# via open-webui
opentelemetry-api==1.24.0
# via chromadb
@@ -380,7 +408,6 @@ ordered-set==4.1.0
# via deepdiff
orjson==3.10.3
# via chromadb
# via duckduckgo-search
# via fastapi
# via langsmith
overrides==7.7.0
@@ -393,6 +420,7 @@ packaging==23.2
# via langfuse
# via marshmallow
# via onnxruntime
# via pytest
# via transformers
# via unstructured-client
pandas==2.2.2
@@ -403,19 +431,24 @@ pathspec==0.12.1
# via black
pcodedmp==1.2.6
# via oletools
peewee==3.17.5
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
@@ -428,6 +461,9 @@ protobuf==4.25.3
# 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
@@ -440,7 +476,8 @@ pyclipper==1.3.0.post5
# via rapidocr-onnxruntime
pycparser==2.22
# via cffi
pydantic==2.7.1
pydantic==2.8.2
# via anthropic
# via chromadb
# via fastapi
# via google-generativeai
@@ -449,30 +486,36 @@ pydantic==2.7.1
# via langfuse
# via langsmith
# via open-webui
pydantic-core==2.18.2
# 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
pyjwt==2.9.0
# via open-webui
pymysql==1.1.0
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.2.0
pypdf==4.3.1
# via open-webui
# via unstructured-client
pypika==0.48.9
# via chromadb
pyproject-hooks==1.1.0
# via build
pyreqwest-impersonate==0.4.7
# via duckduckgo-search
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
@@ -492,7 +535,9 @@ python-magic==0.4.27
python-multipart==0.0.9
# via fastapi
# via open-webui
python-socketio==5.11.2
python-pptx==1.0.0
# via open-webui
python-socketio==5.11.3
# via open-webui
pytube==15.0.0
# via open-webui
@@ -516,15 +561,18 @@ rank-bm25==0.2.2
# via open-webui
rapidfuzz==3.9.0
# via unstructured
rapidocr-onnxruntime==1.3.22
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.2
# via chromadb
requests==2.32.3
# via docker
# via google-api-core
# via huggingface-hub
# via kubernetes
@@ -534,6 +582,7 @@ requests==2.32.2
# via open-webui
# via posthog
# via requests-oauthlib
# via tiktoken
# via transformers
# via unstructured
# via unstructured-client
@@ -556,12 +605,12 @@ scikit-learn==1.4.2
scipy==1.13.0
# via scikit-learn
# via sentence-transformers
sentence-transformers==2.7.0
sentence-transformers==3.0.1
# via open-webui
setuptools==69.5.1
# via ctranslate2
# via opentelemetry-instrumentation
shapely==2.0.4
shapely==2.0.5
# via rapidocr-onnxruntime
shellingham==1.5.4
# via typer
@@ -577,13 +626,17 @@ six==1.16.0
# 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.30
sqlalchemy==2.0.31
# via alembic
# via langchain
# via langchain-community
# via open-webui
starlette==0.37.2
# via fastapi
sympy==1.12
@@ -598,7 +651,10 @@ tenacity==8.3.0
# 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
@@ -609,21 +665,28 @@ tqdm==4.66.4
# 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
@@ -640,7 +703,7 @@ tzlocal==5.2
# via extract-msg
ujson==5.10.0
# via fastapi
unstructured==0.14.0
unstructured==0.15.0
# via open-webui
unstructured-client==0.22.0
# via unstructured
@@ -648,6 +711,7 @@ uritemplate==4.1.1
# via google-api-python-client
urllib3==2.2.1
# via botocore
# via docker
# via kubernetes
# via requests
# via unstructured-client
@@ -657,7 +721,7 @@ uvicorn==0.22.0
# via open-webui
uvloop==0.19.0
# via uvicorn
validators==0.28.1
validators==0.33.0
# via open-webui
watchfiles==0.21.0
# via uvicorn
@@ -676,6 +740,8 @@ 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
+13
View File
@@ -154,3 +154,16 @@ input[type='number'] {
.tippy-box[data-theme~='dark'] {
@apply rounded-lg bg-gray-950 text-xs border border-gray-900 shadow-xl;
}
.password {
-webkit-text-security: disc;
}
.codespan {
color: #eb5757;
border-width: 0px;
padding: 3px 8px;
font-size: 0.8em;
font-weight: 600;
@apply rounded-md dark:bg-gray-800 bg-gray-100 mx-0.5;
}
+1 -1
View File
@@ -1,4 +1,4 @@
<!DOCTYPE html>
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
+56
View File
@@ -131,3 +131,59 @@ export const synthesizeOpenAISpeech = async (
return res;
};
export const getModels = async (token: string = '') => {
let error = null;
const res = await fetch(`${AUDIO_API_BASE_URL}/models`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`
}
})
.then(async (res) => {
if (!res.ok) throw await res.json();
return res.json();
})
.catch((err) => {
error = err.detail;
console.log(err);
return null;
});
if (error) {
throw error;
}
return res;
};
export const getVoices = async (token: string = '') => {
let error = null;
const res = await fetch(`${AUDIO_API_BASE_URL}/voices`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`
}
})
.then(async (res) => {
if (!res.ok) throw await res.json();
return res.json();
})
.catch((err) => {
error = err.detail;
console.log(err);
return null;
});
if (error) {
throw error;
}
return res;
};
+7 -2
View File
@@ -32,10 +32,15 @@ export const createNewChat = async (token: string, chat: object) => {
return res;
};
export const getChatList = async (token: string = '') => {
export const getChatList = async (token: string = '', page: number | null = null) => {
let error = null;
const searchParams = new URLSearchParams();
const res = await fetch(`${WEBUI_API_BASE_URL}/chats/`, {
if (page !== null) {
searchParams.append('page', `${page}`);
}
const res = await fetch(`${WEBUI_API_BASE_URL}/chats/?${searchParams.toString()}`, {
method: 'GET',
headers: {
Accept: 'application/json',
+1
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) => {
-50
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;
+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;
};
+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>
+115 -43
View File
@@ -1,13 +1,19 @@
<script lang="ts">
import { getAudioConfig, updateAudioConfig } from '$lib/apis/audio';
import { user, settings, config } from '$lib/stores';
import { createEventDispatcher, onMount, getContext } from 'svelte';
import { toast } from 'svelte-sonner';
import Switch from '$lib/components/common/Switch.svelte';
import { getBackendConfig } from '$lib/apis';
import SensitiveInput from '$lib/components/common/SensitiveInput.svelte';
import { createEventDispatcher, onMount, getContext } from 'svelte';
const dispatch = createEventDispatcher();
import { getBackendConfig } from '$lib/apis';
import {
getAudioConfig,
updateAudioConfig,
getModels as _getModels,
getVoices as _getVoices
} from '$lib/apis/audio';
import { user, settings, config } from '$lib/stores';
import SensitiveInput from '$lib/components/common/SensitiveInput.svelte';
const i18n = getContext('i18n');
export let saveHandler: Function;
@@ -16,6 +22,7 @@
let TTS_OPENAI_API_BASE_URL = '';
let TTS_OPENAI_API_KEY = '';
let TTS_API_KEY = '';
let TTS_ENGINE = '';
let TTS_MODEL = '';
let TTS_VOICE = '';
@@ -29,30 +36,41 @@
let models = [];
let nonLocalVoices = false;
const getOpenAIVoices = () => {
voices = [
{ name: 'alloy' },
{ name: 'echo' },
{ name: 'fable' },
{ name: 'onyx' },
{ name: 'nova' },
{ name: 'shimmer' }
];
};
const getModels = async () => {
if (TTS_ENGINE === '') {
models = [];
} else {
const res = await _getModels(localStorage.token).catch((e) => {
toast.error(e);
});
const getOpenAIModels = () => {
models = [{ name: 'tts-1' }, { name: 'tts-1-hd' }];
};
const getWebAPIVoices = () => {
const getVoicesLoop = setInterval(async () => {
voices = await speechSynthesis.getVoices();
// do your loop
if (voices.length > 0) {
clearInterval(getVoicesLoop);
if (res) {
console.log(res);
models = res.models;
}
}, 100);
}
};
const getVoices = async () => {
if (TTS_ENGINE === '') {
const getVoicesLoop = setInterval(async () => {
voices = await speechSynthesis.getVoices();
// do your loop
if (voices.length > 0) {
clearInterval(getVoicesLoop);
}
}, 100);
} else {
const res = await _getVoices(localStorage.token).catch((e) => {
toast.error(e);
});
if (res) {
console.log(res);
voices = res.voices;
}
}
};
const updateConfigHandler = async () => {
@@ -60,6 +78,7 @@
tts: {
OPENAI_API_BASE_URL: TTS_OPENAI_API_BASE_URL,
OPENAI_API_KEY: TTS_OPENAI_API_KEY,
API_KEY: TTS_API_KEY,
ENGINE: TTS_ENGINE,
MODEL: TTS_MODEL,
VOICE: TTS_VOICE
@@ -86,6 +105,7 @@
console.log(res);
TTS_OPENAI_API_BASE_URL = res.tts.OPENAI_API_BASE_URL;
TTS_OPENAI_API_KEY = res.tts.OPENAI_API_KEY;
TTS_API_KEY = res.tts.API_KEY;
TTS_ENGINE = res.tts.ENGINE;
TTS_MODEL = res.tts.MODEL;
@@ -98,12 +118,8 @@
STT_MODEL = res.stt.MODEL;
}
if (TTS_ENGINE === 'openai') {
getOpenAIVoices();
getOpenAIModels();
} else {
getWebAPIVoices();
}
await getVoices();
await getModels();
});
</script>
@@ -138,7 +154,7 @@
<div>
<div class="mt-1 flex gap-2 mb-1">
<input
class="flex-1 w-full rounded-l-lg py-2 pl-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
class="flex-1 w-full rounded-lg py-2 pl-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
placeholder={$i18n.t('API Base URL')}
bind:value={STT_OPENAI_API_BASE_URL}
required
@@ -182,19 +198,23 @@
class=" dark:bg-gray-900 w-fit pr-8 rounded px-2 p-1 text-xs bg-transparent outline-none text-right"
bind:value={TTS_ENGINE}
placeholder="Select a mode"
on:change={(e) => {
on:change={async (e) => {
await updateConfigHandler();
await getVoices();
await getModels();
if (e.target.value === 'openai') {
getOpenAIVoices();
TTS_VOICE = 'alloy';
TTS_MODEL = 'tts-1';
} else {
getWebAPIVoices();
TTS_VOICE = '';
TTS_MODEL = '';
}
}}
>
<option value="">{$i18n.t('Web API')}</option>
<option value="openai">{$i18n.t('OpenAI')}</option>
<option value="elevenlabs">{$i18n.t('ElevenLabs')}</option>
</select>
</div>
</div>
@@ -203,7 +223,7 @@
<div>
<div class="mt-1 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"
class="flex-1 w-full rounded-lg py-2 pl-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
placeholder={$i18n.t('API Base URL')}
bind:value={TTS_OPENAI_API_BASE_URL}
required
@@ -212,6 +232,17 @@
<SensitiveInput placeholder={$i18n.t('API Key')} bind:value={TTS_OPENAI_API_KEY} />
</div>
</div>
{:else if TTS_ENGINE === 'elevenlabs'}
<div>
<div class="mt-1 flex gap-2 mb-1">
<input
class="flex-1 w-full rounded-lg py-2 pl-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
placeholder={$i18n.t('API Key')}
bind:value={TTS_API_KEY}
required
/>
</div>
</div>
{/if}
<hr class=" dark:border-gray-850 my-2" />
@@ -252,7 +283,7 @@
<datalist id="voice-list">
{#each voices as voice}
<option value={voice.name} />
<option value={voice.id}>{voice.name}</option>
{/each}
</datalist>
</div>
@@ -263,15 +294,56 @@
<div class="flex w-full">
<div class="flex-1">
<input
list="model-list"
list="tts-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={TTS_MODEL}
placeholder="Select a model"
/>
<datalist id="model-list">
<datalist id="tts-model-list">
{#each models as model}
<option value={model.name} />
<option value={model.id} />
{/each}
</datalist>
</div>
</div>
</div>
</div>
{:else if TTS_ENGINE === 'elevenlabs'}
<div class=" flex gap-2">
<div class="w-full">
<div class=" mb-1.5 text-sm font-medium">{$i18n.t('TTS Voice')}</div>
<div class="flex w-full">
<div class="flex-1">
<input
list="voice-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={TTS_VOICE}
placeholder="Select a voice"
/>
<datalist id="voice-list">
{#each voices as voice}
<option value={voice.id}>{voice.name}</option>
{/each}
</datalist>
</div>
</div>
</div>
<div class="w-full">
<div class=" mb-1.5 text-sm font-medium">{$i18n.t('TTS Model')}</div>
<div class="flex w-full">
<div class="flex-1">
<input
list="tts-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={TTS_MODEL}
placeholder="Select a model"
/>
<datalist id="tts-model-list">
{#each models as model}
<option value={model.id} />
{/each}
</datalist>
</div>
@@ -112,7 +112,7 @@
url: OpenAIUrl,
batch_size: OpenAIBatchSize
}
}
}
: {})
}).catch(async (error) => {
toast.error(error);
@@ -269,7 +269,7 @@
saveHandler();
}}
>
<div class=" space-y-2.5 overflow-y-scroll scrollbar-hidden h-full">
<div class=" space-y-2.5 overflow-y-scroll scrollbar-hidden h-full pr-1.5">
<div class="flex flex-col gap-0.5">
<div class=" mb-0.5 text-sm font-medium">{$i18n.t('General Settings')}</div>
@@ -615,7 +615,7 @@
<div class=" ">
<div class=" text-sm font-medium">{$i18n.t('Query Params')}</div>
<div class=" flex">
<div class=" flex gap-1">
<div class=" flex w-full justify-between">
<div class="self-center text-xs font-medium min-w-fit">{$i18n.t('Top K')}</div>
@@ -632,7 +632,7 @@
</div>
{#if querySettings.hybrid === true}
<div class="flex w-full">
<div class=" flex w-full justify-between">
<div class=" self-center text-xs font-medium min-w-fit">
{$i18n.t('Minimum Score')}
</div>
+74 -48
View File
@@ -25,7 +25,8 @@
user,
socket,
showCallOverlay,
tools
tools,
currentChatPage
} from '$lib/stores';
import {
convertMessagesToHistory,
@@ -80,6 +81,7 @@
let eventConfirmationMessage = '';
let eventConfirmationInput = false;
let eventConfirmationInputPlaceholder = '';
let eventConfirmationInputValue = '';
let eventCallback = null;
let showModelSelector = true;
@@ -108,7 +110,6 @@
};
let params = {};
let valves = {};
$: if (history.currentId !== null) {
let _messages = [];
@@ -182,6 +183,7 @@
eventConfirmationTitle = data.title;
eventConfirmationMessage = data.message;
eventConfirmationInputPlaceholder = data.placeholder;
eventConfirmationInputValue = data?.value ?? '';
} else {
console.log('Unknown message type', data);
}
@@ -281,6 +283,10 @@
if ($page.url.searchParams.get('q')) {
prompt = $page.url.searchParams.get('q') ?? '';
selectedToolIds = ($page.url.searchParams.get('tool_ids') ?? '')
.split(',')
.map((id) => id.trim())
.filter((id) => id);
if (prompt) {
await tick();
@@ -416,7 +422,9 @@
params: params,
files: chatFiles
});
await chats.set(await getChatList(localStorage.token));
currentChatPage.set(1);
await chats.set(await getChatList(localStorage.token, $currentChatPage));
}
}
};
@@ -462,7 +470,9 @@
params: params,
files: chatFiles
});
await chats.set(await getChatList(localStorage.token));
currentChatPage.set(1);
await chats.set(await getChatList(localStorage.token, $currentChatPage));
}
}
};
@@ -569,8 +579,8 @@
let selectedModelIds = modelId
? [modelId]
: atSelectedModel !== undefined
? [atSelectedModel.id]
: selectedModels;
? [atSelectedModel.id]
: selectedModels;
// Create response messages for each selected model
const responseMessageIds = {};
@@ -622,7 +632,9 @@
tags: [],
timestamp: Date.now()
});
await chats.set(await getChatList(localStorage.token));
currentChatPage.set(1);
await chats.set(await getChatList(localStorage.token, $currentChatPage));
await chatId.set(chat.id);
} else {
await chatId.set('local');
@@ -698,7 +710,9 @@
})
);
await chats.set(await getChatList(localStorage.token));
currentChatPage.set(1);
await chats.set(await getChatList(localStorage.token, $currentChatPage));
return _responses;
};
@@ -706,6 +720,7 @@
let _response = null;
const responseMessage = history.messages[responseMessageId];
const userMessage = history.messages[responseMessage.parentId];
// Wait until history/message have been updated
await tick();
@@ -724,11 +739,11 @@
? await getAndUpdateUserLocation(localStorage.token)
: undefined
)}${
responseMessage?.userContext ?? null
(responseMessage?.userContext ?? null)
? `\n\nUser Context:\n${responseMessage?.userContext ?? ''}`
: ''
}`
}
}
: undefined,
...messages
]
@@ -772,11 +787,12 @@
if (model?.info?.meta?.knowledge ?? false) {
files.push(...model.info.meta.knowledge);
}
if (responseMessage?.files) {
files.push(
...responseMessage?.files.filter((item) => ['web_search_results'].includes(item.type))
);
}
files.push(
...(userMessage?.files ?? []).filter((item) =>
['doc', 'file', 'collection'].includes(item.type)
),
...(responseMessage?.files ?? []).filter((item) => ['web_search_results'].includes(item.type))
);
eventTarget.dispatchEvent(
new CustomEvent('chat:start', {
@@ -795,10 +811,10 @@
options: {
...(params ?? $settings.params ?? {}),
stop:
params?.stop ?? $settings?.params?.stop ?? undefined
? (params?.stop ?? $settings.params.stop).map((str) =>
decodeURIComponent(JSON.parse('"' + str.replace(/\"/g, '\\"') + '"'))
)
(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:
@@ -808,7 +824,6 @@
keep_alive: $settings.keepAlive ?? undefined,
tool_ids: selectedToolIds.length > 0 ? selectedToolIds : undefined,
files: files.length > 0 ? files : undefined,
...(Object.keys(valves).length ? { valves } : {}),
session_id: $socket?.id,
chat_id: $chatId,
id: responseMessageId
@@ -862,6 +877,10 @@
} else {
responseMessage.content += data.message.content;
if (navigator.vibrate && ($settings?.hapticFeedback ?? false)) {
navigator.vibrate(5);
}
const sentences = extractSentencesForAudio(responseMessage.content);
sentences.pop();
@@ -943,7 +962,9 @@
params: params,
files: chatFiles
});
await chats.set(await getChatList(localStorage.token));
currentChatPage.set(1);
await chats.set(await getChatList(localStorage.token, $currentChatPage));
}
}
} else {
@@ -1006,17 +1027,20 @@
const sendPromptOpenAI = async (model, userPrompt, responseMessageId, _chatId) => {
let _response = null;
const responseMessage = history.messages[responseMessageId];
const userMessage = history.messages[responseMessage.parentId];
let files = JSON.parse(JSON.stringify(chatFiles));
if (model?.info?.meta?.knowledge ?? false) {
files.push(...model.info.meta.knowledge);
}
if (responseMessage?.files) {
files.push(
...responseMessage?.files.filter((item) => ['web_search_results'].includes(item.type))
);
}
files.push(
...(userMessage?.files ?? []).filter((item) =>
['doc', 'file', 'collection'].includes(item.type)
),
...(responseMessage?.files ?? []).filter((item) => ['web_search_results'].includes(item.type))
);
scrollToBottom();
@@ -1036,10 +1060,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)
@@ -1052,11 +1076,11 @@
? await getAndUpdateUserLocation(localStorage.token)
: undefined
)}${
responseMessage?.userContext ?? null
(responseMessage?.userContext ?? null)
? `\n\nUser Context:\n${responseMessage?.userContext ?? ''}`
: ''
}`
}
}
: undefined,
...messages
]
@@ -1072,7 +1096,7 @@
text:
arr.length - 1 !== idx
? message.content
: message?.raContent ?? message.content
: (message?.raContent ?? message.content)
},
...message.files
.filter((file) => file.type === 'image')
@@ -1083,20 +1107,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).map((str) =>
decodeURIComponent(JSON.parse('"' + str.replace(/\"/g, '\\"') + '"'))
)
(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,
@@ -1105,7 +1129,6 @@
max_tokens: params?.max_tokens ?? $settings?.params?.max_tokens ?? undefined,
tool_ids: selectedToolIds.length > 0 ? selectedToolIds : undefined,
files: files.length > 0 ? files : undefined,
...(Object.keys(valves).length ? { valves } : {}),
session_id: $socket?.id,
chat_id: $chatId,
id: responseMessageId
@@ -1120,7 +1143,6 @@
if (res && res.ok && res.body) {
const textStream = await createOpenAITextStream(res.body, $settings.splitLargeChunks);
let lastUsage = null;
for await (const update of textStream) {
const { value, done, citations, error, usage } = update;
@@ -1146,7 +1168,7 @@
}
if (usage) {
lastUsage = usage;
responseMessage.info = { ...usage, openai: true };
}
if (citations) {
@@ -1159,6 +1181,10 @@
} else {
responseMessage.content += value;
if (navigator.vibrate && ($settings?.hapticFeedback ?? false)) {
navigator.vibrate(5);
}
const sentences = extractSentencesForAudio(responseMessage.content);
sentences.pop();
@@ -1200,10 +1226,6 @@
document.getElementById(`speak-button-${responseMessage.id}`)?.click();
}
if (lastUsage) {
responseMessage.info = { ...lastUsage, openai: true };
}
if ($chatId == _chatId) {
if ($settings.saveChatHistory ?? true) {
chat = await updateChatById(localStorage.token, _chatId, {
@@ -1213,7 +1235,9 @@
params: params,
files: chatFiles
});
await chats.set(await getChatList(localStorage.token));
currentChatPage.set(1);
await chats.set(await getChatList(localStorage.token, $currentChatPage));
}
}
} else {
@@ -1378,7 +1402,9 @@
if ($settings.saveChatHistory ?? true) {
chat = await updateChatById(localStorage.token, _chatId, { title: _title });
await chats.set(await getChatList(localStorage.token));
currentChatPage.set(1);
await chats.set(await getChatList(localStorage.token, $currentChatPage));
}
};
@@ -1484,6 +1510,7 @@
message={eventConfirmationMessage}
input={eventConfirmationInput}
inputPlaceholder={eventConfirmationInputPlaceholder}
inputValue={eventConfirmationInputValue}
on:confirm={(e) => {
if (e.detail) {
eventCallback(e.detail);
@@ -1631,7 +1658,6 @@
bind:show={showControls}
bind:chatFiles
bind:params
bind:valves
/>
</div>
{/if}
@@ -9,9 +9,7 @@
export let models = [];
export let chatId = null;
export let chatFiles = [];
export let valves = {};
export let params = {};
let largeScreen = false;
@@ -50,7 +48,6 @@
}}
{models}
bind:chatFiles
bind:valves
bind:params
/>
</div>
@@ -66,7 +63,6 @@
}}
{models}
bind:chatFiles
bind:valves
bind:params
/>
</div>
@@ -5,13 +5,15 @@
import XMark from '$lib/components/icons/XMark.svelte';
import AdvancedParams from '../Settings/Advanced/AdvancedParams.svelte';
import Valves from '$lib/components/common/Valves.svelte';
import Valves from '$lib/components/chat/Controls/Valves.svelte';
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 valves = {};
export let params = {};
</script>
@@ -28,64 +30,59 @@
</button>
</div>
<div class=" dark:text-gray-200 text-sm font-primary">
<div class=" dark:text-gray-200 text-sm font-primary py-0.5">
{#if chatFiles.length > 0}
<div>
<div class="mb-1.5 font-medium">{$i18n.t('Files')}</div>
<div>
{#each chatFiles as file}
<Collapsible title={$i18n.t('Files')} open={true}>
<div class="flex flex-col gap-1 mt-1.5" slot="content">
{#each chatFiles as file, fileIdx}
<FileItem
className="w-full"
url={`${file?.url}`}
name={file.name}
type={file.type}
size={file?.size}
dismissible={true}
on:dismiss={() => {
// Remove the file from the chatFiles array
chatFiles = chatFiles.filter((f) => f.id !== file.id);
chatFiles.splice(fileIdx, 1);
chatFiles = chatFiles;
}}
/>
{/each}
</div>
</div>
</Collapsible>
<hr class="my-2 border-gray-100 dark:border-gray-800" />
{/if}
{#if models.length === 1 && models[0]?.pipe?.valves_spec}
<div>
<div class=" font-medium">{$i18n.t('Valves')}</div>
<div>
<Valves valvesSpec={models[0]?.pipe?.valves_spec} bind:valves />
</div>
<Collapsible title={$i18n.t('Valves')}>
<div class="text-sm mt-1.5" slot="content">
<Valves />
</div>
<hr class="my-2 border-gray-100 dark:border-gray-800" />
{/if}
<div>
<div class="mb-1.5 font-medium">{$i18n.t('System Prompt')}</div>
<div>
<textarea
bind:value={params.system}
class="w-full rounded-lg px-4 py-3 text-sm dark:text-gray-300 dark:bg-gray-850 border border-gray-100 dark:border-gray-800 outline-none resize-none"
rows="3"
placeholder={$i18n.t('Enter system prompt')}
/>
</div>
</div>
</Collapsible>
<hr class="my-2 border-gray-100 dark:border-gray-800" />
<div>
<div class="mb-1.5 font-medium">{$i18n.t('Advanced Params')}</div>
<div>
<AdvancedParams bind:params />
<Collapsible title={$i18n.t('System Prompt')} open={true}>
<div class=" mt-1.5" slot="content">
<textarea
bind:value={params.system}
class="w-full rounded-lg px-3.5 py-2.5 text-sm dark:text-gray-300 dark:bg-gray-850 border border-gray-100 dark:border-gray-800 outline-none resize-none"
rows="4"
placeholder={$i18n.t('Enter system prompt')}
/>
</div>
</div>
</Collapsible>
<hr class="my-2 border-gray-100 dark:border-gray-800" />
<Collapsible title={$i18n.t('Advanced Params')} open={true}>
<div class="text-sm mt-1.5" slot="content">
<div>
<AdvancedParams admin={$user?.role === 'admin'} bind:params />
</div>
</div>
</Collapsible>
</div>
</div>
@@ -15,18 +15,14 @@
updateUserValvesById as updateFunctionUserValvesById
} from '$lib/apis/functions';
import ManageModal from './Personalization/ManageModal.svelte';
import Tooltip from '$lib/components/common/Tooltip.svelte';
import Spinner from '$lib/components/common/Spinner.svelte';
import Switch from '$lib/components/common/Switch.svelte';
import Valves from '$lib/components/common/Valves.svelte';
const dispatch = createEventDispatcher();
const i18n = getContext('i18n');
export let saveSettings: Function;
let tab = 'tools';
let selectedId = '';
@@ -35,6 +31,19 @@
let valvesSpec = null;
let valves = {};
let debounceTimer;
const debounceSubmitHandler = async () => {
if (debounceTimer) {
clearTimeout(debounceTimer);
}
// Set a new timer
debounceTimer = setTimeout(() => {
submitHandler();
}, 500); // 0.5 second debounce
};
const getUserValves = async () => {
loading = true;
if (tab === 'tools') {
@@ -112,53 +121,45 @@
dispatch('save');
}}
>
<div class="flex flex-col pr-1.5 overflow-y-scroll max-h-[25rem]">
<div>
<div class="flex items-center justify-between mb-2">
<Tooltip content="">
<div class="text-sm font-medium">
{$i18n.t('Manage Valves')}
</div>
</Tooltip>
<div class=" self-end">
<select
class=" dark:bg-gray-900 w-fit pr-8 rounded text-xs bg-transparent outline-none text-right"
bind:value={tab}
placeholder="Select"
>
<option value="tools">{$i18n.t('Tools')}</option>
<option value="functions">{$i18n.t('Functions')}</option>
</select>
</div>
</div>
</div>
<div class="flex flex-col">
<div class="space-y-1">
<div class="flex gap-2">
<div class="flex-1">
<select
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
class=" w-full rounded text-xs py-2 px-1 bg-transparent outline-none"
bind:value={tab}
placeholder="Select"
>
<option value="tools" class="bg-gray-100 dark:bg-gray-800">{$i18n.t('Tools')}</option>
<option value="functions" class="bg-gray-100 dark:bg-gray-800"
>{$i18n.t('Functions')}</option
>
</select>
</div>
<div class="flex-1">
<select
class="w-full rounded py-2 px-1 text-xs bg-transparent outline-none"
bind:value={selectedId}
on:change={async () => {
await tick();
}}
>
{#if tab === 'tools'}
<option value="" selected disabled class="bg-gray-100 dark:bg-gray-700"
<option value="" selected disabled class="bg-gray-100 dark:bg-gray-800"
>{$i18n.t('Select a tool')}</option
>
{#each $tools as tool, toolIdx}
<option value={tool.id} class="bg-gray-100 dark:bg-gray-700">{tool.name}</option>
<option value={tool.id} class="bg-gray-100 dark:bg-gray-800">{tool.name}</option>
{/each}
{:else if tab === 'functions'}
<option value="" selected disabled class="bg-gray-100 dark:bg-gray-700"
<option value="" selected disabled class="bg-gray-100 dark:bg-gray-800"
>{$i18n.t('Select a function')}</option
>
{#each $functions as func, funcIdx}
<option value={func.id} class="bg-gray-100 dark:bg-700">{func.name}</option>
<option value={func.id} class="bg-gray-100 dark:bg-gray-800">{func.name}</option>
{/each}
{/if}
</select>
@@ -167,24 +168,21 @@
</div>
{#if selectedId}
<hr class="dark:border-gray-800 my-3 w-full" />
<hr class="dark:border-gray-800 my-1 w-full" />
<div>
<div class="my-2 text-xs">
{#if !loading}
<Valves {valvesSpec} bind:valves />
<Valves
{valvesSpec}
bind:valves
on:change={() => {
debounceSubmitHandler();
}}
/>
{:else}
<Spinner className="size-5" />
{/if}
</div>
{/if}
</div>
<div class="flex justify-end text-sm font-medium">
<button
class=" px-4 py-2 bg-emerald-700 hover:bg-emerald-800 text-gray-100 transition rounded-lg"
type="submit"
>
{$i18n.t('Save')}
</button>
</div>
</form>
+42 -32
View File
@@ -98,6 +98,7 @@
const uploadFileHandler = async (file) => {
console.log(file);
// Check if the file is an audio file and transcribe/convert it to text file
if (['audio/mpeg', 'audio/wav'].includes(file['type'])) {
const res = await transcribeAudio(localStorage.token, file).catch((error) => {
@@ -112,40 +113,49 @@
}
}
// Upload the file to the server
const uploadedFile = await uploadFile(localStorage.token, file).catch((error) => {
toast.error(error);
return null;
});
const fileItem = {
type: 'file',
file: '',
id: null,
url: '',
name: file.name,
collection_name: '',
status: '',
size: file.size,
error: ''
};
files = [...files, fileItem];
if (uploadedFile) {
const fileItem = {
type: 'file',
file: uploadedFile,
id: uploadedFile.id,
url: `${WEBUI_API_BASE_URL}/files/${uploadedFile.id}`,
name: file.name,
collection_name: '',
status: 'uploaded',
error: ''
};
files = [...files, fileItem];
try {
const uploadedFile = await uploadFile(localStorage.token, file);
// TODO: Check if tools & functions have files support to skip this step to delegate file processing
// Default Upload to VectorDB
if (
SUPPORTED_FILE_TYPE.includes(file['type']) ||
SUPPORTED_FILE_EXTENSIONS.includes(file.name.split('.').at(-1))
) {
processFileItem(fileItem);
if (uploadedFile) {
fileItem.status = 'uploaded';
fileItem.file = uploadedFile;
fileItem.id = uploadedFile.id;
fileItem.url = `${WEBUI_API_BASE_URL}/files/${uploadedFile.id}`;
// TODO: Check if tools & functions have files support to skip this step to delegate file processing
// Default Upload to VectorDB
if (
SUPPORTED_FILE_TYPE.includes(file['type']) ||
SUPPORTED_FILE_EXTENSIONS.includes(file.name.split('.').at(-1))
) {
processFileItem(fileItem);
} else {
toast.error(
$i18n.t(`Unknown file type '{{file_type}}'. Proceeding with the file upload anyway.`, {
file_type: file['type']
})
);
processFileItem(fileItem);
}
} else {
toast.error(
$i18n.t(`Unknown file type '{{file_type}}'. Proceeding with the file upload anyway.`, {
file_type: file['type']
})
);
processFileItem(fileItem);
files = files.filter((item) => item.status !== null);
}
} catch (e) {
toast.error(e);
files = files.filter((item) => item.status !== null);
}
};
@@ -162,7 +172,6 @@
// Remove the failed doc from the files array
// files = files.filter((f) => f.id !== fileItem.id);
toast.error(e);
fileItem.status = 'processed';
files = files;
}
@@ -375,7 +384,7 @@
{#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"
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"
>
<div class="flex items-center gap-2 text-sm dark:text-gray-500">
<img
@@ -545,6 +554,7 @@
<FileItem
name={file.name}
type={file.type}
size={file?.size}
status={file.status}
dismissible={true}
on:dismiss={() => {
@@ -609,10 +609,10 @@
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;"
? '4.25'
: rmsLevel * 100 > 1
? '3.75'
: '3.5'}rem;width: 100%; text-align:center;"
>
{emoji}
</div>
@@ -658,10 +658,10 @@
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
? ' 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"
@@ -691,10 +691,10 @@
style="font-size:{rmsLevel * 100 > 4
? '13'
: rmsLevel * 100 > 2
? '12'
: rmsLevel * 100 > 1
? '11.5'
: '11'}rem;width:100%;text-align:center;"
? '12'
: rmsLevel * 100 > 1
? '11.5'
: '11'}rem;width:100%;text-align:center;"
>
{emoji}
</div>
@@ -740,10 +740,10 @@
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
? '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'} "
@@ -27,7 +27,7 @@
title: $i18n.t('All Documents'),
collection_names: $documents.map((doc) => doc.collection_name)
}
]
]
: []),
...$documents
.reduce((a, e, i, arr) => {
@@ -147,8 +147,8 @@
<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'
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={() => {
@@ -159,13 +159,18 @@
}}
on:focus={() => {}}
>
<div class=" font-medium text-black dark:text-gray-100 line-clamp-1">
<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> -->
</div> -->
</button>
{/each}
</div>
+21 -16
View File
@@ -1,6 +1,6 @@
<script lang="ts">
import { v4 as uuidv4 } from 'uuid';
import { chats, config, settings, user as _user, mobile } from '$lib/stores';
import { chats, config, settings, user as _user, mobile, currentChatPage } from '$lib/stores';
import { tick, getContext, onMount } from 'svelte';
import { toast } from 'svelte-sonner';
@@ -90,7 +90,8 @@
history: history
});
await chats.set(await getChatList(localStorage.token));
currentChatPage.set(1);
await chats.set(await getChatList(localStorage.token, $currentChatPage));
};
const confirmEditResponseMessage = async (messageId, content) => {
@@ -146,12 +147,14 @@
await tick();
const element = document.getElementById('messages-container');
autoScroll = element.scrollHeight - element.scrollTop <= element.clientHeight + 50;
if ($settings?.scrollOnBranchChange ?? true) {
const element = document.getElementById('messages-container');
autoScroll = element.scrollHeight - element.scrollTop <= element.clientHeight + 50;
setTimeout(() => {
scrollToBottom();
}, 100);
setTimeout(() => {
scrollToBottom();
}, 100);
}
};
const showNextMessage = async (message) => {
@@ -195,12 +198,14 @@
await tick();
const element = document.getElementById('messages-container');
autoScroll = element.scrollHeight - element.scrollTop <= element.clientHeight + 50;
if ($settings?.scrollOnBranchChange ?? true) {
const element = document.getElementById('messages-container');
autoScroll = element.scrollHeight - element.scrollTop <= element.clientHeight + 50;
setTimeout(() => {
scrollToBottom();
}, 100);
setTimeout(() => {
scrollToBottom();
}, 100);
}
};
const deleteMessageHandler = async (messageId) => {
@@ -300,7 +305,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"
>
@@ -312,10 +317,10 @@
{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}
@@ -60,8 +60,8 @@
href={document?.metadata?.file_id
? `/api/v1/files/${document?.metadata?.file_id}/content`
: document.source.name.includes('http')
? document.source.name
: `#`}
? document.source.name
: `#`}
target="_blank"
>
{document?.metadata?.name ?? document.source.name}
@@ -1,17 +1,25 @@
<script lang="ts">
import Spinner from '$lib/components/common/Spinner.svelte';
import { copyToClipboard } from '$lib/utils';
import hljs from 'highlight.js';
import 'highlight.js/styles/github-dark.min.css';
import { loadPyodide } from 'pyodide';
import { onMount, tick } from 'svelte';
import mermaid from 'mermaid';
import { getContext, getAllContexts, onMount } from 'svelte';
import { copyToClipboard } from '$lib/utils';
import 'highlight.js/styles/github-dark.min.css';
import PyodideWorker from '$lib/workers/pyodide.worker?worker';
const i18n = getContext('i18n');
export let id = '';
export let token;
export let lang = '';
export let code = '';
let mermaidHtml = null;
let highlightedCode = null;
let executing = false;
@@ -204,70 +212,102 @@ __builtins__.input = input`);
};
let debounceTimeout;
$: if (code) {
// Function to perform the code highlighting
const highlightCode = () => {
highlightedCode = hljs.highlightAuto(code, hljs.getLanguage(lang)?.aliases).value || code;
};
if (lang === 'mermaid' && (token?.raw ?? '').endsWith('```')) {
(async () => {
try {
const { svg } = await mermaid.render(`mermaid-${id}`, code);
mermaidHtml = svg;
} catch (error) {
console.error('Error:', error);
}
})();
} else {
// Function to perform the code highlighting
const highlightCode = () => {
highlightedCode = hljs.highlightAuto(code, hljs.getLanguage(lang)?.aliases).value || code;
};
// Clear the previous timeout if it exists
clearTimeout(debounceTimeout);
// Set a new timeout to debounce the code highlighting
debounceTimeout = setTimeout(highlightCode, 10);
// Clear the previous timeout if it exists
clearTimeout(debounceTimeout);
// Set a new timeout to debounce the code highlighting
debounceTimeout = setTimeout(highlightCode, 10);
}
}
onMount(async () => {
await mermaid.initialize({ startOnLoad: true });
if (lang === 'mermaid' && (token?.raw ?? '').endsWith('```')) {
try {
const { svg } = await mermaid.render(`mermaid-${id}`, code);
mermaidHtml = svg;
} catch (error) {
console.error('Error:', error);
}
}
});
</script>
<div class="mb-4" dir="ltr">
<div
class="flex justify-between bg-[#202123] text-white text-xs px-4 pt-1 pb-0.5 rounded-t-lg overflow-x-auto"
>
<div class="p-1">{@html lang}</div>
<div class="my-2" dir="ltr">
{#if lang === 'mermaid'}
{#if mermaidHtml}
{@html mermaidHtml}
{:else}
<pre class=" mermaid-{id}">{code}</pre>
{/if}
{:else}
<div
class="flex justify-between bg-[#202123] text-white text-xs px-4 pt-1 pb-0.5 rounded-t-lg overflow-x-auto"
>
<div class="p-1">{@html lang}</div>
<div class="flex items-center">
{#if lang.toLowerCase() === 'python' || lang.toLowerCase() === 'py' || (lang === '' && checkPythonCode(code))}
{#if executing}
<div class="copy-code-button bg-none border-none p-1 cursor-not-allowed">Running</div>
{:else}
<button
class="copy-code-button bg-none border-none p-1"
on:click={() => {
executePython(code);
}}>Run</button
>
<div class="flex items-center">
{#if lang.toLowerCase() === 'python' || lang.toLowerCase() === 'py' || (lang === '' && checkPythonCode(code))}
{#if executing}
<div class="copy-code-button bg-none border-none p-1 cursor-not-allowed">Running</div>
{:else}
<button
class="copy-code-button bg-none border-none p-1"
on:click={() => {
executePython(code);
}}>{$i18n.t('Run')}</button
>
{/if}
{/if}
{/if}
<button class="copy-code-button bg-none border-none p-1" on:click={copyCode}
>{copied ? 'Copied' : 'Copy Code'}</button
>
<button class="copy-code-button bg-none border-none p-1" on:click={copyCode}
>{copied ? $i18n.t('Copied') : $i18n.t('Copy Code')}</button
>
</div>
</div>
</div>
<pre
class=" hljs p-4 px-5 overflow-x-auto"
style="border-top-left-radius: 0px; border-top-right-radius: 0px; {(executing ||
stdout ||
stderr ||
result) &&
'border-bottom-left-radius: 0px; border-bottom-right-radius: 0px;'}"><code
class="language-{lang} rounded-t-none whitespace-pre"
>{#if highlightedCode}{@html highlightedCode}{:else}{code}{/if}</code
></pre>
<pre
class=" hljs p-4 px-5 overflow-x-auto"
style="border-top-left-radius: 0px; border-top-right-radius: 0px; {(executing ||
stdout ||
stderr ||
result) &&
'border-bottom-left-radius: 0px; border-bottom-right-radius: 0px;'}"><code
class="language-{lang} rounded-t-none whitespace-pre"
>{#if highlightedCode}{@html highlightedCode}{:else}{code}{/if}</code
></pre>
<div
id="plt-canvas-{id}"
class="bg-[#202123] text-white max-w-full overflow-x-auto scrollbar-hidden"
/>
<div
id="plt-canvas-{id}"
class="bg-[#202123] text-white max-w-full overflow-x-auto scrollbar-hidden"
/>
{#if executing}
<div class="bg-[#202123] text-white px-4 py-4 rounded-b-lg">
<div class=" text-gray-500 text-xs mb-1">STDOUT/STDERR</div>
<div class="text-sm">Running...</div>
</div>
{:else if stdout || stderr || result}
<div class="bg-[#202123] text-white px-4 py-4 rounded-b-lg">
<div class=" text-gray-500 text-xs mb-1">STDOUT/STDERR</div>
<div class="text-sm">{stdout || stderr || result}</div>
</div>
{#if executing}
<div class="bg-[#202123] text-white px-4 py-4 rounded-b-lg">
<div class=" text-gray-500 text-xs mb-1">STDOUT/STDERR</div>
<div class="text-sm">Running...</div>
</div>
{:else if stdout || stderr || result}
<div class="bg-[#202123] text-white px-4 py-4 rounded-b-lg">
<div class=" text-gray-500 text-xs mb-1">STDOUT/STDERR</div>
<div class="text-sm">{stdout || stderr || result}</div>
</div>
{/if}
{/if}
</div>
@@ -118,47 +118,47 @@
currentMessageId = message.id;
let messageId = message.id;
console.log(messageId);
//
let messageChildrenIds = history.messages[messageId].childrenIds;
while (messageChildrenIds.length !== 0) {
messageId = messageChildrenIds.at(-1);
messageChildrenIds = history.messages[messageId].childrenIds;
}
history.currentId = messageId;
dispatch('change');
}
}}
>
<ResponseMessage
message={groupedMessages[model].messages[groupedMessagesIdx[model]]}
siblings={groupedMessages[model].messages.map((m) => m.id)}
isLastMessage={true}
{updateChatMessages}
{confirmEditResponseMessage}
showPreviousMessage={() => showPreviousMessage(model)}
showNextMessage={() => showNextMessage(model)}
{readOnly}
{rateMessage}
{copyToClipboard}
{continueGeneration}
regenerateResponse={async (message) => {
regenerateResponse(message);
await tick();
groupedMessagesIdx[model] = groupedMessages[model].messages.length - 1;
}}
on:save={async (e) => {
console.log('save', e);
{#key history.currentId}
<ResponseMessage
message={groupedMessages[model].messages[groupedMessagesIdx[model]]}
siblings={groupedMessages[model].messages.map((m) => m.id)}
isLastMessage={true}
{updateChatMessages}
{confirmEditResponseMessage}
showPreviousMessage={() => showPreviousMessage(model)}
showNextMessage={() => showNextMessage(model)}
{readOnly}
{rateMessage}
{copyToClipboard}
{continueGeneration}
regenerateResponse={async (message) => {
regenerateResponse(message);
await tick();
groupedMessagesIdx[model] = groupedMessages[model].messages.length - 1;
}}
on:save={async (e) => {
console.log('save', e);
const message = e.detail;
history.messages[message.id] = message;
await updateChatById(localStorage.token, chatId, {
messages: messages,
history: history
});
}}
/>
const message = e.detail;
history.messages[message.id] = message;
await updateChatById(localStorage.token, chatId, {
messages: messages,
history: history
});
}}
/>
{/key}
</div>
{/if}
{/each}
@@ -0,0 +1,30 @@
<script lang="ts">
import Image from '$lib/components/common/Image.svelte';
import CodeBlock from './CodeBlock.svelte';
/* The html content of the tag */
export let html; //: string;
let parsedHTML = [html];
export let images;
export let codes;
// all images are in {{IMAGE_0}}, {{IMAGE_1}}.... format
// all codes are in {{CODE_0}}, {{CODE_1}}.... format
const rules = [];
rules.forEach((rule) => {
parsedHTML = parsedHTML.map((substr) => substr.split(rule.regex)).flat();
});
</script>
{#each parsedHTML as part}
{@const match = rules.find((rule) => rule.regex.test(part))}
{#if match}
<svelte:component this={match.component} {...match.props}>
{@html part}
</svelte:component>
{:else}
{@html part}
{/if}
{/each}
@@ -0,0 +1,9 @@
<script lang="ts">
import katex from 'katex';
import 'katex/contrib/mhchem';
export let content: string;
export let displayMode: boolean = false;
</script>
{@html katex.renderToString(content, { displayMode, throwOnError: false })}
@@ -0,0 +1,45 @@
<script lang="ts">
import type { Token } from 'marked';
import { revertSanitizedResponseContent, unescapeHtml } from '$lib/utils';
import { onMount } from 'svelte';
import Image from '$lib/components/common/Image.svelte';
import KatexRenderer from './KatexRenderer.svelte';
export let id: string;
export let tokens: Token[];
</script>
{#each tokens as token}
{#if token.type === 'escape'}
{unescapeHtml(token.text)}
{:else if token.type === 'html'}
{@html token.text}
{:else if token.type === 'link'}
<a href={token.href} target="_blank" rel="nofollow" title={token.title}>{token.text}</a>
{:else if token.type === 'image'}
<Image src={token.href} alt={token.text} />
{:else if token.type === 'strong'}
<strong>
<svelte:self id={`${id}-strong`} tokens={token.tokens} />
</strong>
{:else if token.type === 'em'}
<em>
<svelte:self id={`${id}-em`} tokens={token.tokens} />
</em>
{:else if token.type === 'codespan'}
<code class="codespan">{revertSanitizedResponseContent(token.raw)}</code>
{:else if token.type === 'br'}
<br />
{:else if token.type === 'del'}
<del>
<svelte:self id={`${id}-del`} tokens={token.tokens} />
</del>
{:else if token.type === 'inlineKatex'}
{#if token.text}
<KatexRenderer content={revertSanitizedResponseContent(token.text)} displayMode={false} />
{/if}
{:else if token.type === 'text'}
{token.raw}
{/if}
{/each}
@@ -0,0 +1,132 @@
<script lang="ts">
import { onMount } from 'svelte';
import type { Token } from 'marked';
import { revertSanitizedResponseContent, unescapeHtml } from '$lib/utils';
import CodeBlock from '$lib/components/chat/Messages/CodeBlock.svelte';
import MarkdownInlineTokens from '$lib/components/chat/Messages/MarkdownInlineTokens.svelte';
import KatexRenderer from './KatexRenderer.svelte';
export let id: string;
export let tokens: Token[];
export let top = true;
const headerComponent = (depth: number) => {
return 'h' + depth;
};
</script>
<!-- {JSON.stringify(tokens)} -->
{#each tokens as token, tokenIdx}
{#if token.type === 'hr'}
<hr />
{:else if token.type === 'heading'}
<svelte:element this={headerComponent(token.depth)}>
<MarkdownInlineTokens id={`${id}-${tokenIdx}-h`} tokens={token.tokens} />
</svelte:element>
{:else if token.type === 'code'}
<CodeBlock
id={`${id}-${tokenIdx}`}
{token}
lang={token?.lang ?? ''}
code={revertSanitizedResponseContent(token?.text ?? '')}
/>
{:else if token.type === 'table'}
<table>
<thead>
<tr>
{#each token.header as header, headerIdx}
<th style={token.align[headerIdx] ? '' : `text-align: ${token.align[headerIdx]}`}>
<MarkdownInlineTokens
id={`${id}-${tokenIdx}-header-${headerIdx}`}
tokens={header.tokens}
/>
</th>
{/each}
</tr>
</thead>
<tbody>
{#each token.rows as row, rowIdx}
<tr>
{#each row ?? [] as cell, cellIdx}
<td style={token.align[cellIdx] ? '' : `text-align: ${token.align[cellIdx]}`}>
<MarkdownInlineTokens
id={`${id}-${tokenIdx}-row-${rowIdx}-${cellIdx}`}
tokens={cell.tokens}
/>
</td>
{/each}
</tr>
{/each}
</tbody>
</table>
{:else if token.type === 'blockquote'}
<blockquote>
<svelte:self id={`${id}-${tokenIdx}`} tokens={token.tokens} />
</blockquote>
{:else if token.type === 'list'}
{#if token.ordered}
<ol start={token.start || 1}>
{#each token.items as item, itemIdx}
<li>
<svelte:self
id={`${id}-${tokenIdx}-${itemIdx}`}
tokens={item.tokens}
top={token.loose}
/>
</li>
{/each}
</ol>
{:else}
<ul>
{#each token.items as item, itemIdx}
<li>
<svelte:self
id={`${id}-${tokenIdx}-${itemIdx}`}
tokens={item.tokens}
top={token.loose}
/>
</li>
{/each}
</ul>
{/if}
{:else if token.type === 'html'}
{@html token.text}
{:else if token.type === 'paragraph'}
<p>
<MarkdownInlineTokens id={`${id}-${tokenIdx}-p`} tokens={token.tokens ?? []} />
</p>
{:else if token.type === 'text'}
{#if top}
<p>
{#if token.tokens}
<MarkdownInlineTokens id={`${id}-${tokenIdx}-t`} tokens={token.tokens} />
{:else}
{unescapeHtml(token.text)}
{/if}
</p>
{:else if token.tokens}
<MarkdownInlineTokens id={`${id}-${tokenIdx}-p`} tokens={token.tokens ?? []} />
{:else}
{unescapeHtml(token.text)}
{/if}
{:else if token.type === 'inlineKatex'}
{#if token.text}
<KatexRenderer
content={revertSanitizedResponseContent(token.text)}
displayMode={token?.displayMode ?? false}
/>
{/if}
{:else if token.type === 'blockKatex'}
{#if token.text}
<KatexRenderer
content={revertSanitizedResponseContent(token.text)}
displayMode={token?.displayMode ?? false}
/>
{/if}
{:else if token.type === 'space'}
{''}
{:else}
{console.log('Unknown token', token)}
{/if}
{/each}
File diff suppressed because it is too large Load Diff
@@ -20,7 +20,10 @@
<ChevronDown strokeWidth="3.5" className="size-3.5 " />
{/if}
</div>
<div class="text-sm border border-gray-300/30 dark:border-gray-700/50 rounded-xl" slot="content">
<div
class="text-sm border border-gray-300/30 dark:border-gray-700/50 rounded-xl mb-1.5"
slot="content"
>
{#if status?.query}
<a
href="https://www.google.com/search?q={status.query}"
@@ -62,8 +62,8 @@
{#if !($settings?.chatBubble ?? true)}
<ProfileImage
src={message.user
? $models.find((m) => m.id === message.user)?.info?.meta?.profile_image_url ?? '/user.png'
: user?.profile_image_url ?? '/user.png'}
? ($models.find((m) => m.id === message.user)?.info?.meta?.profile_image_url ?? '/user.png')
: (user?.profile_image_url ?? '/user.png')}
/>
{/if}
<div class="w-full overflow-hidden pl-1">
@@ -96,11 +96,17 @@
{#if message.files}
<div class="mt-2.5 mb-1 w-full flex flex-col justify-end overflow-x-auto gap-1 flex-wrap">
{#each message.files as file}
<div class={$settings?.chatBubble ?? true ? 'self-end' : ''}>
<div class={($settings?.chatBubble ?? true) ? 'self-end' : ''}>
{#if file.type === 'image'}
<img src={file.url} alt="input" class=" max-h-96 rounded-lg" draggable="false" />
{:else}
<FileItem url={file.url} name={file.name} type={file.type} />
<FileItem
url={file.url}
name={file.name}
type={file.type}
size={file?.size}
colorClassName="bg-white dark:bg-gray-850 "
/>
{/if}
</div>
{/each}
@@ -156,12 +162,12 @@
</div>
{:else}
<div class="w-full">
<div class="flex {$settings?.chatBubble ?? true ? 'justify-end' : ''} mb-2">
<div class="flex {($settings?.chatBubble ?? true) ? 'justify-end' : ''} mb-2">
<div
class="rounded-3xl {$settings?.chatBubble ?? true
class="rounded-3xl {($settings?.chatBubble ?? true)
? `max-w-[90%] px-5 py-2 bg-gray-50 dark:bg-gray-850 ${
message.files ? 'rounded-tr-lg' : ''
}`
}`
: ''} "
>
<pre id="user-message">{message.content}</pre>
@@ -169,7 +175,7 @@
</div>
<div
class=" flex {$settings?.chatBubble ?? true
class=" flex {($settings?.chatBubble ?? true)
? 'justify-end'
: ''} text-gray-600 dark:text-gray-500"
>
@@ -1,6 +1,7 @@
<script lang="ts">
import { DropdownMenu } from 'bits-ui';
import { marked } from 'marked';
import Fuse from 'fuse.js';
import { flyAndScale } from '$lib/utils/transitions';
import { createEventDispatcher, onMount, getContext, tick } from 'svelte';
@@ -33,7 +34,7 @@
[key: string]: any;
} = [];
export let className = 'w-[30rem]';
export let className = 'w-[32rem]';
let show = false;
@@ -43,17 +44,31 @@
let searchValue = '';
let ollamaVersion = null;
$: filteredItems = items.filter(
(item) =>
(searchValue
? item.value.toLowerCase().includes(searchValue.toLowerCase()) ||
item.label.toLowerCase().includes(searchValue.toLowerCase()) ||
(item.model?.info?.meta?.tags ?? []).some((tag) =>
tag.name.toLowerCase().includes(searchValue.toLowerCase())
)
: true) && !(item.model?.info?.meta?.hidden ?? false)
let selectedModelIdx = 0;
const fuse = new Fuse(
items
.filter((item) => !item.model?.info?.meta?.hidden)
.map((item) => {
const _item = {
...item,
modelName: item.model?.name,
tags: item.model?.info?.meta?.tags?.map((tag) => tag.name).join(' '),
desc: item.model?.info?.meta?.description
};
return _item;
}),
{
keys: ['value', 'label', 'tags', 'desc', 'modelName']
}
);
$: filteredItems = searchValue
? fuse.search(searchValue).map((e) => {
return e.item;
})
: items.filter((item) => !item.model?.info?.meta?.hidden);
const pullModelHandler = async () => {
const sanitizedModelTag = searchValue.trim().replace(/^ollama\s+(run|pull)\s+/, '');
@@ -202,6 +217,7 @@
bind:open={show}
onOpenChange={async () => {
searchValue = '';
selectedModelIdx = 0;
window.setTimeout(() => document.getElementById('model-search-input')?.focus(), 0);
}}
closeFocus={false}
@@ -238,19 +254,41 @@
class="w-full text-sm bg-transparent outline-none"
placeholder={searchPlaceholder}
autocomplete="off"
on:keydown={(e) => {
if (e.code === 'Enter' && filteredItems.length > 0) {
value = filteredItems[selectedModelIdx].value;
show = false;
return; // dont need to scroll on selection
} else if (e.code === 'ArrowDown') {
selectedModelIdx = Math.min(selectedModelIdx + 1, filteredItems.length - 1);
} else if (e.code === 'ArrowUp') {
selectedModelIdx = Math.max(selectedModelIdx - 1, 0);
} else {
// if the user types something, reset to the top selection.
selectedModelIdx = 0;
}
const item = document.querySelector(`[data-arrow-selected="true"]`);
item?.scrollIntoView({ block: 'center', inline: 'nearest', behavior: 'instant' });
}}
/>
</div>
<hr class="border-gray-100 dark:border-gray-800" />
{/if}
<div class="px-3 my-2 max-h-64 overflow-y-auto scrollbar-hidden">
{#each filteredItems as item}
<div class="px-3 my-2 max-h-64 overflow-y-auto scrollbar-hidden group">
{#each filteredItems as item, index}
<button
aria-label="model-item"
class="flex w-full text-left font-medium line-clamp-1 select-none items-center rounded-button py-2 pl-3 pr-1.5 text-sm text-gray-700 dark:text-gray-100 outline-none transition-all duration-75 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-lg cursor-pointer data-[highlighted]:bg-muted"
class="flex w-full text-left font-medium line-clamp-1 select-none items-center rounded-button py-2 pl-3 pr-1.5 text-sm text-gray-700 dark:text-gray-100 outline-none transition-all duration-75 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-lg cursor-pointer data-[highlighted]:bg-muted {index ===
selectedModelIdx
? 'bg-gray-100 dark:bg-gray-800 group-hover:bg-transparent'
: ''}"
data-arrow-selected={index === selectedModelIdx}
on:click={() => {
value = item.value;
selectedModelIdx = index;
show = false;
}}
@@ -270,7 +308,14 @@
<div class="flex items-center gap-2">
<div class="flex items-center min-w-fit">
<div class="line-clamp-1">
{item.label}
<div class="flex items-center min-w-fit">
<img
src={item.model?.info?.meta?.profile_image_url ?? '/static/favicon.png'}
alt="Model"
class="rounded-full size-5 flex items-center mr-2"
/>
{item.label}
</div>
</div>
{#if item.model.owned_by === 'ollama' && (item.model.ollama?.details?.parameter_size ?? '') !== ''}
<div class="flex ml-1 items-center translate-y-[0.5px]">
@@ -295,23 +340,11 @@
{/if}
</div>
{#if !$mobile && (item?.model?.info?.meta?.tags ?? []).length > 0}
<div class="flex gap-0.5 self-center items-center h-full translate-y-[0.5px]">
{#each item.model?.info?.meta.tags as tag}
<div
class=" text-xs font-bold px-1 rounded uppercase line-clamp-1 bg-gray-500/20 text-gray-700 dark:text-gray-200"
>
{tag.name}
</div>
{/each}
</div>
{/if}
<!-- {JSON.stringify(item.info)} -->
{#if item.model.owned_by === 'openai'}
<Tooltip content={`${'External'}`}>
<div class="">
<div class="translate-y-[1px]">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
@@ -342,7 +375,7 @@
)
)}`}
>
<div class="">
<div class=" translate-y-[1px]">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
@@ -360,6 +393,20 @@
</div>
</Tooltip>
{/if}
{#if !$mobile && (item?.model?.info?.meta?.tags ?? []).length > 0}
<div class="flex gap-0.5 self-center items-center h-full translate-y-[0.5px]">
{#each item.model?.info?.meta.tags as tag}
<Tooltip content={tag.name}>
<div
class=" text-xs font-bold px-1 rounded uppercase line-clamp-1 bg-gray-500/20 text-gray-700 dark:text-gray-200"
>
{tag.name}
</div>
</Tooltip>
{/each}
</div>
{/if}
</div>
</div>
@@ -65,8 +65,8 @@
{updateAvailable === null
? $i18n.t('Checking for updates...')
: updateAvailable
? `(v${version.latest} ${$i18n.t('available!')})`
: $i18n.t('(latest)')}
? `(v${version.latest} ${$i18n.t('available!')})`
: $i18n.t('(latest)')}
</a>
</div>
@@ -20,6 +20,7 @@
mirostat_tau: null,
top_k: null,
top_p: null,
min_p: null,
tfs_z: null,
num_ctx: null,
num_batch: null,
@@ -28,6 +29,7 @@
use_mmap: null,
use_mlock: null,
num_thread: null,
num_gpu: null,
template: null
};
@@ -385,6 +387,52 @@
{/if}
</div>
<div class=" py-0.5 w-full justify-between">
<div class="flex w-full justify-between">
<div class=" self-center text-xs font-medium">{$i18n.t('Min P')}</div>
<button
class="p-1 px-3 text-xs flex rounded transition flex-shrink-0 outline-none"
type="button"
on:click={() => {
params.min_p = (params?.min_p ?? null) === null ? 0.0 : null;
}}
>
{#if (params?.min_p ?? null) === null}
<span class="ml-2 self-center">{$i18n.t('Default')}</span>
{:else}
<span class="ml-2 self-center">{$i18n.t('Custom')}</span>
{/if}
</button>
</div>
{#if (params?.min_p ?? null) !== null}
<div class="flex mt-0.5 space-x-2">
<div class=" flex-1">
<input
id="steps-range"
type="range"
min="0"
max="1"
step="0.05"
bind:value={params.min_p}
class="w-full h-2 rounded-lg appearance-none cursor-pointer dark:bg-gray-700"
/>
</div>
<div>
<input
bind:value={params.min_p}
type="number"
class=" bg-transparent text-center w-14"
min="0"
max="1"
step="any"
/>
</div>
</div>
{/if}
</div>
<div class=" py-0.5 w-full justify-between">
<div class="flex w-full justify-between">
<div class=" self-center text-xs font-medium">{$i18n.t('Frequency Penalty')}</div>
@@ -817,6 +865,52 @@
{/if}
</div>
<div class=" py-0.5 w-full justify-between">
<div class="flex w-full justify-between">
<div class=" self-center text-xs font-medium">{$i18n.t('num_gpu (Ollama)')}</div>
<button
class="p-1 px-3 text-xs flex rounded transition flex-shrink-0 outline-none"
type="button"
on:click={() => {
params.num_gpu = (params?.num_gpu ?? null) === null ? 0 : null;
}}
>
{#if (params?.num_gpu ?? null) === null}
<span class="ml-2 self-center">{$i18n.t('Default')}</span>
{:else}
<span class="ml-2 self-center">{$i18n.t('Custom')}</span>
{/if}
</button>
</div>
{#if (params?.num_gpu ?? null) !== null}
<div class="flex mt-0.5 space-x-2">
<div class=" flex-1">
<input
id="steps-range"
type="range"
min="0"
max="256"
step="1"
bind:value={params.num_gpu}
class="w-full h-2 rounded-lg appearance-none cursor-pointer dark:bg-gray-700"
/>
</div>
<div class="">
<input
bind:value={params.num_gpu}
type="number"
class=" bg-transparent text-center w-14"
min="0"
max="256"
step="1"
/>
</div>
</div>
{/if}
</div>
<!-- <div class=" py-0.5 w-full justify-between">
<div class="flex w-full justify-between">
<div class=" self-center text-xs font-medium">{$i18n.t('Template')}</div>
+33 -27
View File
@@ -1,7 +1,10 @@
<script lang="ts">
import { user, settings, config } from '$lib/stores';
import { createEventDispatcher, onMount, getContext } from 'svelte';
import { toast } from 'svelte-sonner';
import { createEventDispatcher, onMount, getContext } from 'svelte';
import { user, settings, config } from '$lib/stores';
import { getVoices as _getVoices } from '$lib/apis/audio';
import Switch from '$lib/components/common/Switch.svelte';
const dispatch = createEventDispatcher();
@@ -20,26 +23,26 @@
let voices = [];
let voice = '';
const getOpenAIVoices = () => {
voices = [
{ name: 'alloy' },
{ name: 'echo' },
{ name: 'fable' },
{ name: 'onyx' },
{ name: 'nova' },
{ name: 'shimmer' }
];
};
const getVoices = async () => {
if ($config.audio.tts.engine === '') {
const getVoicesLoop = setInterval(async () => {
voices = await speechSynthesis.getVoices();
const getWebAPIVoices = () => {
const getVoicesLoop = setInterval(async () => {
voices = await speechSynthesis.getVoices();
// do your loop
if (voices.length > 0) {
clearInterval(getVoicesLoop);
}
}, 100);
} else {
const res = await _getVoices(localStorage.token).catch((e) => {
toast.error(e);
});
// do your loop
if (voices.length > 0) {
clearInterval(getVoicesLoop);
if (res) {
console.log(res);
voices = res.voices;
}
}, 100);
}
};
const toggleResponseAutoPlayback = async () => {
@@ -58,14 +61,16 @@
responseAutoPlayback = $settings.responseAutoPlayback ?? false;
STTEngine = $settings?.audio?.stt?.engine ?? '';
voice = $settings?.audio?.tts?.voice ?? $config.audio.tts.voice ?? '';
if ($settings?.audio?.tts?.defaultVoice === $config.audio.tts.voice) {
voice = $settings?.audio?.tts?.voice ?? $config.audio.tts.voice ?? '';
} else {
voice = $config.audio.tts.voice ?? '';
}
nonLocalVoices = $settings.audio?.tts?.nonLocalVoices ?? false;
if ($config.audio.tts.engine === 'openai') {
getOpenAIVoices();
} else {
getWebAPIVoices();
}
await getVoices();
});
</script>
@@ -79,6 +84,7 @@
},
tts: {
voice: voice !== '' ? voice : undefined,
defaultVoice: $config?.audio?.tts?.voice ?? '',
nonLocalVoices: $config.audio.tts.engine === '' ? nonLocalVoices : undefined
}
}
@@ -181,7 +187,7 @@
</div>
</div>
</div>
{:else if $config.audio.tts.engine === 'openai'}
{:else if $config.audio.tts.engine !== ''}
<div>
<div class=" mb-2.5 text-sm font-medium">{$i18n.t('Set Voice')}</div>
<div class="flex w-full">
@@ -195,7 +201,7 @@
<datalist id="voice-list">
{#each voices as voice}
<option value={voice.name} />
<option value={voice.id}>{voice.name}</option>
{/each}
</datalist>
</div>
+12 -4
View File
@@ -2,7 +2,7 @@
import fileSaver from 'file-saver';
const { saveAs } = fileSaver;
import { chats, user, settings } from '$lib/stores';
import { chats, user, settings, scrollPaginationEnabled, currentChatPage } from '$lib/stores';
import {
archiveAllChats,
@@ -62,7 +62,9 @@
}
}
await chats.set(await getChatList(localStorage.token));
currentChatPage.set(1);
await chats.set(await getChatList(localStorage.token, $currentChatPage));
scrollPaginationEnabled.set(true);
};
const exportChats = async () => {
@@ -77,7 +79,10 @@
await archiveAllChats(localStorage.token).catch((error) => {
toast.error(error);
});
await chats.set(await getChatList(localStorage.token));
currentChatPage.set(1);
await chats.set(await getChatList(localStorage.token, $currentChatPage));
scrollPaginationEnabled.set(true);
};
const deleteAllChatsHandler = async () => {
@@ -85,7 +90,10 @@
await deleteAllChats(localStorage.token).catch((error) => {
toast.error(error);
});
await chats.set(await getChatList(localStorage.token));
currentChatPage.set(1);
await chats.set(await getChatList(localStorage.token, $currentChatPage));
scrollPaginationEnabled.set(true);
};
const toggleSaveChatHistory = async () => {
@@ -22,6 +22,7 @@
let responseAutoCopy = false;
let widescreenMode = false;
let splitLargeChunks = false;
let scrollOnBranchChange = true;
let userLocation = false;
// Interface
@@ -33,12 +34,18 @@
let showEmojiInCall = false;
let voiceInterruption = false;
let hapticFeedback = false;
const toggleSplitLargeChunks = async () => {
splitLargeChunks = !splitLargeChunks;
saveSettings({ splitLargeChunks: splitLargeChunks });
};
const togglesScrollOnBranchChange = async () => {
scrollOnBranchChange = !scrollOnBranchChange;
saveSettings({ scrollOnBranchChange: scrollOnBranchChange });
};
const togglewidescreenMode = async () => {
widescreenMode = !widescreenMode;
saveSettings({ widescreenMode: widescreenMode });
@@ -64,6 +71,11 @@
saveSettings({ voiceInterruption: voiceInterruption });
};
const toggleHapticFeedback = async () => {
hapticFeedback = !hapticFeedback;
saveSettings({ hapticFeedback: hapticFeedback });
};
const toggleUserLocation = async () => {
userLocation = !userLocation;
@@ -141,9 +153,12 @@
chatBubble = $settings.chatBubble ?? true;
widescreenMode = $settings.widescreenMode ?? false;
splitLargeChunks = $settings.splitLargeChunks ?? false;
scrollOnBranchChange = $settings.scrollOnBranchChange ?? true;
chatDirection = $settings.chatDirection ?? 'LTR';
userLocation = $settings.userLocation ?? false;
hapticFeedback = $settings.hapticFeedback ?? false;
defaultModelId = $settings?.models?.at(0) ?? '';
if ($config?.default_models) {
defaultModelId = $config.default_models.split(',')[0];
@@ -318,6 +333,28 @@
</div>
</div>
<div>
<div class=" py-0.5 flex w-full justify-between">
<div class=" self-center text-xs">
{$i18n.t('Scroll to bottom when switching between branches')}
</div>
<button
class="p-1 px-3 text-xs flex rounded transition"
on:click={() => {
togglesScrollOnBranchChange();
}}
type="button"
>
{#if scrollOnBranchChange === 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>
</div>
<div>
<div class=" py-0.5 flex w-full justify-between">
<div class=" self-center text-xs">
@@ -409,6 +446,26 @@
</div>
</div>
<div>
<div class=" py-0.5 flex w-full justify-between">
<div class=" self-center text-xs">{$i18n.t('Haptic Feedback')}</div>
<button
class="p-1 px-3 text-xs flex rounded transition"
on:click={() => {
toggleHapticFeedback();
}}
type="button"
>
{#if hapticFeedback === 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>
</div>
<div class=" my-1.5 text-sm font-medium">{$i18n.t('Voice')}</div>
<div>

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