Compare commits

...

1693 Commits

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

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

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

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

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

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

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

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

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

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-08-19 03:06:51 +00:00
Craig Quiter 0550d12106 Black format 2024-08-18 15:04:01 -07:00
Craig Quiter 845628c100 Fix tab format causing build failure 2024-08-18 15:00:49 -07:00
Craig Quiter 7bcdc10539 Optimize imports 2024-08-18 14:19:29 -07:00
Craig Quiter d2f10d50bf Allow seting CORS origin 2024-08-18 14:19:29 -07:00
Timothy Jaeryang Baek 446b2a334a Merge pull request #4713 from FINNSEEFLY/task/adjust-localization
i18n: Update ru-RU and uk-UA localizations
2024-08-18 22:57:34 +02:00
FINNSEEFLY b0fd90ada9 Adjust controls to ensure proper display of localized strings 2024-08-18 22:42:58 +03:00
FINNSEEFLY 6bfd48e412 Adjust uk localization 2024-08-18 22:40:18 +03:00
FINNSEEFLY 07ffd6e231 Adjust ru localization 2024-08-18 22:39:36 +03:00
Timothy J. Baek c675beeda4 refac 2024-08-18 21:14:22 +02:00
Timothy J. Baek fe0cf9506c refac 2024-08-18 21:11:59 +02:00
Timothy J. Baek 7c81509804 feat: merge responses 2024-08-18 20:59:59 +02:00
Timothy J. Baek 65923006a8 chore: format 2024-08-18 18:29:30 +02:00
Timothy J. Baek b02f6db475 refac 2024-08-18 18:28:58 +02:00
Timothy J. Baek 7f394f3f00 refac 2024-08-18 18:21:59 +02:00
Timothy J. Baek fc8524bbfd refac: styling 2024-08-18 18:20:36 +02:00
Timothy J. Baek c9505531fd refac: styling 2024-08-18 17:40:26 +02:00
Timothy J. Baek 1b809fe42e refac: styling 2024-08-18 17:16:22 +02:00
Timothy J. Baek a1b4df1b85 fix: cloned many model chat freezing issue 2024-08-18 16:47:12 +02:00
Timothy Jaeryang Baek 079f37a2d6 Merge pull request #4701 from ndrsfel/fix-rag-embedding-openai-batch-size-environment-variable
fix: RAG with OpenAI embedding models and batch_size environment variable fails silently
2024-08-18 13:24:18 +02:00
Andreas Feldl 0980066363 fix conversion 2024-08-18 12:46:47 +02:00
Timothy Jaeryang Baek a3b6654cbb Merge pull request #4688 from amirsubhi/dev
i18n: Updated ms-MY Translation
2024-08-17 21:57:52 +02:00
amirsubhi f4328325be Update i18n ms-MY Translation
Update Bahasa Malaysia Translation to reflect new update, and some spelling error
2024-08-18 02:17:51 +08:00
Timothy J. Baek 7badff49d8 refac 2024-08-17 19:43:04 +02:00
Timothy J. Baek 176c689f8d fix: unstructured md file parsing issue 2024-08-17 17:20:35 +02:00
Timothy J. Baek fa20b1dc09 chore: format 2024-08-17 17:17:45 +02:00
Timothy J. Baek 2ca9989d20 refac 2024-08-17 17:15:44 +02:00
Timothy J. Baek 3420818c52 refac 2024-08-17 17:11:58 +02:00
Timothy J. Baek 0ae6ca608c refac 2024-08-17 17:01:35 +02:00
Timothy J. Baek 536b40890a refac 2024-08-17 16:57:27 +02:00
Timothy J. Baek d5337917db refac 2024-08-17 16:46:04 +02:00
Timothy J. Baek 15f3ebba93 refac 2024-08-17 16:41:34 +02:00
Timothy J. Baek e71f55e58f refac 2024-08-17 16:32:39 +02:00
Timothy J. Baek fe747382c1 refac 2024-08-17 16:27:11 +02:00
Timothy J. Baek c4946d42e0 refac 2024-08-17 16:24:11 +02:00
Timothy J. Baek c1823b4b73 refac 2024-08-17 16:02:46 +02:00
Timothy J. Baek 862a30842c refac: chat_completion_inlets_handler -> chat_completion_filter_functions_handler 2024-08-17 16:00:18 +02:00
Timothy Jaeryang Baek cbb0940ff8 Merge pull request #4602 from michaelpoluektov/tools-refac-1
refactor, perf: Tools refactor (progress PR 1)
2024-08-17 15:50:40 +02:00
Timothy Jaeryang Baek bd8df3583d Merge pull request #4674 from crizCraig/sanitize-11labs-voiceid
sec: Sanitize 11labs voice id to address semgrep security issue: tainted-path-traversal-stdlib-fastapi
2024-08-17 15:49:56 +02:00
Craig Quiter 5f36807dbe Note tts defaults are from openai 2024-08-16 15:42:15 -07:00
Craig Quiter 442f50303a Sanitize voice_id 2024-08-16 15:10:53 -07:00
Craig Quiter 4560f3b1ae Return a dict from get_available_voices 2024-08-16 15:10:51 -07:00
Craig Quiter 59d2c670ba Optimize imports 2024-08-16 15:10:47 -07:00
Craig Quiter 02577f6a45 Cache elevenlabs voice call (can take 1s) 2024-08-16 15:10:41 -07:00
Timothy J. Baek 094fdd8943 fix 2024-08-16 19:23:36 +02:00
Timothy J. Baek 17169dff1f fix 2024-08-16 19:00:10 +02:00
Timothy J. Baek 28e3e6e8cb refac: many model chat 2024-08-16 18:54:30 +02:00
Timothy J. Baek 4f47053e93 refac 2024-08-16 17:51:50 +02:00
Timothy J. Baek 9025e9dbb1 fix: pseudo html rendering issue 2024-08-16 15:44:18 +02:00
Timothy J. Baek eee1dad217 refac: styling 2024-08-16 15:37:17 +02:00
Timothy J. Baek 769df698be fix: visible backtick in codespan 2024-08-16 15:37:11 +02:00
Timothy J. Baek 4ef042e966 refac 2024-08-16 15:33:14 +02:00
Timothy J. Baek 92062ff722 refac 2024-08-16 15:19:47 +02:00
Timothy J. Baek 623aa08b9e refac 2024-08-16 15:15:06 +02:00
Timothy J. Baek a529343b2b refac 2024-08-16 15:10:21 +02:00
Timothy J. Baek 2161903163 refac 2024-08-16 14:42:51 +02:00
Timothy Jaeryang Baek 3de9a1a130 Merge pull request #4651 from KarlLee830/translate
i18n: Update Chinese translation
2024-08-16 14:12:47 +02:00
Karl Lee 587c1a3ca2 i18n: Update Chinese translation 2024-08-16 16:37:02 +08:00
Timothy J. Baek d224566957 refac: styling 2024-08-16 00:21:57 +02:00
Timothy J. Baek 8ea1a10525 enh: action __event_emitter__ support 2024-08-15 23:55:31 +02:00
Michael Poluektov b6d6094018 Merge branch 'dev' into tools-refac-1 2024-08-15 21:35:31 +01:00
Timothy J. Baek e5e1bac242 chore: format 2024-08-15 17:31:47 +02:00
Timothy J. Baek 8c2ba7f7ea enh: Actions __webui__ flag support 2024-08-15 17:28:43 +02:00
Timothy J. Baek dc6ca61548 enh: temp chat
deprecates chat history setting and introduces temp chat from model selector
2024-08-15 16:54:16 +02:00
Timothy Jaeryang Baek 723caf2a09 Merge pull request #4621 from nthe/main
feat: Set content-type header in Ollama backend
2024-08-15 15:45:51 +02:00
Timothy J. Baek 439cb66672 refac: fuzzy search threshold 2024-08-15 15:44:38 +02:00
Timothy J. Baek dbd5b4c9f1 enh: render markdown user message 2024-08-15 15:41:02 +02:00
Timothy Jaeryang Baek 4dd404ac3b Merge pull request #4614 from sebdanielsson/pwa-maskable-icon
fix: Make PWA icon maskable
2024-08-15 13:25:00 +02:00
Timothy J. Baek ba370438b2 refac: "any maskable" is discouraged 2024-08-15 13:24:47 +02:00
Juraj Onuska f73a60d96c fix: set content-type header in ollama backend 2024-08-15 13:15:12 +02:00
Sebastian afe1f13c5b Make PWA icon maskable 2024-08-15 00:46:22 +02:00
Timothy J. Baek 0554cc6128 enh: shiftkey quick delete for tools and functions 2024-08-15 00:44:23 +02:00
Timothy J. Baek 5a6ece9513 refac: enhanced response content sanitisation
'<' and '>' can be correctly displayed now
2024-08-15 00:08:15 +02:00
Michael Poluektov 4042219b3e minor refac 2024-08-14 21:40:00 +01:00
Michael Poluektov fdc89cbcee tool calling refactor 2024-08-14 21:40:00 +01:00
Michael Poluektov 6df6170c44 add get_configured_tools 2024-08-14 21:40:00 +01:00
Michael Poluektov d598d4bb93 typing and tweaks 2024-08-14 21:40:00 +01:00
Michael Poluektov 790bdcf9fc rename tool calling helpers to use 'tool' instead of 'function' 2024-08-14 21:40:00 +01:00
Michael Poluektov 2efcda837c add try: except back 2024-08-14 21:40:00 +01:00
Michael Poluektov e86688284a factor out get_function_calling_payload 2024-08-14 21:40:00 +01:00
Michael Poluektov ff9d899f9c fix more LSP errors 2024-08-14 21:40:00 +01:00
Michael Poluektov a68b918cbb refactor get_function_call_response 2024-08-14 21:40:00 +01:00
Michael Poluektov 9fb70969d7 factor out get_content_from_response 2024-08-14 21:40:00 +01:00
Michael Poluektov 0c9119d619 move task to metadata 2024-08-14 21:40:00 +01:00
Michael Poluektov 556141cdd8 refactor task 2024-08-14 21:40:00 +01:00
Michael Poluektov 60003c976a rename to chat_completions_inlet_handler for clarity 2024-08-14 21:40:00 +01:00
Michael Poluektov 23f1bee7bd cleanup 2024-08-14 21:40:00 +01:00
Michael Poluektov 589efcdc5f is_chat_completion_request helper, remove nesting 2024-08-14 21:40:00 +01:00
Michael Poluektov 3befadb29f remove unnecessary nesting, remove unused endpoint 2024-08-14 21:40:00 +01:00
Timothy Jaeryang Baek 13b0e7d64a Merge pull request #4434 from open-webui/dev
Create and publish Docker images with specific build args / build-main-image (linux/amd64) (push) Waiting to run
Create and publish Docker images with specific build args / build-main-image (linux/arm64) (push) Waiting to run
Create and publish Docker images with specific build args / build-cuda-image (linux/amd64) (push) Waiting to run
Create and publish Docker images with specific build args / build-cuda-image (linux/arm64) (push) Waiting to run
Create and publish Docker images with specific build args / build-ollama-image (linux/amd64) (push) Waiting to run
Create and publish Docker images with specific build args / build-ollama-image (linux/arm64) (push) Waiting to run
Create and publish Docker images with specific build args / merge-main-images (push) Blocked by required conditions
Create and publish Docker images with specific build args / merge-cuda-images (push) Blocked by required conditions
Create and publish Docker images with specific build args / merge-ollama-images (push) Blocked by required conditions
0.3.13
2024-08-14 21:45:19 +02:00
Timothy J. Baek c8badfe21f refac 2024-08-14 21:42:43 +02:00
Timothy J. Baek 2a1b9cae91 doc: changelog 2024-08-14 21:39:11 +02:00
Timothy J. Baek 5ba7bbdd98 refac 2024-08-14 21:34:08 +02:00
Timothy J. Baek 9435d2044a refac: more robust mermaid chart rendering 2024-08-14 21:16:07 +02:00
Timothy J. Baek 609a42c29c refac: mermaid chart rendering 2024-08-14 17:34:44 +02:00
Timothy J. Baek 6a1e7ab038 chore: version bump 2024-08-14 17:13:37 +02:00
Timothy J. Baek 53daa15b9a chore: format 2024-08-14 17:07:16 +02:00
Timothy J. Baek 55097410f6 feat: haptic feedback on support devices (android)
Co-Authored-By: Danny Liu <dannyjialiliu@gmail.com>
2024-08-14 17:05:43 +02:00
Timothy J. Baek 04e2b6e2bd chore: format 2024-08-14 16:39:02 +02:00
Timothy Jaeryang Baek 9f0c9d973c Merge pull request #4597 from michaelpoluektov/cleanup
refactor: search and replace-able cleanup
2024-08-14 16:30:27 +02:00
Timothy J. Baek 1597e33a74 refac: mermaid chart rendering 2024-08-14 16:27:55 +02:00
Timothy Jaeryang Baek 0b218bbb72 Merge pull request #4598 from michaelpoluektov/cleanup-frontend
refactor: Remove unused frontend functions migrated to backend
2024-08-14 16:13:39 +02:00
Timothy J. Baek 7b21b718fe refac 2024-08-14 16:09:16 +02:00
Timothy J. Baek 6e5b557a1f refac 2024-08-14 16:08:23 +02:00
Timothy J. Baek 6aefc79807 refac: latex 2024-08-14 16:07:39 +02:00
Michael Poluektov ec9e0dadea remove frontend functions migrated to backend 2024-08-14 14:33:20 +01:00
Michael Poluektov 0470146d7b replace Tuple with tuple 2024-08-14 13:58:37 +01:00
Michael Poluektov a518d50477 format backend 2024-08-14 13:49:18 +01:00
Michael Poluektov 29f904db45 remove List imports 2024-08-14 13:46:31 +01:00
Michael Poluektov 038fc48ac0 replace == None with is None 2024-08-14 13:39:53 +01:00
Michael Poluektov 6f72def1ac replace except: with except Exception: 2024-08-14 13:38:19 +01:00
Timothy J. Baek 0ec1f9e331 fix: ENABLE_ADMIN_CHAT_ACCESS disabling user deletion 2024-08-14 12:23:24 +02:00
Timothy Jaeryang Baek 9682806476 Merge pull request #4372 from JTHesse/main
build: Adding ability to install requirements from frontmatter for tools and functions
2024-08-13 18:18:26 +02:00
Timothy Jaeryang Baek fc6fa7887b Merge pull request #4579 from alexandregodard/main
Update main.py
2024-08-13 18:17:27 +02:00
Timothy J. Baek e1e69cfbcb refac: sft -> default to safetensors 2024-08-13 17:15:20 +01:00
Alexandre GODARD 7a8f8960c5 Update main.py
Fix typo in update_reranking_model
2024-08-13 17:51:25 +02:00
Timothy Jaeryang Baek 30c44d431b Merge pull request #4538 from open-webui/dependabot/pip/backend/dev/chromadb-0.5.5
chore(deps): bump chromadb from 0.5.4 to 0.5.5 in /backend
2024-08-13 12:22:56 +02:00
Timothy Jaeryang Baek b177976f29 Merge pull request #4539 from open-webui/dependabot/pip/backend/dev/langchain-0.2.12
chore(deps): bump langchain from 0.2.11 to 0.2.12 in /backend
2024-08-13 12:22:48 +02:00
Timothy J. Baek af6b92a6fa chore: format 2024-08-13 11:22:08 +01:00
Timothy J. Baek a1888b3757 refac 2024-08-13 11:21:17 +01:00
Timothy Jaeryang Baek c75e77cdde Merge pull request #4554 from 5E-324/add-num_gpu
feat: Add advanced parameter num_gpu (Ollama)
2024-08-13 12:15:44 +02:00
Timothy Jaeryang Baek 6447b484c1 Merge pull request #4555 from 5E-324/ollama-params-fix
fix: Ollama parameters not shown in Chat Controls even if the current user is admin.
2024-08-13 12:13:40 +02:00
Timothy Jaeryang Baek 0c5236ac27 Merge pull request #4562 from simonaszilinskas/patch-5
Update Lithuanian translations
2024-08-13 12:13:11 +02:00
Timothy J. Baek 7ef5aa520c chore: format 2024-08-13 11:12:35 +01:00
Simonas d620bcf516 Update Lithuanian translations 2024-08-13 03:10:08 -05:00
Zhuoran b0c2e607b8 Set property "admin" of AdvancedParams in Controls 2024-08-13 10:06:51 +08:00
Zhuoran 56e2d579f2 Add advanced parameter num_gpu (Ollama) 2024-08-13 09:46:38 +08:00
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
Timothy Jaeryang Baek c74e7df6a0 Merge pull request #3948 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.10
2024-07-17 17:51:35 +02:00
Timothy J. Baek 29d8f8d91b refac: wording 2024-07-17 17:30:05 +02:00
Timothy J. Baek 0d1553562d chore: bump 2024-07-17 17:28:53 +02:00
Timothy J. Baek 8e3312c032 doc: changelog 2024-07-17 17:28:22 +02:00
Timothy Jaeryang Baek 878d12da24 Merge pull request #3946 from open-webui/dev
fix
2024-07-17 17:21:03 +02:00
Timothy J. Baek c1dfafcc9e fix 2024-07-17 17:18:57 +02:00
Timothy Jaeryang Baek 6668d79519 Merge pull request #3944 from open-webui/dev
refac
2024-07-17 17:11:57 +02:00
Timothy J. Baek 775a3dc359 refac 2024-07-17 17:11:50 +02:00
Timothy J. Baek 98f935b8f7 fix 2024-07-17 17:00:43 +02:00
Timothy J. Baek 86359ef374 refac 2024-07-17 16:56:11 +02:00
Timothy Jaeryang Baek ef8fbaae28 Merge pull request #3943 from open-webui/dev
fix: web search
2024-07-17 15:40:11 +02:00
Timothy J. Baek 9e48aeca45 fix: web search 2024-07-17 15:39:50 +02:00
Timothy Jaeryang Baek 9518111ed3 Merge pull request #3940 from open-webui/dev
fix: file upload status
2024-07-17 14:22:14 +02:00
Timothy J. Baek 4fea55bac3 fix: file upload status 2024-07-17 14:21:58 +02:00
Timothy Jaeryang Baek 027dd848eb Merge pull request #3933 from open-webui/dev
fix
2024-07-17 13:16:19 +02:00
Timothy J. Baek 98928fc5f0 fix 2024-07-17 13:15:57 +02:00
Timothy Jaeryang Baek 6e843ab563 Merge pull request #3882 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.9
2024-07-17 12:18:37 +02:00
Timothy J. Baek b3a0d47a67 doc: changelog 2024-07-17 12:17:15 +02:00
Timothy J. Baek d492d29153 fix: files overlay 2024-07-17 12:02:54 +02:00
Timothy J. Baek ed21f9f906 refac: styling 2024-07-17 11:53:49 +02:00
Timothy J. Baek bdadfc958c chore: format 2024-07-17 11:48:52 +02:00
Timothy J. Baek 3bbd238d7d refac: styling 2024-07-17 11:43:11 +02:00
Timothy J. Baek 4eecdbadd3 enh: files chat control 2024-07-17 11:39:37 +02:00
Timothy J. Baek a33b0abbe0 refac: styling 2024-07-17 10:19:33 +02:00
Timothy J. Baek 3d0f457306 refac 2024-07-17 10:16:27 +02:00
Timothy Jaeryang Baek ca5f1c1efb Merge pull request #3906 from luisbrandao/dev
i18n: pt_br
2024-07-17 09:43:38 +02:00
Luis Alexandre Deschamps Brandao e72592eac3 i18n: pt_br 2024-07-16 11:37:09 -03:00
Timothy J. Baek 72e9cecb17 fix: allow boolean values in params 2024-07-16 15:15:06 +02:00
Timothy J. Baek 27e7494853 refac: title gen should only run once 2024-07-16 14:00:09 +02:00
Timothy J. Baek f0a8aca0e3 fix: automatic1111 auth key should not be required 2024-07-16 10:33:05 +02:00
Timothy Jaeryang Baek 4514595811 Merge pull request #3893 from luisbrandao/dev
translation pt_br
2024-07-16 00:59:26 -07:00
Luis Alexandre Deschamps Brandao e0bcbf0b79 translation pt_br 2024-07-16 01:44:05 -03:00
Timothy Jaeryang Baek 285725eaab Merge pull request #3888 from aleixdorca/dev
i18n: Update Catalan Translations
2024-07-15 11:25:35 -07:00
aleix 9a4770eb17 i18n: Update Catalan Translations 2024-07-15 20:01:07 +02:00
Timothy Jaeryang Baek 8a11985fce Merge pull request #3887 from OriginalSimon/dev
Added i18n keys and update of Ukrainian translation
2024-07-15 10:06:45 -07:00
SimonOriginal 731b3654d8 add i18n keys 2024-07-15 16:45:29 +02:00
SimonOriginal da4e976d5c add Ukrainian translation for new keys 2024-07-15 16:33:38 +02:00
SimonOriginal b74739f91b refac 9 2024-07-15 16:31:23 +02:00
SimonOriginal 6b5c452a3c refac 8 2024-07-15 16:27:06 +02:00
Timothy J. Baek 40075f69f9 refac 2024-07-15 16:25:00 +02:00
SimonOriginal 7702803e5c refac 7 2024-07-15 16:24:47 +02:00
SimonOriginal 3731f1950a refac 6 2024-07-15 16:22:10 +02:00
SimonOriginal 9ab47cf628 refac 5 2024-07-15 16:20:18 +02:00
SimonOriginal 676688aabf refac 4 2024-07-15 16:14:56 +02:00
SimonOriginal 5958d8775c refac 3 2024-07-15 16:12:55 +02:00
SimonOriginal dfd5de9381 add i18n keys 2024-07-15 15:41:07 +02:00
SimonOriginal ac0164c4cc add Ukrainian translation for new keys 2024-07-15 15:35:49 +02:00
SimonOriginal 9e258ea466 add i18n keys 2024-07-15 15:34:21 +02:00
SimonOriginal 0aacb95700 add Ukrainian translation for new keys 2024-07-15 15:28:33 +02:00
SimonOriginal 3668179dfe refac 2 2024-07-15 15:16:49 +02:00
SimonOriginal fa26b8d7cf refac 2024-07-15 15:14:43 +02:00
SimonOriginal 07283ca0cf Merge branch 'dev' of https://github.com/OriginalSimon/open-webui into dev 2024-07-15 15:02:01 +02:00
SimonOriginal 917dde7edf Fix i18n string syntax 2024-07-15 15:01:34 +02:00
SimonOriginal 8102347296 Fix i18n string syntax 2024-07-15 15:01:02 +02:00
Simon 736d8586f1 Merge branch 'open-webui:dev' into dev 2024-07-15 14:54:51 +02:00
SimonOriginal c97d4bbb50 add i18n keys 2024-07-15 14:54:20 +02:00
Timothy J. Baek 553dcd208c refac 2024-07-15 14:16:28 +02:00
SimonOriginal e19c327279 update Ukrainian translation 2024-07-15 14:15:10 +02:00
Timothy J. Baek 6d0e4852cc chore: bump 2024-07-15 13:57:47 +02:00
Timothy J. Baek d258523a3c refac 2024-07-15 13:09:15 +02:00
Timothy J. Baek dbc352f01b refac: documents file handling 2024-07-15 13:05:38 +02:00
Timothy J. Baek 5e8a74ef74 refac 2024-07-15 12:20:09 +02:00
Timothy J. Baek 3cc3671e74 enh: custom icon support for actions 2024-07-15 12:15:00 +02:00
Timothy J. Baek d4d7c3d8b6 enh: citations link 2024-07-15 11:54:56 +02:00
Timothy J. Baek 69083b6485 revert 2024-07-15 11:09:05 +02:00
Timothy Jaeryang Baek feba1f2e3c Merge pull request #3875 from open-webui/dependabot/pip/backend/dev/pydantic-2.8.2
chore(deps): bump pydantic from 2.7.1 to 2.8.2 in /backend
2024-07-15 02:03:29 -07:00
Timothy Jaeryang Baek bb20251b43 Merge pull request #3878 from open-webui/dependabot/pip/backend/dev/unstructured-0.14.10
chore(deps): bump unstructured from 0.14.9 to 0.14.10 in /backend
2024-07-15 02:03:21 -07:00
Timothy J. Baek 1ec2e74b07 chore: format 2024-07-15 10:58:40 +02:00
Timothy Jaeryang Baek db6f6e62b9 Merge pull request #3876 from open-webui/dependabot/pip/backend/dev/sqlalchemy-2.0.31
chore(deps): bump sqlalchemy from 2.0.30 to 2.0.31 in /backend
2024-07-14 19:39:26 -07:00
Timothy Jaeryang Baek a99743c1c8 Merge pull request #3877 from open-webui/dependabot/pip/backend/dev/duckduckgo-search-approx-eq-6.1.12
chore(deps): update duckduckgo-search requirement from ~=6.1.7 to ~=6.1.12 in /backend
2024-07-14 19:39:00 -07:00
Timothy Jaeryang Baek 04dd0f2b72 Merge pull request #3879 from open-webui/dependabot/pip/backend/dev/chromadb-0.5.4
chore(deps): bump chromadb from 0.5.3 to 0.5.4 in /backend
2024-07-14 19:38:40 -07:00
dependabot[bot] b4eb630fc2 chore(deps): bump chromadb from 0.5.3 to 0.5.4 in /backend
Bumps [chromadb](https://github.com/chroma-core/chroma) from 0.5.3 to 0.5.4.
- [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.3...0.5.4)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-07-15 02:34:49 +00:00
dependabot[bot] 8494b97e05 chore(deps): bump unstructured from 0.14.9 to 0.14.10 in /backend
Bumps [unstructured](https://github.com/Unstructured-IO/unstructured) from 0.14.9 to 0.14.10.
- [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.9...0.14.10)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-07-15 02:34:45 +00:00
dependabot[bot] a98ff3c151 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.7...v6.1.12)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-07-15 02:34:40 +00:00
dependabot[bot] b16967a29f chore(deps): bump sqlalchemy from 2.0.30 to 2.0.31 in /backend
Bumps [sqlalchemy](https://github.com/sqlalchemy/sqlalchemy) from 2.0.30 to 2.0.31.
- [Release notes](https://github.com/sqlalchemy/sqlalchemy/releases)
- [Changelog](https://github.com/sqlalchemy/sqlalchemy/blob/main/CHANGES.rst)
- [Commits](https://github.com/sqlalchemy/sqlalchemy/commits)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-07-15 02:34:36 +00:00
dependabot[bot] 73afa73286 chore(deps): bump pydantic from 2.7.1 to 2.8.2 in /backend
Bumps [pydantic](https://github.com/pydantic/pydantic) from 2.7.1 to 2.8.2.
- [Release notes](https://github.com/pydantic/pydantic/releases)
- [Changelog](https://github.com/pydantic/pydantic/blob/main/HISTORY.md)
- [Commits](https://github.com/pydantic/pydantic/compare/v2.7.1...v2.8.2)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-07-15 02:34:23 +00:00
Timothy Jaeryang Baek 63ab526bfb Merge pull request #3815 from JerryKwan/issue3813
fix the checking logic in get_model_path
2024-07-14 13:53:44 -07:00
Timothy Jaeryang Baek cf92909d9a Merge pull request #3859 from aleixdorca/dev
i18n: Update Catalan Translation
2024-07-14 13:52:45 -07:00
aleix ca6a71749c i18n: Update Catalan Translation 2024-07-14 09:16:44 +02:00
Timothy Jaeryang Baek 79778fa221 Merge pull request #3850 from KarlLee830/translate
i18n: Add some missing i18n keys and update Chinese translation
2024-07-13 10:24:21 -07:00
Timothy Jaeryang Baek 6806b01327 Merge pull request #3839 from Peter-De-Ath/fix-logging
fix: logging - after db migration
2024-07-13 10:23:44 -07:00
Karl Lee 6f06577f18 i18n: Update Chinese translation 2024-07-13 21:26:44 +08:00
Karl Lee bc4bb0b4e5 chore: format 2024-07-13 21:22:38 +08:00
Karl Lee f40aee9d2e i18n: added some missing i18n keys 2024-07-13 21:22:16 +08:00
Karl Lee 26bf1ef621 i18n: Update Chinese translation 2024-07-13 21:14:45 +08:00
Peter De-Ath 03618fd2e0 fix: update logging configuration in env.py 2024-07-12 21:12:30 +01:00
Timothy Jaeryang Baek fa3721bcde Merge pull request #3824 from vquie/fix_translation
i18n: fix german translation for OAUTH_PROVIDER_NAME
2024-07-12 10:16:20 -07:00
Timothy Jaeryang Baek a049b38ab5 Merge pull request #3825 from michaelpoluektov/replace-message-event
feat: Replace message event
2024-07-12 10:15:43 -07:00
Michael Poluektov 540a669dae feat: replace message event 2024-07-12 10:41:03 +01:00
Vitali Quiering e266504463 Fix translation placeholder in German locale
- Corrected placeholder from {{Anbieter}} to {{provider}}
2024-07-12 11:40:10 +02:00
Jerry f07172bfe6 fix the checking logic in get_model_path
fix the checking logic in function get_model_path()
2024-07-12 14:21:37 +08:00
Timothy J. Baek 856845e5f2 refac 2024-07-11 19:05:59 -07:00
Timothy J. Baek 14c0efe300 feat: chat action integration 2024-07-11 18:47:38 -07:00
Timothy J. Baek eb10001eb7 feat: action function 2024-07-11 18:41:00 -07:00
Timothy J. Baek 90c3d68f00 enh: input type event call 2024-07-11 17:30:24 -07:00
Timothy J. Baek 97098edfeb feat: chat valves 2024-07-11 17:18:18 -07:00
Timothy J. Baek 9f9122b6d7 enh: ChatValves 2024-07-11 16:24:59 -07:00
Timothy J. Baek 8dcb3d78dc refac 2024-07-11 15:20:56 -07:00
Timothy J. Baek 4dd77b785a fix 2024-07-11 14:12:44 -07:00
Timothy J. Baek 7d7a29cfb9 fix 2024-07-11 13:53:47 -07:00
Timothy J. Baek f462744fc8 refac 2024-07-11 13:43:44 -07:00
Timothy J. Baek 9ab97b834a revert: reset vector db 2024-07-11 13:22:24 -07:00
Timothy J. Baek 7ba7b959a8 feat: message event 2024-07-11 11:00:42 -07:00
Timothy J. Baek b094153af2 fix 2024-07-11 10:41:13 -07:00
Timothy J. Baek 0ef27bfc5e refac 2024-07-11 10:40:10 -07:00
Timothy Jaeryang Baek a62b0924df Update config.json 2024-07-11 00:02:59 -05:00
Timothy Jaeryang Baek eff736acd2 Merge pull request #3774 from open-webui/dev
fix
2024-07-10 14:45:28 -07:00
Timothy J. Baek 38cc0da990 chore; format 2024-07-10 14:43:59 -07:00
Timothy J. Baek 37ff651723 refac 2024-07-10 14:43:00 -07:00
Timothy J. Baek fae7db89e6 chore: dockerfile 2024-07-10 14:29:39 -07:00
Timothy J. Baek ef21397f0d chore: dockerfile 2024-07-10 14:20:02 -07:00
Timothy Jaeryang Baek 2011cbd210 Merge pull request #3773 from open-webui/dev
fix: db
2024-07-10 13:36:15 -07:00
Timothy J. Baek ebcc5be1bf fix: db 2024-07-10 13:35:52 -07:00
Timothy Jaeryang Baek 3661dd16b8 Merge pull request #3769 from open-webui/dev
chore: requirements
2024-07-10 13:12:47 -07:00
Timothy J. Baek 2aa5d26735 chore: requirements 2024-07-10 13:10:36 -07:00
Timothy Jaeryang Baek d50639971a Merge pull request #3768 from open-webui/dev
dev
2024-07-10 13:07:52 -07:00
Timothy J. Baek fede1e9e3b chore: changelog 2024-07-10 13:06:27 -07:00
Timothy Jaeryang Baek f7777f489e Merge pull request #3753 from jonathan-rohde/fix/store-default-model
fix: preselect default model from config if no setting stored
2024-07-10 12:51:17 -07:00
Timothy Jaeryang Baek 93ae6084d6 Merge pull request #3755 from simonaszilinskas/patch-4
Update translation.json
2024-07-10 12:50:40 -07:00
Simonas 3b39334bae Update translation.json
Word not necessary in Lithuanian so modified it to be an arrow which would look good design wise and would not be weird gramatically
2024-07-10 04:35:16 -05:00
Jonathan Rohde da8ee43481 fix: use default model from config if available 2024-07-10 08:43:28 +02:00
Timothy J. Baek a843e81aaf chore: requirements 2024-07-09 18:23:52 -07:00
Timothy Jaeryang Baek 9bcd4ce5c0 Merge pull request #3559 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.8
2024-07-09 14:25:16 -07:00
Timothy J. Baek b38abf23bb chore: bump 2024-07-09 14:24:46 -07:00
Timothy J. Baek 7f2f45e176 doc: changelog 2024-07-09 14:21:06 -07:00
Timothy Jaeryang Baek 0444497eea Merge pull request #3734 from michaelpoluektov/refactor-main
refactor: Refactor backend/main.py
2024-07-09 12:48:33 -07:00
Michael Poluektov 1d20c27553 refac: use get_task_model_id() 2024-07-09 16:08:54 +01:00
Michael Poluektov 8f23df5749 fix: outlet __event_emitter__ 2024-07-09 15:57:24 +01:00
Michael Poluektov 144581a7df refac: get_sorted_pipelines() 2024-07-09 12:51:13 +01:00
Michael Poluektov 7ffd75b991 refac: black 2024-07-09 12:32:43 +01:00
Michael Poluektov ff474936f8 refac: remove model param 2024-07-09 12:20:28 +01:00
Michael Poluektov d7dd901f01 refac: remove nesting 2024-07-09 12:15:09 +01:00
Michael Poluektov e3e02e04e8 refac: backend/main.py 2024-07-09 11:51:43 +01:00
Michael Poluektov f9e3c47d4a rebase 2024-07-09 11:13:42 +01:00
Timothy J. Baek 24ef5af2a1 fix: code block 2024-07-09 00:41:56 -07:00
Timothy J. Baek f8e2b31756 refac: styling 2024-07-09 00:26:02 -07:00
Timothy J. Baek a11887cee1 enh: i said darker 2024-07-08 23:30:59 -07:00
Timothy J. Baek 78a5748727 refac 2024-07-08 23:21:17 -07:00
Timothy J. Baek 666a8ea5c0 refac 2024-07-08 23:18:35 -07:00
Timothy J. Baek 0bc46b19b3 refac 2024-07-08 23:17:57 -07:00
Timothy J. Baek 3f8af02aaa fix: styling 2024-07-08 23:16:54 -07:00
Timothy J. Baek c9f5029a9d refac 2024-07-08 23:07:23 -07:00
Timothy Jaeryang Baek aa896ba702 Merge pull request #3725 from aleixdorca/dev
i18n: Updated Catalan Translation
2024-07-08 22:25:28 -07:00
Timothy J. Baek c2f4eab8ed refac: splash screen 2024-07-08 22:20:00 -07:00
Timothy J. Baek 47c76ab5fe refac: styling 2024-07-08 22:02:28 -07:00
aleix 2396010d6c i18n: Updated Catalan Translation 2024-07-09 07:01:34 +02:00
Timothy J. Baek 37285b8749 fix: do not include reserved params in specs 2024-07-08 21:52:23 -07:00
Timothy J. Baek 0a08a4d2fb refac 2024-07-08 21:40:22 -07:00
Timothy J. Baek 1b7ff1c5df feat: __event_call__ support 2024-07-08 21:39:06 -07:00
Timothy J. Baek 1d979d9b75 refac 2024-07-08 21:09:43 -07:00
Timothy J. Baek d34e9ef935 refac: styling 2024-07-08 21:09:32 -07:00
Timothy J. Baek d752002836 refac: styling 2024-07-08 20:17:09 -07:00
Timothy J. Baek 7df35d978f refac: styling 2024-07-08 20:15:13 -07:00
Timothy J. Baek 18ea8df685 fix: theme 2024-07-08 20:13:16 -07:00
Timothy J. Baek a23146ebd1 refac: styling 2024-07-08 20:10:00 -07:00
Timothy J. Baek 446d85474e feat: darker oled 2024-07-08 19:55:00 -07:00
Timothy J. Baek 8942d0d0e2 refac: styling 2024-07-08 19:47:30 -07:00
Timothy J. Baek eb1e176f14 chore: format 2024-07-08 19:40:18 -07:00
Timothy J. Baek 30e2ec7544 feat: chat controls integration 2024-07-08 19:26:31 -07:00
Timothy J. Baek 9cea5f75bb refac: styling 2024-07-08 18:31:58 -07:00
Timothy J. Baek b8d153ebb2 feat: chat controls ui 2024-07-08 16:55:12 -07:00
Timothy J. Baek 781ad70598 refac: tooltip 2024-07-08 15:26:43 -07:00
Timothy J. Baek 3598aee71e refac: styling 2024-07-08 14:32:51 -07:00
Timothy J. Baek e8fbb8f181 refac: styling 2024-07-08 14:20:11 -07:00
Timothy J. Baek 088e7b02a9 refac: styling 2024-07-08 13:35:25 -07:00
Timothy J. Baek 4b6ee584c2 fix: alembic 2024-07-08 12:55:27 -07:00
Timothy J. Baek d3ef3a7494 refac 2024-07-08 12:42:52 -07:00
Timothy J. Baek 68d775e1ab chore: rm print 2024-07-08 12:08:27 -07:00
Timothy J. Baek 40abddff9a fix: font 2024-07-08 12:05:16 -07:00
Timothy J. Baek 68d6e738ac fix 2024-07-08 12:01:55 -07:00
Timothy J. Baek 3ddd88dad7 chore: rm print 2024-07-08 12:00:09 -07:00
Timothy J. Baek 42742d03d7 fix: model update 2024-07-08 11:58:36 -07:00
Timothy J. Baek 87f656b029 fix: tools update 2024-07-08 11:46:37 -07:00
Timothy Jaeryang Baek 489ef9b731 Merge pull request #3705 from open-webui/dependabot/pip/backend/dev/langfuse-2.38.0
chore(deps): bump langfuse from 2.36.2 to 2.38.0 in /backend
2024-07-08 11:36:05 -07:00
Timothy Jaeryang Baek 51ae387d59 Merge pull request #3708 from open-webui/dependabot/pip/backend/dev/peewee-3.17.6
chore(deps): bump peewee from 3.17.5 to 3.17.6 in /backend
2024-07-08 11:35:55 -07:00
Timothy Jaeryang Baek 642c5e035d Merge branch 'dev' into dependabot/pip/backend/dev/peewee-3.17.6 2024-07-08 11:35:48 -07:00
Timothy Jaeryang Baek 18962104a2 Merge pull request #3707 from open-webui/dependabot/pip/backend/dev/opencv-python-headless-4.10.0.84
chore(deps): bump opencv-python-headless from 4.9.0.80 to 4.10.0.84 in /backend
2024-07-08 11:35:21 -07:00
Timothy Jaeryang Baek 9eb0f89db8 Merge pull request #3706 from open-webui/dependabot/pip/backend/dev/alembic-1.13.2
chore(deps): bump alembic from 1.13.1 to 1.13.2 in /backend
2024-07-08 11:35:07 -07:00
Timothy Jaeryang Baek 1b660453f0 Merge pull request #3704 from open-webui/dependabot/pip/backend/dev/pytest-approx-eq-8.2.2
chore(deps): update pytest requirement from ~=8.2.1 to ~=8.2.2 in /backend
2024-07-08 11:34:51 -07:00
Timothy J. Baek 3b27acc77e fix 2024-07-08 11:34:24 -07:00
Timothy J. Baek 95426fc6c9 refac: do not use subprocess 2024-07-08 11:27:10 -07:00
Timothy J. Baek 39d3dcd032 fix: db 2024-07-08 10:46:35 -07:00
Timothy Jaeryang Baek 36c0f7f273 Merge pull request #3713 from jonathan-rohde/fix/missing-commits
fix: commit document delete
2024-07-08 10:22:40 -07:00
Jonathan Rohde 3b112375ee feat(documents): commit document delete 2024-07-08 09:14:45 +02:00
Timothy Jaeryang Baek 47ec2425ee Merge pull request #3692 from cheahjs/fix/integration-test
fix: delete docker build cache in integration test
2024-07-07 23:05:15 -07:00
Timothy J. Baek d157af2fc7 chore: format 2024-07-07 23:04:50 -07:00
Timothy J. Baek c1d706dc5a fix: db issues 2024-07-07 23:01:15 -07:00
Timothy Jaeryang Baek a7fb560e84 Merge pull request #3699 from Peter-De-Ath/fix-update-user-email-auth
fix auth update_email_by_id - add db.commit
2024-07-07 22:58:20 -07:00
dependabot[bot] 44bbb40551 chore(deps): bump peewee from 3.17.5 to 3.17.6 in /backend
Bumps [peewee](https://github.com/coleifer/peewee) from 3.17.5 to 3.17.6.
- [Release notes](https://github.com/coleifer/peewee/releases)
- [Changelog](https://github.com/coleifer/peewee/blob/master/CHANGELOG.md)
- [Commits](https://github.com/coleifer/peewee/compare/3.17.5...3.17.6)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-07-08 03:11:22 +00:00
dependabot[bot] d9a393eae1 chore(deps): bump opencv-python-headless in /backend
Bumps [opencv-python-headless](https://github.com/opencv/opencv-python) from 4.9.0.80 to 4.10.0.84.
- [Release notes](https://github.com/opencv/opencv-python/releases)
- [Commits](https://github.com/opencv/opencv-python/commits)

---
updated-dependencies:
- dependency-name: opencv-python-headless
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-07-08 03:11:12 +00:00
dependabot[bot] 842a6a6ee0 chore(deps): bump alembic from 1.13.1 to 1.13.2 in /backend
Bumps [alembic](https://github.com/sqlalchemy/alembic) from 1.13.1 to 1.13.2.
- [Release notes](https://github.com/sqlalchemy/alembic/releases)
- [Changelog](https://github.com/sqlalchemy/alembic/blob/main/CHANGES)
- [Commits](https://github.com/sqlalchemy/alembic/commits)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-07-08 03:11:10 +00:00
dependabot[bot] 404b47274e chore(deps): bump langfuse from 2.36.2 to 2.38.0 in /backend
Bumps [langfuse](https://github.com/langfuse/langfuse) from 2.36.2 to 2.38.0.
- [Release notes](https://github.com/langfuse/langfuse/releases)
- [Commits](https://github.com/langfuse/langfuse/commits/v2.38.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-07-08 03:11:06 +00:00
dependabot[bot] 8eb03c02da 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.1...8.2.2)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-07-08 03:11:04 +00:00
Peter De-Ath 1bb2724282 fix password update in AuthsTable model 2024-07-07 22:27:26 +01:00
Peter De-Ath f971ba0c0b fix email update in AuthsTable model 2024-07-07 22:07:12 +01:00
Timothy J. Baek d51e866505 fix: db 2024-07-07 09:09:09 -07:00
Timothy Jaeryang Baek 068dff6aff Merge pull request #3691 from robertpiosik/replace-deafult-font-to-inter
feat: Replace default UI font, Arimo to Inter
2024-07-07 09:01:22 -07:00
Timothy Jaeryang Baek 7e6c5193d6 Merge pull request #3688 from leobenkel/no-trace-when-success
fix: Remove the tracestack when the collection already exists
2024-07-07 09:00:23 -07:00
Timothy Jaeryang Baek a6dd62082f Merge pull request #3679 from que-nguyen/dev
Updated the missing Vietnamese translation.
2024-07-07 08:59:57 -07:00
Jun Siang Cheah 4ae0a1f3f8 fix: delete docker build cache in integration test 2024-07-07 12:06:42 +01:00
Robert Piosik 21b8072f35 Replace default UI font, Arimo to Inter 2024-07-07 11:27:04 +02:00
Leo Benkel a73a9c7310 Remove the tracestack when the collection already exists 2024-07-06 23:20:41 +02:00
Que Nguyen 08f0a19812 Updated the missing Vietnamese translation. 2024-07-06 22:12:50 +07:00
Timothy J. Baek c9b85bd4a2 fix: delete 2024-07-06 08:10:58 -07:00
Timothy J. Baek 0d70d7c9ac chore: allow more headroom for migration tests 2024-07-05 23:58:13 -07:00
Timothy J. Baek e8ed48bd7a chore: disable backend tests 2024-07-05 23:54:03 -07:00
Timothy J. Baek d5716ae751 chore: format 2024-07-05 23:48:53 -07:00
Timothy Jaeryang Baek 8f6f76682b Merge pull request #3595 from open-webui/dev-migration
feat: db migration
2024-07-05 23:47:01 -07:00
Timothy Jaeryang Baek 4e75150174 Merge pull request #3669 from open-webui/dev-migration-session
dev
2024-07-05 23:39:29 -07:00
Timothy J. Baek 1436bb7c61 enh: handle peewee migration 2024-07-05 23:38:53 -07:00
Timothy Jaeryang Baek d60f06608e Merge pull request #3668 from open-webui/dev
dev
2024-07-05 22:29:13 -07:00
Timothy Jaeryang Baek 97a8491866 Merge pull request #3631 from ricky-davis/CustomCollapsible
feat: Custom Collapsible component
2024-07-05 21:42:54 -07:00
Timothy J. Baek 73899e1c0d refac 2024-07-05 21:37:29 -07:00
Timothy Jaeryang Baek 70efbef0c7 Merge pull request #3636 from Peter-De-Ath/add-scroll-to-settings-tabs
fix: add scroll to admin/user settings tabs
2024-07-05 21:28:17 -07:00
Timothy J. Baek c3c15cbb7e refac 2024-07-05 21:27:59 -07:00
Timothy Jaeryang Baek 89e6044bdb Merge pull request #3657 from aleixdorca/dev
i18n: Update Catalan Translation
2024-07-05 21:21:21 -07:00
Timothy Jaeryang Baek 3928ac1905 Merge pull request #3615 from michaelpoluektov/citations-event
feat: Citations event via __event_emitter__
2024-07-05 21:20:40 -07:00
Timothy J. Baek 983fe4b2bc refac 2024-07-05 21:20:27 -07:00
aleix 8fd128ef1c i18n: Update Catalan Translation 2024-07-05 21:02:38 +02:00
Timothy Jaeryang Baek 45fae65ec1 Merge pull request #3630 from KarlLee830/translate
i18n: Update Chinese translation
2024-07-04 23:14:24 -07:00
Peter De-Ath 8381346378 enh: add sideways scrolling to settings tabs container 2024-07-05 02:05:59 +01:00
Timothy J. Baek 67c2ab006d fix: pipe custom model 2024-07-04 13:41:18 -07:00
Michael Poluektov 55b7c30028 simplify citation API 2024-07-04 18:50:09 +01:00
rdavis ca3f8e6cb5 chore: format 2024-07-04 15:18:21 +00:00
Karl Lee f611533764 i18n: Update Chinese translation 2024-07-04 22:57:32 +08:00
rdavis 78ba18a680 refactor: Update Collapsible component to include dynamic margin for open state 2024-07-04 14:55:48 +00:00
rdavis db58bb5f0f refactor: Removed dependency 2024-07-04 14:15:16 +00:00
rdavis d5c0876a0b refactor: fixed new Collapsible Component to allow passed in classes
chore: format
2024-07-04 14:02:26 +00:00
rdavis 2389c36a70 refactor: Update WebSearchResults.svelte to use new CollapsibleComponent 2024-07-04 13:55:37 +00:00
rdavis d20601dc47 feat: Add custom Collapsible component for collapsible content 2024-07-04 13:53:28 +00:00
Michael Poluektov 0527755600 use data field 2024-07-04 12:21:09 +01:00
Timothy J. Baek 740b6f5c17 fix: pull model 2024-07-04 00:42:18 -07:00
Timothy J. Baek 9a6cbafdef fix: user valves 2024-07-04 00:37:05 -07:00
Timothy J. Baek 8b13755d56 Update auths.py 2024-07-04 00:25:45 -07:00
Timothy J. Baek 8fe2a7bb75 fix 2024-07-03 23:39:16 -07:00
Timothy J. Baek 37a5d2c06b Update db.py 2024-07-03 23:32:46 -07:00
Timothy J. Baek 864646094e refac 2024-07-03 23:32:39 -07:00
Timothy J. Baek 1b65df3acc revert 2024-07-03 21:28:51 -07:00
Timothy J. Baek bfc53b49fd revert 2024-07-03 21:28:14 -07:00
Timothy J. Baek f6dcffab13 fix: pinned chat delete issue 2024-07-03 21:18:40 -07:00
Timothy J. Baek 15f6f7bd15 revert: peewee migrations 2024-07-03 21:12:16 -07:00
Timothy Jaeryang Baek 0d78b63805 Merge pull request #3621 from open-webui/dev
dev
2024-07-03 20:59:14 -07:00
Timothy Jaeryang Baek 3efb8ab6ba Merge pull request #3617 from moblangeois/improve-fr-ca-translations
i18n: Improve French (FR) and French Canadian (CA) translations
2024-07-03 20:40:44 -07:00
Morgan Blangeois f5a33ed3f9 Merge remote-tracking branch 'upstream/dev' into improve-fr-ca-translations 2024-07-04 00:56:40 +02:00
Timothy Jaeryang Baek 08c024d752 Merge pull request #3569 from Semihal/custom-openid-claims
feat: Custom claims for OAuth
2024-07-03 15:56:37 -07:00
Timothy J. Baek c83704d6ca refac: task flag
Co-Authored-By: Michael Poluektov <78477503+michaelpoluektov@users.noreply.github.com>
2024-07-03 15:46:56 -07:00
Morgan Blangeois f548762892 Resolve merge conflicts in French translations 2024-07-04 00:40:56 +02:00
Timothy Jaeryang Baek d0e0aba593 Merge pull request #3611 from bannert1337/i18n-de_DE
i18n(de_DE): added translations for new entries, updated old entries
2024-07-03 15:39:37 -07:00
Michael Poluektov 4e433d9015 wip: citations via __event_emitter__ 2024-07-03 18:18:33 +01:00
Morgan Blangeois 2fedd91ed9 feat: Improve French Canadian (fr-ca) translations 2024-07-03 17:20:57 +02:00
Morgan Blangeois 3cd0b1077d i18n: Improve French translations 2024-07-03 17:09:53 +02:00
bannert 1f026a1811 i18n(de_DE): added translations for new entries, updated old entries 2024-07-03 15:16:54 +02:00
Michael Poluektov 49b4211c06 Merge branch 'dev' of https://github.com/open-webui/open-webui into dev 2024-07-03 10:07:46 +01:00
Michael Poluektov 24c7990fd4 revert not delete on pipe 2024-07-03 09:12:00 +01:00
Timothy J. Baek 4d23957035 revert: model_validate 2024-07-02 21:56:32 -07:00
Timothy J. Baek aa88022624 fix: functions 2024-07-02 21:50:53 -07:00
Timothy J. Baek 44a9b86eec fix: functions 2024-07-02 21:46:56 -07:00
Timothy J. Baek 647aa1966f chore: format 2024-07-02 16:51:30 -07:00
Timothy Jaeryang Baek d0e89a0318 Merge pull request #3327 from jonathan-rohde/feat/sqlalchemy-instead-of-peewee
BREAKING CHANGE/sqlalchemy instead of peewee
2024-07-02 16:40:13 -07:00
Timothy Jaeryang Baek 2c061777ca Merge pull request #3591 from michaelpoluektov/fix-banners-env
fix: fix WEBUI_BANNERS environment variable not working
2024-07-02 16:22:06 -07:00
Timothy J. Baek f57a435576 chore: bump 2024-07-02 16:08:03 -07:00
Timothy J. Baek cf317f8fdf fix 2024-07-02 15:20:50 -07:00
Michael Poluektov 655238dcd7 banners: generic exception 2024-07-02 14:41:59 +01:00
Michael Poluektov 16fa454558 fix banners env 2024-07-02 14:17:36 +01:00
Michael Poluektov 02f242e9e8 keep title, task, function tags for pipelines 2024-07-02 11:52:46 +01:00
Michael Poluektov 09514751b5 Update main.py 2024-07-02 10:57:56 +01:00
Michael Poluektov afd74213cc Update main.py 2024-07-02 10:20:50 +01:00
Timothy J. Baek e4c85921b2 fix: styling 2024-07-02 01:16:57 -07:00
Timothy J. Baek 7725a877d6 enh: get_last_user_message_item 2024-07-02 00:37:21 -07:00
Timothy J. Baek fefa8a81d0 chore: format 2024-07-01 23:12:38 -07:00
Timothy J. Baek 05ec71beb9 enh: pinned chats support 2024-07-01 23:08:01 -07:00
Timothy J. Baek 439ab7a335 refac 2024-07-01 22:18:45 -07:00
Timothy J. Baek 044a0fbbbe enh: bool, literal valves 2024-07-01 22:14:09 -07:00
Timothy J. Baek d97a4d687e refac 2024-07-01 21:41:44 -07:00
Timothy J. Baek 52cae406b4 feat: chat event handler 2024-07-01 20:53:24 -07:00
Timothy J. Baek d6dbd73ec9 fix 2024-07-01 20:15:27 -07:00
Timothy J. Baek a07051f51b feat: __event_emitter__ 2024-07-01 20:05:02 -07:00
Timothy J. Baek e5895af7a0 refac 2024-07-01 19:37:54 -07:00
Timothy J. Baek c7a9b5ccfa refac: chat completion middleware 2024-07-01 19:33:58 -07:00
Timothy J. Baek b62d2a9b28 refac 2024-07-01 17:15:10 -07:00
Timothy J. Baek a392865615 refac 2024-07-01 17:11:09 -07:00
Timothy Jaeryang Baek 3c1ea24374 Merge pull request #3582 from nickovs/tika-document-text
feat: Support Tika for document text extraction
2024-07-01 17:07:40 -07:00
Timothy J. Baek 62ba6a2413 chore: format 2024-07-01 16:52:46 -07:00
Timothy J. Baek b875efab1f enh: validate required open webui version 2024-07-01 16:50:35 -07:00
Timothy J. Baek bd45b7a04b fix 2024-07-01 16:11:44 -07:00
Timothy J. Baek 9c01297191 fix 2024-07-01 16:11:24 -07:00
Timothy J. Baek 6d350fb8bc revert: text split 2024-07-01 16:04:24 -07:00
Timothy J. Baek 7212b7c8aa chore: bump 2024-07-01 15:49:48 -07:00
Timothy J. Baek 9cc46629c2 refac 2024-07-01 15:43:19 -07:00
Timothy Jaeryang Baek a03629178e Merge pull request #3568 from fishhf/dev
fix: use AIOHTTP_CLIENT_TIMEOUT timeout setting for openai streaming response
2024-07-01 14:02:13 -07:00
Timothy Jaeryang Baek 46a084d457 Merge pull request #3580 from bannert1337/i18n--optimize-de_DE
i18n: rework de_DE locale
2024-07-01 13:29:06 -07:00
Nicko van Someren 7aa35a3757 Added HTML and Typescript UI components to support configration of text extraction engine.
Updated RAG /config and /config/update endpoints to support UI updates.

Fixed .dockerignore to prevent Python venv from being copied into Docker image.
2024-07-01 12:10:59 -06:00
bannert 7f937af029 i18n(de_DE): Update German translation strings for better grammar and clarity 2024-07-01 19:01:22 +02:00
bannert 4041484a19 i18n(de-DE): Improve translation consistency and accuracy 2024-07-01 18:55:28 +02:00
bannert 8a38b0a286 feat(config): exclude .vscode/settings.json from version control 2024-07-01 18:52:19 +02:00
bannert 19d4c7f6cd Update languages.json 2024-07-01 18:42:56 +02:00
bannert 4ead3dabd8 Delete src/lib/i18n/locales/de-DE/translation-common.json 2024-07-01 18:41:56 +02:00
bannert 77f5d90be3 Delete src/lib/i18n/locales/de-DE/translation-informal.json 2024-07-01 18:41:49 +02:00
bannert db495a1df0 refactor: Revert changes to i18n/locales/languages.json 2024-07-01 18:40:57 +02:00
bannert 8634140306 refactor: Remove changes to i18n/index.ts 2024-07-01 18:38:52 +02:00
bannert 586de1f5ba feat(i18n): unify German translations and remove formal/informal versions 2024-07-01 18:31:13 +02:00
Jonathan Rohde 2aecd7d0b9 Merge branch 'refs/heads/dev' into feat/sqlalchemy-instead-of-peewee
# Conflicts:
#	backend/requirements.txt
2024-07-01 10:37:56 +02:00
Sergey Mihaylin a94c7e5c09 fix lint 2024-07-01 10:36:21 +03:00
Sergey Mihaylin e475f025b7 fix: merge request fail (remove picture_claim) 2024-07-01 10:25:25 +03:00
Sergey Mihaylin 6e934c2d17 Merge branch 'refs/heads/main' into custom-openid-claims
# Conflicts:
#	backend/main.py
2024-07-01 10:08:38 +03:00
Fish Lung f89fa061e8 fix: use AIOHTTP_CLIENT_TIMEOUT timeout setting for openai streaming response 2024-07-01 14:29:26 +08:00
Timothy Jaeryang Baek f3c1ff9efc Merge pull request #3564 from wanderingmeow/fix-regexp-safari-before-16.4
fix: RegExp "invalid group specifier name" exception on Safari < 16.4
2024-06-30 22:40:53 -07:00
Timothy J. Baek 4547afe0b9 chore: bump 2024-06-30 22:29:03 -07:00
Timothy J. Baek 7955c9ba3b refac 2024-06-30 22:28:43 -07:00
Timothy Jaeryang Baek c52e16a844 Merge pull request #3562 from open-webui/dependabot/pip/backend/dev/sentence-transformers-3.0.1
chore(deps): bump sentence-transformers from 2.7.0 to 3.0.1 in /backend
2024-06-30 19:58:38 -07:00
Timothy Jaeryang Baek c676a78cd7 Merge pull request #3543 from open-webui/dependabot/pip/backend/dev/openpyxl-3.1.5
chore(deps): bump openpyxl from 3.1.2 to 3.1.5 in /backend
2024-06-30 19:57:20 -07:00
Timothy Jaeryang Baek 61b2bcae47 Merge pull request #3563 from open-webui/dependabot/pip/backend/dev/langchain-community-0.2.6
chore(deps): bump langchain-community from 0.2.0 to 0.2.6 in /backend
2024-06-30 19:57:10 -07:00
Timothy Jaeryang Baek ee59cbc634 Merge pull request #3561 from open-webui/dependabot/pip/backend/dev/requests-2.32.3
chore(deps): bump requests from 2.32.2 to 2.32.3 in /backend
2024-06-30 19:56:53 -07:00
Timothy Jaeryang Baek 0c552b4730 Merge pull request #3560 from open-webui/dependabot/pip/backend/dev/langfuse-2.36.2
chore(deps): bump langfuse from 2.33.0 to 2.36.2 in /backend
2024-06-30 19:56:45 -07:00
WanderingMeow de3d49000e fix: RegExp "invalid group specifier name" exception on Safari < 16.4 (#3306, #3371) 2024-07-01 10:42:55 +08:00
dependabot[bot] 8c7fb0312c chore(deps): bump langchain-community from 0.2.0 to 0.2.6 in /backend
Bumps [langchain-community](https://github.com/langchain-ai/langchain) from 0.2.0 to 0.2.6.
- [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.6)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-07-01 02:30:30 +00:00
dependabot[bot] 0ee984b444 chore(deps): bump sentence-transformers from 2.7.0 to 3.0.1 in /backend
Bumps [sentence-transformers](https://github.com/UKPLab/sentence-transformers) from 2.7.0 to 3.0.1.
- [Release notes](https://github.com/UKPLab/sentence-transformers/releases)
- [Commits](https://github.com/UKPLab/sentence-transformers/compare/v2.7.0...v3.0.1)

---
updated-dependencies:
- dependency-name: sentence-transformers
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-07-01 02:30:25 +00:00
dependabot[bot] 2fadc0c68f chore(deps): bump requests from 2.32.2 to 2.32.3 in /backend
Bumps [requests](https://github.com/psf/requests) from 2.32.2 to 2.32.3.
- [Release notes](https://github.com/psf/requests/releases)
- [Changelog](https://github.com/psf/requests/blob/main/HISTORY.md)
- [Commits](https://github.com/psf/requests/compare/v2.32.2...v2.32.3)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-07-01 02:30:22 +00:00
dependabot[bot] 3f31bb0975 chore(deps): bump langfuse from 2.33.0 to 2.36.2 in /backend
Bumps [langfuse](https://github.com/langfuse/langfuse) from 2.33.0 to 2.36.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-01 02:30:20 +00:00
Timothy Jaeryang Baek 5d1573fd1b Merge pull request #3557 from Naufal05R/patch-1
i18n: Update Indonesian translation
2024-06-30 17:46:56 -07:00
Timothy Jaeryang Baek 5c6e30cb5d Merge pull request #3558 from cheahjs/refac/reduce-startup-mem-usage
refac: reduce startup memory usage
2024-06-30 17:46:27 -07:00
Timothy J. Baek a55d6e6077 fix: compare message 2024-06-30 17:45:28 -07:00
Jun Siang Cheah a48ac6a209 refac: lazily load sentence_transformers to reduce start up memory usage 2024-07-01 08:13:56 +08:00
Jun Siang Cheah 17c684369e refac: lazily load faster_whisper to reduce start up memory usage 2024-07-01 08:13:02 +08:00
Naufal Rabbani 11210fb6e4 i18n: Update Indonesian translation 2024-07-01 06:48:57 +07:00
Timothy Jaeryang Baek 824966ad4a Merge pull request #3555 from open-webui/dev
fix: default locale
2024-06-30 14:53:27 -07:00
Timothy J. Baek f77073410e refac 2024-06-30 14:52:18 -07:00
Nicko van Someren 9cf622d981 Added support for using Apache Tika as a document loader.
Added persistent configuration options to configure use and location of Tika service.

Updated backend.apps.rag.main:get_loader() to make use of Tika document loader.
2024-06-30 15:49:15 -06:00
Timothy J. Baek d9a229b1ec fix: default locale 2024-06-30 14:48:05 -07:00
Timothy Jaeryang Baek 7bc88eb00d Merge pull request #3549 from open-webui/dev
refac: ollama non streaming response
2024-06-30 01:40:26 -07:00
Timothy J. Baek f6efda9e2f refac: ollama non stream response 2024-06-30 01:30:19 -07:00
Timothy Jaeryang Baek bb53282cb0 Merge pull request #3542 from open-webui/dependabot/pip/backend/dev/duckduckgo-search-approx-eq-6.1.7
chore(deps): update duckduckgo-search requirement from ~=6.1.5 to ~=6.1.7 in /backend
2024-06-29 20:44:19 -07:00
Timothy Jaeryang Baek c933865172 Merge pull request #3541 from open-webui/dependabot/pip/backend/dev/langchain-chroma-0.1.2
chore(deps): bump langchain-chroma from 0.1.1 to 0.1.2 in /backend
2024-06-29 20:43:56 -07:00
Timothy Jaeryang Baek ce990d3859 Merge pull request #3539 from open-webui/dependabot/pip/backend/dev/chromadb-0.5.3
chore(deps): bump chromadb from 0.5.0 to 0.5.3 in /backend
2024-06-29 20:43:46 -07:00
Timothy Jaeryang Baek 315c8b2af3 Merge pull request #3540 from open-webui/dependabot/pip/backend/dev/python-socketio-5.11.3
chore(deps): bump python-socketio from 5.11.2 to 5.11.3 in /backend
2024-06-29 20:43:31 -07:00
Timothy Jaeryang Baek 66182bac75 Merge pull request #3546 from open-webui/dev
refac: language detection
2024-06-29 20:41:32 -07:00
Timothy J. Baek 5ee7da54a1 refac: language detection 2024-06-29 20:41:06 -07:00
Timothy J. Baek 4b0c422ec5 chore: format 2024-06-29 20:33:10 -07:00
dependabot[bot] 5dacd41278 chore(deps): bump openpyxl from 3.1.2 to 3.1.5 in /backend
Bumps [openpyxl](https://openpyxl.readthedocs.io) from 3.1.2 to 3.1.5.

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-06-30 03:27:46 +00:00
dependabot[bot] 052c582930 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.5...v6.1.7)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-06-30 03:27:43 +00:00
dependabot[bot] 3c0ce5d1b0 chore(deps): bump langchain-chroma from 0.1.1 to 0.1.2 in /backend
Bumps [langchain-chroma](https://github.com/langchain-ai/langchain) from 0.1.1 to 0.1.2.
- [Release notes](https://github.com/langchain-ai/langchain/releases)
- [Commits](https://github.com/langchain-ai/langchain/compare/langchain-chroma==0.1.1...langchain-chroma==0.1.2)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-06-30 03:27:39 +00:00
dependabot[bot] 9d1aa4aebe chore(deps): bump python-socketio from 5.11.2 to 5.11.3 in /backend
Bumps [python-socketio](https://github.com/miguelgrinberg/python-socketio) from 5.11.2 to 5.11.3.
- [Release notes](https://github.com/miguelgrinberg/python-socketio/releases)
- [Changelog](https://github.com/miguelgrinberg/python-socketio/blob/main/CHANGES.md)
- [Commits](https://github.com/miguelgrinberg/python-socketio/compare/v5.11.2...v5.11.3)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-06-30 03:27:34 +00:00
dependabot[bot] e540f8a4e4 chore(deps): bump chromadb from 0.5.0 to 0.5.3 in /backend
Bumps [chromadb](https://github.com/chroma-core/chroma) from 0.5.0 to 0.5.3.
- [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.0...0.5.3)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-06-30 03:27:27 +00:00
Timothy Jaeryang Baek 4900ac5136 Merge pull request #3536 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.7
2024-06-29 20:26:59 -07:00
Timothy J. Baek 58d8dd94fc chore: bump 2024-06-29 20:26:51 -07:00
Timothy J. Baek df8d1dacc4 refac: browser language detection 2024-06-29 20:04:04 -07:00
Timothy Jaeryang Baek 56137acd94 Merge pull request #3507 from open-webui/dependabot/pip/authlib-1.3.1
chore(deps): bump authlib from 1.3.0 to 1.3.1
2024-06-29 19:52:43 -07:00
Timothy J. Baek 41c55fbb81 enh: boilerplate 2024-06-29 19:49:16 -07:00
Timothy J. Baek 16454c0de5 chore: format 2024-06-29 19:47:23 -07:00
Timothy J. Baek d0567832cc fix 2024-06-29 19:40:54 -07:00
bannert 9c2313a1bd feat(i18n): improve init function with cleaner code and nullish coalescing operator
- Use object destructuring for clearer variable assignment.
   - Utilize the nullish coalescing operator to simplify ternary expressions.
   - Refactored the import statement to remove duplication.
2024-06-30 02:44:03 +02:00
Timothy Jaeryang Baek 61972ca97d Merge pull request #3519 from Naufal05R/main
i18n: Add Indonesian translate
2024-06-29 13:17:12 -07:00
Timothy Jaeryang Baek 73135b6e51 Merge pull request #3511 from Peter-De-Ath/ollama-chat-no-stream
fix: isInstance of streamingResponse but content-type is application/json
2024-06-29 13:16:32 -07:00
Timothy Jaeryang Baek 27709c835c Merge pull request #3497 from aguvener/dev
i18n: tr_TR translation updated with the latest changes
2024-06-29 13:15:59 -07:00
Timothy Jaeryang Baek d3a67b44cd Merge pull request #3499 from Semihal/fix-oauth-openid
fix: First OIDC account is not admin
2024-06-29 13:15:42 -07:00
Timothy Jaeryang Baek 58398b6eeb Merge pull request #3514 from KarlLee830/translate
i18n: Update Chinese translation
2024-06-29 13:14:35 -07:00
Timothy Jaeryang Baek 98ae064cd0 Merge pull request #3525 from aleixdorca/dev
i18n: Updated Catalan Translations
2024-06-29 13:14:23 -07:00
Aleix Dorca 776082dc87 i18n: Updated Catalan Translation 2024-06-29 20:34:31 +02:00
aleix 6e03a1c168 i18n: Updated Catalan Translation 2024-06-29 17:25:11 +02:00
aleix 45cb1c0fcb Updated Catalan Translation 2024-06-29 17:21:03 +02:00
Naufal Rabbani 5b848dcf54 added indonesian language 2024-06-29 16:25:42 +07:00
Naufal Rabbani 2ef899220a added indonesian language 2024-06-29 16:18:45 +07:00
Karl Lee 8ab4b7ab47 i18n: Update Chinese translation 2024-06-29 04:22:04 +08:00
Peter De-Ath 269db0748a fix: isInstance of streamingResponse but content-type is application/json 2024-06-28 20:32:07 +01:00
bannert 6ffa189814 i18n: split locale for German formal and informal
translation-common.json is the base
translation-formal.json and translation-informal.json extend the base
2024-06-28 20:03:36 +02:00
bannert 24f84a6069 feat(i18n): fixed a word in German translation 2024-06-28 19:19:45 +02:00
bannert c138445944 feat(i18n): update and optimize de_DE 2024-06-28 19:02:50 +02:00
dependabot[bot] 4d3e1ffbe6 chore(deps): bump authlib from 1.3.0 to 1.3.1
Bumps [authlib](https://github.com/lepture/authlib) from 1.3.0 to 1.3.1.
- [Release notes](https://github.com/lepture/authlib/releases)
- [Changelog](https://github.com/lepture/authlib/blob/master/docs/changelog.rst)
- [Commits](https://github.com/lepture/authlib/compare/v1.3.0...v1.3.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-06-28 16:53:45 +00:00
Sergey Mihaylin 9f32e9ef60 fix username claim 2024-06-28 17:08:32 +03:00
Sergey Mihaylin 0c3f9a16e3 custom env for set custom claims for openid 2024-06-28 16:31:40 +03:00
Sergey Mihaylin 7d10dacad6 Fix: set jwt_token to cookie (instead of token from OIDC) 2024-06-28 16:20:57 +03:00
Sergey Mihaylin 57c330576d Fix: admin role for OIDC signup for first user 2024-06-28 16:20:34 +03:00
aguvener 3cc5da835c Update translation.json for minor change 2024-06-28 13:18:44 +03:00
aguvener 9111970580 Update translation.json 2024-06-28 13:06:50 +03:00
Jonathan Rohde 5391f4c1f7 feat(sqlalchemy): add new column 2024-06-28 09:21:07 +02:00
Jonathan Rohde df47c496d3 Merge branch 'refs/heads/dev' into feat/sqlalchemy-instead-of-peewee
# Conflicts:
#	backend/apps/webui/models/functions.py
#	backend/apps/webui/routers/chats.py
2024-06-28 09:19:56 +02:00
Timothy Jaeryang Baek 24b638fcde Merge pull request #3486 from open-webui/dev
fix: trusted sign in
2024-06-27 21:44:54 -07:00
Timothy J. Baek cd9170ed24 fix: trusted sign in 2024-06-27 21:44:35 -07:00
Timothy Jaeryang Baek f7da94ff85 Merge pull request #3485 from open-webui/dev
fix: WEBUI_AUTH
2024-06-27 21:43:51 -07:00
Timothy J. Baek feba50f68c fix: WEBUI_AUTH 2024-06-27 21:43:19 -07:00
Timothy Jaeryang Baek b0724811df Merge pull request #3477 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
fix: changelog
2024-06-27 13:38:44 -07:00
Timothy J. Baek 1dd54ced41 fix: changelog 2024-06-27 13:38:28 -07:00
Timothy Jaeryang Baek 1eebb85f48 Merge pull request #3323 from open-webui/dev
0.3.6
2024-06-27 13:36:02 -07:00
Timothy J. Baek b224ba0030 refac: styling 2024-06-27 13:15:54 -07:00
Timothy J. Baek 46ae25826d chore: format 2024-06-27 13:13:16 -07:00
Timothy J. Baek c954f1b4de refac 2024-06-27 13:12:37 -07:00
Timothy J. Baek edbd07f893 feat: global filter 2024-06-27 13:04:12 -07:00
Timothy J. Baek c8c85ba7fc refac 2024-06-27 12:16:55 -07:00
Timothy J. Baek c262d9ad4f doc: changelog 2024-06-27 11:30:30 -07:00
Timothy J. Baek 3f5f410453 refac 2024-06-27 11:29:59 -07:00
Timothy J. Baek 3c7f45ced4 fix 2024-06-27 11:12:07 -07:00
Timothy J. Baek c07da8d1f3 chore: format 2024-06-27 10:56:00 -07:00
Timothy Jaeryang Baek d3b9f03bdb Merge pull request #3470 from djismgaming/dev
**i18n** Spanish translation updates
2024-06-27 10:51:21 -07:00
Timothy Jaeryang Baek 433abe5a25 Merge pull request #3471 from KarlLee830/translate 2024-06-27 10:37:32 -07:00
Karl Lee b59684328d i18n: Update Chinese translation 2024-06-27 20:59:43 +08:00
Ismael 850d4aac70 additional Spanish strings updated 2024-06-27 07:33:38 -04:00
Ismael bae87ec4ae Update translation.json
Spanish translation updates
2024-06-27 07:08:03 -04:00
Timothy J. Baek a5138b7621 doc: changelog 2024-06-27 03:24:16 -07:00
Timothy J. Baek 4e6d165d00 refac 2024-06-27 00:43:31 -07:00
Jonathan Rohde 827b1e58e9 feat(sqlalchemy): execute tests in github actions 2024-06-27 07:48:31 +02:00
Jonathan Rohde 23e4d9daff feat(sqlalchemy): formatting 2024-06-27 07:48:26 +02:00
Jonathan Rohde d4b6b7c4e8 feat(sqlalchemy): reverted not needed api change 2024-06-27 07:48:08 +02:00
Jonathan Rohde 642c352c69 feat(sqlalchemy): rebase 2024-06-27 07:48:08 +02:00
Jonathan Rohde d88bd51e3c feat(sqlalchemy): format backend 2024-06-27 07:48:08 +02:00
Jonathan Rohde 2fb27adbf6 feat(sqlalchemy): add missing file 2024-06-27 07:48:08 +02:00
Jonathan Rohde 8f939cf55b feat(sqlalchemy): some fixes 2024-06-27 07:48:08 +02:00
Jonathan Rohde a9b148791d feat(sqlalchemy): fix wrong column types 2024-06-27 07:48:08 +02:00
Jonathan Rohde da403f3e3c feat(sqlalchemy): use session factory instead of context manager 2024-06-27 07:48:08 +02:00
Jonathan Rohde eb01e8d275 feat(sqlalchemy): use scoped session 2024-06-27 07:48:08 +02:00
Jonathan Rohde c134eab27a feat(sqlalchemy): format backend 2024-06-27 07:48:08 +02:00
Jonathan Rohde 320e658595 feat(sqlalchemy): cleanup fixes 2024-06-27 07:48:08 +02:00
Jonathan Rohde 070d9083d5 feat(sqlalchemy): use subprocess to do migrations 2024-06-27 07:48:08 +02:00
Jonathan Rohde bee835cb65 feat(sqlalchemy): remove session reference from router 2024-06-27 07:48:08 +02:00
Jonathan Rohde df09d0830a feat(sqlalchemy): Replace peewee with sqlalchemy 2024-06-27 07:48:08 +02:00
Timothy J. Baek 8dac2a2140 refac 2024-06-26 10:22:31 -07:00
Timothy Jaeryang Baek aad23af3a3 Merge pull request #3456 from SimonOriginal/dev
i18n: Update of the Ukrainian translation
2024-06-26 09:47:26 -07:00
SimonOriginal 755850e453 add translation for new keys 2024-06-26 17:17:48 +02:00
Timothy J. Baek 581e7800fa fix 2024-06-26 00:36:35 -07:00
Timothy J. Baek 0ec4d56593 fix 2024-06-26 00:23:24 -07:00
Timothy J. Baek 0e7db89117 feat: import/export community 2024-06-26 00:22:26 -07:00
Timothy J. Baek bc5e39d31b chore: format 2024-06-25 16:31:33 -07:00
Timothy J. Baek 6cd47a3c9b refac 2024-06-25 16:30:49 -07:00
Timothy Jaeryang Baek 70c985feec Merge pull request #3447 from ben-z/stt-model-configurable
fix: Make STT model configurable
2024-06-25 15:26:57 -07:00
Ben Zhang 044bd00386 Make STT model configurable 2024-06-25 21:46:12 +00:00
Timothy Jaeryang Baek b0ed8ded48 Merge pull request #3437 from silentoplayz/patch-1
feat: case insensitive prompt/models selection chat input
2024-06-25 12:55:08 -07:00
Timothy J. Baek 8bdbd2f9fb fix: rm conversationMode 2024-06-25 12:27:00 -07:00
silentoplayz ef619321ce Update Models.svelte
feat: case-insensitive @model-name search
2024-06-25 18:22:38 +00:00
silentoplayz 01ad197973 Update PromptCommands.svelte
feat: case-insensitive prompt search
2024-06-25 18:19:52 +00:00
Timothy J. Baek a92c5381fb chore: format 2024-06-25 08:51:52 -07:00
Timothy Jaeryang Baek 3750e69e09 Merge pull request #3418 from cheahjs/feat/hide-tokens-in-ui
feat: hide all API keys by default in admin settings
2024-06-25 08:40:18 -07:00
Timothy Jaeryang Baek 771ac86163 Merge pull request #3417 from ricky-davis/addDeleteConfirmationToolsFunctions
feat: Added delete confirmation dialogs for Prompts, Tools, and Functions.
2024-06-25 08:18:36 -07:00
Timothy J. Baek 36e88d479b chore: format 2024-06-25 08:01:05 -07:00
Timothy J. Baek c147147001 chore: format 2024-06-25 08:00:32 -07:00
rdavis 4d5e161a3e Updated Functions and Tools to use existing selected* Variable
Applied formatting
2024-06-25 08:28:43 -05:00
Jun Siang Cheah f8f6943128 refac: use new SensitiveInput component 2024-06-25 20:15:29 +08:00
Jun Siang Cheah d5b91fb084 feat: hide all API keys by default in admin settings 2024-06-25 19:53:22 +08:00
rdavis 263d4bf496 Added delete confirmation dialogs for Prompts, Tools, and Functions. 2024-06-24 22:50:35 -05:00
Timothy Jaeryang Baek 16a8eebd8d Merge pull request #3328 from FuturePrayer/AUTOMATIC1111_api_auth_support
feat: Supports making requests to the Automatic1111 backend when running with the --api-auth parameter
2024-06-24 19:40:55 -07:00
Timothy Jaeryang Baek d17dc59246 Merge pull request #2574 from cheahjs/feat/oauth
feat: experimental SSO support for Google, Microsoft, and OIDC
2024-06-24 19:05:58 -07:00
Timothy Jaeryang Baek 09082a070b Merge pull request #3407 from jonathan-rohde/feat/case-insensitive-tag-selection
feat/case insensitive tag selection
2024-06-24 18:52:54 -07:00
Timothy J. Baek a40d0ec3cb chore: requirements 2024-06-24 17:53:07 -07:00
Timothy J. Baek bf4dcc10af fix: modal esc 2024-06-24 17:39:43 -07:00
Timothy J. Baek 503d16f49c chore: 333
Co-Authored-By: Jannes Höke <3945220+jh0ker@users.noreply.github.com>
2024-06-24 16:06:15 -07:00
Timothy J. Baek 9504c9c9b4 fix 2024-06-24 15:32:49 -07:00
Timothy J. Baek 284ab648b6 fix 2024-06-24 15:21:42 -07:00
Timothy J. Baek 0f4f01cee9 fix 2024-06-24 14:00:48 -07:00
Timothy J. Baek 68b3cce0fd fix: query memory 2024-06-24 14:00:08 -07:00
Timothy J. Baek d361404a60 refac 2024-06-24 13:33:03 -07:00
Timothy J. Baek 465c3a9987 refac: playground styling 2024-06-24 13:11:49 -07:00
Timothy J. Baek 4c9fa6cf37 enh: pipe handling 2024-06-24 12:56:41 -07:00
Timothy J. Baek 837944dcc5 fix 2024-06-24 12:26:07 -07:00
Timothy J. Baek 9fb30d4787 fix 2024-06-24 12:23:12 -07:00
Timothy J. Baek 345b3491a7 fix: i18n 2024-06-24 12:21:19 -07:00
Timothy Jaeryang Baek b4c9e46357 Merge pull request #3408 from SimonOriginal/dev
Added i18n keys and update of Ukrainian translation
2024-06-24 11:39:04 -07:00
Timothy J. Baek f6082579c9 fix: wakelock 2024-06-24 11:29:21 -07:00
Timothy J. Baek 1c4e7f0324 refac 2024-06-24 11:17:18 -07:00
Simon 920fc29bdd Merge branch 'dev' into dev 2024-06-24 19:50:35 +02:00
SimonOriginal d9c2b6e890 Add translation 2024-06-24 19:49:29 +02:00
Timothy J. Baek 74a4f642fd refac: valves save 2024-06-24 10:47:40 -07:00
SimonOriginal e0e9fcaa24 Add keys i18n 2024-06-24 19:38:26 +02:00
Timothy J. Baek 6bad71adca fix 2024-06-24 10:37:57 -07:00
SimonOriginal fbbffc4386 Add key adn translation 2024-06-24 19:04:31 +02:00
SimonOriginal 8dc9735873 start parser i18n 2024-06-24 18:24:21 +02:00
SimonOriginal 5e9e1108a3 fix errors 2024-06-24 18:21:29 +02:00
SimonOriginal 855b5508d0 start parser 2024-06-24 18:14:19 +02:00
SimonOriginal 2ece4e7cbb Add Ukrainian translation for keys + formatin 2024-06-24 18:12:39 +02:00
SimonOriginal 665f87e84a add toasts keys i18n 2024-06-24 18:09:45 +02:00
SimonOriginal 3d1de6144e Add Ukrainian translation for keys 2024-06-24 17:25:23 +02:00
SimonOriginal 9c81d84e16 Add toasts i18n key 2024-06-24 17:18:39 +02:00
Jonathan Rohde c7855b3b9d feat(chat): formatting 2024-06-24 16:47:02 +02:00
Jonathan Rohde 09e95b8d3c feat(chat): ignore upper/lower case to select document/tag/collection 2024-06-24 16:44:44 +02:00
SimonOriginal 85b3b81617 fix key Database 2024-06-24 16:23:08 +02:00
SimonOriginal 06a337061b Add keys i18n 2024-06-24 16:19:56 +02:00
SimonOriginal a8198d7a7f Update of Ukrainian translation 2024-06-24 16:07:51 +02:00
Jun Siang Cheah 79f8620b34 fix: format 2024-06-24 18:50:55 +08:00
Jun Siang Cheah ca8c098f62 refac: update migrations to match dev 2024-06-24 18:48:28 +08:00
Jun Siang Cheah f26d80dcae Merge remote-tracking branch 'upstream/dev' into feat/oauth 2024-06-24 18:46:48 +08:00
Timothy J. Baek f54a66b86b chore: format 2024-06-23 23:31:56 -07:00
Timothy Jaeryang Baek 655ba475cc Merge pull request #3402 from josephrocca/patch-1
fix: scrollToBottom button container obstructing clicks on buttons beneath it
2024-06-23 23:30:33 -07:00
Timothy J. Baek a82154de3f refac 2024-06-23 23:15:24 -07:00
josephrocca 82b44740db Fix scrollToBottom button container obstructing clicks on buttons beneath it 2024-06-24 14:10:32 +08:00
Timothy J. Baek f33ca4c9a5 refac 2024-06-23 23:10:26 -07:00
Timothy J. Baek bc73cb1390 enh: iframe message event listener 2024-06-23 21:48:06 -07:00
Timothy J. Baek 37a052327a chore: format 2024-06-23 21:14:31 -07:00
Timothy J. Baek 51adb1c04a enh: frontmatter version 2024-06-23 21:07:22 -07:00
Timothy J. Baek 9788633ce1 refac: wording 2024-06-23 21:01:07 -07:00
Timothy J. Baek 0b8f5c2232 enh: manifest modal 2024-06-23 21:00:34 -07:00
Timothy J. Baek be2340d4ef refac: styling 2024-06-23 20:40:12 -07:00
Timothy J. Baek 5c0015cd66 fix: frontmatter 2024-06-23 20:37:41 -07:00
Timothy J. Baek abf212c28f enh: tools & functions frontmatter 2024-06-23 20:31:40 -07:00
Timothy J. Baek 8b99870189 enh: filter function priority valve support 2024-06-23 20:11:08 -07:00
Timothy J. Baek f4a2ae5eac enh: list valve 2024-06-23 19:49:48 -07:00
Timothy J. Baek 0250f69da0 fix: valves 2024-06-23 19:48:16 -07:00
Jun Siang Cheah 99e7b328a4 refac: add better logging for oauth errors 2024-06-24 10:43:53 +08:00
Timothy J. Baek 5f2d37dce5 fix: valves 2024-06-23 19:37:35 -07:00
Timothy J. Baek 2eb15ea1fc feat: SAFE_MODE 2024-06-23 19:28:33 -07:00
Timothy J. Baek ab700a16be refac: all valves input should be required 2024-06-23 19:20:09 -07:00
Timothy J. Baek 0cf936f9e8 refac 2024-06-23 19:18:13 -07:00
Timothy J. Baek 26e735618e fix: tools valves 2024-06-23 19:10:52 -07:00
Timothy J. Baek db9e5e008f refac: styling 2024-06-23 19:09:53 -07:00
Timothy Jaeryang Baek 65dbf9ba3f Merge pull request #3400 from open-webui/valves
feat: tools & functions valves
2024-06-23 19:07:32 -07:00
Timothy J. Baek dc25f44d31 fix: handle default 2024-06-23 19:05:56 -07:00
Timothy J. Baek 627705a347 feat: valves 2024-06-23 19:02:27 -07:00
Timothy J. Baek 3a629ffe00 feat: global filter 2024-06-23 18:39:27 -07:00
Timothy J. Baek d8c112d8b0 feat: function toggle support 2024-06-23 18:34:42 -07:00
Timothy J. Baek 3034f3d310 refac: styling 2024-06-23 18:06:41 -07:00
Timothy J. Baek 120b1857b2 enh: valves 2024-06-23 18:05:33 -07:00
Timothy J. Baek 58e69230a9 refac: styling 2024-06-23 17:44:31 -07:00
Timothy J. Baek fc2001da83 refac: styling 2024-06-23 17:41:59 -07:00
Timothy J. Baek 5d7de927e5 enh: valve field desc support 2024-06-23 17:34:25 -07:00
Timothy J. Baek 7cd4a3cd1a chore: requirements 2024-06-23 17:22:11 -07:00
Timothy J. Baek e52ed258f0 refac: styling 2024-06-23 17:18:36 -07:00
Timothy J. Baek 9c57402269 chore: format 2024-06-23 16:04:33 -07:00
Timothy J. Baek 7a218d77b7 refac: workspace 2024-06-23 16:03:21 -07:00
Timothy J. Baek ecf1d89a8b refac: prompts ui 2024-06-23 15:57:16 -07:00
Timothy J. Baek 57455f084b refac: functions ui 2024-06-23 15:51:19 -07:00
Timothy J. Baek 44e3d384d5 refac: tools ui 2024-06-23 15:45:17 -07:00
Timothy J. Baek 8e2c377a21 refac: extractSentences 2024-06-22 16:33:20 -07:00
Timothy J. Baek 6ee94c5e97 chore: format 2024-06-22 16:15:19 -07:00
Timothy J. Baek 6e084b4a73 enh: voice call skip code block & expression 2024-06-22 16:13:13 -07:00
Timothy Jaeryang Baek fd96c9c68d Merge pull request #3380 from Yash-1511/main
feat: add jina_search as new websearch provider
2024-06-22 15:19:38 -07:00
Timothy J. Baek 60e5adc70e enh: files api allow filename 2024-06-22 14:49:00 -07:00
Timothy J. Baek de367e488d fix 2024-06-22 14:08:23 -07:00
Timothy J. Baek df71d7c63b fix 2024-06-22 14:07:11 -07:00
Timothy J. Baek 5b64c28f33 refac 2024-06-22 14:06:19 -07:00
Timothy J. Baek 03bb4bcda6 refac: tool specs 2024-06-22 13:47:57 -07:00
Timothy J. Baek 2d795cffe0 refac: styling 2024-06-22 13:41:27 -07:00
Timothy J. Baek bcefa71b03 refac: styling 2024-06-22 13:39:17 -07:00
Timothy J. Baek b441511359 enh: wake lock 2024-06-22 13:35:18 -07:00
Timothy J. Baek 3af8c5b03d refac: wording 2024-06-22 13:22:26 -07:00
Timothy J. Baek d0f34baaa3 feat: voice interruption toggle 2024-06-22 13:21:36 -07:00
Timothy J. Baek 98c18b3032 fix: web search 2024-06-22 13:13:46 -07:00
Timothy J. Baek c1971fd8d7 refac: show default instead of none 2024-06-22 13:01:26 -07:00
Timothy J. Baek cf6447eb2a feat: function exception handler 2024-06-22 12:43:30 -07:00
Timothy J. Baek 9205b90af6 fix 2024-06-22 12:26:03 -07:00
Timothy J. Baek 6ce91de7e0 fix 2024-06-22 12:25:02 -07:00
Timothy J. Baek f524238910 fix 2024-06-22 12:24:46 -07:00
Timothy J. Baek 646832ba8c refac 2024-06-22 12:23:37 -07:00
Timothy J. Baek 6ccb5e8f67 feat: user valves support 2024-06-22 12:14:12 -07:00
Timothy J. Baek d362fd027e feat: user valves integration 2024-06-22 12:08:32 -07:00
Timothy J. Baek 15fc23df87 feat: user valves endpoints 2024-06-22 11:26:33 -07:00
Yash-1511 7c9fb9199e feat: add jina_search as new websearch provider 2024-06-22 20:06:15 +05:30
Timothy J. Baek 8345bb55d4 refac: styling 2024-06-22 02:29:33 -07:00
Timothy J. Baek e4af3852f7 refac: allow class in tools 2024-06-22 02:29:22 -07:00
Timothy J. Baek 0ac8e97447 refac: styling 2024-06-22 01:43:40 -07:00
Timothy J. Baek a8a451344c refac 2024-06-22 01:42:28 -07:00
Timothy J. Baek ae567796ee refac 2024-06-22 01:39:53 -07:00
Timothy Jaeryang Baek 9220fdc4a4 Merge pull request #3367 from aleixdorca/dev
i18n: Updated Catalan Translation
2024-06-21 23:26:45 -07:00
aleix b06d7dd56a i18n: Update Catalan Translation 2024-06-21 20:52:25 +02:00
Timothy J. Baek 14fd3a8aca refac 2024-06-21 11:05:55 -07:00
Timothy Jaeryang Baek 74ba2d6548 Merge pull request #3346 from KarlLee830/translate
i18n: Update Chinese translation
2024-06-21 11:02:04 -07:00
Timothy Jaeryang Baek 372bbcdf34 Merge pull request #3364 from PeterDaveHello/Improve-zh-TW
i18n: Improve Traditional Chinese(zh-TW) locale
2024-06-21 11:01:12 -07:00
Peter Dave Hello 0c7df2cde2 Improve Traditional Chinese(zh-TW) locale 2024-06-22 01:44:34 +08:00
Jun Siang Cheah 981f384154 refac: modify oauth login logic for unique email addresses 2024-06-21 18:25:19 +01:00
Jun Siang Cheah e011e7b695 fix: set auth cookie during oauth login 2024-06-21 14:35:57 +01:00
Jun Siang Cheah 416e8d1ef9 fix: db migration sync with dev 2024-06-21 14:35:57 +01:00
Jun Siang Cheah 49a00d61ac feat: show oauth sub in admin panel 2024-06-21 14:35:57 +01:00
Jun Siang Cheah 983112d17c feat: fetch and store oauth profile pictures as data URLs 2024-06-21 14:35:54 +01:00
Jun Siang Cheah 922dfae51c fix: broken tuple expansion 2024-06-21 13:44:10 +01:00
Jun Siang Cheah 4aab460905 Merge remote-tracking branch 'upstream/dev' into feat/oauth 2024-06-21 13:43:19 +01:00
Karl Lee f0b5008fc8 i18n: Update Chinese translation 2024-06-21 15:44:06 +08:00
Karl Lee 4d66e39402 i18n: Update Chinese translation 2024-06-21 11:45:57 +08:00
Timothy J. Baek a2ea6b1b5b enh: tool async support 2024-06-20 20:40:03 -07:00
Timothy J. Baek 4370f233a1 feat: pipe async support 2024-06-20 20:37:04 -07:00
Timothy J. Baek 5621025c12 feat: async filter support 2024-06-20 20:26:28 -07:00
Timothy J. Baek 6bb2f41812 feat: tool citation 2024-06-20 14:14:12 -07:00
Timothy J. Baek 58ae91369e refac 2024-06-20 13:49:04 -07:00
Timothy Jaeryang Baek c72e911f6a Merge pull request #3337 from SimonOriginal/dev
Added {$i18n.t('')} keys, updated Ukrainian translation
2024-06-20 13:19:37 -07:00
Timothy J. Baek f1de635988 refac: cookie 2024-06-20 13:14:58 -07:00
SimonOriginal f62d033e6b Fixed indentation in About.svelte 2024-06-20 22:05:48 +02:00
SimonOriginal 7c304d225b fix key {.t('Created by')} 2024-06-20 21:57:33 +02:00
SimonOriginal 89b259acb8 add key i18n + update translation 2024-06-20 21:53:34 +02:00
SimonOriginal 571dfd8a69 ukrainian translation update 2024-06-20 21:51:29 +02:00
SimonOriginal e29e54a8d2 add key i18n 2024-06-20 21:49:52 +02:00
SimonOriginal 05e251da0e fix translation 2024-06-20 21:44:01 +02:00
Timothy J. Baek f342f8adc7 refac: code highlight optimisation 2024-06-20 12:27:34 -07:00
Timothy Jaeryang Baek deb4e36095 Merge pull request #3331 from SimonOriginal/dev 2024-06-20 11:25:30 -07:00
Timothy Jaeryang Baek 619ee152b7 Merge pull request #3332 from KarlLee830/translate 2024-06-20 11:22:19 -07:00
Timothy Jaeryang Baek 6e04ca85cb Merge pull request #3333 from perfectra1n/dev 2024-06-20 11:21:51 -07:00
perf3ct 9f39bcb60e Fix /tags endpoint in postgres integration test
/api/health it is

/health it is
2024-06-20 09:30:30 -07:00
Karl Lee 64814098a3 i18n: Update Chinese translation 2024-06-20 23:58:35 +08:00
SimonOriginal 1bd6626ca5 Ukrainian translation update to v0.3.5 2024-06-20 17:36:17 +02:00
sihuangwlp 36de6576b8 fix issue with i18n 2024-06-20 21:21:37 +08:00
Timothy J. Baek 71a7750c9d refac: styling 2024-06-20 05:05:16 -07:00
Timothy J. Baek 5a2c2770a4 chore: format 2024-06-20 04:53:23 -07:00
Timothy Jaeryang Baek 09a81eb225 Merge pull request #3321 from open-webui/functions
feat: functions
2024-06-20 04:52:32 -07:00
Timothy J. Baek 9ebd308d28 refac 2024-06-20 04:51:51 -07:00
Timothy J. Baek 59fa2f8f26 refac: pipe function support 2024-06-20 04:47:40 -07:00
Timothy J. Baek d6e4aef607 feat: pipe function 2024-06-20 04:38:59 -07:00
Timothy J. Baek de26a78a16 refac 2024-06-20 04:21:55 -07:00
Timothy J. Baek c689356b31 refac 2024-06-20 03:57:36 -07:00
Timothy J. Baek 015772ef9a refac 2024-06-20 03:45:13 -07:00
Timothy J. Baek e20baad601 refac 2024-06-20 03:35:21 -07:00
Timothy J. Baek a3f09949c0 refac 2024-06-20 03:29:50 -07:00
Timothy J. Baek afd270523c feat: filter func outlet 2024-06-20 03:23:50 -07:00
Timothy J. Baek 3101ff143b refac: disable continuing with error message 2024-06-20 02:47:27 -07:00
Timothy J. Baek f14ca48334 refac 2024-06-20 02:42:56 -07:00
Timothy J. Baek 96d7c3e99f fix: raise error 2024-06-20 02:37:36 -07:00
Timothy J. Baek c4bd60114e feat: filter inlet support 2024-06-20 02:30:00 -07:00
Timothy J. Baek 6b8a7b9939 refac: chat completion middleware 2024-06-20 02:06:10 -07:00
Timothy J. Baek 448ca9d836 refac 2024-06-20 01:51:39 -07:00
Timothy J. Baek 08cc20cb93 feat: filter selector model 2024-06-20 01:44:52 -07:00
Timothy J. Baek bf5775e07a refac 2024-06-20 01:16:31 -07:00
Timothy J. Baek 9108df177c refac: comments 2024-06-20 01:12:09 -07:00
Timothy J. Baek 40cde07e5c feat: function filter example boilerplate 2024-06-20 01:07:55 -07:00
Timothy J. Baek 43e08c6afa refac 2024-06-20 00:54:58 -07:00
Timothy J. Baek 27f8afebab feat: function db migration 2024-06-20 00:49:11 -07:00
Timothy J. Baek f68aba687e feat: functions router 2024-06-20 00:37:02 -07:00
sihuangwlp e16ae92edc complate missing field AUTOMATIC1111_API_AUTH 2024-06-20 14:53:38 +08:00
sihuangwlp d3160166e9 Complete missing commas 2024-06-20 14:38:13 +08:00
sihuangwlp bec04279aa Add AUTOMATIC1111_API_AUTH support 2024-06-20 14:15:49 +08:00
Timothy J. Baek f9283bc311 enh: pipeline user email support 2024-06-19 17:19:35 -07:00
Timothy J. Baek d5b76b5ed2 enh: iframe support 2024-06-19 16:51:29 -07:00
Timothy J. Baek 7b64b40270 refac 2024-06-19 15:26:35 -07:00
Timothy J. Baek e3668a2f1c refac 2024-06-19 14:56:51 -07:00
Timothy J. Baek ab270c1682 fix 2024-06-19 14:49:35 -07:00
Timothy Jaeryang Baek 1e0453221d Merge pull request #3221 from perfectra1n/feature-external-db-reconnect
feat: external db reconnect
2024-06-19 14:40:03 -07:00
Timothy J. Baek b36c525ebc enh: cookie auth 2024-06-19 14:38:09 -07:00
Timothy J. Baek 1b100660af fix: image 2024-06-19 13:31:01 -07:00
Timothy Jaeryang Baek 1007f67ab1 Merge pull request #3301 from KarlLee830/translate
i18n: Update Chinese translation
2024-06-19 12:24:44 -07:00
Karl Lee ac37b50be2 Update Chinese translation 2024-06-20 01:17:26 +08:00
Timothy Jaeryang Baek ece48d640a Merge pull request #3293 from que-nguyen/patch-1
Refine and update Vietnamese translations in translation.json
2024-06-19 08:49:39 -07:00
Que Nguyen 8125fd3554 Update: Refine and update Vietnamese translations 2024-06-19 18:23:47 +07:00
Que Nguyen 9e87012489 Fix: Rename 'whitelist' to 'filter_list' in function 2024-06-19 18:22:29 +07:00
Timothy J. Baek b6ad539379 refac 2024-06-18 19:44:32 -07:00
Timothy J. Baek ce659b9e1c chore: format 2024-06-18 19:20:20 -07:00
Timothy J. Baek dad7af6de1 enh: tool __model__ param support 2024-06-18 18:50:36 -07:00
Timothy J. Baek dcac1a3cb7 enh: tool __id__ param support for cache dir 2024-06-18 18:14:18 -07:00
Timothy J. Baek 6f9a31eba5 feat: tool cache dir 2024-06-18 18:07:51 -07:00
Timothy J. Baek eef125d085 refac: settings order 2024-06-18 17:48:56 -07:00
Timothy J. Baek ff9b94bab7 refac: playground 2024-06-18 17:41:47 -07:00
Timothy J. Baek d1eb2c2207 fix: model selector focus 2024-06-18 17:16:33 -07:00
Timothy J. Baek 807cfdecfb fix 2024-06-18 17:09:29 -07:00
Timothy J. Baek 31f768bf8a fix 2024-06-18 17:07:13 -07:00
Timothy J. Baek a2e1ea103c feat: tools file handler support 2024-06-18 16:45:03 -07:00
Timothy J. Baek d6ab954f81 refac: styling 2024-06-18 16:11:10 -07:00
Timothy J. Baek 514c7f1520 fix: rag 2024-06-18 16:08:42 -07:00
Timothy J. Baek bcc27e3852 fix 2024-06-18 15:48:25 -07:00
Timothy J. Baek 35026849df refac: styling 2024-06-18 15:39:50 -07:00
Timothy J. Baek d5a1030000 refac: uploads delete 2024-06-18 15:20:04 -07:00
Timothy J. Baek 9fa8633dcb fix: styling 2024-06-18 15:11:59 -07:00
Timothy J. Baek 1a22ae54a2 fix: styling 2024-06-18 14:58:19 -07:00
Timothy J. Baek 20e4f6cc16 refac 2024-06-18 14:55:18 -07:00
Timothy J. Baek eb21750466 fix: files 2024-06-18 14:38:23 -07:00
Timothy J. Baek d93160799f refac: files 2024-06-18 14:37:12 -07:00
Timothy J. Baek b4bdea6d85 fix: files 2024-06-18 14:33:44 -07:00
Timothy J. Baek 83986620ee refac 2024-06-18 14:15:08 -07:00
Timothy J. Baek 9e7b7a895e refac: file upload 2024-06-18 13:50:18 -07:00
Timothy Jaeryang Baek 1000bcaeb7 Merge pull request #3274 from KarlLee830/translate
i18n: Update Chinese translation
2024-06-18 13:07:16 -07:00
perfectra1n 7e061d19ca Merge branch 'open-webui:main' into feature-external-db-reconnect 2024-06-18 12:11:28 -07:00
Timothy J. Baek 7c2a198370 feat: file db migration 2024-06-18 11:38:50 -07:00
Timothy J. Baek 146e550239 feat: files endpoint 2024-06-18 11:36:55 -07:00
Karl Lee 4d9220c2c8 i18n: Update Chinese translation 2024-06-19 02:21:34 +08:00
Karl Lee 3c0678fb84 i18n: Update Chinese translation 2024-06-19 02:17:18 +08:00
Karl Lee 6985567758 Update Chinese translation 2024-06-19 02:10:01 +08:00
Karl Lee 5a5bf20d42 chore: format 2024-06-19 02:03:33 +08:00
Timothy J. Baek ba7091c25b refac 2024-06-18 10:36:06 -07:00
Timothy Jaeryang Baek e076803f98 Merge pull request #3256 from shing100/dev
i18n: Update Korean Localization for Open-WebUI
2024-06-18 10:34:33 -07:00
Timothy J. Baek 453f9be16c refac 2024-06-18 10:26:53 -07:00
Timothy J. Baek 19b67f4975 feat: functions scaffold 2024-06-18 10:23:04 -07:00
Geun, Lim 0a3390947c i18n: Korean Localization for Open-WebUI
In the updated translation, we changed the theme to own language and changed the other language a little more naturally.
2024-06-18 14:56:37 +09:00
Timothy J. Baek 47d9c9fc74 refac 2024-06-17 20:46:42 -07:00
perf3ct 81b2416923 format 2024-06-17 16:47:09 -07:00
perf3ct 59c6ff727a borrow some of the previous PRs reconnection code 2024-06-17 16:44:20 -07:00
Timothy J. Baek 493fe562ac refac 2024-06-17 14:39:21 -07:00
Timothy J. Baek b1d83fc42c chore: format 2024-06-17 14:32:23 -07:00
Timothy Jaeryang Baek 20f052eb37 Merge pull request #3112 from que-nguyen/searxng
Domain whitelisting for web search results
2024-06-17 14:30:17 -07:00
Timothy J. Baek edfe20b2e1 fix 2024-06-17 14:28:56 -07:00
Timothy J. Baek a4748af822 chore: bump pyodide 2024-06-17 14:26:10 -07:00
Timothy J. Baek 46e570bd04 fix: styling 2024-06-17 13:52:17 -07:00
Timothy J. Baek 686c5081e6 fix: model system prompt variable support 2024-06-17 13:47:48 -07:00
Timothy J. Baek 84bd4994cd refac: toolkit editor 2024-06-17 13:38:07 -07:00
Timothy J. Baek 55dfc2013a enh: __messages__ support for tools 2024-06-17 13:28:29 -07:00
Timothy J. Baek 4a3362f889 fix: help 2024-06-17 11:46:23 -07:00
Timothy J. Baek 44e145c6e9 fix: styling 2024-06-17 11:32:10 -07:00
Timothy J. Baek eb98631ff6 refac 2024-06-17 11:11:54 -07:00
perf3ct 48e1356ed9 add logging for user upon db connection 2024-06-17 10:34:19 -07:00
perf3ct 5c655f298b stop even using pooled DBs in peewee 2024-06-17 09:56:31 -07:00
Timothy Jaeryang Baek cc4e192a51 Merge pull request #3235 from seokho-son/dev 2024-06-17 08:53:42 -07:00
perf3ct 981866eb93 use autoconnect and stop using the mixin 2024-06-17 07:50:47 -07:00
Seokho Son cc98e89baa Complete Korean Localization for Open-WebUI
Signed-off-by: Seokho Son <shsongist@gmail.com>
2024-06-17 22:00:53 +09:00
Timothy J. Baek d8a1d7eb36 feat: character utils 2024-06-17 03:05:09 -07:00
Timothy J. Baek 2f7120a73a refac 2024-06-17 02:54:56 -07:00
Timothy J. Baek a60d6c316e fix 2024-06-17 02:28:12 -07:00
Timothy J. Baek ab62228877 feat: character card support 2024-06-17 02:26:07 -07:00
Timothy J. Baek f1b350cbe6 enh: use model profile image in call 2024-06-17 01:55:06 -07:00
Timothy J. Baek 9023a60d0d refac 2024-06-17 01:42:04 -07:00
Timothy J. Baek 49fdb68908 chore: format 2024-06-17 01:36:43 -07:00
Timothy Jaeryang Baek db7d63d11a Merge pull request #3227 from KarlLee830/translate
Add some missing i18n keys and update Chinese translation
2024-06-17 01:35:03 -07:00
Karl Lee 720ff35edf Add some missing i18n keys and update Chinese translation
Add some missing i18n keys and update Chinese translation
2024-06-17 16:32:36 +08:00
Timothy J. Baek 5fa355e1ae feat: background image 2024-06-17 01:31:22 -07:00
Que Nguyen c487385980 Set filter_list as optional param in serpstack.py 2024-06-17 14:38:11 +07:00
Que Nguyen bcb84235b1 Set filter_list as optional param in serply.py 2024-06-17 14:37:52 +07:00
Que Nguyen 6b8290fa6d Set filter_list as optional param in serper.py 2024-06-17 14:37:26 +07:00
Que Nguyen 9c446d9fb4 Set filter_list as optional param in searxng.py 2024-06-17 14:36:56 +07:00
Que Nguyen 3cc0e3ecb6 Refactor rag/main.py
Renamed function get_filtered_results
2024-06-17 14:36:26 +07:00
Que Nguyen d8beed13b4 Set filter_list as optional param in google_pse.py 2024-06-17 14:35:27 +07:00
Que Nguyen 7d2ad8c4bf Set filter_list as optional param in duckduckgo.py 2024-06-17 14:34:59 +07:00
Que Nguyen a02139ba9d Set filter_list as optional param in brave.py 2024-06-17 14:34:17 +07:00
Que Nguyen b3d136b3b3 Refactored config.py
Renamed RAG_WEB_SEARCH_WHITE_LIST_DOMAINS to RAG_WEB_SEARCH_DOMAIN_FILTER_LIST
2024-06-17 14:33:23 +07:00
Que Nguyen a3ac9ee774 Refactor main.py
Rename RAG_WEB_SEARCH_WHITE_LIST_DOMAINS to RAG_WEB_SEARCH_DOMAIN_FILTER_LIST
2024-06-17 14:31:44 +07:00
Que Nguyen 75e51ecf6d Merge branch 'open-webui:main' into searxng 2024-06-17 14:28:02 +07:00
Timothy J. Baek a28ad06bf0 fix 2024-06-16 23:36:21 -07:00
Timothy Jaeryang Baek 9e4dd4b86f Merge pull request #3159 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.5
2024-06-16 22:52:44 -07:00
Timothy J. Baek 4559c8af74 doc: changelog 2024-06-16 22:52:27 -07:00
Timothy J. Baek 4a67ae1195 fix: message delete issue 2024-06-16 22:28:26 -07:00
Timothy J. Baek 1efa25eed5 chore: format 2024-06-16 21:55:08 -07:00
Timothy J. Baek 54b65a89fd refac: call overlay 2024-06-16 21:52:50 -07:00
Timothy Jaeryang Baek b4008b0b30 Merge pull request #3140 from ricky-davis/sortable-users
feat: Sortable users in admin panel
2024-06-16 21:49:08 -07:00
Timothy Jaeryang Baek 16c95991d3 Merge pull request #3145 from ricky-davis/sortable-chats
feat: Sortable chats in admin panel
2024-06-16 21:48:13 -07:00
Timothy Jaeryang Baek f3bd81073d Merge pull request #3202 from JohnTheNerd/main
feat: added ability to set user name for federated auth
2024-06-16 17:56:29 -07:00
Timothy J. Baek 6d9ed32929 refac 2024-06-16 17:19:18 -07:00
Timothy J. Baek ad2ffc33d8 refac: voice call tap to interrupt 2024-06-16 16:50:57 -07:00
Timothy J. Baek e183b0e5ff chore: format 2024-06-16 15:34:15 -07:00
Timothy J. Baek 4b6b33b08b feat: user_location 2024-06-16 15:32:26 -07:00
perf3ct 10fa887eab fix peewee and playhouse connections to retry 2024-06-16 15:25:48 -07:00
John Karabudak c00a6fa02a added ability to set user name for federated auth
this commit adds an optional environment variable named `WEBUI_AUTH_TRUSTED_NAME_HEADER`, which sets the user's name to the contents of that header. this only happens if the user is just being created, just like how the trusted e-mail header works.

if the environment variable or header is not present, we fall back to the original behavior which is to re-use the user e-mail address.

Co-Authored-By: Nikita Borzykh <sample@fastmail.com>
2024-06-16 18:44:10 -02:30
Timothy J. Baek 8e62c36148 enh: AIOHTTP_CLIENT_TIMEOUT None support 2024-06-16 13:56:49 -07:00
Timothy Jaeryang Baek f76a569527 Merge pull request #3219 from JohnTheNerd/comfyui-configuration
feat: added support for Stable Diffusion 3 alongside more extensive ComfyUI configuration
2024-06-16 13:50:38 -07:00
Timothy Jaeryang Baek 9a6c2fafe3 Merge pull request #3204 from JohnTheNerd/fix-github-actions
build: fixed GitHub actions on usernames with uppercase characters
2024-06-16 13:50:00 -07:00
Timothy J. Baek 1c355929fc refac 2024-06-16 12:09:55 -06:00
John Karabudak ea074fa9bf added Stable Diffusion 3 support alongside ComfyUI configuration
this commit adds four environment variables:

- COMFYUI_CFG_SCALE
- COMFYUI_SAMPLER
- COMFYUI_SCHEDULER
- COMFYUI_SD3 (merely setting this at all will enable SD3 mode)
2024-06-16 15:30:52 -02:30
John Karabudak 60ee08a8ac made the environment variable name IMAGE_NAME as requested 2024-06-16 15:27:13 -02:30
Timothy J. Baek 9f7ef209fa fix: chatlist 2024-06-16 11:36:15 -06:00
Timothy J. Baek fc1a66ea76 feat: current_time, current_datetime 2024-06-16 10:39:48 -06:00
Timothy J. Baek 5d0b77e64a fix: shift delete 2024-06-16 10:27:34 -06:00
Timothy J. Baek a4810a5e42 refac 2024-06-16 10:24:16 -06:00
Timothy Jaeryang Baek 39c466d542 Merge pull request #3212 from robbie-cahill/patch-1
Add host.docker.internal overide for docker dev
2024-06-16 09:07:59 -07:00
Timothy Jaeryang Baek c8418da8c1 Merge pull request #3217 from theasp/better-body-logging
chore: Log API request bodies at debug level
2024-06-16 09:07:22 -07:00
perf3ct 75d713057c Merge remote-tracking branch 'upstream/main' into feature-external-db-reconnect 2024-06-16 09:03:57 -07:00
Andrew Phillips c0c875eae2 Use log.debug() for logging request bodies for the backend API 2024-06-16 12:40:16 -03:00
Andrew Phillips 3eba963d03 Remove redundant logging 2024-06-16 12:38:20 -03:00
Robbie 78a145f181 Add host.docker.internal overide 2024-06-16 21:16:07 +10:00
Jun Siang Cheah 4ff17acc1b Merge remote-tracking branch 'upstream/dev' into feat/oauth 2024-06-16 08:31:05 +01:00
Timothy Jaeryang Baek 9928114ca8 Merge pull request #3205 from Peter-De-Ath/chat-selectors-dark-mode
style: update chat selectors dark mode colors
2024-06-15 22:33:22 -07:00
Timothy J. Baek e5f4f64102 enh: remove trailing slash 2024-06-15 23:32:12 -06:00
Peter De-Ath 7a98c1750f enh: update dark mode colors on chat selectors - models 2024-06-16 03:18:15 +01:00
Peter De-Ath ee279b7976 enh: update dark mode colors on chat selectors 2024-06-16 02:55:27 +01:00
John Karabudak dbfb6d5993 fixed GitHub actions on usernames with uppercase characters 2024-06-15 22:42:29 -02:30
Que Nguyen a02ba52de8 Merge branch 'dev' into searxng 2024-06-15 23:44:31 +07:00
Timothy J. Baek 1275371e10 fix: md model desc 2024-06-15 10:43:10 -06:00
Timothy J. Baek f56da1a39f refac 2024-06-15 04:41:48 -06:00
Timothy J. Baek fe9685867e enh: model desc md support 2024-06-15 04:32:18 -06:00
Timothy J. Baek dfa2cf9e6d refac: AIOHTTP_CLIENT_TIMEOUT default value should match 2024-06-15 04:25:21 -06:00
Timothy J. Baek 262501304c enh: model preset delete confirmation 2024-06-15 04:13:30 -06:00
Timothy J. Baek a0dbc970a9 enh: model delete confirmation 2024-06-15 04:07:40 -06:00
Timothy J. Baek 2abb6788fe enh: user delete confirmation 2024-06-15 04:04:29 -06:00
Timothy J. Baek 91cec11500 refac 2024-06-15 04:02:20 -06:00
Timothy J. Baek 68aca76805 fix 2024-06-15 03:39:21 -06:00
Timothy J. Baek 2f501aee14 chore: format 2024-06-15 03:36:17 -06:00
Timothy J. Baek 3c599e24e5 refac 2024-06-15 03:35:44 -06:00
Timothy Jaeryang Baek a6ee7415d8 Merge pull request #3116 from Peter-De-Ath/memories-edit
feat: add abilty to edit memories
2024-06-15 01:58:57 -07:00
Timothy J. Baek d3a3d27f70 chore: format 2024-06-15 02:55:02 -06:00
Timothy Jaeryang Baek 16f9dcf17a Merge pull request #3184 from PierreMesure/better-swedish
i18n: Better Swedish translation 🇸🇪
2024-06-15 01:53:27 -07:00
Jun Siang Cheah f49d814dc0 Merge remote-tracking branch 'upstream/main' into feat/oauth 2024-06-15 09:51:18 +01:00
Pierre Mesure cb988e58ba Better Swedish translation 2024-06-15 09:13:27 +02:00
Peter De-Ath bec00e7e64 fix: change update_memory to correct naming convention
fix: update update_memory to POST
2024-06-14 21:23:34 +01:00
Timothy J. Baek 7ba98ad498 feat: chat item shift shorcut 2024-06-14 13:02:07 -07:00
Timothy J. Baek aba2ca39d4 enh: delete confirm dialog 2024-06-14 11:24:15 -07:00
Timothy Jaeryang Baek 8db439a0d1 Merge pull request #3177 from Yash-1511/main
feat: add tavily web search in web search provider
2024-06-14 11:09:59 -07:00
Timothy Jaeryang Baek aec8e47fbb Merge pull request #3174 from KarlLee830/translate
Update Chinese translation
2024-06-14 11:09:22 -07:00
Timothy J. Baek d03a00758f refac: sidebar 2024-06-14 11:06:19 -07:00
Yash-1511 717bbe62a6 doc: update readme 2024-06-14 20:51:50 +05:30
Yash-1511 b9da72560a feat: add tavily web search in web search provider 2024-06-14 20:44:11 +05:30
Karl Lee 460bc43846 Update Chinese translation
Update Chinese translation
2024-06-14 22:18:56 +08:00
Timothy Jaeryang Baek 26505079b8 Merge pull request #3107 from TheTerrasque/defect/ollama-long-response-timeout
Fix: ollama long response timeout
2024-06-14 02:45:55 -07:00
Timothy J. Baek 3de4c6189b fix: voice input sensitivity 2024-06-14 02:41:27 -07:00
Timothy J. Baek 2e4f060ebb feat: emoji call 2024-06-14 02:30:36 -07:00
Timothy Jaeryang Baek 53858c9b0e Merge pull request #3161 from sammcj/num_batch
feat: add num_keep, num_batch
2024-06-14 00:14:46 -07:00
Timothy J. Baek 454a386612 refac 2024-06-14 00:10:52 -07:00
Timothy J. Baek 346caaa1db chore: format 2024-06-14 00:07:46 -07:00
Timothy J. Baek 42ba43fc81 fix 2024-06-14 00:05:01 -07:00
Sam McLeod 3f5e36271f feat: add num_keep, num_batch 2024-06-14 16:21:20 +10:00
Timothy J. Baek 18ae5860bc refac 2024-06-13 20:16:23 -07:00
Timothy J. Baek 7f70de99d3 refac: voice call 2024-06-13 20:15:23 -07:00
rdavis 385fcfe8d0 Swapped from inline style to using tailwind class. 2024-06-14 00:29:31 +00:00
rdavis 09a6206d28 Merge branch 'sortable-users' of github.com:ricky-davis/open-webui into sortable-users 2024-06-13 19:27:23 -05:00
rdavis b0d9aa38d2 Swapped from inline style to using tailwind class. 2024-06-14 00:07:21 +00:00
rdavis 26575c5086 Changed column header text to match property.
Removed debugging code.
2024-06-13 23:59:15 +00:00
rdavis 91d53530e6 Added the ability to sort chats in the admin panel chats modal
Added "Updated at" column to the admin panel chats modal.
2024-06-13 23:24:52 +00:00
rdavis c07e7221e5 Added the ability to sort users in the admin panel 2024-06-13 15:00:02 -05:00
rdavis 5844d0525a Added the ability to sort users in the admin panel 2024-06-13 19:01:13 +00:00
Timothy Jaeryang Baek 7ea572fdca Merge pull request #3138 from open-webui/main
dev
2024-06-13 11:15:21 -07:00
Timothy J. Baek 162643a4b1 fix 2024-06-13 11:14:33 -07:00
Timothy Jaeryang Baek 4727e5cbb1 Merge pull request #3128 from open-webui/voice-enh
enh: voice
2024-06-13 02:32:23 -07:00
Timothy J. Baek b8136951e4 refac 2024-06-13 02:29:56 -07:00
Timothy J. Baek 5300d2c531 refac 2024-06-13 01:28:15 -07:00
Timothy J. Baek d6fd2a8228 refac 2024-06-12 21:18:53 -07:00
Peter De-Ath 493e3068d8 enh: ability to edit memories 2024-06-13 02:42:07 +01:00
Que Nguyen 7b5f434a07 Implement domain whitelisting for web search results 2024-06-13 07:14:48 +07:00
Mikael Turøy e130ad74d1 Added timeout setting for ollama streaming response 2024-06-12 22:50:15 +02:00
Timothy Jaeryang Baek a382e82dec Merge pull request #3105 from open-webui/dev
fix/refac: docs
2024-06-12 13:45:43 -07:00
Timothy J. Baek bdd2ac0015 fix/refac: docs 2024-06-12 13:45:13 -07:00
Timothy Jaeryang Baek c0a06f7db4 Merge pull request #3103 from open-webui/dev
fix: filter pipeline
2024-06-12 13:35:43 -07:00
Timothy J. Baek e82027310d fix 2024-06-12 13:34:34 -07:00
Timothy J. Baek c6c0bc19d8 fix: filter pipeline 2024-06-12 13:31:05 -07:00
Timothy Jaeryang Baek 33bb787b15 Merge pull request #3095 from open-webui/dev
refac
2024-06-12 11:47:47 -07:00
Timothy J. Baek c794d59fd5 revert: do not change the default 2024-06-12 11:47:19 -07:00
Timothy J. Baek 1fefafb254 refac 2024-06-12 11:44:05 -07:00
Timothy Jaeryang Baek 90dadf0bec Merge pull request #3073 from que-nguyen/searxng
Set searxng language to auto and enable safesearch (moderate).
2024-06-12 11:26:10 -07:00
Timothy Jaeryang Baek 95ff355737 Merge pull request #3094 from open-webui/dev
fix: chat deletion
2024-06-12 11:23:31 -07:00
Timothy J. Baek c2d41b0376 fix: chat deletion 2024-06-12 11:23:11 -07:00
Timothy Jaeryang Baek 8f3c9b391b Merge pull request #3093 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
fix
2024-06-12 11:21:44 -07:00
Timothy J. Baek 5a9d883a93 fix 2024-06-12 11:21:23 -07:00
Timothy Jaeryang Baek c2e8af5ed1 Merge pull request #3090 from open-webui/dev
0.3.4
2024-06-12 11:13:41 -07:00
Timothy J. Baek 70a85d6111 doc: changelog 2024-06-12 11:13:24 -07:00
Timothy J. Baek 1163745a03 revert 2024-06-12 11:08:05 -07:00
Timothy J. Baek 529fcaa5c9 fix: document query save 2024-06-12 11:07:04 -07:00
Timothy J. Baek 681fd4ff3b fix: tools api 2024-06-12 10:55:26 -07:00
Que Nguyen 305ec59d76 Set searxng language as 'auto' and enable safesearch (moderate).
Configure searxng with language param set to auto and add "safesearch": 1 (moderate) for safer web results.
2024-06-12 21:33:33 +07:00
Timothy Jaeryang Baek 0917fa6f4a Merge pull request #3056 from open-webui/dev
refac
2024-06-12 03:15:25 -07:00
Timothy J. Baek c7d3969a53 refac 2024-06-12 03:13:59 -07:00
Timothy Jaeryang Baek c41b33c9c0 Merge pull request #3013 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.3
2024-06-12 02:14:37 -07:00
Timothy J. Baek 7131ac24a7 chore: format 2024-06-12 02:12:39 -07:00
Timothy J. Baek 501cd2071c fix: fr translation 2024-06-12 02:10:38 -07:00
Timothy J. Baek 85a1233733 refac: styling 2024-06-12 02:09:00 -07:00
Timothy J. Baek 670fa83e2c doc: changelog 2024-06-12 02:05:12 -07:00
Timothy J. Baek d03a53e755 doc: changelog 2024-06-12 01:58:38 -07:00
Timothy J. Baek 2ec416bd6b doc: readme 2024-06-12 01:49:13 -07:00
Timothy J. Baek b1a391ca5d doc: readme 2024-06-12 01:46:28 -07:00
Timothy J. Baek c0ca447041 chore: format 2024-06-12 01:37:53 -07:00
Timothy Jaeryang Baek 5d3db15eca Merge pull request #3049 from que-nguyen/dev
Refactor URL validation function
2024-06-12 01:36:34 -07:00
Timothy J. Baek eead69068c fix: type casting 2024-06-12 01:35:42 -07:00
Timothy J. Baek edae1ca4c7 chore: format 2024-06-12 01:32:22 -07:00
Timothy J. Baek 0c9cc2baa3 refac: message 2024-06-12 01:28:30 -07:00
Timothy J. Baek 326ac41422 refac: bypass confirm dialog for edit 2024-06-12 01:22:52 -07:00
Timothy J. Baek dd2941bb43 feat: import confim dialog 2024-06-12 01:20:12 -07:00
Timothy J. Baek a691ee08d8 feat: confirm dialog 2024-06-12 01:17:46 -07:00
Timothy J. Baek e8fc522eba chore: format 2024-06-12 00:18:22 -07:00
Timothy J. Baek 482a41e49b fix 2024-06-12 00:16:54 -07:00
Timothy J. Baek ae3d8ce0c0 refac 2024-06-12 00:15:45 -07:00
Timothy J. Baek a2e30ace02 refac: her 2024-06-12 00:13:35 -07:00
Timothy J. Baek cde9672a09 enh: her 2024-06-12 00:05:20 -07:00
Timothy J. Baek db6ca856de Update requirements-dev.lock 2024-06-11 19:16:06 -07:00
Timothy J. Baek bb5e615841 chore: python requirements 2024-06-11 19:14:47 -07:00
Timothy J. Baek 48ac4d9f2a fix 2024-06-11 18:23:42 -07:00
Que Nguyen eb7bba81fe Refactor URL validation function
- The check for private IP addresses often did not yield the expected results, especially with errors like: `[Errno -2] Name or service not known`.
- Removed the check for private IP addresses in the URL validation process.
- Simplified the `validate_url` function to focus on validating the URL format and checking the existence of the URL using a HEAD request.
2024-06-12 08:15:04 +07:00
Timothy J. Baek a43cb1eb25 refac: styling 2024-06-11 17:09:10 -07:00
Timothy J. Baek e2d8872060 fix: e2e 2024-06-11 17:03:49 -07:00
Timothy J. Baek ede584320f refac: tools edit 2024-06-11 16:54:21 -07:00
Timothy J. Baek bc458d3ee0 refac 2024-06-11 16:15:54 -07:00
Timothy J. Baek 0d1f7265c4 refac 2024-06-11 16:15:19 -07:00
Timothy J. Baek 539201a032 chore: format 2024-06-11 15:44:36 -07:00
Timothy J. Baek 50ad2aa215 chore: format 2024-06-11 15:44:27 -07:00
Timothy J. Baek 5cd28c04b8 feat: model tools assignment 2024-06-11 15:29:46 -07:00
Timothy J. Baek c3ce4d7f6a refac 2024-06-11 14:36:46 -07:00
Timothy J. Baek 9bd054490f refac 2024-06-11 14:32:01 -07:00
Timothy J. Baek 2be14d57bf fix 2024-06-11 14:30:18 -07:00
Timothy J. Baek 61e0a85aed feat: export tool 2024-06-11 12:24:36 -07:00
Timothy J. Baek e483c6b598 refac: styling 2024-06-11 12:18:30 -07:00
Timothy J. Baek db3b075796 refac: input menu tools 2024-06-11 12:16:11 -07:00
Timothy J. Baek 0bb26ae504 refac: tools 2024-06-11 11:31:14 -07:00
Timothy J. Baek 9d16dd997a refac 2024-06-11 11:15:43 -07:00
Timothy J. Baek e4fe1fff97 feat: tool __user__ email 2024-06-11 10:54:11 -07:00
Timothy J. Baek 37ed31b0e2 feat: tools warning 2024-06-11 10:47:46 -07:00
Timothy J. Baek dc82e681dd refac 2024-06-11 10:43:36 -07:00
Timothy J. Baek 4944824544 enh: __user__ example 2024-06-11 10:36:35 -07:00
Timothy J. Baek 67cd9b76ae refac: __user__ param 2024-06-11 10:35:13 -07:00
Timothy J. Baek 8a86f32700 feat: user hook 2024-06-11 10:19:59 -07:00
Timothy Jaeryang Baek f62b15d8da Merge pull request #3042 from sime2408/fit/translations-croatian
Updated Croatian translation for user interface text
2024-06-11 09:55:17 -07:00
Timothy Jaeryang Baek d709038b5b Merge pull request #3029 from Yash-1511/main
feat: add DuckDuckGo search functionality using duckduckgo_search library
2024-06-11 09:53:26 -07:00
Timothy Jaeryang Baek a2631ed38e Merge pull request #3041 from cocktailpeanut/main
feat: custom HOST env variable on windows
2024-06-11 09:52:45 -07:00
Timothy Jaeryang Baek 8ffd8c3505 Merge pull request #3035 from ClassicOldSong/main
Fix frontend stops responding when backend throws error
2024-06-11 09:52:26 -07:00
sime2408 f74df0433b Updated Croatian translation for user interface text
Refined Croatian locale translations to improve understanding and clarity for Croatian users. Adjustments made include improving command descriptions, correcting grammar, and adapting the terminology to better fit the software context.
2024-06-11 18:52:08 +02:00
Timothy Jaeryang Baek 93fa98245b Merge pull request #3033 from que-nguyen/dev
Fixed the issue where a single URL error disrupts the Web Search
2024-06-11 09:52:07 -07:00
Timothy Jaeryang Baek e7727aea9e Merge pull request #3024 from KarlLee830/translate
Update Chinese Translation
2024-06-11 09:51:34 -07:00
cocktailpeanut 884eb9f7ee update 2024-06-11 12:32:29 -04:00
Yukino Song 912b75f8ac Fix frontend stops responding when backend throw error 2024-06-11 23:25:07 +08:00
Que Nguyen 3bec60b80c Fixed the issue where a single URL error disrupts the data loading process in Web Search mode
To address the unresolved issue in the LangChain library where a single URL error disrupts the data loading process, the lazy_load method in the WebBaseLoader class has been modified. The enhanced method now handles exceptions appropriately, logging errors and continuing with the remaining URLs.
2024-06-11 22:06:14 +07:00
Yash-1511 07b08ef67e add: duckduckgo-search dependencies added in requirements.txt 2024-06-11 19:53:57 +05:30
Yash-1511 83f9475584 feat: add DuckDuckGo search functionality using duckduckgo_search library 2024-06-11 19:49:08 +05:30
Karl Lee 1ad5456fe0 Update Chinese Translation 2024-06-11 20:46:15 +08:00
Timothy J. Baek bf1936de34 refac 2024-06-11 02:12:24 -07:00
Timothy J. Baek 827dd1b044 refac 2024-06-11 01:58:26 -07:00
Timothy J. Baek b2028d187d chore: format 2024-06-11 01:34:29 -07:00
Timothy J. Baek 1e1b372d07 fix 2024-06-11 01:18:05 -07:00
Timothy J. Baek bd5a8567ef refac: tools & rag 2024-06-11 01:10:24 -07:00
Timothy J. Baek fc46532955 fix 2024-06-11 00:58:52 -07:00
Timothy Jaeryang Baek 5f29a9edf4 Merge pull request #2998 from que-nguyen/dev
Completed Vietnamese translation
2024-06-11 00:52:15 -07:00
Timothy J. Baek 91bc65c7da fix 2024-06-11 00:50:30 -07:00
Timothy J. Baek a91db7f7a6 refac 2024-06-11 00:43:42 -07:00
Timothy Jaeryang Baek e6fe3ada57 Merge pull request #3006 from open-webui/tools
feat: tools
2024-06-11 00:37:53 -07:00
Timothy J. Baek 354683296b refac 2024-06-11 00:37:31 -07:00
Timothy J. Baek 5237439e29 feat: tool desc 2024-06-11 00:32:16 -07:00
Timothy J. Baek 049b3136e8 refac 2024-06-11 00:24:25 -07:00
Timothy J. Baek 3d6f5f418d feat: tools full integration 2024-06-11 00:18:45 -07:00
Timothy J. Baek a27175d672 feat: fc integration 2024-06-10 23:40:27 -07:00
Timothy J. Baek ff1cd306d8 refac 2024-06-10 22:38:48 -07:00
Timothy J. Baek aa7d25600f refac 2024-06-10 22:33:25 -07:00
Timothy J. Baek 0bae7ca615 feat: tool selector input menu 2024-06-10 22:29:24 -07:00
Timothy J. Baek 3578b5e337 refac: clone 2024-06-10 22:16:49 -07:00
Timothy J. Baek 8464b30485 refac 2024-06-10 22:10:53 -07:00
Timothy J. Baek c961964647 enh: tool id validation 2024-06-10 21:59:06 -07:00
Timothy J. Baek dd7ac4c53a refac: search 2024-06-10 21:55:34 -07:00
Timothy J. Baek 4601a0246f fix: tool edit 2024-06-10 21:53:51 -07:00
Timothy J. Baek 25c0eca414 feat: edit tool spinner 2024-06-10 21:47:25 -07:00
Timothy J. Baek ca8be1ee4a feat: import tools 2024-06-10 21:38:29 -07:00
Timothy J. Baek 1611a3aa70 feat: export tools 2024-06-10 21:36:13 -07:00
Timothy J. Baek b434ebf3ad feat: tools integration 2024-06-10 21:33:46 -07:00
Timothy Jaeryang Baek d2ed99e201 Merge pull request #3008 from open-webui/main
dev
2024-06-10 21:07:35 -07:00
Timothy Jaeryang Baek 06976c4551 Merge pull request #3007 from open-webui/fix
fix
2024-06-10 21:07:13 -07:00
Timothy J. Baek 626643104e fix 2024-06-10 21:06:46 -07:00
Timothy J. Baek c5683dd24c refac 2024-06-10 21:05:06 -07:00
Timothy J. Baek 6589464ddf refac 2024-06-10 20:58:47 -07:00
Timothy J. Baek e27c264081 feat: tools apis 2024-06-10 20:43:11 -07:00
Timothy J. Baek 3a96e1f109 feat: tools backend 2024-06-10 20:39:55 -07:00
Timothy J. Baek c4629c8c2d refac: tools 2024-06-10 19:30:48 -07:00
Timothy J. Baek adea3af8d7 refac 2024-06-10 19:19:53 -07:00
Timothy Jaeryang Baek ec516e95fa Merge pull request #3001 from open-webui/main
dev
2024-06-10 18:56:21 -07:00
Timothy Jaeryang Baek 481fd9f88a Merge pull request #3000 from open-webui/fix
fix
2024-06-10 18:56:01 -07:00
Timothy J. Baek 2bbaf1234c fix 2024-06-10 18:55:45 -07:00
Timothy Jaeryang Baek 690c25215d Merge pull request #2999 from open-webui/fix
fix
2024-06-10 18:55:18 -07:00
Timothy J. Baek b72243c166 fix 2024-06-10 18:54:52 -07:00
Timothy J. Baek 8f2e799615 fix 2024-06-10 18:53:33 -07:00
Que Nguyen 8ecf262457 Completed Vietnamese translation 2024-06-11 08:49:06 +07:00
Timothy Jaeryang Baek 7fb785901e Merge pull request #2997 from open-webui/main
dev
2024-06-10 18:02:06 -07:00
Timothy Jaeryang Baek 5dc9b93916 Merge pull request #2996 from open-webui/fix
fix: regeneration
2024-06-10 17:58:42 -07:00
Timothy J. Baek 1241bc3e89 fix: regeneration 2024-06-10 17:58:07 -07:00
Timothy J. Baek c5ed3452d2 refac 2024-06-10 17:52:12 -07:00
Timothy J. Baek c2e6e44714 refac 2024-06-10 17:33:40 -07:00
Timothy J. Baek 6e7e575a18 refac 2024-06-10 17:30:07 -07:00
Timothy J. Baek f43b545bdd refac 2024-06-10 17:24:42 -07:00
Timothy J. Baek 12a04b118f refac: styling 2024-06-10 17:22:44 -07:00
Timothy J. Baek 8b1e2ce279 feat: code format 2024-06-10 17:12:48 -07:00
Timothy J. Baek fb0f106afe refac: styling 2024-06-10 16:37:33 -07:00
Timothy J. Baek dd423f43de feat: default tools template 2024-06-10 16:35:42 -07:00
Timothy J. Baek f2bd3fdf19 chore: rm console log 2024-06-10 16:16:08 -07:00
Timothy J. Baek 8ad52f0fcc feat: code editor 2024-06-10 16:02:23 -07:00
Timothy J. Baek 5a3736f1ee feat: tools page 2024-06-10 15:24:26 -07:00
Timothy J. Baek 54bbfa5e89 chore: npm 2024-06-10 14:49:03 -07:00
Timothy Jaeryang Baek 3933db2c91 Merge pull request #2992 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.2
2024-06-10 13:53:56 -07:00
Timothy J. Baek dc8f0987e1 doc: changelog 2024-06-10 13:53:47 -07:00
Timothy J. Baek 644f0fe6c3 chore: version bump 2024-06-10 13:52:35 -07:00
Timothy J. Baek 87b04b0fc3 fix: params 2024-06-10 13:41:13 -07:00
Timothy J. Baek 455403a4a4 fix 2024-06-10 13:29:28 -07:00
Timothy J. Baek 547b990041 refac: params 2024-06-10 13:29:09 -07:00
Timothy Jaeryang Baek fcf8f2a704 Merge pull request #2952 from arkohut/tolerant-readonly-filesystem
fix: tolerant readonly filesystem for copy favicon to static dir
2024-06-10 11:59:06 -07:00
Timothy Jaeryang Baek 496e798cc7 Merge pull request #2973 from KarlLee830/translate
i18n: Add some missing i18n keys and update Chinese translation
2024-06-10 11:58:47 -07:00
Timothy Jaeryang Baek e01b3569d5 Merge pull request #2983 from pedrosilva/ptPTtranslation
pt-PT translation
2024-06-10 11:58:34 -07:00
Timothy J. Baek a75b68c19c refac: message status history 2024-06-10 11:40:58 -07:00
Timothy J. Baek 34f04c53fc fix: audio 2024-06-10 11:04:00 -07:00
Pedro Silva 2b87ab32ff Updated pt-PT translation 2024-06-10 15:27:06 +01:00
arkohut d20f6cb45b fix: use Exception to handle all errors 2024-06-10 21:27:35 +08:00
Karl Lee 48c9fb0ddf Update Chinese translation 2024-06-10 18:02:52 +08:00
Timothy Jaeryang Baek c609cc23eb Merge pull request #2964 from googio/dev
feat: adding Serply as another web search provider
2024-06-10 01:50:04 -07:00
Timothy Jaeryang Baek 3566002b0b doc 2024-06-10 03:49:23 -05:00
Timothy J. Baek 140d7ff721 refac 2024-06-10 01:07:24 -07:00
Timothy J. Baek 7349e1d8c8 refac 2024-06-10 00:56:13 -07:00
Timothy J. Baek bf5a62298c refac 2024-06-10 00:15:46 -07:00
Timothy J. Baek 2972774d87 refac 2024-06-10 00:07:47 -07:00
Timothy Jaeryang Baek 75d455ac8f Merge pull request #2967 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.1
2024-06-09 19:09:28 -07:00
Timothy J. Baek 64b6a8c5db doc: changelog 2024-06-09 19:08:57 -07:00
Timothy Jaeryang Baek 8947da57af Merge pull request #2966 from open-webui/dev
fix
2024-06-09 19:00:08 -07:00
Timothy J. Baek 16f09fff63 fix 2024-06-09 18:59:45 -07:00
teampen 14eb667fc8 add changes to web search in admin settings 2024-06-09 21:52:08 -04:00
teampen 79bf8d6dd3 undo changes to deprecated web search params 2024-06-09 21:43:13 -04:00
teampen 14d33f0fcc Merge branch 'add-serply' into dev 2024-06-09 21:40:50 -04:00
teampen 4dcec4855e adding Serply as an alternative web search 2024-06-09 21:39:46 -04:00
Timothy Jaeryang Baek 96a004d4d8 Merge pull request #2921 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.0
2024-06-09 18:11:53 -07:00
Timothy J. Baek 1fa16d73f0 doc: changelog 2024-06-09 18:02:42 -07:00
Timothy J. Baek d2cd803a57 doc: changelog 2024-06-09 18:01:40 -07:00
Timothy J. Baek e685953df7 doc: changelog 2024-06-09 17:57:16 -07:00
Timothy J. Baek d6520e7fd8 fix: document selector 2024-06-09 17:55:23 -07:00
Timothy J. Baek c54e3d5bf4 doc: changelog 2024-06-09 17:52:22 -07:00
teampen efb4a710c8 adding Serply as an alternative web search 2024-06-09 20:44:34 -04:00
Timothy J. Baek a71b824728 chore: format 2024-06-09 17:39:33 -07:00
Timothy J. Baek bc1a7ab567 fix: playground 2024-06-09 17:38:08 -07:00
Timothy J. Baek b565301a47 feat: knowledge integration 2024-06-09 17:34:42 -07:00
Timothy J. Baek 78272aed8d fix 2024-06-09 17:20:23 -07:00
Timothy J. Baek 3f7913b36f feat: knowledge selector 2024-06-09 17:17:35 -07:00
Timothy J. Baek e1edf9227d feat: model knowledge ui 2024-06-09 16:34:34 -07:00
Timothy J. Baek a8d709102f refac: styling 2024-06-09 16:17:10 -07:00
Timothy J. Baek 9032ab8857 refac 2024-06-09 16:05:29 -07:00
Timothy J. Baek c9137edf5a refac 2024-06-09 15:59:33 -07:00
Timothy J. Baek d11c373cf1 fix: styling 2024-06-09 15:36:25 -07:00
Timothy J. Baek 2755ef62d9 feat: threshold setting 2024-06-09 15:29:55 -07:00
Timothy J. Baek 8debb71197 feat: search query threshold 2024-06-09 15:19:36 -07:00
Timothy J. Baek 8b4867deb5 refac: styling 2024-06-09 15:12:38 -07:00
Timothy J. Baek a93645aee8 refac 2024-06-09 15:08:23 -07:00
Timothy J. Baek 28ca6fb678 refac 2024-06-09 15:04:33 -07:00
Timothy J. Baek da4586f27d refac: task models 2024-06-09 15:00:38 -07:00
Timothy J. Baek 591cd993c2 refac: search query task 2024-06-09 14:53:10 -07:00
Timothy J. Baek aa1bb4fb6d refac 2024-06-09 14:26:49 -07:00
Timothy J. Baek 5e7237b9cb refac: title generation 2024-06-09 14:25:31 -07:00
Timothy J. Baek 84defafc14 feat: unified chat completions endpoint 2024-06-09 13:17:44 -07:00
Timothy Jaeryang Baek 7b1404f490 Merge pull request #2936 from SimonOriginal/dev
Ukrainian translation update
2024-06-09 13:01:33 -07:00
Timothy Jaeryang Baek 9b38788aa2 Merge pull request #2937 from mindspawn/cuda-fix
Enable case independent environment variable testing in start.sh
2024-06-09 13:01:17 -07:00
Timothy J. Baek c44fc82ecd refac: openai 2024-06-09 12:43:54 -07:00
Timothy J. Baek 8b6f422d45 refac: styling 2024-06-09 12:15:48 -07:00
Timothy J. Baek 2eb8c3456f refac 2024-06-09 12:13:13 -07:00
Timothy J. Baek fa9835a7ad refac: styling 2024-06-09 12:08:16 -07:00
Timothy J. Baek 3db9d19ae3 refac 2024-06-09 11:57:33 -07:00
Timothy Jaeryang Baek 762db11bf4 Merge pull request #2939 from KarlLee830/translate
Add some missing i18n keys
2024-06-09 11:14:50 -07:00
arkohut 8e9e429a91 fix: tolerant readonly filesystem for copy favicon to static dir 2024-06-09 23:23:28 +08:00
Timothy J. Baek f2b9a5f5bf refac: rag 2024-06-09 03:01:25 -07:00
Timothy J. Baek 277fc3feac refac: styling 2024-06-09 02:44:50 -07:00
Timothy J. Baek 9053bfdadf refac: styling 2024-06-09 02:41:52 -07:00
Timothy J. Baek 414ab53144 refac: banners 2024-06-09 02:39:18 -07:00
Timothy J. Baek 4a7d3a076c fix: styling 2024-06-09 02:29:56 -07:00
Timothy J. Baek 8198807fc9 refac: document settings > admin settings 2024-06-09 02:28:52 -07:00
Timothy J. Baek e556c02fe9 refac: styling 2024-06-09 02:14:05 -07:00
Timothy J. Baek f7de01cbc2 refac: styling 2024-06-09 02:05:36 -07:00
Timothy J. Baek 1bc96de620 refac: styling 2024-06-09 02:01:03 -07:00
Timothy J. Baek 23489105fc refac: styling 2024-06-09 01:59:39 -07:00
Timothy Jaeryang Baek 8c95a8be3a Merge pull request #2943 from choltha/fix/temperature-params
fix: model settings temperature not passed correctly from model settings to ollama/openai api
2024-06-09 01:56:18 -07:00
Timothy J. Baek d00c8b00df refac 2024-06-09 01:55:56 -07:00
Timothy J. Baek 4bae43096b refac: settings 2024-06-09 01:54:50 -07:00
Timothy J. Baek d90e138773 refac: settings 2024-06-09 01:44:08 -07:00
Timothy J. Baek bb15eb016e fix: message input delay 2024-06-09 01:22:21 -07:00
Christoph Holthaus 97b39115a1 fix: temperature not passed correctly 2024-06-09 09:49:24 +02:00
Timothy J. Baek cc53fd8379 fix: params 2024-06-08 20:09:59 -07:00
Timothy J. Baek 5342063e24 fix: import model 2024-06-08 19:21:37 -07:00
Timothy J. Baek e274a0dc3e fix: base model verification 2024-06-08 17:21:23 -07:00
Karl Lee 9c3920a489 Update Chinese translation 2024-06-09 04:54:58 +08:00
Timothy J. Baek d1da3dde65 refac: migration 2024-06-08 13:45:33 -07:00
Karl Lee 0ccd96c950 Add some missing i18n keys 2024-06-09 04:42:22 +08:00
Timothy J. Baek 3499ec3f79 fix: tts 2024-06-08 13:18:42 -07:00
Timothy J. Baek 40ea18d54d refac: auth styling 2024-06-08 13:08:03 -07:00
Timothy Jaeryang Baek ffbc480bb4 Merge pull request #2930 from KarlLee830/translate
Update Chinese translation
2024-06-08 13:03:22 -07:00
Timothy Jaeryang Baek 9b839e5c9f Merge pull request #2927 from arkohut/tolerant-no-write-permission-for-static-dir
fix: tolerant no write permission for copy favicon to static dir
2024-06-08 13:03:08 -07:00
mindspawn 575f23cde9 Update start.sh to be case-independent when testing env vars 2024-06-08 10:59:59 -07:00
SimonOriginal 711317555c Ukrainian translation update 2024-06-08 18:08:53 +02:00
Timothy J. Baek 063e1ee46c refac: call 2024-06-08 03:27:56 -07:00
Karl Lee 0d0f8aac15 Upload Chinese tranlsation 2024-06-08 18:13:19 +08:00
Timothy J. Baek 5cbb79fa6e fix: safari audio issue 2024-06-08 02:07:19 -07:00
Timothy J. Baek 277e7aead7 refac 2024-06-08 00:52:19 -07:00
arkohut d3558fdb33 fix: tolerant no write permission for copy favicon to static dir 2024-06-08 13:56:08 +08:00
Timothy J. Baek 1271d1dccc feat: allow image from prompt {{CLIPBOARD}} 2024-06-07 22:44:10 -07:00
Timothy J. Baek 3be1cb98d3 fix: ollama embedding list 2024-06-07 22:32:45 -07:00
Timothy J. Baek c2a7e7f510 doc: readme 2024-06-07 22:17:11 -07:00
Timothy Jaeryang Baek dbde628141 Merge pull request #2923 from mindspawn/outlook-msg
Support Outlook Message File Format
2024-06-07 21:50:03 -07:00
mindspawn 2412f31ed9 Update Dockerfile to revert duplicated pip install 2024-06-07 21:47:50 -07:00
Timothy J. Baek 8df0c9e0dd doc: readme 2024-06-07 21:47:31 -07:00
mindspawn 6f9148ac4c Update main.py 2024-06-07 21:41:30 -07:00
Timothy J. Baek d7d8896e43 fix: active users 2024-06-07 21:38:09 -07:00
mindspawn d5a2a8a880 Update Dockerfile 2024-06-07 21:20:59 -07:00
mindspawn cff8534f33 Update constants.ts 2024-06-07 21:19:46 -07:00
mindspawn f69bc57fed Update requirements.txt 2024-06-07 21:18:35 -07:00
mindspawn 4ecc1c06d3 Update main.py 2024-06-07 21:18:04 -07:00
Timothy J. Baek f78e6a5fba fix: auto playback 2024-06-07 21:10:00 -07:00
Timothy J. Baek 411e6c2fe4 fix: styling 2024-06-07 21:06:26 -07:00
Timothy J. Baek 369816ace0 chore: format 2024-06-07 21:03:45 -07:00
Timothy J. Baek 06e4b87c9e refac: audio 2024-06-07 20:57:15 -07:00
Timothy J. Baek c6b74a3cf9 refac: audio 2024-06-07 20:55:50 -07:00
Timothy J. Baek 27417dd771 refac: audio 2024-06-07 20:51:53 -07:00
Timothy J. Baek 16615010f7 refac 2024-06-07 20:35:50 -07:00
Timothy J. Baek e516374d54 feat: external stt 2024-06-07 20:31:52 -07:00
Timothy J. Baek 55dc6c1b3b refac: audio 2024-06-07 20:18:48 -07:00
Timothy J. Baek da47c2dfa3 refac: active users 2024-06-07 17:35:01 -07:00
Timothy J. Baek 694d1708a3 fix: styling 2024-06-07 16:58:33 -07:00
Timothy J. Baek f9aa03bc6d feat: dark splash screen 2024-06-07 16:57:02 -07:00
Timothy J. Baek bd3e5d7bfa refac 2024-06-07 16:20:50 -07:00
Timothy J. Baek 5743df2280 refac: styling 2024-06-07 15:55:02 -07:00
Timothy J. Baek e3efe2c565 fix: safari video 2024-06-07 15:31:58 -07:00
Timothy J. Baek 32c4748749 refac 2024-06-07 15:18:45 -07:00
Timothy J. Baek f4a15f9590 chore: version bump 2024-06-07 15:16:24 -07:00
Timothy J. Baek f92ef3211b refac: permission 2024-06-07 15:12:34 -07:00
Timothy J. Baek 508e8eb4b1 refac 2024-06-07 15:08:21 -07:00
Timothy J. Baek dfc2fb65af refac: styling 2024-06-07 15:01:55 -07:00
Timothy J. Baek d7ce408d25 refac 2024-06-07 15:00:42 -07:00
Timothy J. Baek 374f647048 fix 2024-06-07 14:56:10 -07:00
Timothy J. Baek 00018085ed refac 2024-06-07 14:55:32 -07:00
Timothy J. Baek d17cdc8068 feat: video devices support 2024-06-07 14:49:36 -07:00
Timothy J. Baek e5ad76615c feat: video call 2024-06-07 14:08:04 -07:00
Jun Siang Cheah bba4c4242f feat: add WEBUI_SESSION_COOKIE_SECURE 2024-06-07 09:13:42 +01:00
Timothy J. Baek 54420f5300 refac: styling 2024-06-07 01:04:51 -07:00
Timothy J. Baek f076f81e0c fix: safari compatibility issue 2024-06-07 00:57:53 -07:00
Timothy J. Baek ea80285e9c fix: disable empty string 2024-06-07 00:37:54 -07:00
Timothy J. Baek f43d255e8f refac: wip tooltip 2024-06-07 00:28:34 -07:00
Timothy J. Baek fc3a31e3d5 refac: optimisation 2024-06-07 00:27:05 -07:00
Timothy J. Baek 4e640daf83 feat: submit prompt integration 2024-06-07 00:04:47 -07:00
Timothy J. Baek 404bb3fd67 refac: call overlay 2024-06-06 23:42:31 -07:00
Timothy J. Baek eacce66a82 refac 2024-06-06 23:36:47 -07:00
Timothy J. Baek 4f19a92dc5 refac 2024-06-06 23:29:08 -07:00
Timothy J. Baek 5196b65c2e feat: call ui scaffold 2024-06-06 22:30:19 -07:00
Timothy J. Baek 340b716a90 refac: voice recording web api 2024-06-06 21:56:09 -07:00
Timothy Jaeryang Baek ff4cf16742 Merge pull request #2900 from open-webui/voice
feat: voice note
2024-06-06 21:01:41 -07:00
Timothy J. Baek 7eaf83cb7d fix: silence monitoring 2024-06-06 21:00:41 -07:00
Timothy J. Baek 4a73a01c24 refac 2024-06-06 20:44:42 -07:00
Timothy J. Baek 1a9dee50ce refac: styling 2024-06-06 20:35:18 -07:00
Timothy J. Baek f97f6601f7 feat: voice input refac 2024-06-06 20:33:23 -07:00
Timothy Jaeryang Baek 5ab5231c73 Merge pull request #2886 from KarlLee830/translate
i18n: Added missing i18n key
2024-06-06 11:18:48 -07:00
Karl Lee 046dd02a6d Added missing i18n key 2024-06-07 01:52:03 +08:00
Timothy Jaeryang Baek 72e1615fe1 Merge pull request #2873 from KarlLee830/translate
i18n: Added missing i18n keys for the pending overlay and other pages.
2024-06-06 04:10:47 -07:00
Timothy Jaeryang Baek b512501d9d Merge pull request #2870 from mindspawn/proxy-fix2
Fix proxy not being used for /api/version/updates
2024-06-06 04:06:36 -07:00
Karl Lee 36cc3c24e6 Added some missing i18n keys 2024-06-06 15:07:09 +08:00
Karl Lee 62bfcf35e5 Update translation.json 2024-06-06 14:19:36 +08:00
Karl Lee 9bec2cea12 Update translation 2024-06-06 13:58:25 +08:00
Karl Lee f4a122a4bb Added missing i18n
Added missing i18n keys and update Chinese translation.
2024-06-06 13:47:40 +08:00
Karl Lee 34aea03c02 Update Chinese translation 2024-06-06 13:29:45 +08:00
Karl Lee e7fb86190a added missing i18n keys 2024-06-06 13:12:45 +08:00
Karl Lee 194564e253 Update Chinese translation 2024-06-06 13:03:11 +08:00
Timothy J. Baek d4b10097d2 fix: sidebar 2024-06-05 18:31:15 -07:00
mindspawn 3069fa8074 Update main.py to enable http_proxy use for checking for new versions 2024-06-05 17:24:59 -07:00
Timothy J. Baek c2bbb37fc1 fix: sidebar 2024-06-05 14:20:26 -07:00
Timothy J. Baek e52568e7cb feat: upload pipeline 2024-06-05 13:57:48 -07:00
Timothy Jaeryang Baek bc4aeeeec8 Merge pull request #2866 from bartowski1182/patch-1
Update CONTRIBUTING.md to include proper first step
2024-06-05 13:26:19 -07:00
Bartowski 25ee5f6cb0 Update CONTRIBUTING.md
Update step one to be opening a discussion rather than an issue
2024-06-05 16:24:24 -04:00
Timothy Jaeryang Baek 882ad219ea Merge pull request #2862 from jannikstdl/locale-update
i18n: added missing i18n keys + german locales
2024-06-05 12:43:34 -07:00
Jannik Streidl c108d863f6 added missing i18n keys + german locales 2024-06-05 20:42:41 +02:00
Jun Siang Cheah ae376ec8fe Merge remote-tracking branch 'upstream/dev' into feat/oauth 2024-06-05 19:23:52 +01:00
Jun Siang Cheah af4f8aa589 feat: add WEBUI_SESSION_COOKIE_SAME_SITE for when open webui is embedded 2024-06-05 19:21:42 +01:00
Timothy Jaeryang Baek a8d80f936a Merge pull request #2857 from open-webui/dev
refac: styling
2024-06-05 10:12:23 -07:00
Timothy J. Baek 1bb7fc7c92 refac: styling 2024-06-05 10:12:01 -07:00
Беклемишев Петр Алексеевич ad32a2ef3c Drop mysql restarts 2024-05-31 13:26:23 +07:00
Беклемишев Петр Алексеевич e59e1f5049 Fix rebase artifacts 2024-05-30 20:44:13 +07:00
Беклемишев Петр Алексеевич e1fa453eda Test reconnection to postgres in gh actions 2024-05-30 20:41:10 +07:00
Беклемишев Петр Алексеевич dfbc125947 Reconnect to postgresql & mysql external databases when getting disconnected 2024-05-30 20:41:06 +07:00
Jun Siang Cheah a1f4706aa1 fix: OAUTH_MERGE_ACCOUNTS_BY_EMAIL not being correctly read 2024-05-27 18:16:36 +01:00
Jun Siang Cheah 6a36039a59 Merge remote-tracking branch 'upstream/dev' into feat/oauth 2024-05-27 18:14:43 +01:00
Jun Siang Cheah 985fdca585 refac: move things around, uplift oauth endpoints 2024-05-27 18:07:38 +01:00
Jun Siang Cheah 06dbf59742 feat: make oauth config persist into config.json 2024-05-27 17:21:24 +01:00
Jun Siang Cheah 4a4e323a44 refac: rename OPENID -> OAUTH 2024-05-27 17:17:34 +01:00
Jun Siang Cheah 776bb2892c fix: bad user get 2024-05-26 13:07:41 +01:00
Jun Siang Cheah 7385016e36 fix: only get user by emails without oauth sub 2024-05-26 11:59:21 +01:00
Jun Siang Cheah b3557d05b1 fix: bad authlib version 2024-05-26 11:32:42 +01:00
Jun Siang Cheah 0210a105bf feat: experimental SSO support for Google, Microsoft, and OIDC 2024-05-26 11:26:15 +01:00
Jun Siang Cheah a842d8d62b deps: add authlib 2024-05-26 11:25:29 +01:00
333 changed files with 43175 additions and 13195 deletions
+2 -1
View File
@@ -10,7 +10,8 @@ node_modules
vite.config.js.timestamp-*
vite.config.ts.timestamp-*
__pycache__
.env
.idea
venv
_old
uploads
.ipynb_checkpoints
+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.]
@@ -3,9 +3,10 @@ updates:
- package-ecosystem: pip
directory: '/backend'
schedule:
interval: weekly
interval: monthly
target-branch: 'dev'
- package-ecosystem: 'github-actions'
directory: '/'
schedule:
# Check for updates to GitHub Actions every week
interval: 'weekly'
interval: monthly
+55 -53
View File
@@ -10,61 +10,63 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Checkout repository
uses: actions/checkout@v4
- name: Check for changes in package.json
run: |
git diff --cached --diff-filter=d package.json || {
echo "No changes to package.json"
exit 1
}
- name: Get version number from package.json
id: get_version
run: |
VERSION=$(jq -r '.version' package.json)
echo "::set-output name=version::$VERSION"
- name: Check for changes in package.json
run: |
git diff --cached --diff-filter=d package.json || {
echo "No changes to package.json"
exit 1
}
- name: Extract latest CHANGELOG entry
id: changelog
run: |
CHANGELOG_CONTENT=$(awk 'BEGIN {print_section=0;} /^## \[/ {if (print_section == 0) {print_section=1;} else {exit;}} print_section {print;}' CHANGELOG.md)
CHANGELOG_ESCAPED=$(echo "$CHANGELOG_CONTENT" | sed ':a;N;$!ba;s/\n/%0A/g')
echo "Extracted latest release notes from CHANGELOG.md:"
echo -e "$CHANGELOG_CONTENT"
echo "::set-output name=content::$CHANGELOG_ESCAPED"
- name: Get version number from package.json
id: get_version
run: |
VERSION=$(jq -r '.version' package.json)
echo "::set-output name=version::$VERSION"
- name: Create GitHub release
uses: actions/github-script@v7
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const changelog = `${{ steps.changelog.outputs.content }}`;
const release = await github.rest.repos.createRelease({
owner: context.repo.owner,
repo: context.repo.repo,
tag_name: `v${{ steps.get_version.outputs.version }}`,
name: `v${{ steps.get_version.outputs.version }}`,
body: changelog,
})
console.log(`Created release ${release.data.html_url}`)
- name: Extract latest CHANGELOG entry
id: changelog
run: |
CHANGELOG_CONTENT=$(awk 'BEGIN {print_section=0;} /^## \[/ {if (print_section == 0) {print_section=1;} else {exit;}} print_section {print;}' CHANGELOG.md)
CHANGELOG_ESCAPED=$(echo "$CHANGELOG_CONTENT" | sed ':a;N;$!ba;s/\n/%0A/g')
echo "Extracted latest release notes from CHANGELOG.md:"
echo -e "$CHANGELOG_CONTENT"
echo "::set-output name=content::$CHANGELOG_ESCAPED"
- name: Upload package to GitHub release
uses: actions/upload-artifact@v4
with:
name: package
path: .
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Create GitHub release
uses: actions/github-script@v7
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const changelog = `${{ steps.changelog.outputs.content }}`;
const release = await github.rest.repos.createRelease({
owner: context.repo.owner,
repo: context.repo.repo,
tag_name: `v${{ steps.get_version.outputs.version }}`,
name: `v${{ steps.get_version.outputs.version }}`,
body: changelog,
})
console.log(`Created release ${release.data.html_url}`)
- name: Trigger Docker build workflow
uses: actions/github-script@v7
with:
script: |
github.rest.actions.createWorkflowDispatch({
owner: context.repo.owner,
repo: context.repo.repo,
workflow_id: 'docker-build.yaml',
ref: 'v${{ steps.get_version.outputs.version }}',
})
- name: Upload package to GitHub release
uses: actions/upload-artifact@v4
with:
name: package
path: |
.
!.git
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Trigger Docker build workflow
uses: actions/github-script@v7
with:
script: |
github.rest.actions.createWorkflowDispatch({
owner: context.repo.owner,
repo: context.repo.repo,
workflow_id: 'docker-build.yaml',
ref: 'v${{ steps.get_version.outputs.version }}',
})
+54 -2
View File
@@ -11,8 +11,6 @@ on:
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
FULL_IMAGE_NAME: ghcr.io/${{ github.repository }}
jobs:
build-main-image:
@@ -28,6 +26,15 @@ jobs:
- linux/arm64
steps:
# GitHub Packages requires the entire repository name to be in lowercase
# although the repository owner has a lowercase username, this prevents some people from running actions after forking
- name: Set repository and image name to lowercase
run: |
echo "IMAGE_NAME=${IMAGE_NAME,,}" >>${GITHUB_ENV}
echo "FULL_IMAGE_NAME=ghcr.io/${IMAGE_NAME,,}" >>${GITHUB_ENV}
env:
IMAGE_NAME: '${{ github.repository }}'
- name: Prepare
run: |
platform=${{ matrix.platform }}
@@ -116,6 +123,15 @@ jobs:
- linux/arm64
steps:
# GitHub Packages requires the entire repository name to be in lowercase
# although the repository owner has a lowercase username, this prevents some people from running actions after forking
- name: Set repository and image name to lowercase
run: |
echo "IMAGE_NAME=${IMAGE_NAME,,}" >>${GITHUB_ENV}
echo "FULL_IMAGE_NAME=ghcr.io/${IMAGE_NAME,,}" >>${GITHUB_ENV}
env:
IMAGE_NAME: '${{ github.repository }}'
- name: Prepare
run: |
platform=${{ matrix.platform }}
@@ -207,6 +223,15 @@ jobs:
- linux/arm64
steps:
# GitHub Packages requires the entire repository name to be in lowercase
# although the repository owner has a lowercase username, this prevents some people from running actions after forking
- name: Set repository and image name to lowercase
run: |
echo "IMAGE_NAME=${IMAGE_NAME,,}" >>${GITHUB_ENV}
echo "FULL_IMAGE_NAME=ghcr.io/${IMAGE_NAME,,}" >>${GITHUB_ENV}
env:
IMAGE_NAME: '${{ github.repository }}'
- name: Prepare
run: |
platform=${{ matrix.platform }}
@@ -289,6 +314,15 @@ jobs:
runs-on: ubuntu-latest
needs: [ build-main-image ]
steps:
# GitHub Packages requires the entire repository name to be in lowercase
# although the repository owner has a lowercase username, this prevents some people from running actions after forking
- name: Set repository and image name to lowercase
run: |
echo "IMAGE_NAME=${IMAGE_NAME,,}" >>${GITHUB_ENV}
echo "FULL_IMAGE_NAME=ghcr.io/${IMAGE_NAME,,}" >>${GITHUB_ENV}
env:
IMAGE_NAME: '${{ github.repository }}'
- name: Download digests
uses: actions/download-artifact@v4
with:
@@ -335,6 +369,15 @@ jobs:
runs-on: ubuntu-latest
needs: [ build-cuda-image ]
steps:
# GitHub Packages requires the entire repository name to be in lowercase
# although the repository owner has a lowercase username, this prevents some people from running actions after forking
- name: Set repository and image name to lowercase
run: |
echo "IMAGE_NAME=${IMAGE_NAME,,}" >>${GITHUB_ENV}
echo "FULL_IMAGE_NAME=ghcr.io/${IMAGE_NAME,,}" >>${GITHUB_ENV}
env:
IMAGE_NAME: '${{ github.repository }}'
- name: Download digests
uses: actions/download-artifact@v4
with:
@@ -382,6 +425,15 @@ jobs:
runs-on: ubuntu-latest
needs: [ build-ollama-image ]
steps:
# GitHub Packages requires the entire repository name to be in lowercase
# although the repository owner has a lowercase username, this prevents some people from running actions after forking
- name: Set repository and image name to lowercase
run: |
echo "IMAGE_NAME=${IMAGE_NAME,,}" >>${GITHUB_ENV}
echo "FULL_IMAGE_NAME=ghcr.io/${IMAGE_NAME,,}" >>${GITHUB_ENV}
env:
IMAGE_NAME: '${{ github.repository }}'
- name: Download digests
uses: actions/download-artifact@v4
with:
+68 -17
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
@@ -25,7 +32,11 @@ jobs:
--file docker-compose.api.yaml \
--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: |
@@ -67,6 +78,28 @@ jobs:
path: compose-logs.txt
if-no-files-found: ignore
# pytest:
# name: Run Backend Tests
# runs-on: ubuntu-latest
# steps:
# - uses: actions/checkout@v4
# - name: Set up Python
# uses: actions/setup-python@v4
# with:
# python-version: ${{ matrix.python-version }}
# - name: Install dependencies
# run: |
# python -m pip install --upgrade pip
# pip install -r backend/requirements.txt
# - name: pytest run
# run: |
# ls -al
# cd backend
# PYTHONPATH=. pytest . -o log_cli=true -o log_cli_level=INFO
migration_test:
name: Run Migration Tests
runs-on: ubuntu-latest
@@ -82,18 +115,18 @@ jobs:
--health-retries 5
ports:
- 5432:5432
# mysql:
# image: mysql
# env:
# MYSQL_ROOT_PASSWORD: mysql
# MYSQL_DATABASE: mysql
# options: >-
# --health-cmd "mysqladmin ping -h localhost"
# --health-interval 10s
# --health-timeout 5s
# --health-retries 5
# ports:
# - 3306:3306
# mysql:
# image: mysql
# env:
# MYSQL_ROOT_PASSWORD: mysql
# MYSQL_DATABASE: mysql
# options: >-
# --health-cmd "mysqladmin ping -h localhost"
# --health-interval 10s
# --health-timeout 5s
# --health-retries 5
# ports:
# - 3306:3306
steps:
- name: Checkout Repository
uses: actions/checkout@v4
@@ -126,11 +159,11 @@ jobs:
cd backend
uvicorn main:app --port "8080" --forwarded-allow-ips '*' &
UVICORN_PID=$!
# Wait up to 20 seconds for the server to start
for i in {1..20}; do
# Wait up to 40 seconds for the server to start
for i in {1..40}; do
curl -s http://localhost:8080/api/config > /dev/null && break
sleep 1
if [ $i -eq 20 ]; then
if [ $i -eq 40 ]; then
echo "Server failed to start"
kill -9 $UVICORN_PID
exit 1
@@ -142,7 +175,6 @@ jobs:
echo "Server has stopped"
exit 1
fi
- name: Test backend with Postgres
if: success() || steps.sqlite.conclusion == 'failure'
@@ -171,6 +203,25 @@ jobs:
exit 1
fi
# Check that service will reconnect to postgres when connection will be closed
status_code=$(curl --write-out %{http_code} -s --output /dev/null http://localhost:8081/health/db)
if [[ "$status_code" -ne 200 ]] ; then
echo "Server has failed before postgres reconnect check"
exit 1
fi
echo "Terminating all connections to postgres..."
python -c "import os, psycopg2 as pg2; \
conn = pg2.connect(dsn=os.environ['DATABASE_URL'].replace('+pool', '')); \
cur = conn.cursor(); \
cur.execute('SELECT pg_terminate_backend(psa.pid) FROM pg_stat_activity psa WHERE datname = current_database() AND pid <> pg_backend_pid();')"
status_code=$(curl --write-out %{http_code} -s --output /dev/null http://localhost:8081/health/db)
if [[ "$status_code" -ne 200 ]] ; then
echo "Server has not reconnected to postgres after connection was closed: returned status $status_code"
exit 1
fi
# - name: Test backend with MySQL
# if: success() || steps.sqlite.conclusion == 'failure' || steps.postgres.conclusion == 'failure'
# env:
+1
View File
@@ -306,3 +306,4 @@ dist
# cypress artifacts
cypress/videos
cypress/screenshots
.vscode/settings.json
+335
View File
@@ -5,6 +5,341 @@ 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.15] - 2024-08-21
### Added
- **🔗 Temporary Chat Activation**: Integrated a new URL parameter 'temporary-chat=true' to enable temporary chat sessions directly through the URL.
- **🌄 ComfyUI Seed Node Support**: Introduced seed node support in ComfyUI for image generation, allowing users to specify node IDs for randomized seed assignment.
### Fixed
- **🛠️ Tools and Functions**: Resolved a critical issue where Tools and Functions were not properly functioning, restoring full capability and reliability to these essential features.
- **🔘 Chat Action Button in Many Model Chat**: Fixed the malfunctioning of chat action buttons in many model chat environments, ensuring a smoother and more responsive user interaction.
- **⏪ Many Model Chat Compatibility**: Restored backward compatibility for many model chats.
## [0.3.14] - 2024-08-21
### Added
- **🛠️ Custom ComfyUI Workflow**: Deprecating several older environment variables, this enhancement introduces a new, customizable workflow for a more tailored user experience.
- **🔀 Merge Responses in Many Model Chat**: Enhances the dialogue by merging responses from multiple models into a single, coherent reply, improving the interaction quality in many model chats.
- **✅ Multiple Instances of Same Model in Chats**: Enhanced many model chat to support adding multiple instances of the same model.
- **🔧 Quick Actions in Model Workspace**: Enhanced Shift key quick actions for hiding/unhiding and deleting models, facilitating a smoother workflow.
- **🗨️ Markdown Rendering in User Messages**: User messages are now rendered in Markdown, enhancing readability and interaction.
- **💬 Temporary Chat Feature**: Introduced a temporary chat feature, deprecating the old chat history setting to enhance user interaction flexibility.
- **🖋️ User Message Editing**: Enhanced the user chat editing feature to allow saving changes without sending, providing more flexibility in message management.
- **🛡️ Security Enhancements**: Various security improvements implemented across the platform to ensure safer user experiences.
- **🌍 Updated Translations**: Enhanced translations for Chinese, Ukrainian, and Bahasa Malaysia, improving localization and user comprehension.
### Fixed
- **📑 Mermaid Rendering Issue**: Addressed issues with Mermaid chart rendering to ensure clean and clear visual data representation.
- **🎭 PWA Icon Maskability**: Fixed the Progressive Web App icon to be maskable, ensuring proper display on various device home screens.
- **🔀 Cloned Model Chat Freezing Issue**: Fixed a bug where cloning many model chats would cause freezing, enhancing stability and responsiveness.
- **🔍 Generic Error Handling and Refinements**: Various minor fixes and refinements to address previously untracked issues, ensuring smoother operations.
### Changed
- **🖼️ Image Generation Refactor**: Overhauled image generation processes for improved efficiency and quality.
- **🔨 Refactor Tool and Function Calling**: Refactored tool and function calling mechanisms for improved clarity and maintainability.
- **🌐 Backend Library Updates**: Updated critical backend libraries including SQLAlchemy, uvicorn[standard], faster-whisper, bcrypt, and boto3 for enhanced performance and security.
### Removed
- **🚫 Deprecated ComfyUI Environment Variables**: Removed several outdated environment variables related to ComfyUI settings, simplifying configuration management.
## [0.3.13] - 2024-08-14
### Added
- **🎨 Enhanced Markdown Rendering**: Significant improvements in rendering markdown, ensuring smooth and reliable display of LaTeX and Mermaid charts, enhancing user experience with more robust visual content.
- **🔄 Auto-Install Tools & Functions Python Dependencies**: For 'Tools' and 'Functions', Open WebUI now automatically install extra python requirements specified in the frontmatter, streamlining setup processes and customization.
- **🌀 OAuth Email Claim Customization**: Introduced an 'OAUTH_EMAIL_CLAIM' variable to allow customization of the default "email" claim within OAuth configurations, providing greater flexibility in authentication processes.
- **📶 Websocket Reconnection**: Enhanced reliability with the capability to automatically reconnect when a websocket is closed, ensuring consistent and stable communication.
- **🤳 Haptic Feedback on Support Devices**: Android devices now support haptic feedback for an immersive tactile experience during certain interactions.
### Fixed
- **🛠️ ComfyUI Performance Improvement**: Addressed an issue causing FastAPI to stall when ComfyUI image generation was active; now runs in a separate thread to prevent UI unresponsiveness.
- **🔀 Session Handling**: Fixed an issue mandating session_id on client-side to ensure smoother session management and transitions.
- **🖋️ Minor Bug Fixes and Format Corrections**: Various minor fixes including typo corrections, backend formatting improvements, and test amendments enhancing overall system stability and performance.
### Changed
- **🚀 Migration to SvelteKit 2**: Upgraded the underlying framework to SvelteKit version 2, offering enhanced speed, better code structure, and improved deployment capabilities.
- **🧹 General Cleanup and Refactoring**: Performed broad cleanup and refactoring across the platform, improving code efficiency and maintaining high standards of code health.
- **🚧 Integration Testing Improvements**: Modified how Cypress integration tests detect chat messages and updated sharing tests for better reliability and accuracy.
- **📁 Standardized '.safetensors' File Extension**: Renamed the '.sft' file extension to '.safetensors' for ComfyUI workflows, standardizing file formats across the platform.
### Removed
- **🗑️ Deprecated Frontend Functions**: Removed frontend functions that were migrated to backend to declutter the codebase and reduce redundancy.
## [0.3.12] - 2024-08-07
### Added
- **🔄 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
- **🔄 Improved File Upload**: Addressed the issue where file uploads lacked animation.
- **💬 Chat Continuity**: Fixed a problem where existing chats were not functioning properly in some instances.
- **🗂️ Chat File Reset**: Resolved the issue of chat files not resetting for new conversations, now ensuring a clean slate for each chat session.
- **📁 Document Workspace Uploads**: Corrected the handling of document uploads in the workspace using the Files API.
## [0.3.9] - 2024-07-17
### Added
- **📁 Files Chat Controls**: We've reverted to the old file handling behavior where uploaded files are always included. You can now manage files directly within the chat controls section, giving you the ability to remove files as needed.
- **🔧 "Action" Function Support**: Introducing a new "Action" function to write custom buttons to the message toolbar. This feature enables more interactive messaging, with documentation coming soon.
- **📜 Citations Handling**: For newly uploaded files in documents workspace, citations will now display the actual filename. Additionally, you can click on these filenames to open the file in a new tab for easier access.
- **🛠️ Event Emitter and Call Updates**: Enhanced 'event_emitter' to allow message replacement and 'event_call' to support text input for Tools and Functions. Detailed documentation will be provided shortly.
- **🎨 Styling Refactor**: Various styling updates for a cleaner and more cohesive user interface.
- **🌐 Enhanced Translations**: Improved translations for Catalan, Ukrainian, and Brazilian Portuguese.
### Fixed
- **🔧 Chat Controls Priority**: Resolved an issue where Chat Controls values were being overridden by model information parameters. The priority is now Chat Controls, followed by Global Settings, then Model Settings.
- **🪲 Debug Logs**: Fixed an issue where debug logs were not being logged properly.
- **🔑 Automatic1111 Auth Key**: The auth key for Automatic1111 is no longer required.
- **📝 Title Generation**: Ensured that the title generation runs only once, even when multiple models are in a chat.
- **✅ Boolean Values in Params**: Added support for boolean values in parameters.
- **🖼️ Files Overlay Styling**: Fixed the styling issue with the files overlay.
### Changed
- **⬆️ Dependency Updates**
- Upgraded 'pydantic' from version 2.7.1 to 2.8.2.
- Upgraded 'sqlalchemy' from version 2.0.30 to 2.0.31.
- Upgraded 'unstructured' from version 0.14.9 to 0.14.10.
- Upgraded 'chromadb' from version 0.5.3 to 0.5.4.
## [0.3.8] - 2024-07-09
### Added
- **💬 Chat Controls**: Easily adjust parameters for each chat session, offering more precise control over your interactions.
- **📌 Pinned Chats**: Support for pinned chats, allowing you to keep important conversations easily accessible.
- **📄 Apache Tika Integration**: Added support for using Apache Tika as a document loader, enhancing document processing capabilities.
- **🛠️ Custom Environment for OpenID Claims**: Allows setting custom claims for OpenID, providing more flexibility in user authentication.
- **🔧 Enhanced Tools & Functions API**: Introduced 'event_emitter' and 'event_call', now you can also add citations for better documentation and tracking. Detailed documentation will be provided on our documentation website.
- **↔️ Sideways Scrolling in Settings**: Settings tabs container now supports horizontal scrolling for easier navigation.
- **🌑 Darker OLED Theme**: Includes a new, darker OLED theme and improved styling for the light theme, enhancing visual appeal.
- **🌐 Language Updates**: Updated translations for Indonesian, German, French, and Catalan languages, expanding accessibility.
### Fixed
- **⏰ OpenAI Streaming Timeout**: Resolved issues with OpenAI streaming response using the 'AIOHTTP_CLIENT_TIMEOUT' setting, ensuring reliable performance.
- **💡 User Valves**: Fixed malfunctioning user valves, ensuring proper functionality.
- **🔄 Collapsible Components**: Addressed issues with collapsible components not working, restoring expected behavior.
### Changed
- **🗃️ Database Backend**: Switched from Peewee to SQLAlchemy for improved concurrency support, enhancing database performance.
- **⬆️ ChromaDB Update**: Upgraded to version 0.5.3. Ensure your remote ChromaDB instance matches this version.
- **🔤 Primary Font Styling**: Updated primary font to Archivo for better visual consistency.
- **🔄 Font Change for Windows**: Replaced Arimo with Inter font for Windows users, improving readability.
- **🚀 Lazy Loading**: Implemented lazy loading for 'faster_whisper' and 'sentence_transformers' to reduce startup memory usage.
- **📋 Task Generation Payload**: Task generations now include only the "task" field in the body instead of "title".
## [0.3.7] - 2024-06-29
### Added
- **🌐 Enhanced Internationalization (i18n)**: Newly introduced Indonesian translation, and updated translations for Turkish, Chinese, and Catalan languages to improve user accessibility.
### Fixed
- **🕵️‍♂️ Browser Language Detection**: Corrected the issue where the application was not properly detecting and adapting to the browser's language settings.
- **🔐 OIDC Admin Role Assignment**: Fixed a bug where the admin role was not being assigned to the first user who signed up via OpenID Connect (OIDC).
- **💬 Chat/Completions Endpoint**: Resolved an issue where the chat/completions endpoint was non-functional when the stream option was set to False.
- **🚫 'WEBUI_AUTH' Configuration**: Addressed the problem where setting 'WEBUI_AUTH' to False was not being applied correctly.
### Changed
- **📦 Dependency Update**: Upgraded 'authlib' from version 1.3.0 to 1.3.1 to ensure better security and performance enhancements.
## [0.3.6] - 2024-06-27
### Added
- **✨ "Functions" Feature**: You can now utilize "Functions" like filters (middleware) and pipe (model) functions directly within the WebUI. While largely compatible with Pipelines, these native functions can be executed easily within Open WebUI. Example use cases for filter functions include usage monitoring, real-time translation, moderation, and automemory. For pipe functions, the scope ranges from Cohere and Anthropic integration directly within Open WebUI, enabling "Valves" for per-user OpenAI API key usage, and much more. If you encounter issues, SAFE_MODE has been introduced.
- **📁 Files API**: Compatible with OpenAI, this feature allows for custom Retrieval-Augmented Generation (RAG) in conjunction with the Filter Function. More examples will be shared on our community platform and official documentation website.
- **🛠️ Tool Enhancements**: Tools now support citations and "Valves". Documentation will be available shortly.
- **🔗 Iframe Support via Files API**: Enables rendering HTML directly into your chat interface using functions and tools. Use cases include playing games like DOOM and Snake, displaying a weather applet, and implementing Anthropic "artifacts"-like features. Stay tuned for updates on our community platform and documentation.
- **🔒 Experimental OAuth Support**: New experimental OAuth support. Check our documentation for more details.
- **🖼️ Custom Background Support**: Set a custom background from Settings > Interface to personalize your experience.
- **🔑 AUTOMATIC1111_API_AUTH Support**: Enhanced security for the AUTOMATIC1111 API.
- **🎨 Code Highlight Optimization**: Improved code highlighting features.
- **🎙️ Voice Interruption Feature**: Reintroduced and now toggleable from Settings > Interface.
- **💤 Wakelock API**: Now in use to prevent screen dimming during important tasks.
- **🔐 API Key Privacy**: All API keys are now hidden by default for better security.
- **🔍 New Web Search Provider**: Added jina_search as a new option.
- **🌐 Enhanced Internationalization (i18n)**: Improved Korean translation and updated Chinese and Ukrainian translations.
### Fixed
- **🔧 Conversation Mode Issue**: Fixed the issue where Conversation Mode remained active after being removed from settings.
- **📏 Scroll Button Obstruction**: Resolved the issue where the scrollToBottom button container obstructed clicks on buttons beneath it.
### Changed
- **⏲️ AIOHTTP_CLIENT_TIMEOUT**: Now set to 'None' by default for improved configuration flexibility.
- **📞 Voice Call Enhancements**: Improved by skipping code blocks and expressions during calls.
- **🚫 Error Message Handling**: Disabled the continuation of operations with error messages.
- **🗂️ Playground Relocation**: Moved the Playground from the workspace to the user menu for better user experience.
## [0.3.5] - 2024-06-16
### Added
- **📞 Enhanced Voice Call**: Text-to-speech (TTS) callback now operates in real-time for each sentence, reducing latency by not waiting for full completion.
- **👆 Tap to Interrupt**: During a call, you can now stop the assistant from speaking by simply tapping, instead of using voice. This resolves the issue of the speaker's voice being mistakenly registered as input.
- **😊 Emoji Call**: Toggle this feature on from the Settings > Interface, allowing LLMs to express emotions using emojis during voice calls for a more dynamic interaction.
- **🖱️ Quick Archive/Delete**: Use the Shift key + mouseover on the chat list to swiftly archive or delete items.
- **📝 Markdown Support in Model Descriptions**: You can now format model descriptions with markdown, enabling bold text, links, etc.
- **🧠 Editable Memories**: Adds the capability to modify memories.
- **📋 Admin Panel Sorting**: Introduces the ability to sort users/chats within the admin panel.
- **🌑 Dark Mode for Quick Selectors**: Dark mode now available for chat quick selectors (prompts, models, documents).
- **🔧 Advanced Parameters**: Adds 'num_keep' and 'num_batch' to advanced parameters for customization.
- **📅 Dynamic System Prompts**: New variables '{{CURRENT_DATETIME}}', '{{CURRENT_TIME}}', '{{USER_LOCATION}}' added for system prompts. Ensure '{{USER_LOCATION}}' is toggled on from Settings > Interface.
- **🌐 Tavily Web Search**: Includes Tavily as a web search provider option.
- **🖊️ Federated Auth Usernames**: Ability to set user names for federated authentication.
- **🔗 Auto Clean URLs**: When adding connection URLs, trailing slashes are now automatically removed.
- **🌐 Enhanced Translations**: Improved Chinese and Swedish translations.
### Fixed
- **⏳ AIOHTTP_CLIENT_TIMEOUT**: Introduced a new environment variable 'AIOHTTP_CLIENT_TIMEOUT' for requests to Ollama lasting longer than 5 minutes. Default is 300 seconds; set to blank ('') for no timeout.
- **❌ Message Delete Freeze**: Resolved an issue where message deletion would sometimes cause the web UI to freeze.
## [0.3.4] - 2024-06-12
### Fixed
- **🔒 Mixed Content with HTTPS Issue**: Resolved a problem where mixed content (HTTP and HTTPS) was causing security warnings and blocking resources on HTTPS sites.
- **🔍 Web Search Issue**: Addressed the problem where web search functionality was not working correctly. The 'ENABLE_RAG_LOCAL_WEB_FETCH' option has been reintroduced to restore proper web searching capabilities.
- **💾 RAG Template Not Being Saved**: Fixed an issue where the RAG template was not being saved correctly, ensuring your custom templates are now preserved as expected.
## [0.3.3] - 2024-06-12
### Added
- **🛠️ Native Python Function Calling**: Introducing native Python function calling within Open WebUI. Weve also included a built-in code editor to seamlessly develop and integrate function code within the 'Tools' workspace. With this, you can significantly enhance your LLMs capabilities by creating custom RAG pipelines, web search tools, and even agent-like features such as sending Discord messages.
- **🌐 DuckDuckGo Integration**: Added DuckDuckGo as a web search provider, giving you more search options.
- **🌏 Enhanced Translations**: Improved translations for Vietnamese and Chinese languages, making the interface more accessible.
### Fixed
- **🔗 Web Search URL Error Handling**: Fixed the issue where a single URL error would disrupt the data loading process in Web Search mode. Now, such errors will be handled gracefully to ensure uninterrupted data loading.
- **🖥️ Frontend Responsiveness**: Resolved the problem where the frontend would stop responding if the backend encounters an error while downloading a model. Improved error handling to maintain frontend stability.
- **🔧 Dependency Issues in pip**: Fixed issues related to pip installations, ensuring all dependencies are correctly managed to prevent installation errors.
## [0.3.2] - 2024-06-10
### Added
- **🔍 Web Search Query Status**: The web search query will now persist in the results section to aid in easier debugging and tracking of search queries.
- **🌐 New Web Search Provider**: We have added Serply as a new option for web search providers, giving you more choices for your search needs.
- **🌏 Improved Translations**: We've enhanced translations for Chinese and Portuguese.
### Fixed
- **🎤 Audio File Upload Issue**: The bug that prevented audio files from being uploaded in chat input has been fixed, ensuring smooth communication.
- **💬 Message Input Handling**: Improved the handling of message inputs by instantly clearing images and text after sending, along with immediate visual indications when a response message is loading, enhancing user feedback.
- **⚙️ Parameter Registration and Validation**: Fixed the issue where parameters were not registering in certain cases and addressed the problem where users were unable to save due to invalid input errors.
## [0.3.1] - 2024-06-09
### Fixed
- **💬 Chat Functionality**: Resolved the issue where chat functionality was not working for specific models.
## [0.3.0] - 2024-06-09
### Added
- **📚 Knowledge Support for Models**: Attach documents directly to models from the models workspace, enhancing the information available to each model.
- **🎙️ Hands-Free Voice Call Feature**: Initiate voice calls without needing to use your hands, making interactions more seamless.
- **📹 Video Call Feature**: Enable video calls with supported vision models like Llava and GPT-4o, adding a visual dimension to your communications.
- **🎛️ Enhanced UI for Voice Recording**: Improved user interface for the voice recording feature, making it more intuitive and user-friendly.
- **🌐 External STT Support**: Now support for external Speech-To-Text services, providing more flexibility in choosing your STT provider.
- **⚙️ Unified Settings**: Consolidated settings including document settings under a new admin settings section for easier management.
- **🌑 Dark Mode Splash Screen**: A new splash screen for dark mode, ensuring a consistent and visually appealing experience for dark mode users.
- **📥 Upload Pipeline**: Directly upload pipelines from the admin settings > pipelines section, streamlining the pipeline management process.
- **🌍 Improved Language Support**: Enhanced support for Chinese and Ukrainian languages, better catering to a global user base.
### Fixed
- **🛠️ Playground Issue**: Fixed the playground not functioning properly, ensuring a smoother user experience.
- **🔥 Temperature Parameter Issue**: Corrected the issue where the temperature value '0' was not being passed correctly.
- **📝 Prompt Input Clearing**: Resolved prompt input textarea not being cleared right away, ensuring a clean slate for new inputs.
- **✨ Various UI Styling Issues**: Fixed numerous user interface styling problems for a more cohesive look.
- **👥 Active Users Display**: Fixed active users showing active sessions instead of actual users, now reflecting accurate user activity.
- **🌐 Community Platform Compatibility**: The Community Platform is back online and fully compatible with Open WebUI.
### Changed
- **📝 RAG Implementation**: Updated the RAG (Retrieval-Augmented Generation) implementation to use a system prompt for context, instead of overriding the user's prompt.
- **🔄 Settings Relocation**: Moved Models, Connections, Audio, and Images settings to the admin settings for better organization.
- **✍️ Improved Title Generation**: Enhanced the default prompt for title generation, yielding better results.
- **🔧 Backend Task Management**: Tasks like title generation and search query generation are now managed on the backend side and controlled only by the admin.
- **🔍 Editable Search Query Prompt**: You can now edit the search query generation prompt, offering more control over how queries are generated.
- **📏 Prompt Length Threshold**: Set the prompt length threshold for search query generation from the admin settings, giving more customization options.
- **📣 Settings Consolidation**: Merged the Banners admin setting with the Interface admin setting for a more streamlined settings area.
## [0.2.5] - 2024-06-05
### Added
+5 -3
View File
@@ -97,6 +97,7 @@ RUN if [ "$USE_OLLAMA" = "true" ]; then \
apt-get update && \
# Install pandoc and netcat
apt-get install -y --no-install-recommends pandoc netcat-openbsd curl && \
apt-get install -y --no-install-recommends gcc python3-dev && \
# for RAG OCR
apt-get install -y --no-install-recommends ffmpeg libsm6 libxext6 && \
# install helper tools
@@ -107,8 +108,9 @@ RUN if [ "$USE_OLLAMA" = "true" ]; then \
rm -rf /var/lib/apt/lists/*; \
else \
apt-get update && \
# Install pandoc and netcat
apt-get install -y --no-install-recommends pandoc netcat-openbsd curl jq && \
# Install pandoc, netcat and gcc
apt-get install -y --no-install-recommends pandoc gcc netcat-openbsd curl jq && \
apt-get install -y --no-install-recommends gcc python3-dev && \
# for RAG OCR
apt-get install -y --no-install-recommends ffmpeg libsm6 libxext6 && \
# cleanup
@@ -149,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
+16 -3
View File
@@ -29,11 +29,15 @@ Open WebUI is an [extensible](https://github.com/open-webui/pipelines), feature-
- ✒️🔢 **Full Markdown and LaTeX Support**: Elevate your LLM experience with comprehensive Markdown and LaTeX capabilities for enriched interaction.
- 🎤📹 **Hands-Free Voice/Video Call**: Experience seamless communication with integrated hands-free voice and video call features, allowing for a more dynamic and interactive chat environment.
- 🛠️ **Model Builder**: Easily create Ollama models via the Web UI. Create and add custom characters/agents, customize chat elements, and import models effortlessly through [Open WebUI Community](https://openwebui.com/) integration.
- 🐍 **Native Python Function Calling Tool**: Enhance your LLMs with built-in code editor support in the tools workspace. Bring Your Own Function (BYOF) by simply adding your pure Python functions, enabling seamless integration with LLMs.
- 📚 **Local RAG Integration**: Dive into the future of chat interactions with groundbreaking Retrieval Augmented Generation (RAG) support. This feature seamlessly integrates document interactions into your chat experience. You can load documents directly into the chat or add files to your document library, effortlessly accessing them using the `#` command before a query.
- 🔍 **Web Search for RAG**: Perform web searches using providers like `SearXNG`, `Google PSE`, `Brave Search`, `serpstack`, and `serper`, and inject the results directly into your chat experience.
- 🔍 **Web Search for RAG**: Perform web searches using providers like `SearXNG`, `Google PSE`, `Brave Search`, `serpstack`, `serper`, `Serply`, `DuckDuckGo` and `TavilySearch` and inject the results directly into your chat experience.
- 🌐 **Web Browsing Capability**: Seamlessly integrate websites into your chat experience using the `#` command followed by a URL. This feature allows you to incorporate web content directly into your conversations, enhancing the richness and depth of your interactions.
@@ -146,10 +150,19 @@ docker run --rm --volume /var/run/docker.sock:/var/run/docker.sock containrrr/wa
In the last part of the command, replace `open-webui` with your container name if it is different.
### Moving from Ollama WebUI to Open WebUI
Check our Migration Guide available in our [Open WebUI Documentation](https://docs.openwebui.com/migration/).
### Using the Dev Branch 🌙
> [!WARNING]
> The `:dev` branch contains the latest unstable features and changes. Use it at your own risk as it may have bugs or incomplete features.
If you want to try out the latest bleeding-edge features and are okay with occasional instability, you can use the `:dev` tag like this:
```bash
docker run -d -p 3000:8080 -v open-webui:/app/backend/data --name open-webui --add-host=host.docker.internal:host-gateway --restart always ghcr.io/open-webui/open-webui:dev
```
## What's Next? 🌟
Discover upcoming features on our roadmap in the [Open WebUI Documentation](https://docs.openwebui.com/roadmap/).
+4
View File
@@ -18,6 +18,10 @@ If you're experiencing connection issues, its often due to the WebUI docker c
docker run -d --network=host -v open-webui:/app/backend/data -e OLLAMA_BASE_URL=http://127.0.0.1:11434 --name open-webui --restart always ghcr.io/open-webui/open-webui:main
```
### Error on Slow Reponses for Ollama
Open WebUI has a default timeout of 5 minutes for Ollama to finish generating the response. If needed, this can be adjusted via the environment variable AIOHTTP_CLIENT_TIMEOUT, which sets the timeout in seconds.
### General Connection Errors
**Ensure Ollama Version is Up-to-Date**: Always start by checking that you have the latest version of Ollama. Visit [Ollama's official site](https://ollama.com/) for the latest updates.
+114
View File
@@ -0,0 +1,114 @@
# A generic, single database configuration.
[alembic]
# path to migration scripts
script_location = migrations
# template used to generate migration file names; The default value is %%(rev)s_%%(slug)s
# Uncomment the line below if you want the files to be prepended with date and time
# file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s
# sys.path path, will be prepended to sys.path if present.
# defaults to the current working directory.
prepend_sys_path = .
# timezone to use when rendering the date within the migration file
# as well as the filename.
# If specified, requires the python>=3.9 or backports.zoneinfo library.
# Any required deps can installed by adding `alembic[tz]` to the pip requirements
# string value is passed to ZoneInfo()
# leave blank for localtime
# timezone =
# max length of characters to apply to the
# "slug" field
# truncate_slug_length = 40
# set to 'true' to run the environment during
# the 'revision' command, regardless of autogenerate
# revision_environment = false
# set to 'true' to allow .pyc and .pyo files without
# a source .py file to be detected as revisions in the
# versions/ directory
# sourceless = false
# version location specification; This defaults
# to migrations/versions. When using multiple version
# directories, initial revisions must be specified with --version-path.
# The path separator used here should be the separator specified by "version_path_separator" below.
# version_locations = %(here)s/bar:%(here)s/bat:migrations/versions
# version path separator; As mentioned above, this is the character used to split
# version_locations. The default within new alembic.ini files is "os", which uses os.pathsep.
# If this key is omitted entirely, it falls back to the legacy behavior of splitting on spaces and/or commas.
# Valid values for version_path_separator are:
#
# version_path_separator = :
# version_path_separator = ;
# version_path_separator = space
version_path_separator = os # Use os.pathsep. Default configuration used for new projects.
# set to 'true' to search source files recursively
# in each "version_locations" directory
# new in Alembic version 1.10
# recursive_version_locations = false
# the output encoding used when revision files
# are written from script.py.mako
# output_encoding = utf-8
# sqlalchemy.url = REPLACE_WITH_DATABASE_URL
[post_write_hooks]
# post_write_hooks defines scripts or Python functions that are run
# on newly generated revision scripts. See the documentation for further
# detail and examples
# format using "black" - use the console_scripts runner, against the "black" entrypoint
# hooks = black
# black.type = console_scripts
# black.entrypoint = black
# black.options = -l 79 REVISION_SCRIPT_FILENAME
# lint with attempts to fix using "ruff" - use the exec runner, execute a binary
# hooks = ruff
# ruff.type = exec
# ruff.executable = %(here)s/.venv/bin/ruff
# ruff.options = --fix REVISION_SCRIPT_FILENAME
# Logging configuration
[loggers]
keys = root,sqlalchemy,alembic
[handlers]
keys = console
[formatters]
keys = generic
[logger_root]
level = WARN
handlers = console
qualname =
[logger_sqlalchemy]
level = WARN
handlers =
qualname = sqlalchemy.engine
[logger_alembic]
level = INFO
handlers =
qualname = alembic
[handler_console]
class = StreamHandler
args = (sys.stderr,)
level = NOTSET
formatter = generic
[formatter_generic]
format = %(levelname)-5.5s [%(name)s] %(message)s
datefmt = %H:%M:%S
+411 -113
View File
@@ -1,5 +1,12 @@
import os
import hashlib
import json
import logging
import os
import uuid
from functools import lru_cache
from pathlib import Path
import requests
from fastapi import (
FastAPI,
Request,
@@ -8,44 +15,36 @@ from fastapi import (
status,
UploadFile,
File,
Form,
)
from fastapi.responses import StreamingResponse, JSONResponse, FileResponse
from fastapi.middleware.cors import CORSMiddleware
from faster_whisper import WhisperModel
from fastapi.responses import FileResponse
from pydantic import BaseModel
import requests
import hashlib
from pathlib import Path
import json
from constants import ERROR_MESSAGES
from utils.utils import (
decode_token,
get_current_user,
get_verified_user,
get_admin_user,
)
from utils.misc import calculate_sha256
from config import (
SRC_LOG_LEVELS,
CACHE_DIR,
UPLOAD_DIR,
WHISPER_MODEL,
WHISPER_MODEL_DIR,
WHISPER_MODEL_AUTO_UPDATE,
DEVICE_TYPE,
AUDIO_OPENAI_API_BASE_URL,
AUDIO_OPENAI_API_KEY,
AUDIO_OPENAI_API_MODEL,
AUDIO_OPENAI_API_VOICE,
AUDIO_STT_OPENAI_API_BASE_URL,
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,
AUDIO_TTS_MODEL,
AUDIO_TTS_VOICE,
AppConfig,
CORS_ALLOW_ORIGIN,
)
from constants import ERROR_MESSAGES
from utils.utils import (
get_current_user,
get_verified_user,
get_admin_user,
)
log = logging.getLogger(__name__)
@@ -54,17 +53,25 @@ log.setLevel(SRC_LOG_LEVELS["AUDIO"])
app = FastAPI()
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_origins=CORS_ALLOW_ORIGIN,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
app.state.config = AppConfig()
app.state.config.OPENAI_API_BASE_URL = AUDIO_OPENAI_API_BASE_URL
app.state.config.OPENAI_API_KEY = AUDIO_OPENAI_API_KEY
app.state.config.OPENAI_API_MODEL = AUDIO_OPENAI_API_MODEL
app.state.config.OPENAI_API_VOICE = AUDIO_OPENAI_API_VOICE
app.state.config.STT_OPENAI_API_BASE_URL = AUDIO_STT_OPENAI_API_BASE_URL
app.state.config.STT_OPENAI_API_KEY = AUDIO_STT_OPENAI_API_KEY
app.state.config.STT_ENGINE = AUDIO_STT_ENGINE
app.state.config.STT_MODEL = AUDIO_STT_MODEL
app.state.config.TTS_OPENAI_API_BASE_URL = AUDIO_TTS_OPENAI_API_BASE_URL
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"
@@ -74,41 +81,105 @@ SPEECH_CACHE_DIR = Path(CACHE_DIR).joinpath("./audio/speech/")
SPEECH_CACHE_DIR.mkdir(parents=True, exist_ok=True)
class OpenAIConfigUpdateForm(BaseModel):
url: str
key: str
model: str
speaker: str
class TTSConfigForm(BaseModel):
OPENAI_API_BASE_URL: str
OPENAI_API_KEY: str
API_KEY: str
ENGINE: str
MODEL: str
VOICE: str
class STTConfigForm(BaseModel):
OPENAI_API_BASE_URL: str
OPENAI_API_KEY: str
ENGINE: str
MODEL: str
class AudioConfigUpdateForm(BaseModel):
tts: TTSConfigForm
stt: STTConfigForm
from pydub import AudioSegment
from pydub.utils import mediainfo
def is_mp4_audio(file_path):
"""Check if the given file is an MP4 audio file."""
if not os.path.isfile(file_path):
print(f"File not found: {file_path}")
return False
info = mediainfo(file_path)
if (
info.get("codec_name") == "aac"
and info.get("codec_type") == "audio"
and info.get("codec_tag_string") == "mp4a"
):
return True
return False
def convert_mp4_to_wav(file_path, output_path):
"""Convert MP4 audio file to WAV format."""
audio = AudioSegment.from_file(file_path, format="mp4")
audio.export(output_path, format="wav")
print(f"Converted {file_path} to {output_path}")
@app.get("/config")
async def get_openai_config(user=Depends(get_admin_user)):
async def get_audio_config(user=Depends(get_admin_user)):
return {
"OPENAI_API_BASE_URL": app.state.config.OPENAI_API_BASE_URL,
"OPENAI_API_KEY": app.state.config.OPENAI_API_KEY,
"OPENAI_API_MODEL": app.state.config.OPENAI_API_MODEL,
"OPENAI_API_VOICE": app.state.config.OPENAI_API_VOICE,
"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,
},
"stt": {
"OPENAI_API_BASE_URL": app.state.config.STT_OPENAI_API_BASE_URL,
"OPENAI_API_KEY": app.state.config.STT_OPENAI_API_KEY,
"ENGINE": app.state.config.STT_ENGINE,
"MODEL": app.state.config.STT_MODEL,
},
}
@app.post("/config/update")
async def update_openai_config(
form_data: OpenAIConfigUpdateForm, user=Depends(get_admin_user)
async def update_audio_config(
form_data: AudioConfigUpdateForm, user=Depends(get_admin_user)
):
if form_data.key == "":
raise HTTPException(status_code=400, detail=ERROR_MESSAGES.API_KEY_NOT_FOUND)
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
app.state.config.OPENAI_API_BASE_URL = form_data.url
app.state.config.OPENAI_API_KEY = form_data.key
app.state.config.OPENAI_API_MODEL = form_data.model
app.state.config.OPENAI_API_VOICE = form_data.speaker
app.state.config.STT_OPENAI_API_BASE_URL = form_data.stt.OPENAI_API_BASE_URL
app.state.config.STT_OPENAI_API_KEY = form_data.stt.OPENAI_API_KEY
app.state.config.STT_ENGINE = form_data.stt.ENGINE
app.state.config.STT_MODEL = form_data.stt.MODEL
return {
"status": True,
"OPENAI_API_BASE_URL": app.state.config.OPENAI_API_BASE_URL,
"OPENAI_API_KEY": app.state.config.OPENAI_API_KEY,
"OPENAI_API_MODEL": app.state.config.OPENAI_API_MODEL,
"OPENAI_API_VOICE": app.state.config.OPENAI_API_VOICE,
"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,
},
"stt": {
"OPENAI_API_BASE_URL": app.state.config.STT_OPENAI_API_BASE_URL,
"OPENAI_API_KEY": app.state.config.STT_OPENAI_API_KEY,
"ENGINE": app.state.config.STT_ENGINE,
"MODEL": app.state.config.STT_MODEL,
},
}
@@ -124,47 +195,118 @@ 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.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"
r = None
try:
r = requests.post(
url=f"{app.state.config.OPENAI_API_BASE_URL}/audio/speech",
data=body,
headers=headers,
stream=True,
)
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.raise_for_status()
r = None
try:
r = requests.post(
url=f"{app.state.config.TTS_OPENAI_API_BASE_URL}/audio/speech",
data=body,
headers=headers,
stream=True,
)
# 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)
r.raise_for_status()
with open(file_body_path, "w") as f:
json.dump(json.loads(body.decode("utf-8")), f)
# 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)
# Return the saved file
return FileResponse(file_path)
with open(file_body_path, "w") as f:
json.dump(json.loads(body.decode("utf-8")), f)
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}"
# Return the saved file
return FileResponse(file_path)
raise HTTPException(
status_code=r.status_code if r != None else 500,
detail=error_detail,
)
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,
)
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", "")
if voice_id not in get_available_voices():
raise HTTPException(
status_code=400,
detail="Invalid voice id",
)
url = f"https://api.elevenlabs.io/v1/text-to-speech/{voice_id}"
headers = {
"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")
@@ -181,41 +323,112 @@ def transcribe(
)
try:
filename = file.filename
file_path = f"{UPLOAD_DIR}/{filename}"
ext = file.filename.split(".")[-1]
id = uuid.uuid4()
filename = f"{id}.{ext}"
file_dir = f"{CACHE_DIR}/audio/transcriptions"
os.makedirs(file_dir, exist_ok=True)
file_path = f"{file_dir}/{filename}"
print(filename)
contents = file.file.read()
with open(file_path, "wb") as f:
f.write(contents)
f.close()
whisper_kwargs = {
"model_size_or_path": WHISPER_MODEL,
"device": whisper_device_type,
"compute_type": "int8",
"download_root": WHISPER_MODEL_DIR,
"local_files_only": not WHISPER_MODEL_AUTO_UPDATE,
}
if app.state.config.STT_ENGINE == "":
from faster_whisper import WhisperModel
log.debug(f"whisper_kwargs: {whisper_kwargs}")
whisper_kwargs = {
"model_size_or_path": WHISPER_MODEL,
"device": whisper_device_type,
"compute_type": "int8",
"download_root": WHISPER_MODEL_DIR,
"local_files_only": not WHISPER_MODEL_AUTO_UPDATE,
}
try:
model = WhisperModel(**whisper_kwargs)
except:
log.warning(
"WhisperModel initialization failed, attempting download with local_files_only=False"
log.debug(f"whisper_kwargs: {whisper_kwargs}")
try:
model = WhisperModel(**whisper_kwargs)
except Exception:
log.warning(
"WhisperModel initialization failed, attempting download with local_files_only=False"
)
whisper_kwargs["local_files_only"] = False
model = WhisperModel(**whisper_kwargs)
segments, info = model.transcribe(file_path, beam_size=5)
log.info(
"Detected language '%s' with probability %f"
% (info.language, info.language_probability)
)
whisper_kwargs["local_files_only"] = False
model = WhisperModel(**whisper_kwargs)
segments, info = model.transcribe(file_path, beam_size=5)
log.info(
"Detected language '%s' with probability %f"
% (info.language, info.language_probability)
)
transcript = "".join([segment.text for segment in list(segments)])
transcript = "".join([segment.text for segment in list(segments)])
data = {"text": transcript.strip()}
return {"text": transcript.strip()}
# save the transcript to a json file
transcript_file = f"{file_dir}/{id}.json"
with open(transcript_file, "w") as f:
json.dump(data, f)
print(data)
return data
elif app.state.config.STT_ENGINE == "openai":
if is_mp4_audio(file_path):
print("is_mp4_audio")
os.rename(file_path, file_path.replace(".wav", ".mp4"))
# Convert MP4 audio file to WAV format
convert_mp4_to_wav(file_path.replace(".wav", ".mp4"), file_path)
headers = {"Authorization": f"Bearer {app.state.config.STT_OPENAI_API_KEY}"}
files = {"file": (filename, open(file_path, "rb"))}
data = {"model": app.state.config.STT_MODEL}
print(files, data)
r = None
try:
r = requests.post(
url=f"{app.state.config.STT_OPENAI_API_BASE_URL}/audio/transcriptions",
headers=headers,
files=files,
data=data,
)
r.raise_for_status()
data = r.json()
# save the transcript to a json file
transcript_file = f"{file_dir}/{id}.json"
with open(transcript_file, "w") as f:
json.dump(data, f)
print(data)
return data
except Exception as e:
log.exception(e)
error_detail = "Open WebUI: Server Connection Error"
if r is not None:
try:
res = r.json()
if "error" in res:
error_detail = f"External: {res['error']['message']}"
except Exception:
error_detail = f"External: {e}"
raise HTTPException(
status_code=r.status_code if r != None else 500,
detail=error_detail,
)
except Exception as e:
log.exception(e)
@@ -224,3 +437,88 @@ 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() -> dict:
"""Returns {voice_id: voice_name} dict"""
ret = {}
if app.state.config.TTS_ENGINE == "openai":
ret = {
"alloy": "alloy",
"echo": "echo",
"fable": "fable",
"onyx": "onyx",
"nova": "nova",
"shimmer": "shimmer",
}
elif app.state.config.TTS_ENGINE == "elevenlabs":
try:
ret = get_elevenlabs_voices()
except Exception as e:
# Avoided @lru_cache with exception
pass
return ret
@lru_cache
def get_elevenlabs_voices() -> dict:
"""
Note, set the following in your .env file to use Elevenlabs:
AUDIO_TTS_ENGINE=elevenlabs
AUDIO_TTS_API_KEY=sk_... # Your Elevenlabs API key
AUDIO_TTS_VOICE=EXAVITQu4vr4xnSDxMaL # From https://api.elevenlabs.io/v1/voices
AUDIO_TTS_MODEL=eleven_multilingual_v2
"""
headers = {
"xi-api-key": app.state.config.TTS_API_KEY,
"Content-Type": "application/json",
}
try:
# TODO: Add retries
response = requests.get("https://api.elevenlabs.io/v1/voices", headers=headers)
response.raise_for_status()
voices_data = response.json()
voices = {}
for voice in voices_data.get("voices", []):
voices[voice["voice_id"]] = voice["name"]
except requests.RequestException as e:
# Avoid @lru_cache with exception
log.error(f"Error fetching voices: {str(e)}")
raise RuntimeError(f"Error fetching voices: {str(e)}")
return voices
@app.get("/voices")
async def get_voices(user=Depends(get_verified_user)):
return {"voices": [{"id": k, "name": v} for k, v in get_available_voices().items()]}
+243 -207
View File
@@ -1,26 +1,10 @@
import re
import requests
from fastapi import (
FastAPI,
Request,
Depends,
HTTPException,
status,
UploadFile,
File,
Form,
)
from fastapi.middleware.cors import CORSMiddleware
from faster_whisper import WhisperModel
from constants import ERROR_MESSAGES
from utils.utils import (
get_current_user,
get_admin_user,
)
from apps.images.utils.comfyui import ImageGenerationPayload, comfyui_generate_image
from utils.misc import calculate_sha256
from typing import Optional
from pydantic import BaseModel
from pathlib import Path
@@ -29,23 +13,40 @@ import uuid
import base64
import json
import logging
import re
import requests
from utils.utils import (
get_verified_user,
get_admin_user,
)
from apps.images.utils.comfyui import (
ComfyUIWorkflow,
ComfyUIGenerateImageForm,
comfyui_generate_image,
)
from constants import ERROR_MESSAGES
from config import (
SRC_LOG_LEVELS,
CACHE_DIR,
IMAGE_GENERATION_ENGINE,
ENABLE_IMAGE_GENERATION,
AUTOMATIC1111_BASE_URL,
AUTOMATIC1111_API_AUTH,
COMFYUI_BASE_URL,
COMFYUI_WORKFLOW,
COMFYUI_WORKFLOW_NODES,
IMAGES_OPENAI_API_BASE_URL,
IMAGES_OPENAI_API_KEY,
IMAGE_GENERATION_MODEL,
IMAGE_SIZE,
IMAGE_STEPS,
CORS_ALLOW_ORIGIN,
AppConfig,
)
log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["IMAGES"])
@@ -55,7 +56,7 @@ IMAGE_CACHE_DIR.mkdir(parents=True, exist_ok=True)
app = FastAPI()
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_origins=CORS_ALLOW_ORIGIN,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
@@ -71,10 +72,11 @@ app.state.config.OPENAI_API_KEY = IMAGES_OPENAI_API_KEY
app.state.config.MODEL = IMAGE_GENERATION_MODEL
app.state.config.AUTOMATIC1111_BASE_URL = AUTOMATIC1111_BASE_URL
app.state.config.AUTOMATIC1111_API_AUTH = AUTOMATIC1111_API_AUTH
app.state.config.COMFYUI_BASE_URL = COMFYUI_BASE_URL
app.state.config.COMFYUI_WORKFLOW = COMFYUI_WORKFLOW
app.state.config.COMFYUI_WORKFLOW_NODES = COMFYUI_WORKFLOW_NODES
app.state.config.IMAGE_SIZE = IMAGE_SIZE
app.state.config.IMAGE_STEPS = IMAGE_STEPS
@@ -83,157 +85,203 @@ app.state.config.IMAGE_STEPS = IMAGE_STEPS
@app.get("/config")
async def get_config(request: Request, user=Depends(get_admin_user)):
return {
"engine": app.state.config.ENGINE,
"enabled": app.state.config.ENABLED,
"engine": app.state.config.ENGINE,
"openai": {
"OPENAI_API_BASE_URL": app.state.config.OPENAI_API_BASE_URL,
"OPENAI_API_KEY": app.state.config.OPENAI_API_KEY,
},
"automatic1111": {
"AUTOMATIC1111_BASE_URL": app.state.config.AUTOMATIC1111_BASE_URL,
"AUTOMATIC1111_API_AUTH": app.state.config.AUTOMATIC1111_API_AUTH,
},
"comfyui": {
"COMFYUI_BASE_URL": app.state.config.COMFYUI_BASE_URL,
"COMFYUI_WORKFLOW": app.state.config.COMFYUI_WORKFLOW,
"COMFYUI_WORKFLOW_NODES": app.state.config.COMFYUI_WORKFLOW_NODES,
},
}
class ConfigUpdateForm(BaseModel):
engine: str
class OpenAIConfigForm(BaseModel):
OPENAI_API_BASE_URL: str
OPENAI_API_KEY: str
class Automatic1111ConfigForm(BaseModel):
AUTOMATIC1111_BASE_URL: str
AUTOMATIC1111_API_AUTH: str
class ComfyUIConfigForm(BaseModel):
COMFYUI_BASE_URL: str
COMFYUI_WORKFLOW: str
COMFYUI_WORKFLOW_NODES: list[dict]
class ConfigForm(BaseModel):
enabled: bool
engine: str
openai: OpenAIConfigForm
automatic1111: Automatic1111ConfigForm
comfyui: ComfyUIConfigForm
@app.post("/config/update")
async def update_config(form_data: ConfigUpdateForm, user=Depends(get_admin_user)):
async def update_config(form_data: ConfigForm, user=Depends(get_admin_user)):
app.state.config.ENGINE = form_data.engine
app.state.config.ENABLED = form_data.enabled
app.state.config.OPENAI_API_BASE_URL = form_data.openai.OPENAI_API_BASE_URL
app.state.config.OPENAI_API_KEY = form_data.openai.OPENAI_API_KEY
app.state.config.AUTOMATIC1111_BASE_URL = (
form_data.automatic1111.AUTOMATIC1111_BASE_URL
)
app.state.config.AUTOMATIC1111_API_AUTH = (
form_data.automatic1111.AUTOMATIC1111_API_AUTH
)
app.state.config.COMFYUI_BASE_URL = form_data.comfyui.COMFYUI_BASE_URL
app.state.config.COMFYUI_WORKFLOW = form_data.comfyui.COMFYUI_WORKFLOW
app.state.config.COMFYUI_WORKFLOW_NODES = form_data.comfyui.COMFYUI_WORKFLOW_NODES
return {
"engine": app.state.config.ENGINE,
"enabled": app.state.config.ENABLED,
"engine": app.state.config.ENGINE,
"openai": {
"OPENAI_API_BASE_URL": app.state.config.OPENAI_API_BASE_URL,
"OPENAI_API_KEY": app.state.config.OPENAI_API_KEY,
},
"automatic1111": {
"AUTOMATIC1111_BASE_URL": app.state.config.AUTOMATIC1111_BASE_URL,
"AUTOMATIC1111_API_AUTH": app.state.config.AUTOMATIC1111_API_AUTH,
},
"comfyui": {
"COMFYUI_BASE_URL": app.state.config.COMFYUI_BASE_URL,
"COMFYUI_WORKFLOW": app.state.config.COMFYUI_WORKFLOW,
"COMFYUI_WORKFLOW_NODES": app.state.config.COMFYUI_WORKFLOW_NODES,
},
}
class EngineUrlUpdateForm(BaseModel):
AUTOMATIC1111_BASE_URL: Optional[str] = None
COMFYUI_BASE_URL: Optional[str] = None
@app.get("/url")
async def get_engine_url(user=Depends(get_admin_user)):
return {
"AUTOMATIC1111_BASE_URL": app.state.config.AUTOMATIC1111_BASE_URL,
"COMFYUI_BASE_URL": app.state.config.COMFYUI_BASE_URL,
}
@app.post("/url/update")
async def update_engine_url(
form_data: EngineUrlUpdateForm, user=Depends(get_admin_user)
):
if form_data.AUTOMATIC1111_BASE_URL == None:
app.state.config.AUTOMATIC1111_BASE_URL = AUTOMATIC1111_BASE_URL
def get_automatic1111_api_auth():
if app.state.config.AUTOMATIC1111_API_AUTH is None:
return ""
else:
url = form_data.AUTOMATIC1111_BASE_URL.strip("/")
auth1111_byte_string = app.state.config.AUTOMATIC1111_API_AUTH.encode("utf-8")
auth1111_base64_encoded_bytes = base64.b64encode(auth1111_byte_string)
auth1111_base64_encoded_string = auth1111_base64_encoded_bytes.decode("utf-8")
return f"Basic {auth1111_base64_encoded_string}"
@app.get("/config/url/verify")
async def verify_url(user=Depends(get_admin_user)):
if app.state.config.ENGINE == "automatic1111":
try:
r = requests.head(url)
app.state.config.AUTOMATIC1111_BASE_URL = url
r = requests.get(
url=f"{app.state.config.AUTOMATIC1111_BASE_URL}/sdapi/v1/options",
headers={"authorization": get_automatic1111_api_auth()},
)
r.raise_for_status()
return True
except Exception as e:
app.state.config.ENABLED = False
raise HTTPException(status_code=400, detail=ERROR_MESSAGES.INVALID_URL)
elif app.state.config.ENGINE == "comfyui":
try:
r = requests.get(url=f"{app.state.config.COMFYUI_BASE_URL}/object_info")
r.raise_for_status()
return True
except Exception as e:
app.state.config.ENABLED = False
raise HTTPException(status_code=400, detail=ERROR_MESSAGES.INVALID_URL)
else:
return True
def set_image_model(model: str):
app.state.config.MODEL = model
if app.state.config.ENGINE in ["", "automatic1111"]:
api_auth = get_automatic1111_api_auth()
r = requests.get(
url=f"{app.state.config.AUTOMATIC1111_BASE_URL}/sdapi/v1/options",
headers={"authorization": api_auth},
)
options = r.json()
if model != options["sd_model_checkpoint"]:
options["sd_model_checkpoint"] = model
r = requests.post(
url=f"{app.state.config.AUTOMATIC1111_BASE_URL}/sdapi/v1/options",
json=options,
headers={"authorization": api_auth},
)
return app.state.config.MODEL
def get_image_model():
if app.state.config.ENGINE == "openai":
return app.state.config.MODEL if app.state.config.MODEL else "dall-e-2"
elif app.state.config.ENGINE == "comfyui":
return app.state.config.MODEL if app.state.config.MODEL else ""
elif app.state.config.ENGINE == "automatic1111" or app.state.config.ENGINE == "":
try:
r = requests.get(
url=f"{app.state.config.AUTOMATIC1111_BASE_URL}/sdapi/v1/options",
headers={"authorization": get_automatic1111_api_auth()},
)
options = r.json()
return options["sd_model_checkpoint"]
except Exception as e:
app.state.config.ENABLED = False
raise HTTPException(status_code=400, detail=ERROR_MESSAGES.DEFAULT(e))
if form_data.COMFYUI_BASE_URL == None:
app.state.config.COMFYUI_BASE_URL = COMFYUI_BASE_URL
else:
url = form_data.COMFYUI_BASE_URL.strip("/")
try:
r = requests.head(url)
app.state.config.COMFYUI_BASE_URL = url
except Exception as e:
raise HTTPException(status_code=400, detail=ERROR_MESSAGES.DEFAULT(e))
class ImageConfigForm(BaseModel):
MODEL: str
IMAGE_SIZE: str
IMAGE_STEPS: int
@app.get("/image/config")
async def get_image_config(user=Depends(get_admin_user)):
return {
"AUTOMATIC1111_BASE_URL": app.state.config.AUTOMATIC1111_BASE_URL,
"COMFYUI_BASE_URL": app.state.config.COMFYUI_BASE_URL,
"status": True,
"MODEL": app.state.config.MODEL,
"IMAGE_SIZE": app.state.config.IMAGE_SIZE,
"IMAGE_STEPS": app.state.config.IMAGE_STEPS,
}
class OpenAIConfigUpdateForm(BaseModel):
url: str
key: str
@app.post("/image/config/update")
async def update_image_config(form_data: ImageConfigForm, user=Depends(get_admin_user)):
app.state.config.MODEL = form_data.MODEL
@app.get("/openai/config")
async def get_openai_config(user=Depends(get_admin_user)):
return {
"OPENAI_API_BASE_URL": app.state.config.OPENAI_API_BASE_URL,
"OPENAI_API_KEY": app.state.config.OPENAI_API_KEY,
}
@app.post("/openai/config/update")
async def update_openai_config(
form_data: OpenAIConfigUpdateForm, user=Depends(get_admin_user)
):
if form_data.key == "":
raise HTTPException(status_code=400, detail=ERROR_MESSAGES.API_KEY_NOT_FOUND)
app.state.config.OPENAI_API_BASE_URL = form_data.url
app.state.config.OPENAI_API_KEY = form_data.key
return {
"status": True,
"OPENAI_API_BASE_URL": app.state.config.OPENAI_API_BASE_URL,
"OPENAI_API_KEY": app.state.config.OPENAI_API_KEY,
}
class ImageSizeUpdateForm(BaseModel):
size: str
@app.get("/size")
async def get_image_size(user=Depends(get_admin_user)):
return {"IMAGE_SIZE": app.state.config.IMAGE_SIZE}
@app.post("/size/update")
async def update_image_size(
form_data: ImageSizeUpdateForm, user=Depends(get_admin_user)
):
pattern = r"^\d+x\d+$" # Regular expression pattern
if re.match(pattern, form_data.size):
app.state.config.IMAGE_SIZE = form_data.size
return {
"IMAGE_SIZE": app.state.config.IMAGE_SIZE,
"status": True,
}
pattern = r"^\d+x\d+$"
if re.match(pattern, form_data.IMAGE_SIZE):
app.state.config.IMAGE_SIZE = form_data.IMAGE_SIZE
else:
raise HTTPException(
status_code=400,
detail=ERROR_MESSAGES.INCORRECT_FORMAT(" (e.g., 512x512)."),
)
class ImageStepsUpdateForm(BaseModel):
steps: int
@app.get("/steps")
async def get_image_size(user=Depends(get_admin_user)):
return {"IMAGE_STEPS": app.state.config.IMAGE_STEPS}
@app.post("/steps/update")
async def update_image_size(
form_data: ImageStepsUpdateForm, user=Depends(get_admin_user)
):
if form_data.steps >= 0:
app.state.config.IMAGE_STEPS = form_data.steps
return {
"IMAGE_STEPS": app.state.config.IMAGE_STEPS,
"status": True,
}
if form_data.IMAGE_STEPS >= 0:
app.state.config.IMAGE_STEPS = form_data.IMAGE_STEPS
else:
raise HTTPException(
status_code=400,
detail=ERROR_MESSAGES.INCORRECT_FORMAT(" (e.g., 50)."),
)
return {
"MODEL": app.state.config.MODEL,
"IMAGE_SIZE": app.state.config.IMAGE_SIZE,
"IMAGE_STEPS": app.state.config.IMAGE_STEPS,
}
@app.get("/models")
def get_models(user=Depends(get_current_user)):
def get_models(user=Depends(get_verified_user)):
try:
if app.state.config.ENGINE == "openai":
return [
@@ -241,20 +289,54 @@ def get_models(user=Depends(get_current_user)):
{"id": "dall-e-3", "name": "DALL·E 3"},
]
elif app.state.config.ENGINE == "comfyui":
# TODO - get models from comfyui
r = requests.get(url=f"{app.state.config.COMFYUI_BASE_URL}/object_info")
info = r.json()
return list(
map(
lambda model: {"id": model, "name": model},
info["CheckpointLoaderSimple"]["input"]["required"]["ckpt_name"][0],
)
)
workflow = json.loads(app.state.config.COMFYUI_WORKFLOW)
model_node_id = None
else:
for node in app.state.config.COMFYUI_WORKFLOW_NODES:
if node["type"] == "model":
if node["node_ids"]:
model_node_id = node["node_ids"][0]
break
if model_node_id:
model_list_key = None
print(workflow[model_node_id]["class_type"])
for key in info[workflow[model_node_id]["class_type"]]["input"][
"required"
]:
if "_name" in key:
model_list_key = key
break
if model_list_key:
return list(
map(
lambda model: {"id": model, "name": model},
info[workflow[model_node_id]["class_type"]]["input"][
"required"
][model_list_key][0],
)
)
else:
return list(
map(
lambda model: {"id": model, "name": model},
info["CheckpointLoaderSimple"]["input"]["required"][
"ckpt_name"
][0],
)
)
elif (
app.state.config.ENGINE == "automatic1111" or app.state.config.ENGINE == ""
):
r = requests.get(
url=f"{app.state.config.AUTOMATIC1111_BASE_URL}/sdapi/v1/sd-models"
url=f"{app.state.config.AUTOMATIC1111_BASE_URL}/sdapi/v1/sd-models",
headers={"authorization": get_automatic1111_api_auth()},
)
models = r.json()
return list(
@@ -268,65 +350,11 @@ def get_models(user=Depends(get_current_user)):
raise HTTPException(status_code=400, detail=ERROR_MESSAGES.DEFAULT(e))
@app.get("/models/default")
async def get_default_model(user=Depends(get_admin_user)):
try:
if app.state.config.ENGINE == "openai":
return {
"model": (
app.state.config.MODEL if app.state.config.MODEL else "dall-e-2"
)
}
elif app.state.config.ENGINE == "comfyui":
return {"model": (app.state.config.MODEL if app.state.config.MODEL else "")}
else:
r = requests.get(
url=f"{app.state.config.AUTOMATIC1111_BASE_URL}/sdapi/v1/options"
)
options = r.json()
return {"model": options["sd_model_checkpoint"]}
except Exception as e:
app.state.config.ENABLED = False
raise HTTPException(status_code=400, detail=ERROR_MESSAGES.DEFAULT(e))
class UpdateModelForm(BaseModel):
model: str
def set_model_handler(model: str):
if app.state.config.ENGINE in ["openai", "comfyui"]:
app.state.config.MODEL = model
return app.state.config.MODEL
else:
r = requests.get(
url=f"{app.state.config.AUTOMATIC1111_BASE_URL}/sdapi/v1/options"
)
options = r.json()
if model != options["sd_model_checkpoint"]:
options["sd_model_checkpoint"] = model
r = requests.post(
url=f"{app.state.config.AUTOMATIC1111_BASE_URL}/sdapi/v1/options",
json=options,
)
return options
@app.post("/models/default/update")
def update_default_model(
form_data: UpdateModelForm,
user=Depends(get_current_user),
):
return set_model_handler(form_data.model)
class GenerateImageForm(BaseModel):
model: Optional[str] = None
prompt: str
n: int = 1
size: Optional[str] = None
n: int = 1
negative_prompt: Optional[str] = None
@@ -392,11 +420,10 @@ def save_url_image(url):
@app.post("/generations")
def generate_image(
async def image_generations(
form_data: GenerateImageForm,
user=Depends(get_current_user),
user=Depends(get_verified_user),
):
width, height = tuple(map(int, app.state.config.IMAGE_SIZE.split("x")))
r = None
@@ -443,7 +470,6 @@ def generate_image(
return images
elif app.state.config.ENGINE == "comfyui":
data = {
"prompt": form_data.prompt,
"width": width,
@@ -457,11 +483,20 @@ def generate_image(
if form_data.negative_prompt is not None:
data["negative_prompt"] = form_data.negative_prompt
data = ImageGenerationPayload(**data)
res = comfyui_generate_image(
form_data = ComfyUIGenerateImageForm(
**{
"workflow": ComfyUIWorkflow(
**{
"workflow": app.state.config.COMFYUI_WORKFLOW,
"nodes": app.state.config.COMFYUI_WORKFLOW_NODES,
}
),
**data,
}
)
res = await comfyui_generate_image(
app.state.config.MODEL,
data,
form_data,
user.id,
app.state.config.COMFYUI_BASE_URL,
)
@@ -475,13 +510,15 @@ def generate_image(
file_body_path = IMAGE_CACHE_DIR.joinpath(f"{image_filename}.json")
with open(file_body_path, "w") as f:
json.dump(data.model_dump(exclude_none=True), f)
json.dump(form_data.model_dump(exclude_none=True), f)
log.debug(f"images: {images}")
return images
else:
elif (
app.state.config.ENGINE == "automatic1111" or app.state.config.ENGINE == ""
):
if form_data.model:
set_model_handler(form_data.model)
set_image_model(form_data.model)
data = {
"prompt": form_data.prompt,
@@ -499,10 +536,10 @@ def generate_image(
r = requests.post(
url=f"{app.state.config.AUTOMATIC1111_BASE_URL}/sdapi/v1/txt2img",
json=data,
headers={"authorization": get_automatic1111_api_auth()},
)
res = r.json()
log.debug(f"res: {res}")
images = []
@@ -519,7 +556,6 @@ def generate_image(
except Exception as e:
error = e
if r != None:
data = r.json()
if "error" in data:
+60 -135
View File
@@ -1,5 +1,5 @@
import asyncio
import websocket # NOTE: websocket-client (https://github.com/websocket-client/websocket-client)
import uuid
import json
import urllib.request
import urllib.parse
@@ -15,116 +15,6 @@ from pydantic import BaseModel
from typing import Optional
COMFYUI_DEFAULT_PROMPT = """
{
"3": {
"inputs": {
"seed": 0,
"steps": 20,
"cfg": 8,
"sampler_name": "euler",
"scheduler": "normal",
"denoise": 1,
"model": [
"4",
0
],
"positive": [
"6",
0
],
"negative": [
"7",
0
],
"latent_image": [
"5",
0
]
},
"class_type": "KSampler",
"_meta": {
"title": "KSampler"
}
},
"4": {
"inputs": {
"ckpt_name": "model.safetensors"
},
"class_type": "CheckpointLoaderSimple",
"_meta": {
"title": "Load Checkpoint"
}
},
"5": {
"inputs": {
"width": 512,
"height": 512,
"batch_size": 1
},
"class_type": "EmptyLatentImage",
"_meta": {
"title": "Empty Latent Image"
}
},
"6": {
"inputs": {
"text": "Prompt",
"clip": [
"4",
1
]
},
"class_type": "CLIPTextEncode",
"_meta": {
"title": "CLIP Text Encode (Prompt)"
}
},
"7": {
"inputs": {
"text": "Negative Prompt",
"clip": [
"4",
1
]
},
"class_type": "CLIPTextEncode",
"_meta": {
"title": "CLIP Text Encode (Prompt)"
}
},
"8": {
"inputs": {
"samples": [
"3",
0
],
"vae": [
"4",
2
]
},
"class_type": "VAEDecode",
"_meta": {
"title": "VAE Decode"
}
},
"9": {
"inputs": {
"filename_prefix": "ComfyUI",
"images": [
"8",
0
]
},
"class_type": "SaveImage",
"_meta": {
"title": "Save Image"
}
}
}
"""
def queue_prompt(prompt, client_id, base_url):
log.info("queue_prompt")
@@ -182,38 +72,71 @@ def get_images(ws, prompt, client_id, base_url):
return {"data": output_images}
class ImageGenerationPayload(BaseModel):
class ComfyUINodeInput(BaseModel):
type: Optional[str] = None
node_ids: list[str] = []
key: Optional[str] = "text"
value: Optional[str] = None
class ComfyUIWorkflow(BaseModel):
workflow: str
nodes: list[ComfyUINodeInput]
class ComfyUIGenerateImageForm(BaseModel):
workflow: ComfyUIWorkflow
prompt: str
negative_prompt: Optional[str] = ""
steps: Optional[int] = None
seed: Optional[int] = None
negative_prompt: Optional[str] = None
width: int
height: int
n: int = 1
steps: Optional[int] = None
seed: Optional[int] = None
def comfyui_generate_image(
model: str, payload: ImageGenerationPayload, client_id, base_url
async def comfyui_generate_image(
model: str, payload: ComfyUIGenerateImageForm, client_id, base_url
):
ws_url = base_url.replace("http://", "ws://").replace("https://", "wss://")
workflow = json.loads(payload.workflow.workflow)
comfyui_prompt = json.loads(COMFYUI_DEFAULT_PROMPT)
comfyui_prompt["4"]["inputs"]["ckpt_name"] = model
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)
)
for node in payload.workflow.nodes:
if node.type:
if node.type == "model":
for node_id in node.node_ids:
workflow[node_id]["inputs"][node.key] = model
elif node.type == "prompt":
for node_id in node.node_ids:
workflow[node_id]["inputs"]["text"] = payload.prompt
elif node.type == "negative_prompt":
for node_id in node.node_ids:
workflow[node_id]["inputs"]["text"] = payload.negative_prompt
elif node.type == "width":
for node_id in node.node_ids:
workflow[node_id]["inputs"]["width"] = payload.width
elif node.type == "height":
for node_id in node.node_ids:
workflow[node_id]["inputs"]["height"] = payload.height
elif node.type == "n":
for node_id in node.node_ids:
workflow[node_id]["inputs"]["batch_size"] = payload.n
elif node.type == "steps":
for node_id in node.node_ids:
workflow[node_id]["inputs"]["steps"] = payload.steps
elif node.type == "seed":
seed = (
payload.seed
if payload.seed
else random.randint(0, 18446744073709551614)
)
for node_id in node.node_ids:
workflow[node_id]["inputs"][node.key] = seed
else:
for node_id in node.node_ids:
workflow[node_id]["inputs"][node.key] = node.value
try:
ws = websocket.WebSocket()
@@ -224,7 +147,9 @@ def comfyui_generate_image(
return None
try:
images = get_images(ws, comfyui_prompt, client_id, base_url)
log.info("Sending workflow to WebSocket server.")
log.info(f"Workflow: {workflow}")
images = await asyncio.to_thread(get_images, ws, workflow, client_id, base_url)
except Exception as e:
log.exception(f"Error while receiving images: {e}")
images = None
+148 -401
View File
@@ -1,59 +1,54 @@
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.models import get_model_id_from_custom_model_id
from config import (
SRC_LOG_LEVELS,
OLLAMA_BASE_URLS,
ENABLE_OLLAMA_API,
AIOHTTP_CLIENT_TIMEOUT,
ENABLE_MODEL_FILTER,
MODEL_FILTER_LIST,
UPLOAD_DIR,
AppConfig,
CORS_ALLOW_ORIGIN,
)
from utils.misc import (
calculate_sha256,
apply_model_params_to_body_ollama,
apply_model_params_to_body_openai,
apply_model_system_prompt_to_body,
)
from utils.misc import calculate_sha256
log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["OLLAMA"])
@@ -61,7 +56,7 @@ log.setLevel(SRC_LOG_LEVELS["OLLAMA"])
app = FastAPI()
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_origins=CORS_ALLOW_ORIGIN,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
@@ -120,7 +115,7 @@ async def get_ollama_api_urls(user=Depends(get_admin_user)):
class UrlUpdateForm(BaseModel):
urls: List[str]
urls: list[str]
@app.post("/urls/update")
@@ -153,19 +148,33 @@ async def cleanup_response(
await session.close()
async def post_streaming_url(url: str, payload: str):
async def post_streaming_url(url: str, payload: Union[str, bytes], stream: bool = True):
r = None
try:
session = aiohttp.ClientSession(trust_env=True)
r = await session.post(url, data=payload)
session = aiohttp.ClientSession(
trust_env=True, timeout=aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT)
)
r = await session.post(
url,
data=payload,
headers={"Content-Type": "application/json"},
)
r.raise_for_status()
return StreamingResponse(
r.content,
status_code=r.status,
headers=dict(r.headers),
background=BackgroundTask(cleanup_response, response=r, session=session),
)
if stream:
return StreamingResponse(
r.content,
status_code=r.status,
headers=dict(r.headers),
background=BackgroundTask(
cleanup_response, response=r, session=session
),
)
else:
res = await r.json()
await cleanup_response(r, session)
return res
except Exception as e:
error_detail = "Open WebUI: Server Connection Error"
if r is not None:
@@ -173,7 +182,7 @@ async def post_streaming_url(url: str, payload: str):
res = await r.json()
if "error" in res:
error_detail = f"Ollama: {res['error']}"
except:
except Exception:
error_detail = f"Ollama: {e}"
raise HTTPException(
@@ -198,9 +207,6 @@ def merge_models_lists(model_lists):
return list(merged_models.values())
# user=Depends(get_current_user)
async def get_all_models():
log.info("get_all_models()")
@@ -231,7 +237,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:
@@ -262,7 +268,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(
@@ -275,8 +281,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")
@@ -316,7 +321,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(
@@ -339,8 +344,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}
@@ -360,7 +363,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:
@@ -410,7 +413,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:
@@ -421,13 +424,14 @@ async def copy_model(
url = app.state.config.OLLAMA_BASE_URLS[url_idx]
log.info(f"url: {url}")
r = requests.request(
method="POST",
url=f"{url}/api/copy",
headers={"Content-Type": "application/json"},
data=form_data.model_dump_json(exclude_none=True).encode(),
)
try:
r = requests.request(
method="POST",
url=f"{url}/api/copy",
data=form_data.model_dump_json(exclude_none=True).encode(),
)
r.raise_for_status()
log.debug(f"r.text: {r.text}")
@@ -441,7 +445,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(
@@ -457,7 +461,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:
@@ -469,12 +473,13 @@ async def delete_model(
url = app.state.config.OLLAMA_BASE_URLS[url_idx]
log.info(f"url: {url}")
r = requests.request(
method="DELETE",
url=f"{url}/api/delete",
headers={"Content-Type": "application/json"},
data=form_data.model_dump_json(exclude_none=True).encode(),
)
try:
r = requests.request(
method="DELETE",
url=f"{url}/api/delete",
data=form_data.model_dump_json(exclude_none=True).encode(),
)
r.raise_for_status()
log.debug(f"r.text: {r.text}")
@@ -488,7 +493,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(
@@ -509,12 +514,13 @@ async def show_model_info(form_data: ModelNameForm, user=Depends(get_verified_us
url = app.state.config.OLLAMA_BASE_URLS[url_idx]
log.info(f"url: {url}")
r = requests.request(
method="POST",
url=f"{url}/api/show",
headers={"Content-Type": "application/json"},
data=form_data.model_dump_json(exclude_none=True).encode(),
)
try:
r = requests.request(
method="POST",
url=f"{url}/api/show",
data=form_data.model_dump_json(exclude_none=True).encode(),
)
r.raise_for_status()
return r.json()
@@ -526,7 +532,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(
@@ -549,7 +555,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:
@@ -566,12 +572,13 @@ async def generate_embeddings(
url = app.state.config.OLLAMA_BASE_URLS[url_idx]
log.info(f"url: {url}")
r = requests.request(
method="POST",
url=f"{url}/api/embeddings",
headers={"Content-Type": "application/json"},
data=form_data.model_dump_json(exclude_none=True).encode(),
)
try:
r = requests.request(
method="POST",
url=f"{url}/api/embeddings",
data=form_data.model_dump_json(exclude_none=True).encode(),
)
r.raise_for_status()
return r.json()
@@ -583,7 +590,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(
@@ -596,10 +603,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:
@@ -616,12 +622,13 @@ def generate_ollama_embeddings(
url = app.state.config.OLLAMA_BASE_URLS[url_idx]
log.info(f"url: {url}")
r = requests.request(
method="POST",
url=f"{url}/api/embeddings",
headers={"Content-Type": "application/json"},
data=form_data.model_dump_json(exclude_none=True).encode(),
)
try:
r = requests.request(
method="POST",
url=f"{url}/api/embeddings",
data=form_data.model_dump_json(exclude_none=True).encode(),
)
r.raise_for_status()
data = r.json()
@@ -631,7 +638,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"
@@ -640,16 +647,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
@@ -667,8 +674,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:
@@ -693,12 +699,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
@@ -706,6 +712,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(
@@ -713,152 +731,55 @@ async def generate_chat_completion(
url_idx: Optional[int] = None,
user=Depends(get_verified_user),
):
log.debug(
"form_data.model_dump_json(exclude_none=True).encode(): {0} ".format(
form_data.model_dump_json(exclude_none=True).encode()
)
)
payload = {
**form_data.model_dump(exclude_none=True),
}
payload = {**form_data.model_dump(exclude_none=True)}
log.debug(f"{payload = }")
if "metadata" in payload:
del payload["metadata"]
model_id = form_data.model
model_info = Models.get_model_by_id(model_id)
if model_info:
print(model_info)
if model_info.base_model_id:
payload["model"] = model_info.base_model_id
model_info.params = model_info.params.model_dump()
params = model_info.params.model_dump()
if model_info.params:
payload["options"] = {}
if params:
if payload.get("options") is None:
payload["options"] = {}
if model_info.params.get("mirostat", None):
payload["options"]["mirostat"] = model_info.params.get("mirostat", None)
if model_info.params.get("mirostat_eta", None):
payload["options"]["mirostat_eta"] = model_info.params.get(
"mirostat_eta", None
)
if model_info.params.get("mirostat_tau", None):
payload["options"]["mirostat_tau"] = model_info.params.get(
"mirostat_tau", None
)
if model_info.params.get("num_ctx", None):
payload["options"]["num_ctx"] = model_info.params.get("num_ctx", None)
if model_info.params.get("repeat_last_n", None):
payload["options"]["repeat_last_n"] = model_info.params.get(
"repeat_last_n", None
)
if model_info.params.get("frequency_penalty", None):
payload["options"]["repeat_penalty"] = model_info.params.get(
"frequency_penalty", None
)
if model_info.params.get("temperature", None):
payload["options"]["temperature"] = model_info.params.get(
"temperature", None
)
if model_info.params.get("seed", None):
payload["options"]["seed"] = model_info.params.get("seed", None)
if model_info.params.get("stop", 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):
payload["options"]["tfs_z"] = model_info.params.get("tfs_z", None)
if model_info.params.get("max_tokens", None):
payload["options"]["num_predict"] = model_info.params.get(
"max_tokens", None
)
if model_info.params.get("top_k", None):
payload["options"]["top_k"] = model_info.params.get("top_k", None)
if model_info.params.get("top_p", None):
payload["options"]["top_p"] = model_info.params.get("top_p", None)
if model_info.params.get("use_mmap", None):
payload["options"]["use_mmap"] = model_info.params.get("use_mmap", None)
if model_info.params.get("use_mlock", None):
payload["options"]["use_mlock"] = model_info.params.get(
"use_mlock", None
)
if model_info.params.get("num_thread", None):
payload["options"]["num_thread"] = model_info.params.get(
"num_thread", None
)
if model_info.params.get("system", None):
# Check if the payload already has a system message
# If not, add a system message to the payload
if payload.get("messages"):
for message in payload["messages"]:
if message.get("role") == "system":
message["content"] = (
model_info.params.get("system", None) + message["content"]
)
break
else:
payload["messages"].insert(
0,
{
"role": "system",
"content": model_info.params.get("system", None),
},
)
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),
payload["options"] = apply_model_params_to_body_ollama(
params, payload["options"]
)
payload = apply_model_system_prompt_to_body(params, payload, user)
url = app.state.config.OLLAMA_BASE_URLS[url_idx]
if ":" not in payload["model"]:
payload["model"] = f"{payload['model']}:latest"
url = get_ollama_url(url_idx, payload["model"])
log.info(f"url: {url}")
print(payload)
log.debug(payload)
return await post_streaming_url(f"{url}/api/chat", json.dumps(payload))
# TODO: we should update this part once Ollama supports other types
class OpenAIChatMessageContent(BaseModel):
type: str
model_config = ConfigDict(extra="allow")
class OpenAIChatMessage(BaseModel):
role: str
content: str
content: Union[str, OpenAIChatMessageContent]
model_config = ConfigDict(extra="allow")
class OpenAIChatCompletionForm(BaseModel):
model: str
messages: List[OpenAIChatMessage]
messages: list[OpenAIChatMessage]
model_config = ConfigDict(extra="allow")
@@ -866,77 +787,39 @@ class OpenAIChatCompletionForm(BaseModel):
@app.post("/v1/chat/completions")
@app.post("/v1/chat/completions/{url_idx}")
async def generate_openai_chat_completion(
form_data: OpenAIChatCompletionForm,
form_data: dict,
url_idx: Optional[int] = None,
user=Depends(get_verified_user),
):
completion_form = OpenAIChatCompletionForm(**form_data)
payload = {**completion_form.model_dump(exclude_none=True, exclude=["metadata"])}
if "metadata" in payload:
del payload["metadata"]
payload = {
**form_data.model_dump(exclude_none=True),
}
model_id = form_data.model
model_id = completion_form.model
model_info = Models.get_model_by_id(model_id)
if model_info:
print(model_info)
if model_info.base_model_id:
payload["model"] = model_info.base_model_id
model_info.params = model_info.params.model_dump()
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)
if model_info.params.get("system", None):
# Check if the payload already has a system message
# If not, add a system message to the payload
if payload.get("messages"):
for message in payload["messages"]:
if message.get("role") == "system":
message["content"] = (
model_info.params.get("system", None) + message["content"]
)
break
else:
payload["messages"].insert(
0,
{
"role": "system",
"content": model_info.params.get("system", None),
},
)
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}")
return await post_streaming_url(f"{url}/v1/chat/completions", json.dumps(payload))
return await post_streaming_url(
f"{url}/v1/chat/completions",
json.dumps(payload),
stream=payload.get("stream", False),
)
@app.get("/v1/models")
@@ -945,7 +828,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:
@@ -1000,7 +883,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(
@@ -1026,7 +909,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
@@ -1083,19 +965,14 @@ async def download_file_stream(
raise "Ollama: Could not create blob, Please try again."
# def number_generator():
# for i in range(1, 101):
# yield f"data: {i}\n"
# url = "https://huggingface.co/TheBloke/stablelm-zephyr-3b-GGUF/resolve/main/stablelm-zephyr-3b.Q2_K.gguf"
@app.post("/models/download")
@app.post("/models/download/{url_idx}")
async def download_model(
form_data: UrlForm,
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):
@@ -1104,7 +981,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]
@@ -1122,8 +999,12 @@ async def download_model(
@app.post("/models/upload")
@app.post("/models/upload/{url_idx}")
def upload_model(file: UploadFile = File(...), url_idx: Optional[int] = None):
if url_idx == None:
def upload_model(
file: UploadFile = File(...),
url_idx: Optional[int] = None,
user=Depends(get_admin_user),
):
if url_idx is None:
url_idx = 0
ollama_url = app.state.config.OLLAMA_BASE_URLS[url_idx]
@@ -1185,137 +1066,3 @@ def upload_model(file: UploadFile = File(...), url_idx: Optional[int] = None):
yield f"data: {json.dumps(res)}\n\n"
return StreamingResponse(file_process_stream(), media_type="text/event-stream")
# async def upload_model(file: UploadFile = File(), url_idx: Optional[int] = None):
# if url_idx == None:
# url_idx = 0
# url = app.state.config.OLLAMA_BASE_URLS[url_idx]
# file_location = os.path.join(UPLOAD_DIR, file.filename)
# total_size = file.size
# async def file_upload_generator(file):
# print(file)
# try:
# async with aiofiles.open(file_location, "wb") as f:
# completed_size = 0
# while True:
# chunk = await file.read(1024*1024)
# if not chunk:
# break
# await f.write(chunk)
# completed_size += len(chunk)
# progress = (completed_size / total_size) * 100
# print(progress)
# yield f'data: {json.dumps({"status": "uploading", "percentage": progress, "total": total_size, "completed": completed_size, "done": False})}\n'
# except Exception as e:
# print(e)
# yield f"data: {json.dumps({'status': 'error', 'message': str(e)})}\n"
# finally:
# await file.close()
# print("done")
# yield f'data: {json.dumps({"status": "completed", "percentage": 100, "total": total_size, "completed": completed_size, "done": True})}\n'
# return StreamingResponse(
# file_upload_generator(copy.deepcopy(file)), media_type="text/event-stream"
# )
@app.api_route("/{path:path}", methods=["GET", "POST", "PUT", "DELETE"])
async def deprecated_proxy(
path: str, request: Request, user=Depends(get_verified_user)
):
url = app.state.config.OLLAMA_BASE_URLS[0]
target_url = f"{url}/{path}"
body = await request.body()
headers = dict(request.headers)
if user.role in ["user", "admin"]:
if path in ["pull", "delete", "push", "copy", "create"]:
if user.role != "admin":
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
)
else:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
)
headers.pop("host", None)
headers.pop("authorization", None)
headers.pop("origin", None)
headers.pop("referer", None)
r = None
def get_request():
nonlocal r
request_id = str(uuid.uuid4())
try:
REQUEST_POOL.append(request_id)
def stream_content():
try:
if path == "generate":
data = json.loads(body.decode("utf-8"))
if data.get("stream", True):
yield json.dumps({"id": request_id, "done": False}) + "\n"
elif path == "chat":
yield json.dumps({"id": request_id, "done": False}) + "\n"
for chunk in r.iter_content(chunk_size=8192):
if request_id in REQUEST_POOL:
yield chunk
else:
log.warning("User: canceled request")
break
finally:
if hasattr(r, "close"):
r.close()
if request_id in REQUEST_POOL:
REQUEST_POOL.remove(request_id)
r = requests.request(
method=request.method,
url=target_url,
data=body,
headers=headers,
stream=True,
)
r.raise_for_status()
# r.close()
return StreamingResponse(
stream_content(),
status_code=r.status_code,
headers=dict(r.headers),
)
except Exception as e:
raise e
try:
return await run_in_threadpool(get_request)
except Exception as e:
error_detail = "Open WebUI: Server Connection Error"
if r is not None:
try:
res = r.json()
if "error" in res:
error_detail = f"Ollama: {res['error']}"
except:
error_detail = f"Ollama: {e}"
raise HTTPException(
status_code=r.status_code if r else 500,
detail=error_detail,
)
+177 -168
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,25 +12,29 @@ 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_current_user,
get_verified_user,
get_admin_user,
)
from utils.misc import (
apply_model_params_to_body_openai,
apply_model_system_prompt_to_body,
)
from config import (
SRC_LOG_LEVELS,
ENABLE_OPENAI_API,
AIOHTTP_CLIENT_TIMEOUT,
OPENAI_API_BASE_URLS,
OPENAI_API_KEYS,
CACHE_DIR,
ENABLE_MODEL_FILTER,
MODEL_FILTER_LIST,
AppConfig,
CORS_ALLOW_ORIGIN,
)
from typing import List, Optional
from typing import Optional, Literal, overload
import hashlib
@@ -42,7 +46,7 @@ log.setLevel(SRC_LOG_LEVELS["OPENAI"])
app = FastAPI()
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_origins=CORS_ALLOW_ORIGIN,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
@@ -65,8 +69,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
@@ -88,11 +90,11 @@ async def update_config(form_data: OpenAIConfigForm, user=Depends(get_admin_user
class UrlsUpdateForm(BaseModel):
urls: List[str]
urls: list[str]
class KeysUpdateForm(BaseModel):
keys: List[str]
keys: list[str]
@app.get("/urls")
@@ -171,7 +173,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(
@@ -230,72 +232,76 @@ 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
@app.get("/models")
@app.get("/models/{url_idx}")
async def get_models(url_idx: Optional[int] = None, user=Depends(get_current_user)):
if url_idx == None:
async def get_models(url_idx: Optional[int] = None, user=Depends(get_verified_user)):
if url_idx is None:
models = await get_all_models()
if app.state.config.ENABLE_MODEL_FILTER:
if user.role == "user":
@@ -336,7 +342,7 @@ async def get_models(url_idx: Optional[int] = None, user=Depends(get_current_use
res = r.json()
if "error" in res:
error_detail = f"External: {res['error']}"
except:
except Exception:
error_detail = f"External: {e}"
raise HTTPException(
@@ -345,128 +351,68 @@ async def get_models(url_idx: Optional[int] = None, user=Depends(get_current_use
)
@app.api_route("/{path:path}", methods=["GET", "POST", "PUT", "DELETE"])
async def proxy(path: str, request: Request, user=Depends(get_verified_user)):
@app.post("/chat/completions")
@app.post("/chat/completions/{url_idx}")
async def generate_chat_completion(
form_data: dict,
url_idx: Optional[int] = None,
user=Depends(get_verified_user),
):
idx = 0
payload = {**form_data}
body = await request.body()
# TODO: Remove below after gpt-4-vision fix from Open AI
# Try to decode the body of the request from bytes to a UTF-8 string (Require add max_token to fix gpt-4-vision)
if "metadata" in payload:
del payload["metadata"]
payload = None
model_id = form_data.get("model")
model_info = Models.get_model_by_id(model_id)
try:
if "chat/completions" in path:
body = body.decode("utf-8")
body = json.loads(body)
if model_info:
if model_info.base_model_id:
payload["model"] = model_info.base_model_id
payload = {**body}
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_id = body.get("model")
model_info = Models.get_model_by_id(model_id)
model = app.state.MODELS[payload.get("model")]
idx = model["urlIdx"]
if model_info:
print(model_info)
if model_info.base_model_id:
payload["model"] = model_info.base_model_id
if "pipeline" in model and model.get("pipeline"):
payload["user"] = {
"name": user.name,
"id": user.id,
"email": user.email,
"role": user.role,
}
model_info.params = model_info.params.model_dump()
# Convert the modified body back to JSON
payload = json.dumps(payload)
if model_info.params:
if model_info.params.get("temperature", None):
payload["temperature"] = int(
model_info.params.get("temperature")
)
if model_info.params.get("top_p", None):
payload["top_p"] = int(model_info.params.get("top_p", None))
if model_info.params.get("max_tokens", None):
payload["max_tokens"] = int(
model_info.params.get("max_tokens", None)
)
if model_info.params.get("frequency_penalty", None):
payload["frequency_penalty"] = int(
model_info.params.get("frequency_penalty", None)
)
if model_info.params.get("seed", None):
payload["seed"] = model_info.params.get("seed", None)
if model_info.params.get("stop", None):
payload["stop"] = (
[
bytes(stop, "utf-8").decode("unicode_escape")
for stop in model_info.params["stop"]
]
if model_info.params.get("stop", None)
else None
)
if model_info.params.get("system", None):
# Check if the payload already has a system message
# If not, add a system message to the payload
if payload.get("messages"):
for message in payload["messages"]:
if message.get("role") == "system":
message["content"] = (
model_info.params.get("system", None)
+ message["content"]
)
break
else:
payload["messages"].insert(
0,
{
"role": "system",
"content": model_info.params.get("system", None),
},
)
else:
pass
model = app.state.MODELS[payload.get("model")]
idx = model["urlIdx"]
if "pipeline" in model and model.get("pipeline"):
payload["user"] = {"name": user.name, "id": user.id}
# Check if the model is "gpt-4-vision-preview" and set "max_tokens" to 4000
# This is a workaround until OpenAI fixes the issue with this model
if payload.get("model") == "gpt-4-vision-preview":
if "max_tokens" not in payload:
payload["max_tokens"] = 4000
log.debug("Modified payload:", payload)
# Convert the modified body back to JSON
payload = json.dumps(payload)
except json.JSONDecodeError as e:
log.error("Error loading request body into a dictionary:", e)
print(payload)
log.debug(payload)
url = app.state.config.OPENAI_API_BASE_URLS[idx]
key = app.state.config.OPENAI_API_KEYS[idx]
target_url = f"{url}/{path}"
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
streaming = False
try:
session = aiohttp.ClientSession(trust_env=True)
session = aiohttp.ClientSession(
trust_env=True, timeout=aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT)
)
r = await session.request(
method=request.method,
url=target_url,
data=payload if payload else body,
method="POST",
url=f"{url}/chat/completions",
data=payload,
headers=headers,
)
@@ -495,7 +441,70 @@ 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:
if not streaming and session:
if r:
r.close()
await session.close()
@app.api_route("/{path:path}", methods=["GET", "POST", "PUT", "DELETE"])
async def proxy(path: str, request: Request, user=Depends(get_verified_user)):
idx = 0
body = await request.body()
url = app.state.config.OPENAI_API_BASE_URLS[idx]
key = app.state.config.OPENAI_API_KEYS[idx]
target_url = f"{url}/{path}"
headers = {}
headers["Authorization"] = f"Bearer {key}"
headers["Content-Type"] = "application/json"
r = None
session = None
streaming = False
try:
session = aiohttp.ClientSession(trust_env=True)
r = await session.request(
method=request.method,
url=target_url,
data=body,
headers=headers,
)
r.raise_for_status()
# Check if response is SSE
if "text/event-stream" in r.headers.get("Content-Type", ""):
streaming = True
return StreamingResponse(
r.content,
status_code=r.status,
headers=dict(r.headers),
background=BackgroundTask(
cleanup_response, response=r, session=session
),
)
else:
response_data = await r.json()
return response_data
except Exception as e:
log.exception(e)
error_detail = "Open WebUI: Server Connection Error"
if r is not None:
try:
res = await r.json()
print(res)
if "error" in res:
error_detail = f"External: {res['error']['message'] if 'message' in res['error'] else res['error']}"
except Exception:
error_detail = f"External: {e}"
raise HTTPException(status_code=r.status if r else 500, detail=error_detail)
finally:
+309 -68
View File
@@ -8,12 +8,15 @@ from fastapi import (
Form,
)
from fastapi.middleware.cors import CORSMiddleware
import requests
import os, shutil, logging, re
from datetime import datetime
from pathlib import Path
from typing import List, Union, Sequence
from typing import Union, Sequence, Iterator, Any
from chromadb.utils.batch_utils import create_batches
from langchain_core.documents import Document
from langchain_community.document_loaders import (
WebBaseLoader,
@@ -30,6 +33,7 @@ from langchain_community.document_loaders import (
UnstructuredExcelLoader,
UnstructuredPowerPointLoader,
YoutubeLoader,
OutlookMessageLoader,
)
from langchain.text_splitter import RecursiveCharacterTextSplitter
@@ -44,13 +48,14 @@ import mimetypes
import uuid
import json
import sentence_transformers
from apps.webui.models.documents import (
Documents,
DocumentForm,
DocumentResponse,
)
from apps.webui.models.files import (
Files,
)
from apps.rag.utils import (
get_model_path,
@@ -67,7 +72,10 @@ from apps.rag.search.main import SearchResult
from apps.rag.search.searxng import search_searxng
from apps.rag.search.serper import search_serper
from apps.rag.search.serpstack import search_serpstack
from apps.rag.search.serply import search_serply
from apps.rag.search.duckduckgo import search_duckduckgo
from apps.rag.search.tavily import search_tavily
from apps.rag.search.jina_search import search_jina
from utils.misc import (
calculate_sha256,
@@ -75,7 +83,7 @@ from utils.misc import (
sanitize_filename,
extract_folders_after_data_docs,
)
from utils.utils import get_current_user, get_admin_user
from utils.utils import get_verified_user, get_admin_user
from config import (
AppConfig,
@@ -83,6 +91,8 @@ from config import (
SRC_LOG_LEVELS,
UPLOAD_DIR,
DOCS_DIR,
CONTENT_EXTRACTION_ENGINE,
TIKA_SERVER_URL,
RAG_TOP_K,
RAG_RELEVANCE_THRESHOLD,
RAG_EMBEDDING_ENGINE,
@@ -106,6 +116,7 @@ from config import (
YOUTUBE_LOADER_LANGUAGE,
ENABLE_RAG_WEB_SEARCH,
RAG_WEB_SEARCH_ENGINE,
RAG_WEB_SEARCH_DOMAIN_FILTER_LIST,
SEARXNG_QUERY_URL,
GOOGLE_PSE_API_KEY,
GOOGLE_PSE_ENGINE_ID,
@@ -113,9 +124,12 @@ from config import (
SERPSTACK_API_KEY,
SERPSTACK_HTTPS,
SERPER_API_KEY,
SERPLY_API_KEY,
TAVILY_API_KEY,
RAG_WEB_SEARCH_RESULT_COUNT,
RAG_WEB_SEARCH_CONCURRENT_REQUESTS,
RAG_EMBEDDING_OPENAI_BATCH_SIZE,
CORS_ALLOW_ORIGIN,
)
from constants import ERROR_MESSAGES
@@ -135,6 +149,9 @@ app.state.config.ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION = (
ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION
)
app.state.config.CONTENT_EXTRACTION_ENGINE = CONTENT_EXTRACTION_ENGINE
app.state.config.TIKA_SERVER_URL = TIKA_SERVER_URL
app.state.config.CHUNK_SIZE = CHUNK_SIZE
app.state.config.CHUNK_OVERLAP = CHUNK_OVERLAP
@@ -157,6 +174,7 @@ app.state.YOUTUBE_LOADER_TRANSLATION = None
app.state.config.ENABLE_RAG_WEB_SEARCH = ENABLE_RAG_WEB_SEARCH
app.state.config.RAG_WEB_SEARCH_ENGINE = RAG_WEB_SEARCH_ENGINE
app.state.config.RAG_WEB_SEARCH_DOMAIN_FILTER_LIST = RAG_WEB_SEARCH_DOMAIN_FILTER_LIST
app.state.config.SEARXNG_QUERY_URL = SEARXNG_QUERY_URL
app.state.config.GOOGLE_PSE_API_KEY = GOOGLE_PSE_API_KEY
@@ -165,6 +183,8 @@ app.state.config.BRAVE_SEARCH_API_KEY = BRAVE_SEARCH_API_KEY
app.state.config.SERPSTACK_API_KEY = SERPSTACK_API_KEY
app.state.config.SERPSTACK_HTTPS = SERPSTACK_HTTPS
app.state.config.SERPER_API_KEY = SERPER_API_KEY
app.state.config.SERPLY_API_KEY = SERPLY_API_KEY
app.state.config.TAVILY_API_KEY = TAVILY_API_KEY
app.state.config.RAG_WEB_SEARCH_RESULT_COUNT = RAG_WEB_SEARCH_RESULT_COUNT
app.state.config.RAG_WEB_SEARCH_CONCURRENT_REQUESTS = RAG_WEB_SEARCH_CONCURRENT_REQUESTS
@@ -174,6 +194,8 @@ def update_embedding_model(
update_model: bool = False,
):
if embedding_model and app.state.config.RAG_EMBEDDING_ENGINE == "":
import sentence_transformers
app.state.sentence_transformer_ef = sentence_transformers.SentenceTransformer(
get_model_path(embedding_model, update_model),
device=DEVICE_TYPE,
@@ -188,6 +210,8 @@ def update_reranking_model(
update_model: bool = False,
):
if reranking_model:
import sentence_transformers
app.state.sentence_transformer_rf = sentence_transformers.CrossEncoder(
get_model_path(reranking_model, update_model),
device=DEVICE_TYPE,
@@ -217,12 +241,9 @@ app.state.EMBEDDING_FUNCTION = get_embedding_function(
app.state.config.RAG_EMBEDDING_OPENAI_BATCH_SIZE,
)
origins = ["*"]
app.add_middleware(
CORSMiddleware,
allow_origins=origins,
allow_origins=CORS_ALLOW_ORIGIN,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
@@ -353,7 +374,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,
@@ -372,6 +393,10 @@ async def get_rag_config(user=Depends(get_admin_user)):
return {
"status": True,
"pdf_extract_images": app.state.config.PDF_EXTRACT_IMAGES,
"content_extraction": {
"engine": app.state.config.CONTENT_EXTRACTION_ENGINE,
"tika_server_url": app.state.config.TIKA_SERVER_URL,
},
"chunk": {
"chunk_size": app.state.config.CHUNK_SIZE,
"chunk_overlap": app.state.config.CHUNK_OVERLAP,
@@ -392,6 +417,8 @@ async def get_rag_config(user=Depends(get_admin_user)):
"serpstack_api_key": app.state.config.SERPSTACK_API_KEY,
"serpstack_https": app.state.config.SERPSTACK_HTTPS,
"serper_api_key": app.state.config.SERPER_API_KEY,
"serply_api_key": app.state.config.SERPLY_API_KEY,
"tavily_api_key": app.state.config.TAVILY_API_KEY,
"result_count": app.state.config.RAG_WEB_SEARCH_RESULT_COUNT,
"concurrent_requests": app.state.config.RAG_WEB_SEARCH_CONCURRENT_REQUESTS,
},
@@ -399,13 +426,18 @@ async def get_rag_config(user=Depends(get_admin_user)):
}
class ContentExtractionConfig(BaseModel):
engine: str = ""
tika_server_url: Optional[str] = None
class ChunkParamUpdateForm(BaseModel):
chunk_size: int
chunk_overlap: int
class YoutubeLoaderConfig(BaseModel):
language: List[str]
language: list[str]
translation: Optional[str] = None
@@ -419,6 +451,8 @@ class WebSearchConfig(BaseModel):
serpstack_api_key: Optional[str] = None
serpstack_https: Optional[bool] = None
serper_api_key: Optional[str] = None
serply_api_key: Optional[str] = None
tavily_api_key: Optional[str] = None
result_count: Optional[int] = None
concurrent_requests: Optional[int] = None
@@ -430,6 +464,7 @@ class WebConfig(BaseModel):
class ConfigUpdateForm(BaseModel):
pdf_extract_images: Optional[bool] = None
content_extraction: Optional[ContentExtractionConfig] = None
chunk: Optional[ChunkParamUpdateForm] = None
youtube: Optional[YoutubeLoaderConfig] = None
web: Optional[WebConfig] = None
@@ -443,6 +478,11 @@ async def update_rag_config(form_data: ConfigUpdateForm, user=Depends(get_admin_
else app.state.config.PDF_EXTRACT_IMAGES
)
if form_data.content_extraction is not None:
log.info(f"Updating text settings: {form_data.content_extraction}")
app.state.config.CONTENT_EXTRACTION_ENGINE = form_data.content_extraction.engine
app.state.config.TIKA_SERVER_URL = form_data.content_extraction.tika_server_url
if form_data.chunk is not None:
app.state.config.CHUNK_SIZE = form_data.chunk.chunk_size
app.state.config.CHUNK_OVERLAP = form_data.chunk.chunk_overlap
@@ -469,6 +509,8 @@ async def update_rag_config(form_data: ConfigUpdateForm, user=Depends(get_admin_
app.state.config.SERPSTACK_API_KEY = form_data.web.search.serpstack_api_key
app.state.config.SERPSTACK_HTTPS = form_data.web.search.serpstack_https
app.state.config.SERPER_API_KEY = form_data.web.search.serper_api_key
app.state.config.SERPLY_API_KEY = form_data.web.search.serply_api_key
app.state.config.TAVILY_API_KEY = form_data.web.search.tavily_api_key
app.state.config.RAG_WEB_SEARCH_RESULT_COUNT = form_data.web.search.result_count
app.state.config.RAG_WEB_SEARCH_CONCURRENT_REQUESTS = (
form_data.web.search.concurrent_requests
@@ -477,6 +519,10 @@ async def update_rag_config(form_data: ConfigUpdateForm, user=Depends(get_admin_
return {
"status": True,
"pdf_extract_images": app.state.config.PDF_EXTRACT_IMAGES,
"content_extraction": {
"engine": app.state.config.CONTENT_EXTRACTION_ENGINE,
"tika_server_url": app.state.config.TIKA_SERVER_URL,
},
"chunk": {
"chunk_size": app.state.config.CHUNK_SIZE,
"chunk_overlap": app.state.config.CHUNK_OVERLAP,
@@ -497,6 +543,8 @@ async def update_rag_config(form_data: ConfigUpdateForm, user=Depends(get_admin_
"serpstack_api_key": app.state.config.SERPSTACK_API_KEY,
"serpstack_https": app.state.config.SERPSTACK_HTTPS,
"serper_api_key": app.state.config.SERPER_API_KEY,
"serply_api_key": app.state.config.SERPLY_API_KEY,
"tavily_api_key": app.state.config.TAVILY_API_KEY,
"result_count": app.state.config.RAG_WEB_SEARCH_RESULT_COUNT,
"concurrent_requests": app.state.config.RAG_WEB_SEARCH_CONCURRENT_REQUESTS,
},
@@ -505,7 +553,7 @@ async def update_rag_config(form_data: ConfigUpdateForm, user=Depends(get_admin_
@app.get("/template")
async def get_rag_template(user=Depends(get_current_user)):
async def get_rag_template(user=Depends(get_verified_user)):
return {
"status": True,
"template": app.state.config.RAG_TEMPLATE,
@@ -562,7 +610,7 @@ class QueryDocForm(BaseModel):
@app.post("/query/doc")
def query_doc_handler(
form_data: QueryDocForm,
user=Depends(get_current_user),
user=Depends(get_verified_user),
):
try:
if app.state.config.ENABLE_RAG_HYBRID_SEARCH:
@@ -592,7 +640,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
@@ -602,7 +650,7 @@ class QueryCollectionsForm(BaseModel):
@app.post("/query/collection")
def query_collection_handler(
form_data: QueryCollectionsForm,
user=Depends(get_current_user),
user=Depends(get_verified_user),
):
try:
if app.state.config.ENABLE_RAG_HYBRID_SEARCH:
@@ -633,7 +681,7 @@ def query_collection_handler(
@app.post("/youtube")
def store_youtube_video(form_data: UrlForm, user=Depends(get_current_user)):
def store_youtube_video(form_data: UrlForm, user=Depends(get_verified_user)):
try:
loader = YoutubeLoader.from_youtube_url(
form_data.url,
@@ -662,7 +710,7 @@ def store_youtube_video(form_data: UrlForm, user=Depends(get_current_user)):
@app.post("/web")
def store_web(form_data: UrlForm, user=Depends(get_current_user)):
def store_web(form_data: UrlForm, user=Depends(get_verified_user)):
# "https://www.gutenberg.org/files/1727/1727-h/1727-h.htm"
try:
loader = get_web_loader(
@@ -693,7 +741,7 @@ def get_web_loader(url: Union[str, Sequence[str]], verify_ssl: bool = True):
# Check if the URL is valid
if not validate_url(url):
raise ValueError(ERROR_MESSAGES.INVALID_URL)
return WebBaseLoader(
return SafeWebBaseLoader(
url,
verify_ssl=verify_ssl,
requests_per_second=RAG_WEB_SEARCH_CONCURRENT_REQUESTS,
@@ -744,7 +792,8 @@ def search_web(engine: str, query: str) -> list[SearchResult]:
- BRAVE_SEARCH_API_KEY
- SERPSTACK_API_KEY
- SERPER_API_KEY
- SERPLY_API_KEY
- TAVILY_API_KEY
Args:
query (str): The query to search for
"""
@@ -756,6 +805,7 @@ def search_web(engine: str, query: str) -> list[SearchResult]:
app.state.config.SEARXNG_QUERY_URL,
query,
app.state.config.RAG_WEB_SEARCH_RESULT_COUNT,
app.state.config.RAG_WEB_SEARCH_DOMAIN_FILTER_LIST,
)
else:
raise Exception("No SEARXNG_QUERY_URL found in environment variables")
@@ -769,6 +819,7 @@ def search_web(engine: str, query: str) -> list[SearchResult]:
app.state.config.GOOGLE_PSE_ENGINE_ID,
query,
app.state.config.RAG_WEB_SEARCH_RESULT_COUNT,
app.state.config.RAG_WEB_SEARCH_DOMAIN_FILTER_LIST,
)
else:
raise Exception(
@@ -780,6 +831,7 @@ def search_web(engine: str, query: str) -> list[SearchResult]:
app.state.config.BRAVE_SEARCH_API_KEY,
query,
app.state.config.RAG_WEB_SEARCH_RESULT_COUNT,
app.state.config.RAG_WEB_SEARCH_DOMAIN_FILTER_LIST,
)
else:
raise Exception("No BRAVE_SEARCH_API_KEY found in environment variables")
@@ -789,6 +841,7 @@ def search_web(engine: str, query: str) -> list[SearchResult]:
app.state.config.SERPSTACK_API_KEY,
query,
app.state.config.RAG_WEB_SEARCH_RESULT_COUNT,
app.state.config.RAG_WEB_SEARCH_DOMAIN_FILTER_LIST,
https_enabled=app.state.config.SERPSTACK_HTTPS,
)
else:
@@ -799,16 +852,47 @@ def search_web(engine: str, query: str) -> list[SearchResult]:
app.state.config.SERPER_API_KEY,
query,
app.state.config.RAG_WEB_SEARCH_RESULT_COUNT,
app.state.config.RAG_WEB_SEARCH_DOMAIN_FILTER_LIST,
)
else:
raise Exception("No SERPER_API_KEY found in environment variables")
elif engine == "serply":
if app.state.config.SERPLY_API_KEY:
return search_serply(
app.state.config.SERPLY_API_KEY,
query,
app.state.config.RAG_WEB_SEARCH_RESULT_COUNT,
app.state.config.RAG_WEB_SEARCH_DOMAIN_FILTER_LIST,
)
else:
raise Exception("No SERPLY_API_KEY found in environment variables")
elif engine == "duckduckgo":
return search_duckduckgo(
query,
app.state.config.RAG_WEB_SEARCH_RESULT_COUNT,
app.state.config.RAG_WEB_SEARCH_DOMAIN_FILTER_LIST,
)
elif engine == "tavily":
if app.state.config.TAVILY_API_KEY:
return search_tavily(
app.state.config.TAVILY_API_KEY,
query,
app.state.config.RAG_WEB_SEARCH_RESULT_COUNT,
)
else:
raise Exception("No TAVILY_API_KEY found in environment variables")
elif engine == "jina":
return search_jina(query, app.state.config.RAG_WEB_SEARCH_RESULT_COUNT)
else:
raise Exception("No search engine API key found in environment variables")
@app.post("/web/search")
def store_web_search(form_data: SearchForm, user=Depends(get_current_user)):
def store_web_search(form_data: SearchForm, user=Depends(get_verified_user)):
try:
logging.info(
f"trying to web search with {app.state.config.RAG_WEB_SEARCH_ENGINE, form_data.query}"
)
web_results = search_web(
app.state.config.RAG_WEB_SEARCH_ENGINE, form_data.query
)
@@ -844,7 +928,9 @@ def store_web_search(form_data: SearchForm, user=Depends(get_current_user)):
)
def store_data_in_vector_db(data, collection_name, overwrite: bool = False) -> bool:
def store_data_in_vector_db(
data, collection_name, metadata: Optional[dict] = None, overwrite: bool = False
) -> bool:
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=app.state.config.CHUNK_SIZE,
@@ -856,7 +942,7 @@ def store_data_in_vector_db(data, collection_name, overwrite: bool = False) -> b
if len(docs) > 0:
log.info(f"store_data_in_vector_db {docs}")
return store_docs_in_vector_db(docs, collection_name, overwrite), None
return store_docs_in_vector_db(docs, collection_name, metadata, overwrite), None
else:
raise ValueError(ERROR_MESSAGES.EMPTY_CONTENT)
@@ -870,14 +956,23 @@ def store_text_in_vector_db(
add_start_index=True,
)
docs = text_splitter.create_documents([text], metadatas=[metadata])
return store_docs_in_vector_db(docs, collection_name, overwrite)
return store_docs_in_vector_db(docs, collection_name, overwrite=overwrite)
def store_docs_in_vector_db(docs, collection_name, overwrite: bool = False) -> bool:
def store_docs_in_vector_db(
docs, collection_name, metadata: Optional[dict] = None, overwrite: bool = False
) -> bool:
log.info(f"store_docs_in_vector_db {docs} {collection_name}")
texts = [doc.page_content for doc in docs]
metadatas = [doc.metadata for doc in docs]
metadatas = [{**doc.metadata, **(metadata if metadata else {})} for doc in docs]
# ChromaDB does not like datetime formats
# for meta-data so convert them to string.
for metadata in metadatas:
for key, value in metadata.items():
if isinstance(value, datetime):
metadata[key] = str(value)
try:
if overwrite:
@@ -911,13 +1006,49 @@ def store_docs_in_vector_db(docs, collection_name, overwrite: bool = False) -> b
return True
except Exception as e:
log.exception(e)
if e.__class__.__name__ == "UniqueConstraintError":
return True
log.exception(e)
return False
class TikaLoader:
def __init__(self, file_path, mime_type=None):
self.file_path = file_path
self.mime_type = mime_type
def load(self) -> list[Document]:
with open(self.file_path, "rb") as f:
data = f.read()
if self.mime_type is not None:
headers = {"Content-Type": self.mime_type}
else:
headers = {}
endpoint = app.state.config.TIKA_SERVER_URL
if not endpoint.endswith("/"):
endpoint += "/"
endpoint += "tika/text"
r = requests.put(endpoint, data=data, headers=headers)
if r.ok:
raw_metadata = r.json()
text = raw_metadata.get("X-TIKA:content", "<No text content found>")
if "Content-Type" in raw_metadata:
headers["Content-Type"] = raw_metadata["Content-Type"]
log.info("Tika extracted text: %s", text)
return [Document(page_content=text, metadata=headers)]
else:
raise Exception(f"Error calling Tika: {r.reason}")
def get_loader(filename: str, file_content_type: str, file_path: str):
file_ext = filename.split(".")[-1].lower()
known_type = True
@@ -965,47 +1096,68 @@ def get_loader(filename: str, file_content_type: str, file_path: str):
"swift",
"vue",
"svelte",
"msg",
"ex",
"exs",
"erl",
"tsx",
"jsx",
"hs",
"lhs",
]
if file_ext == "pdf":
loader = PyPDFLoader(
file_path, extract_images=app.state.config.PDF_EXTRACT_IMAGES
)
elif file_ext == "csv":
loader = CSVLoader(file_path)
elif file_ext == "rst":
loader = UnstructuredRSTLoader(file_path, mode="elements")
elif file_ext == "xml":
loader = UnstructuredXMLLoader(file_path)
elif file_ext in ["htm", "html"]:
loader = BSHTMLLoader(file_path, open_encoding="unicode_escape")
elif file_ext == "md":
loader = UnstructuredMarkdownLoader(file_path)
elif file_content_type == "application/epub+zip":
loader = UnstructuredEPubLoader(file_path)
elif (
file_content_type
== "application/vnd.openxmlformats-officedocument.wordprocessingml.document"
or file_ext in ["doc", "docx"]
if (
app.state.config.CONTENT_EXTRACTION_ENGINE == "tika"
and app.state.config.TIKA_SERVER_URL
):
loader = Docx2txtLoader(file_path)
elif file_content_type in [
"application/vnd.ms-excel",
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
] or file_ext in ["xls", "xlsx"]:
loader = UnstructuredExcelLoader(file_path)
elif file_content_type in [
"application/vnd.ms-powerpoint",
"application/vnd.openxmlformats-officedocument.presentationml.presentation",
] or file_ext in ["ppt", "pptx"]:
loader = UnstructuredPowerPointLoader(file_path)
elif file_ext in known_source_ext or (
file_content_type and file_content_type.find("text/") >= 0
):
loader = TextLoader(file_path, autodetect_encoding=True)
if file_ext in known_source_ext or (
file_content_type and file_content_type.find("text/") >= 0
):
loader = TextLoader(file_path, autodetect_encoding=True)
else:
loader = TikaLoader(file_path, file_content_type)
else:
loader = TextLoader(file_path, autodetect_encoding=True)
known_type = False
if file_ext == "pdf":
loader = PyPDFLoader(
file_path, extract_images=app.state.config.PDF_EXTRACT_IMAGES
)
elif file_ext == "csv":
loader = CSVLoader(file_path)
elif file_ext == "rst":
loader = UnstructuredRSTLoader(file_path, mode="elements")
elif file_ext == "xml":
loader = UnstructuredXMLLoader(file_path)
elif file_ext in ["htm", "html"]:
loader = BSHTMLLoader(file_path, open_encoding="unicode_escape")
elif file_ext == "md":
loader = UnstructuredMarkdownLoader(file_path)
elif file_content_type == "application/epub+zip":
loader = UnstructuredEPubLoader(file_path)
elif (
file_content_type
== "application/vnd.openxmlformats-officedocument.wordprocessingml.document"
or file_ext in ["doc", "docx"]
):
loader = Docx2txtLoader(file_path)
elif file_content_type in [
"application/vnd.ms-excel",
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
] or file_ext in ["xls", "xlsx"]:
loader = UnstructuredExcelLoader(file_path)
elif file_content_type in [
"application/vnd.ms-powerpoint",
"application/vnd.openxmlformats-officedocument.presentationml.presentation",
] or file_ext in ["ppt", "pptx"]:
loader = UnstructuredPowerPointLoader(file_path)
elif file_ext == "msg":
loader = OutlookMessageLoader(file_path)
elif file_ext in known_source_ext or (
file_content_type and file_content_type.find("text/") >= 0
):
loader = TextLoader(file_path, autodetect_encoding=True)
else:
loader = TextLoader(file_path, autodetect_encoding=True)
known_type = False
return loader, known_type
@@ -1014,7 +1166,7 @@ def get_loader(filename: str, file_content_type: str, file_path: str):
def store_doc(
collection_name: Optional[str] = Form(None),
file: UploadFile = File(...),
user=Depends(get_current_user),
user=Depends(get_verified_user),
):
# "https://www.gutenberg.org/files/1727/1727-h/1727-h.htm"
@@ -1031,7 +1183,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()
@@ -1067,6 +1219,68 @@ def store_doc(
)
class ProcessDocForm(BaseModel):
file_id: str
collection_name: Optional[str] = None
@app.post("/process/doc")
def process_doc(
form_data: ProcessDocForm,
user=Depends(get_verified_user),
):
try:
file = Files.get_file_by_id(form_data.file_id)
file_path = file.meta.get("path", f"{UPLOAD_DIR}/{file.filename}")
f = open(file_path, "rb")
collection_name = form_data.collection_name
if collection_name is None:
collection_name = calculate_sha256(f)[:63]
f.close()
loader, known_type = get_loader(
file.filename, file.meta.get("content_type"), file_path
)
data = loader.load()
try:
result = store_data_in_vector_db(
data,
collection_name,
{
"file_id": form_data.file_id,
"name": file.meta.get("name", file.filename),
},
)
if result:
return {
"status": True,
"collection_name": collection_name,
"known_type": known_type,
"filename": file.meta.get("name", file.filename),
}
except Exception as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=e,
)
except Exception as e:
log.exception(e)
if "No pandoc was found" in str(e):
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.PANDOC_NOT_INSTALLED,
)
else:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.DEFAULT(e),
)
class TextRAGForm(BaseModel):
name: str
content: str
@@ -1076,11 +1290,11 @@ class TextRAGForm(BaseModel):
@app.post("/text")
def store_text(
form_data: TextRAGForm,
user=Depends(get_current_user),
user=Depends(get_verified_user),
):
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(
@@ -1123,7 +1337,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(
@@ -1209,6 +1423,33 @@ def reset(user=Depends(get_admin_user)) -> bool:
return True
class SafeWebBaseLoader(WebBaseLoader):
"""WebBaseLoader with enhanced error handling for URLs."""
def lazy_load(self) -> Iterator[Document]:
"""Lazy load text from the url(s) in web_path with error handling."""
for path in self.web_paths:
try:
soup = self._scrape(path, bs_kwargs=self.bs_kwargs)
text = soup.get_text(**self.bs_get_text_kwargs)
# Build metadata
metadata = {"source": path}
if title := soup.find("title"):
metadata["title"] = title.get_text()
if description := soup.find("meta", attrs={"name": "description"}):
metadata["description"] = description.get(
"content", "No description found."
)
if html := soup.find("html"):
metadata["language"] = html.get("lang", "No language found.")
yield Document(page_content=text, metadata=metadata)
except Exception as e:
# Log the error and continue with the next URL
log.error(f"Error loading {path}: {e}")
if ENV == "dev":
@app.get("/ef")
+8 -3
View File
@@ -1,15 +1,17 @@
import logging
from typing import Optional
import requests
from apps.rag.search.main import SearchResult
from apps.rag.search.main import SearchResult, get_filtered_results
from config import SRC_LOG_LEVELS
log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["RAG"])
def search_brave(api_key: str, query: str, count: int) -> list[SearchResult]:
def search_brave(
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.
Args:
@@ -29,6 +31,9 @@ def search_brave(api_key: str, query: str, count: int) -> list[SearchResult]:
json_response = response.json()
results = json_response.get("web", {}).get("results", [])
if filter_list:
results = get_filtered_results(results, filter_list)
return [
SearchResult(
link=result["url"], title=result.get("title"), snippet=result.get("snippet")
+49
View File
@@ -0,0 +1,49 @@
import logging
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
log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["RAG"])
def search_duckduckgo(
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.
Args:
query (str): The query to search for
count (int): The number of results to return
Returns:
list[SearchResult]: A list of search results
"""
# Use the DDGS context manager to create a DDGS object
with DDGS() as ddgs:
# Use the ddgs.text() method to perform the search
ddgs_gen = ddgs.text(
query, safesearch="moderate", max_results=count, backend="api"
)
# Check if there are search results
if ddgs_gen:
# Convert the search results into a list
search_results = [r for r in ddgs_gen]
# Create an empty list to store the SearchResult objects
results = []
# Iterate over each search result
for result in search_results:
# Create a SearchResult object and append it to the results list
results.append(
SearchResult(
link=result["href"],
title=result.get("title"),
snippet=result.get("body"),
)
)
if filter_list:
results = get_filtered_results(results, filter_list)
# Return the list of search results
return results
+9 -3
View File
@@ -1,9 +1,9 @@
import json
import logging
from typing import Optional
import requests
from apps.rag.search.main import SearchResult
from apps.rag.search.main import SearchResult, get_filtered_results
from config import SRC_LOG_LEVELS
log = logging.getLogger(__name__)
@@ -11,7 +11,11 @@ log.setLevel(SRC_LOG_LEVELS["RAG"])
def search_google_pse(
api_key: str, search_engine_id: str, query: str, count: int
api_key: str,
search_engine_id: str,
query: str,
count: int,
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.
@@ -35,6 +39,8 @@ def search_google_pse(
json_response = response.json()
results = json_response.get("items", [])
if filter_list:
results = get_filtered_results(results, filter_list)
return [
SearchResult(
link=result["link"],
+41
View File
@@ -0,0 +1,41 @@
import logging
import requests
from yarl import URL
from apps.rag.search.main import SearchResult
from config import SRC_LOG_LEVELS
log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["RAG"])
def search_jina(query: str, count: int) -> list[SearchResult]:
"""
Search using Jina's Search API and return the results as a list of SearchResult objects.
Args:
query (str): The query to search for
count (int): The number of results to return
Returns:
list[SearchResult]: A list of search results
"""
jina_search_endpoint = "https://s.jina.ai/"
headers = {
"Accept": "application/json",
}
url = str(URL(jina_search_endpoint + query))
response = requests.get(url, headers=headers)
response.raise_for_status()
data = response.json()
results = []
for result in data["data"][:count]:
results.append(
SearchResult(
link=result["url"],
title=result.get("title"),
snippet=result.get("content"),
)
)
return results
+12 -1
View File
@@ -1,8 +1,19 @@
from typing import Optional
from urllib.parse import urlparse
from pydantic import BaseModel
def get_filtered_results(results, filter_list):
if not filter_list:
return results
filtered_results = []
for result in results:
domain = urlparse(result["url"]).netloc
if any(domain.endswith(filtered_domain) for filtered_domain in filter_list):
filtered_results.append(result)
return filtered_results
class SearchResult(BaseModel):
link: str
title: Optional[str]
+15 -6
View File
@@ -1,9 +1,9 @@
import logging
import requests
from typing import List
from typing import Optional
from apps.rag.search.main import SearchResult
from apps.rag.search.main import SearchResult, get_filtered_results
from config import SRC_LOG_LEVELS
log = logging.getLogger(__name__)
@@ -11,8 +11,12 @@ log.setLevel(SRC_LOG_LEVELS["RAG"])
def search_searxng(
query_url: str, query: str, count: int, **kwargs
) -> List[SearchResult]:
query_url: str,
query: str,
count: int,
filter_list: Optional[list[str]] = None,
**kwargs,
) -> list[SearchResult]:
"""
Search a SearXNG instance for a given query and return the results as a list of SearchResult objects.
@@ -25,11 +29,12 @@ def search_searxng(
Keyword Args:
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.
@@ -37,6 +42,7 @@ def search_searxng(
# Default values for optional parameters are provided as empty strings or None when not specified.
language = kwargs.get("language", "en-US")
safesearch = kwargs.get("safesearch", "1")
time_range = kwargs.get("time_range", "")
categories = "".join(kwargs.get("categories", []))
@@ -44,6 +50,7 @@ def search_searxng(
"q": query,
"format": "json",
"pageno": 1,
"safesearch": safesearch,
"language": language,
"time_range": time_range,
"categories": categories,
@@ -75,6 +82,8 @@ def search_searxng(
json_response = response.json()
results = json_response.get("results", [])
sorted_results = sorted(results, key=lambda x: x.get("score", 0), reverse=True)
if filter_list:
sorted_results = get_filtered_results(sorted_results, filter_list)
return [
SearchResult(
link=result["url"], title=result.get("title"), snippet=result.get("content")
+7 -3
View File
@@ -1,16 +1,18 @@
import json
import logging
from typing import Optional
import requests
from apps.rag.search.main import SearchResult
from apps.rag.search.main import SearchResult, get_filtered_results
from config import SRC_LOG_LEVELS
log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["RAG"])
def search_serper(api_key: str, query: str, count: int) -> list[SearchResult]:
def search_serper(
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.
Args:
@@ -29,6 +31,8 @@ def search_serper(api_key: str, query: str, count: int) -> list[SearchResult]:
results = sorted(
json_response.get("organic", []), key=lambda x: x.get("position", 0)
)
if filter_list:
results = get_filtered_results(results, filter_list)
return [
SearchResult(
link=result["link"],
+70
View File
@@ -0,0 +1,70 @@
import json
import logging
from typing import Optional
import requests
from urllib.parse import urlencode
from apps.rag.search.main import SearchResult, get_filtered_results
from config import SRC_LOG_LEVELS
log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["RAG"])
def search_serply(
api_key: str,
query: str,
count: int,
hl: str = "us",
limit: int = 10,
device_type: str = "desktop",
proxy_location: str = "US",
filter_list: Optional[list[str]] = None,
) -> list[SearchResult]:
"""Search using serper.dev's API and return the results as a list of SearchResult objects.
Args:
api_key (str): A serply.io API key
query (str): The query to search for
hl (str): Host Language code to display results in (reference https://developers.google.com/custom-search/docs/xml_results?hl=en#wsInterfaceLanguages)
limit (int): The maximum number of results to return [10-100, defaults to 10]
"""
log.info("Searching with Serply")
url = "https://api.serply.io/v1/search/"
query_payload = {
"q": query,
"language": "en",
"num": limit,
"gl": proxy_location.upper(),
"hl": hl.lower(),
}
url = f"{url}{urlencode(query_payload)}"
headers = {
"X-API-KEY": api_key,
"X-User-Agent": device_type,
"User-Agent": "open-webui",
"X-Proxy-Location": proxy_location,
}
response = requests.request("GET", url, headers=headers)
response.raise_for_status()
json_response = response.json()
log.info(f"results from serply search: {json_response}")
results = sorted(
json_response.get("results", []), key=lambda x: x.get("realPosition", 0)
)
if filter_list:
results = get_filtered_results(results, filter_list)
return [
SearchResult(
link=result["link"],
title=result.get("title"),
snippet=result.get("description"),
)
for result in results[:count]
]
+9 -3
View File
@@ -1,9 +1,9 @@
import json
import logging
from typing import Optional
import requests
from apps.rag.search.main import SearchResult
from apps.rag.search.main import SearchResult, get_filtered_results
from config import SRC_LOG_LEVELS
log = logging.getLogger(__name__)
@@ -11,7 +11,11 @@ log.setLevel(SRC_LOG_LEVELS["RAG"])
def search_serpstack(
api_key: str, query: str, count: int, https_enabled: bool = True
api_key: str,
query: str,
count: int,
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.
@@ -35,6 +39,8 @@ def search_serpstack(
results = sorted(
json_response.get("organic_results", []), key=lambda x: x.get("position", 0)
)
if filter_list:
results = get_filtered_results(results, filter_list)
return [
SearchResult(
link=result["url"], title=result.get("title"), snippet=result.get("snippet")
+39
View File
@@ -0,0 +1,39 @@
import logging
import requests
from apps.rag.search.main import SearchResult
from config import SRC_LOG_LEVELS
log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["RAG"])
def search_tavily(api_key: str, query: str, count: int) -> list[SearchResult]:
"""Search using Tavily's Search API and return the results as a list of SearchResult objects.
Args:
api_key (str): A Tavily Search API key
query (str): The query to search for
Returns:
list[SearchResult]: A list of search results
"""
url = "https://api.tavily.com/search"
data = {"query": query, "api_key": api_key}
response = requests.post(url, json=data)
response.raise_for_status()
json_response = response.json()
raw_search_results = json_response.get("results", [])
return [
SearchResult(
link=result["url"],
title=result.get("title", ""),
snippet=result.get("content"),
)
for result in raw_search_results[:count]
]
+206
View File
@@ -0,0 +1,206 @@
{
"ads": [],
"ads_count": 0,
"answers": [],
"results": [
{
"title": "Apple",
"link": "https://www.apple.com/",
"description": "Discover the innovative world of Apple and shop everything iPhone, iPad, Apple Watch, Mac, and Apple TV, plus explore accessories, entertainment, ...",
"additional_links": [
{
"text": "AppleApplehttps://www.apple.com",
"href": "https://www.apple.com/"
}
],
"cite": {},
"subdomains": [
{
"title": "Support",
"link": "https://support.apple.com/",
"description": "SupportContact - iPhone Support - Billing and Subscriptions - Apple Repair"
},
{
"title": "Store",
"link": "https://www.apple.com/store",
"description": "StoreShop iPhone - Shop iPad - App Store - Shop Mac - ..."
},
{
"title": "Mac",
"link": "https://www.apple.com/mac/",
"description": "MacMacBook Air - MacBook Pro - iMac - Compare Mac models - Mac mini"
},
{
"title": "iPad",
"link": "https://www.apple.com/ipad/",
"description": "iPadShop iPad - iPad Pro - iPad Air - Compare iPad models - ..."
},
{
"title": "Watch",
"link": "https://www.apple.com/watch/",
"description": "WatchShop Apple Watch - Series 9 - SE - Ultra 2 - Nike - Hermès - ..."
}
],
"realPosition": 1
},
{
"title": "Apple",
"link": "https://www.apple.com/",
"description": "Discover the innovative world of Apple and shop everything iPhone, iPad, Apple Watch, Mac, and Apple TV, plus explore accessories, entertainment, ...",
"additional_links": [
{
"text": "AppleApplehttps://www.apple.com",
"href": "https://www.apple.com/"
}
],
"cite": {},
"realPosition": 2
},
{
"title": "Apple Inc.",
"link": "https://en.wikipedia.org/wiki/Apple_Inc.",
"description": "Apple Inc. (formerly Apple Computer, Inc.) is an American multinational corporation and technology company headquartered in Cupertino, California, ...",
"additional_links": [
{
"text": "Apple Inc.Wikipediahttps://en.wikipedia.org wiki Apple_Inc",
"href": "https://en.wikipedia.org/wiki/Apple_Inc."
},
{
"text": "",
"href": "https://en.wikipedia.org/wiki/Apple_Inc."
},
{
"text": "History",
"href": "https://en.wikipedia.org/wiki/History_of_Apple_Inc."
},
{
"text": "List of Apple products",
"href": "https://en.wikipedia.org/wiki/List_of_Apple_products"
},
{
"text": "Litigation involving Apple Inc.",
"href": "https://en.wikipedia.org/wiki/Litigation_involving_Apple_Inc."
},
{
"text": "Apple Park",
"href": "https://en.wikipedia.org/wiki/Apple_Park"
}
],
"cite": {
"domain": "https://en.wikipedia.org wiki Apple_Inc",
"span": " wiki Apple_Inc"
},
"realPosition": 3
},
{
"title": "Apple Inc. (AAPL) Company Profile & Facts",
"link": "https://finance.yahoo.com/quote/AAPL/profile/",
"description": "Apple Inc. designs, manufactures, and markets smartphones, personal computers, tablets, wearables, and accessories worldwide. The company offers iPhone, a line ...",
"additional_links": [
{
"text": "Apple Inc. (AAPL) Company Profile & FactsYahoo Financehttps://finance.yahoo.com quote AAPL profile",
"href": "https://finance.yahoo.com/quote/AAPL/profile/"
}
],
"cite": {
"domain": "https://finance.yahoo.com quote AAPL profile",
"span": " quote AAPL profile"
},
"realPosition": 4
},
{
"title": "Apple Inc - Company Profile and News",
"link": "https://www.bloomberg.com/profile/company/AAPL:US",
"description": "Apple Inc. Apple Inc. designs, manufactures, and markets smartphones, personal computers, tablets, wearables and accessories, and sells a variety of related ...",
"additional_links": [
{
"text": "Apple Inc - Company Profile and NewsBloomberghttps://www.bloomberg.com company AAPL:US",
"href": "https://www.bloomberg.com/profile/company/AAPL:US"
},
{
"text": "",
"href": "https://www.bloomberg.com/profile/company/AAPL:US"
}
],
"cite": {
"domain": "https://www.bloomberg.com company AAPL:US",
"span": " company AAPL:US"
},
"realPosition": 5
},
{
"title": "Apple Inc. | History, Products, Headquarters, & Facts",
"link": "https://www.britannica.com/money/Apple-Inc",
"description": "May 22, 2024 — Apple Inc. is an American multinational technology company that revolutionized the technology sector through its innovation of computer ...",
"additional_links": [
{
"text": "Apple Inc. | History, Products, Headquarters, & FactsBritannicahttps://www.britannica.com money Apple-Inc",
"href": "https://www.britannica.com/money/Apple-Inc"
},
{
"text": "",
"href": "https://www.britannica.com/money/Apple-Inc"
}
],
"cite": {
"domain": "https://www.britannica.com money Apple-Inc",
"span": " money Apple-Inc"
},
"realPosition": 6
}
],
"shopping_ads": [],
"places": [
{
"title": "Apple Inc."
},
{
"title": "Apple Inc"
},
{
"title": "Apple Inc"
}
],
"related_searches": {
"images": [],
"text": [
{
"title": "apple inc full form",
"link": "https://www.google.com/search?sca_esv=6b6df170a5c9891b&sca_upv=1&q=Apple+Inc+full+form&sa=X&ved=2ahUKEwjLxuSJwM-GAxUHODQIHYuJBhgQ1QJ6BAhPEAE"
},
{
"title": "apple company history",
"link": "https://www.google.com/search?sca_esv=6b6df170a5c9891b&sca_upv=1&q=Apple+company+history&sa=X&ved=2ahUKEwjLxuSJwM-GAxUHODQIHYuJBhgQ1QJ6BAhOEAE"
},
{
"title": "apple store",
"link": "https://www.google.com/search?sca_esv=6b6df170a5c9891b&sca_upv=1&q=Apple+Store&sa=X&ved=2ahUKEwjLxuSJwM-GAxUHODQIHYuJBhgQ1QJ6BAhQEAE"
},
{
"title": "apple id",
"link": "https://www.google.com/search?sca_esv=6b6df170a5c9891b&sca_upv=1&q=Apple+id&sa=X&ved=2ahUKEwjLxuSJwM-GAxUHODQIHYuJBhgQ1QJ6BAhSEAE"
},
{
"title": "apple inc industry",
"link": "https://www.google.com/search?sca_esv=6b6df170a5c9891b&sca_upv=1&q=Apple+Inc+industry&sa=X&ved=2ahUKEwjLxuSJwM-GAxUHODQIHYuJBhgQ1QJ6BAhREAE"
},
{
"title": "apple login",
"link": "https://www.google.com/search?sca_esv=6b6df170a5c9891b&sca_upv=1&q=Apple+login&sa=X&ved=2ahUKEwjLxuSJwM-GAxUHODQIHYuJBhgQ1QJ6BAhTEAE"
}
]
},
"image_results": [],
"carousel": [],
"total": 2450000000,
"knowledge_graph": "",
"related_questions": [
"What does the Apple Inc do?",
"Why did Apple change to Apple Inc?",
"Who owns Apple Inc.?",
"What is Apple Inc best known for?"
],
"carousel_count": 0,
"ts": 2.491065263748169,
"device_type": null
}
+28 -79
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,
@@ -20,7 +20,7 @@ from langchain.retrievers import (
from typing import Optional
from utils.misc import get_last_user_message, add_or_update_system_message
from config import SRC_LOG_LEVELS, CHROMA_CLIENT
log = logging.getLogger(__name__)
@@ -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)
@@ -236,63 +236,38 @@ def get_embedding_function(
return lambda query: generate_multiple(query, func)
def rag_messages(
docs,
def get_rag_context(
files,
messages,
template,
embedding_function,
k,
reranking_function,
r,
hybrid_search,
):
log.debug(f"docs: {docs} {messages} {embedding_function} {reranking_function}")
last_user_message_idx = None
for i in range(len(messages) - 1, -1, -1):
if messages[i]["role"] == "user":
last_user_message_idx = i
break
user_message = messages[last_user_message_idx]
if isinstance(user_message["content"], list):
# Handle list content input
content_type = "list"
query = ""
for content_item in user_message["content"]:
if content_item["type"] == "text":
query = content_item["text"]
break
elif isinstance(user_message["content"], str):
# Handle text content input
content_type = "text"
query = user_message["content"]
else:
# Fallback in case the input does not match expected types
content_type = None
query = ""
log.debug(f"files: {files} {messages} {embedding_function} {reranking_function}")
query = get_last_user_message(messages)
extracted_collections = []
relevant_contexts = []
for doc in docs:
for file in files:
context = None
collection_names = (
doc["collection_names"]
if doc["type"] == "collection"
else [doc["collection_name"]]
file["collection_names"]
if file["type"] == "collection"
else [file["collection_name"]]
)
collection_names = set(collection_names).difference(extracted_collections)
if not collection_names:
log.debug(f"skipping {doc} as it has already been extracted")
log.debug(f"skipping {file} as it has already been extracted")
continue
try:
if doc["type"] == "text":
context = doc["content"]
if file["type"] == "text":
context = file["content"]
else:
if hybrid_search:
context = query_collection_with_hybrid_search(
@@ -315,18 +290,20 @@ def rag_messages(
context = None
if context:
relevant_contexts.append({**context, "source": doc})
relevant_contexts.append({**context, "source": file})
extracted_collections.extend(collection_names)
context_string = ""
contexts = []
citations = []
for context in relevant_contexts:
try:
if "documents" in context:
context_string += "\n\n".join(
[text for text in context["documents"][0] if text is not None]
contexts.append(
"\n\n".join(
[text for text in context["documents"][0] if text is not None]
)
)
if "metadatas" in context:
@@ -340,35 +317,7 @@ def rag_messages(
except Exception as e:
log.exception(e)
context_string = context_string.strip()
ra_content = rag_template(
template=template,
context=context_string,
query=query,
)
log.debug(f"ra_content: {ra_content}")
if content_type == "list":
new_content = []
for content_item in user_message["content"]:
if content_item["type"] == "text":
# Update the text item's content with ra_content
new_content.append({"type": "text", "text": ra_content})
else:
# Keep other types of content as they are
new_content.append(content_item)
new_user_message = {**user_message, "content": new_content}
else:
new_user_message = {
**user_message,
"content": ra_content,
}
messages[last_user_message_idx] = new_user_message
return messages, citations
return contexts, citations
def get_model_path(model: str, update_model: bool = False):
@@ -462,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(
@@ -493,8 +442,6 @@ from langchain_core.documents import BaseDocumentCompressor, Document
from langchain_core.callbacks import Callbacks
from langchain_core.pydantic_v1 import Extra
from sentence_transformers import util
class RerankCompressor(BaseDocumentCompressor):
embedding_function: Any
@@ -519,6 +466,8 @@ class RerankCompressor(BaseDocumentCompressor):
[(query, doc.page_content) for doc in documents]
)
else:
from sentence_transformers import util
query_embedding = self.embedding_function(query)
document_embedding = self.embedding_function(
[doc.page_content for doc in documents]
+62 -23
View File
@@ -10,7 +10,7 @@ app = socketio.ASGIApp(sio, socketio_path="/ws/socket.io")
# Dictionary to maintain the user pool
SESSION_POOL = {}
USER_POOL = {}
USAGE_POOL = {}
# Timeout duration in seconds
@@ -19,8 +19,6 @@ TIMEOUT_DURATION = 3
@sio.event
async def connect(sid, environ, auth):
print("connect ", sid)
user = None
if auth and "token" in auth:
data = decode_token(auth["token"])
@@ -29,10 +27,14 @@ async def connect(sid, environ, auth):
user = Users.get_user_by_id(data["id"])
if user:
USER_POOL[sid] = user.id
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(len(set(USER_POOL)))
await sio.emit("user-count", {"count": len(set(USER_POOL))})
await sio.emit("usage", {"models": get_models_in_use()})
@@ -42,24 +44,30 @@ 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:
USER_POOL[sid] = user.id
print(f"user {user.name}({user.id}) connected with session 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(len(set(USER_POOL)))
await sio.emit("user-count", {"count": len(set(USER_POOL))})
print(f"user {user.name}({user.id}) connected with session ID {sid}")
await sio.emit("user-count", {"count": len(set(USER_POOL))})
@sio.on("user-count")
async def user_count(sid):
print("user-count", sid)
await sio.emit("user-count", {"count": len(set(USER_POOL))})
@@ -68,15 +76,12 @@ def get_models_in_use():
models_in_use = []
for model_id, data in USAGE_POOL.items():
models_in_use.append(model_id)
print(f"Models in use: {models_in_use}")
return models_in_use
@sio.on("usage")
async def usage(sid, data):
print(f'Received "usage" event from {sid}: {data}')
model_id = data["model"]
# Cancel previous callback if there is one
@@ -103,7 +108,6 @@ async def usage(sid, data):
async def remove_after_timeout(sid, model_id):
try:
print("remove_after_timeout", sid, model_id)
await asyncio.sleep(TIMEOUT_DURATION)
if model_id in USAGE_POOL:
print(USAGE_POOL[model_id]["sids"])
@@ -113,7 +117,6 @@ async def remove_after_timeout(sid, model_id):
if len(USAGE_POOL[model_id]["sids"]) == 0:
del USAGE_POOL[model_id]
print(f"Removed usage data for {model_id} due to timeout")
# Broadcast the usage data to all clients
await sio.emit("usage", {"models": get_models_in_use()})
except asyncio.CancelledError:
@@ -123,10 +126,46 @@ async def remove_after_timeout(sid, model_id):
@sio.event
async def disconnect(sid):
if sid in USER_POOL:
disconnected_user = USER_POOL.pop(sid)
print(f"user {disconnected_user} disconnected with session ID {sid}")
if sid in SESSION_POOL:
user_id = SESSION_POOL[sid]
del SESSION_POOL[sid]
USER_POOL[user_id].remove(sid)
if len(USER_POOL[user_id]) == 0:
del USER_POOL[user_id]
await sio.emit("user-count", {"count": len(USER_POOL)})
else:
print(f"Unknown session ID {sid} disconnected")
def get_event_emitter(request_info):
async def __event_emitter__(event_data):
await sio.emit(
"chat-events",
{
"chat_id": request_info["chat_id"],
"message_id": request_info["message_id"],
"data": event_data,
},
to=request_info["session_id"],
)
return __event_emitter__
def get_event_call(request_info):
async def __event_call__(event_data):
response = await sio.call(
"chat-events",
{
"chat_id": request_info["chat_id"],
"message_id": request_info["message_id"],
"data": event_data,
},
to=request_info["session_id"],
)
return response
return __event_call__
+86 -15
View File
@@ -1,17 +1,39 @@
import json
from peewee import *
from peewee_migrate import Router
from playhouse.db_url import connect
from config import SRC_LOG_LEVELS, DATA_DIR, DATABASE_URL, BACKEND_DIR
import os
import logging
import json
from contextlib import contextmanager
from peewee_migrate import Router
from apps.webui.internal.wrappers import register_connection
from typing import Optional, Any
from typing_extensions import Self
from sqlalchemy import create_engine, types, Dialect
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker, scoped_session
from sqlalchemy.sql.type_api import _T
from config import SRC_LOG_LEVELS, DATA_DIR, DATABASE_URL, BACKEND_DIR
log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["DB"])
class JSONField(TextField):
class JSONField(types.TypeDecorator):
impl = types.Text
cache_ok = True
def process_bind_param(self, value: Optional[_T], dialect: Dialect) -> Any:
return json.dumps(value)
def process_result_value(self, value: Optional[_T], dialect: Dialect) -> Any:
if value is not None:
return json.loads(value)
def copy(self, **kw: Any) -> Self:
return JSONField(self.impl.length)
def db_value(self, value):
return json.dumps(value)
@@ -28,12 +50,61 @@ if os.path.exists(f"{DATA_DIR}/ollama.db"):
else:
pass
DB = connect(DATABASE_URL)
log.info(f"Connected to a {DB.__class__.__name__} database.")
router = Router(
DB,
migrate_dir=BACKEND_DIR / "apps" / "webui" / "internal" / "migrations",
logger=log,
# Workaround to handle the peewee migration
# This is required to ensure the peewee migration is handled before the alembic migration
def handle_peewee_migration(DATABASE_URL):
try:
# Replace the postgresql:// with postgres:// and %40 with @ in the DATABASE_URL
db = register_connection(
DATABASE_URL.replace("postgresql://", "postgres://").replace("%40", "@")
)
migrate_dir = BACKEND_DIR / "apps" / "webui" / "internal" / "migrations"
router = Router(db, logger=log, migrate_dir=migrate_dir)
router.run()
db.close()
# check if db connection has been closed
except Exception as e:
log.error(f"Failed to initialize the database connection: {e}")
raise
finally:
# Properly closing the database connection
if db and not db.is_closed():
db.close()
# Assert if db connection has been closed
assert db.is_closed(), "Database connection is still open."
handle_peewee_migration(DATABASE_URL)
SQLALCHEMY_DATABASE_URL = DATABASE_URL
if "sqlite" in SQLALCHEMY_DATABASE_URL:
engine = create_engine(
SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False}
)
else:
engine = create_engine(SQLALCHEMY_DATABASE_URL, pool_pre_ping=True)
SessionLocal = sessionmaker(
autocommit=False, autoflush=False, bind=engine, expire_on_commit=False
)
router.run()
DB.connect(reuse_if_open=True)
Base = declarative_base()
Session = scoped_session(SessionLocal)
# Dependency
def get_session():
db = SessionLocal()
try:
yield db
finally:
db.close()
get_db = contextmanager(get_session)
@@ -0,0 +1,61 @@
"""Peewee migrations -- 009_add_models.py.
Some examples (model - class or model name)::
> Model = migrator.orm['table_name'] # Return model in current state by name
> Model = migrator.ModelClass # Return model in current state by name
> migrator.sql(sql) # Run custom SQL
> migrator.run(func, *args, **kwargs) # Run python function with the given args
> migrator.create_model(Model) # Create a model (could be used as decorator)
> migrator.remove_model(model, cascade=True) # Remove a model
> migrator.add_fields(model, **fields) # Add fields to a model
> migrator.change_fields(model, **fields) # Change fields
> migrator.remove_fields(model, *field_names, cascade=True)
> migrator.rename_field(model, old_field_name, new_field_name)
> migrator.rename_table(model, new_table_name)
> migrator.add_index(model, *col_names, unique=False)
> migrator.add_not_null(model, *field_names)
> migrator.add_default(model, field_name, default)
> migrator.add_constraint(model, name, sql)
> migrator.drop_index(model, *col_names)
> migrator.drop_not_null(model, *field_names)
> migrator.drop_constraints(model, *constraints)
"""
from contextlib import suppress
import peewee as pw
from peewee_migrate import Migrator
with suppress(ImportError):
import playhouse.postgres_ext as pw_pext
def migrate(migrator: Migrator, database: pw.Database, *, fake=False):
"""Write your migrations here."""
@migrator.create_model
class Tool(pw.Model):
id = pw.TextField(unique=True)
user_id = pw.TextField()
name = pw.TextField()
content = pw.TextField()
specs = pw.TextField()
meta = pw.TextField()
created_at = pw.BigIntegerField(null=False)
updated_at = pw.BigIntegerField(null=False)
class Meta:
table_name = "tool"
def rollback(migrator: Migrator, database: pw.Database, *, fake=False):
"""Write your rollback migrations here."""
migrator.remove_model("tool")
@@ -0,0 +1,48 @@
"""Peewee migrations -- 002_add_local_sharing.py.
Some examples (model - class or model name)::
> Model = migrator.orm['table_name'] # Return model in current state by name
> Model = migrator.ModelClass # Return model in current state by name
> migrator.sql(sql) # Run custom SQL
> migrator.run(func, *args, **kwargs) # Run python function with the given args
> migrator.create_model(Model) # Create a model (could be used as decorator)
> migrator.remove_model(model, cascade=True) # Remove a model
> migrator.add_fields(model, **fields) # Add fields to a model
> migrator.change_fields(model, **fields) # Change fields
> migrator.remove_fields(model, *field_names, cascade=True)
> migrator.rename_field(model, old_field_name, new_field_name)
> migrator.rename_table(model, new_table_name)
> migrator.add_index(model, *col_names, unique=False)
> migrator.add_not_null(model, *field_names)
> migrator.add_default(model, field_name, default)
> migrator.add_constraint(model, name, sql)
> migrator.drop_index(model, *col_names)
> migrator.drop_not_null(model, *field_names)
> migrator.drop_constraints(model, *constraints)
"""
from contextlib import suppress
import peewee as pw
from peewee_migrate import Migrator
with suppress(ImportError):
import playhouse.postgres_ext as pw_pext
def migrate(migrator: Migrator, database: pw.Database, *, fake=False):
"""Write your migrations here."""
# Adding fields info to the 'user' table
migrator.add_fields("user", info=pw.TextField(null=True))
def rollback(migrator: Migrator, database: pw.Database, *, fake=False):
"""Write your rollback migrations here."""
# Remove the settings field
migrator.remove_fields("user", "info")
@@ -0,0 +1,55 @@
"""Peewee migrations -- 009_add_models.py.
Some examples (model - class or model name)::
> Model = migrator.orm['table_name'] # Return model in current state by name
> Model = migrator.ModelClass # Return model in current state by name
> migrator.sql(sql) # Run custom SQL
> migrator.run(func, *args, **kwargs) # Run python function with the given args
> migrator.create_model(Model) # Create a model (could be used as decorator)
> migrator.remove_model(model, cascade=True) # Remove a model
> migrator.add_fields(model, **fields) # Add fields to a model
> migrator.change_fields(model, **fields) # Change fields
> migrator.remove_fields(model, *field_names, cascade=True)
> migrator.rename_field(model, old_field_name, new_field_name)
> migrator.rename_table(model, new_table_name)
> migrator.add_index(model, *col_names, unique=False)
> migrator.add_not_null(model, *field_names)
> migrator.add_default(model, field_name, default)
> migrator.add_constraint(model, name, sql)
> migrator.drop_index(model, *col_names)
> migrator.drop_not_null(model, *field_names)
> migrator.drop_constraints(model, *constraints)
"""
from contextlib import suppress
import peewee as pw
from peewee_migrate import Migrator
with suppress(ImportError):
import playhouse.postgres_ext as pw_pext
def migrate(migrator: Migrator, database: pw.Database, *, fake=False):
"""Write your migrations here."""
@migrator.create_model
class File(pw.Model):
id = pw.TextField(unique=True)
user_id = pw.TextField()
filename = pw.TextField()
meta = pw.TextField()
created_at = pw.BigIntegerField(null=False)
class Meta:
table_name = "file"
def rollback(migrator: Migrator, database: pw.Database, *, fake=False):
"""Write your rollback migrations here."""
migrator.remove_model("file")
@@ -0,0 +1,61 @@
"""Peewee migrations -- 009_add_models.py.
Some examples (model - class or model name)::
> Model = migrator.orm['table_name'] # Return model in current state by name
> Model = migrator.ModelClass # Return model in current state by name
> migrator.sql(sql) # Run custom SQL
> migrator.run(func, *args, **kwargs) # Run python function with the given args
> migrator.create_model(Model) # Create a model (could be used as decorator)
> migrator.remove_model(model, cascade=True) # Remove a model
> migrator.add_fields(model, **fields) # Add fields to a model
> migrator.change_fields(model, **fields) # Change fields
> migrator.remove_fields(model, *field_names, cascade=True)
> migrator.rename_field(model, old_field_name, new_field_name)
> migrator.rename_table(model, new_table_name)
> migrator.add_index(model, *col_names, unique=False)
> migrator.add_not_null(model, *field_names)
> migrator.add_default(model, field_name, default)
> migrator.add_constraint(model, name, sql)
> migrator.drop_index(model, *col_names)
> migrator.drop_not_null(model, *field_names)
> migrator.drop_constraints(model, *constraints)
"""
from contextlib import suppress
import peewee as pw
from peewee_migrate import Migrator
with suppress(ImportError):
import playhouse.postgres_ext as pw_pext
def migrate(migrator: Migrator, database: pw.Database, *, fake=False):
"""Write your migrations here."""
@migrator.create_model
class Function(pw.Model):
id = pw.TextField(unique=True)
user_id = pw.TextField()
name = pw.TextField()
type = pw.TextField()
content = pw.TextField()
meta = pw.TextField()
created_at = pw.BigIntegerField(null=False)
updated_at = pw.BigIntegerField(null=False)
class Meta:
table_name = "function"
def rollback(migrator: Migrator, database: pw.Database, *, fake=False):
"""Write your rollback migrations here."""
migrator.remove_model("function")
@@ -0,0 +1,50 @@
"""Peewee migrations -- 009_add_models.py.
Some examples (model - class or model name)::
> Model = migrator.orm['table_name'] # Return model in current state by name
> Model = migrator.ModelClass # Return model in current state by name
> migrator.sql(sql) # Run custom SQL
> migrator.run(func, *args, **kwargs) # Run python function with the given args
> migrator.create_model(Model) # Create a model (could be used as decorator)
> migrator.remove_model(model, cascade=True) # Remove a model
> migrator.add_fields(model, **fields) # Add fields to a model
> migrator.change_fields(model, **fields) # Change fields
> migrator.remove_fields(model, *field_names, cascade=True)
> migrator.rename_field(model, old_field_name, new_field_name)
> migrator.rename_table(model, new_table_name)
> migrator.add_index(model, *col_names, unique=False)
> migrator.add_not_null(model, *field_names)
> migrator.add_default(model, field_name, default)
> migrator.add_constraint(model, name, sql)
> migrator.drop_index(model, *col_names)
> migrator.drop_not_null(model, *field_names)
> migrator.drop_constraints(model, *constraints)
"""
from contextlib import suppress
import peewee as pw
from peewee_migrate import Migrator
with suppress(ImportError):
import playhouse.postgres_ext as pw_pext
def migrate(migrator: Migrator, database: pw.Database, *, fake=False):
"""Write your migrations here."""
migrator.add_fields("tool", valves=pw.TextField(null=True))
migrator.add_fields("function", valves=pw.TextField(null=True))
migrator.add_fields("function", is_active=pw.BooleanField(default=False))
def rollback(migrator: Migrator, database: pw.Database, *, fake=False):
"""Write your rollback migrations here."""
migrator.remove_fields("tool", "valves")
migrator.remove_fields("function", "valves")
migrator.remove_fields("function", "is_active")
@@ -0,0 +1,45 @@
"""Peewee migrations -- 017_add_user_oauth_sub.py.
Some examples (model - class or model name)::
> Model = migrator.orm['table_name'] # Return model in current state by name
> Model = migrator.ModelClass # Return model in current state by name
> migrator.sql(sql) # Run custom SQL
> migrator.run(func, *args, **kwargs) # Run python function with the given args
> migrator.create_model(Model) # Create a model (could be used as decorator)
> migrator.remove_model(model, cascade=True) # Remove a model
> migrator.add_fields(model, **fields) # Add fields to a model
> migrator.change_fields(model, **fields) # Change fields
> migrator.remove_fields(model, *field_names, cascade=True)
> migrator.rename_field(model, old_field_name, new_field_name)
> migrator.rename_table(model, new_table_name)
> migrator.add_index(model, *col_names, unique=False)
> migrator.add_not_null(model, *field_names)
> migrator.add_default(model, field_name, default)
> migrator.add_constraint(model, name, sql)
> migrator.drop_index(model, *col_names)
> migrator.drop_not_null(model, *field_names)
> migrator.drop_constraints(model, *constraints)
"""
from contextlib import suppress
import peewee as pw
from peewee_migrate import Migrator
with suppress(ImportError):
import playhouse.postgres_ext as pw_pext
def migrate(migrator: Migrator, database: pw.Database, *, fake=False):
"""Write your migrations here."""
migrator.add_fields(
"user",
oauth_sub=pw.TextField(null=True, unique=True),
)
def rollback(migrator: Migrator, database: pw.Database, *, fake=False):
"""Write your rollback migrations here."""
migrator.remove_fields("user", "oauth_sub")
@@ -0,0 +1,49 @@
"""Peewee migrations -- 017_add_user_oauth_sub.py.
Some examples (model - class or model name)::
> Model = migrator.orm['table_name'] # Return model in current state by name
> Model = migrator.ModelClass # Return model in current state by name
> migrator.sql(sql) # Run custom SQL
> migrator.run(func, *args, **kwargs) # Run python function with the given args
> migrator.create_model(Model) # Create a model (could be used as decorator)
> migrator.remove_model(model, cascade=True) # Remove a model
> migrator.add_fields(model, **fields) # Add fields to a model
> migrator.change_fields(model, **fields) # Change fields
> migrator.remove_fields(model, *field_names, cascade=True)
> migrator.rename_field(model, old_field_name, new_field_name)
> migrator.rename_table(model, new_table_name)
> migrator.add_index(model, *col_names, unique=False)
> migrator.add_not_null(model, *field_names)
> migrator.add_default(model, field_name, default)
> migrator.add_constraint(model, name, sql)
> migrator.drop_index(model, *col_names)
> migrator.drop_not_null(model, *field_names)
> migrator.drop_constraints(model, *constraints)
"""
from contextlib import suppress
import peewee as pw
from peewee_migrate import Migrator
with suppress(ImportError):
import playhouse.postgres_ext as pw_pext
def migrate(migrator: Migrator, database: pw.Database, *, fake=False):
"""Write your migrations here."""
migrator.add_fields(
"function",
is_global=pw.BooleanField(default=False),
)
def rollback(migrator: Migrator, database: pw.Database, *, fake=False):
"""Write your rollback migrations here."""
migrator.remove_fields("function", "is_global")
@@ -1,21 +0,0 @@
# Database Migrations
This directory contains all the database migrations for the web app.
Migrations are done using the [`peewee-migrate`](https://github.com/klen/peewee_migrate) library.
Migrations are automatically ran at app startup.
## Creating a migration
Have you made a change to the schema of an existing model?
You will need to create a migration file to ensure that existing databases are updated for backwards compatibility.
1. Have a database file (`webui.db`) that has the old schema prior to any of your changes.
2. Make your changes to the models.
3. From the `backend` directory, run the following command:
```bash
pw_migrate create --auto --auto-source apps.webui.models --database sqlite:///${SQLITE_DB} --directory apps/web/internal/migrations ${MIGRATION_NAME}
```
- `$SQLITE_DB` should be the path to the database file.
- `$MIGRATION_NAME` should be a descriptive name for the migration.
4. The migration file will be created in the `apps/web/internal/migrations` directory.
+72
View File
@@ -0,0 +1,72 @@
from contextvars import ContextVar
from peewee import *
from peewee import PostgresqlDatabase, InterfaceError as PeeWeeInterfaceError
import logging
from playhouse.db_url import connect, parse
from playhouse.shortcuts import ReconnectMixin
from config import SRC_LOG_LEVELS
log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["DB"])
db_state_default = {"closed": None, "conn": None, "ctx": None, "transactions": None}
db_state = ContextVar("db_state", default=db_state_default.copy())
class PeeweeConnectionState(object):
def __init__(self, **kwargs):
super().__setattr__("_state", db_state)
super().__init__(**kwargs)
def __setattr__(self, name, value):
self._state.get()[name] = value
def __getattr__(self, name):
value = self._state.get()[name]
return value
class CustomReconnectMixin(ReconnectMixin):
reconnect_errors = (
# psycopg2
(OperationalError, "termin"),
(InterfaceError, "closed"),
# peewee
(PeeWeeInterfaceError, "closed"),
)
class ReconnectingPostgresqlDatabase(CustomReconnectMixin, PostgresqlDatabase):
pass
def register_connection(db_url):
db = connect(db_url)
if isinstance(db, PostgresqlDatabase):
# Enable autoconnect for SQLite databases, managed by Peewee
db.autoconnect = True
db.reuse_if_open = True
log.info("Connected to PostgreSQL database")
# Get the connection details
connection = parse(db_url)
# Use our custom database class that supports reconnection
db = ReconnectingPostgresqlDatabase(
connection["database"],
user=connection["user"],
password=connection["password"],
host=connection["host"],
port=connection["port"],
)
db.connect(reuse_if_open=True)
elif isinstance(db, SqliteDatabase):
# Enable autoconnect for SQLite databases, managed by Peewee
db.autoconnect = True
db.reuse_if_open = True
log.info("Connected to SQLite database")
else:
raise ValueError("Unsupported database connection")
return db
+304 -11
View File
@@ -1,19 +1,34 @@
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 apps.webui.routers import (
auths,
users,
chats,
documents,
tools,
models,
prompts,
configs,
memories,
utils,
files,
functions,
)
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 (
openai_chat_chunk_message_template,
openai_chat_completion_message_template,
apply_model_params_to_body_openai,
apply_model_system_prompt_to_body,
)
from utils.tools import get_tools
from config import (
WEBUI_BUILD_HASH,
SHOW_ADMIN_DETAILS,
ADMIN_EMAIL,
WEBUI_AUTH,
@@ -21,23 +36,39 @@ from config import (
DEFAULT_PROMPT_SUGGESTIONS,
DEFAULT_USER_ROLE,
ENABLE_SIGNUP,
ENABLE_LOGIN_FORM,
USER_PERMISSIONS,
WEBHOOK_URL,
WEBUI_AUTH_TRUSTED_EMAIL_HEADER,
WEBUI_AUTH_TRUSTED_NAME_HEADER,
JWT_EXPIRES_IN,
WEBUI_BANNERS,
AppConfig,
ENABLE_COMMUNITY_SHARING,
ENABLE_MESSAGE_RATING,
AppConfig,
OAUTH_USERNAME_CLAIM,
OAUTH_PICTURE_CLAIM,
OAUTH_EMAIL_CLAIM,
CORS_ALLOW_ORIGIN,
)
app = FastAPI()
from apps.socket.main import get_event_call, get_event_emitter
origins = ["*"]
import inspect
import json
from typing import Iterator, Generator, AsyncGenerator
from pydantic import BaseModel
app = FastAPI()
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
app.state.config.SHOW_ADMIN_DETAILS = SHOW_ADMIN_DETAILS
@@ -52,19 +83,26 @@ app.state.config.WEBHOOK_URL = WEBHOOK_URL
app.state.config.BANNERS = WEBUI_BANNERS
app.state.config.ENABLE_COMMUNITY_SHARING = ENABLE_COMMUNITY_SHARING
app.state.config.ENABLE_MESSAGE_RATING = ENABLE_MESSAGE_RATING
app.state.config.OAUTH_USERNAME_CLAIM = OAUTH_USERNAME_CLAIM
app.state.config.OAUTH_PICTURE_CLAIM = OAUTH_PICTURE_CLAIM
app.state.config.OAUTH_EMAIL_CLAIM = OAUTH_EMAIL_CLAIM
app.state.MODELS = {}
app.state.AUTH_TRUSTED_EMAIL_HEADER = WEBUI_AUTH_TRUSTED_EMAIL_HEADER
app.state.TOOLS = {}
app.state.FUNCTIONS = {}
app.add_middleware(
CORSMiddleware,
allow_origins=origins,
allow_origins=CORS_ALLOW_ORIGIN,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
app.include_router(configs.router, prefix="/configs", tags=["configs"])
app.include_router(auths.router, prefix="/auths", tags=["auths"])
app.include_router(users.router, prefix="/users", tags=["users"])
app.include_router(chats.router, prefix="/chats", tags=["chats"])
@@ -72,9 +110,12 @@ app.include_router(chats.router, prefix="/chats", tags=["chats"])
app.include_router(documents.router, prefix="/documents", tags=["documents"])
app.include_router(models.router, prefix="/models", tags=["models"])
app.include_router(prompts.router, prefix="/prompts", tags=["prompts"])
app.include_router(memories.router, prefix="/memories", tags=["memories"])
app.include_router(configs.router, prefix="/configs", tags=["configs"])
app.include_router(memories.router, prefix="/memories", tags=["memories"])
app.include_router(files.router, prefix="/files", tags=["files"])
app.include_router(tools.router, prefix="/tools", tags=["tools"])
app.include_router(functions.router, prefix="/functions", tags=["functions"])
app.include_router(utils.router, prefix="/utils", tags=["utils"])
@@ -86,3 +127,255 @@ async def get_status():
"default_models": app.state.config.DEFAULT_MODELS,
"default_prompt_suggestions": app.state.config.DEFAULT_PROMPT_SUGGESTIONS,
}
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:
function_module = get_function_module(pipe.id)
# Check if function is a manifold
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
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}"
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,
}
)
else:
pipe_flag = {"type": "pipe"}
if hasattr(function_module, "ChatValves"):
pipe_flag["valves_spec"] = function_module.ChatValves.schema()
pipe_models.append(
{
"id": pipe.id,
"name": pipe.name,
"object": "model",
"created": pipe.created_at,
"owned_by": "openai",
"pipe": pipe_flag,
}
)
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 = form_data.pop("metadata", {})
files = metadata.get("files", [])
tool_ids = metadata.get("tool_ids", [])
# Check if tool_ids is None
if tool_ids is None:
tool_ids = []
__event_emitter__ = None
__event_call__ = None
__task__ = None
if metadata:
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)
extra_params = {
"__event_emitter__": __event_emitter__,
"__event_call__": __event_call__,
"__task__": __task__,
}
tools_params = {
**extra_params,
"__model__": app.state.MODELS[form_data["model"]],
"__messages__": form_data["messages"],
"__files__": files,
}
tools = get_tools(app, tool_ids, user, tools_params)
extra_params["__tools__"] = tools
if model_info:
if model_info.base_model_id:
form_data["model"] = model_info.base_model_id
params = model_info.params.model_dump()
form_data = apply_model_params_to_body_openai(params, form_data)
form_data = apply_model_system_prompt_to_body(params, form_data, user)
pipe_id = get_pipe_id(form_data)
function_module = get_function_module(pipe_id)
pipe = function_module.pipe
params = get_function_params(function_module, form_data, user, extra_params)
if form_data["stream"]:
async def stream_content():
try:
res = await execute_pipe(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"
return
if isinstance(res, str):
message = openai_chat_chunk_message_template(form_data["model"], res)
yield f"data: {json.dumps(message)}\n\n"
if isinstance(res, Iterator):
for line in res:
yield process_line(form_data, line)
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)
+70 -60
View File
@@ -1,14 +1,13 @@
from pydantic import BaseModel
from typing import List, Union, Optional
import time
from typing import Optional
import uuid
import logging
from peewee import *
from sqlalchemy import String, Column, Boolean, Text
from apps.webui.models.users import UserModel, Users
from utils.utils import verify_password
from apps.webui.internal.db import DB
from apps.webui.internal.db import Base, get_db
from config import SRC_LOG_LEVELS
@@ -20,14 +19,13 @@ log.setLevel(SRC_LOG_LEVELS["MODELS"])
####################
class Auth(Model):
id = CharField(unique=True)
email = CharField()
password = TextField()
active = BooleanField()
class Auth(Base):
__tablename__ = "auth"
class Meta:
database = DB
id = Column(String, primary_key=True)
email = Column(String)
password = Column(Text)
active = Column(Boolean)
class AuthModel(BaseModel):
@@ -94,9 +92,6 @@ class AddUserForm(SignupForm):
class AuthsTable:
def __init__(self, db):
self.db = db
self.db.create_tables([Auth])
def insert_new_auth(
self,
@@ -105,36 +100,47 @@ class AuthsTable:
name: str,
profile_image_url: str = "/user.png",
role: str = "pending",
oauth_sub: Optional[str] = None,
) -> Optional[UserModel]:
log.info("insert_new_auth")
with get_db() as db:
id = str(uuid.uuid4())
log.info("insert_new_auth")
auth = AuthModel(
**{"id": id, "email": email, "password": password, "active": True}
)
result = Auth.create(**auth.model_dump())
id = str(uuid.uuid4())
user = Users.insert_new_user(id, name, email, profile_image_url, role)
auth = AuthModel(
**{"id": id, "email": email, "password": password, "active": True}
)
result = Auth(**auth.model_dump())
db.add(result)
if result and user:
return user
else:
return None
user = Users.insert_new_user(
id, name, email, profile_image_url, role, oauth_sub
)
db.commit()
db.refresh(result)
if result and user:
return user
else:
return None
def authenticate_user(self, email: str, password: str) -> Optional[UserModel]:
log.info(f"authenticate_user: {email}")
try:
auth = Auth.get(Auth.email == email, Auth.active == True)
if auth:
if verify_password(password, auth.password):
user = Users.get_user_by_id(auth.id)
return user
with get_db() as db:
auth = db.query(Auth).filter_by(email=email, active=True).first()
if auth:
if verify_password(password, auth.password):
user = Users.get_user_by_id(auth.id)
return user
else:
return None
else:
return None
else:
return None
except:
except Exception:
return None
def authenticate_user_by_api_key(self, api_key: str) -> Optional[UserModel]:
@@ -146,52 +152,56 @@ 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]:
log.info(f"authenticate_user_by_trusted_header: {email}")
try:
auth = Auth.get(Auth.email == email, Auth.active == True)
if auth:
user = Users.get_user_by_id(auth.id)
return user
except:
with get_db() as db:
auth = db.query(Auth).filter_by(email=email, active=True).first()
if auth:
user = Users.get_user_by_id(auth.id)
return user
except Exception:
return None
def update_user_password_by_id(self, id: str, new_password: str) -> bool:
try:
query = Auth.update(password=new_password).where(Auth.id == id)
result = query.execute()
return True if result == 1 else False
except:
with get_db() as db:
result = (
db.query(Auth).filter_by(id=id).update({"password": new_password})
)
db.commit()
return True if result == 1 else False
except Exception:
return False
def update_email_by_id(self, id: str, email: str) -> bool:
try:
query = Auth.update(email=email).where(Auth.id == id)
result = query.execute()
return True if result == 1 else False
except:
with get_db() as db:
result = db.query(Auth).filter_by(id=id).update({"email": email})
db.commit()
return True if result == 1 else False
except Exception:
return False
def delete_auth_by_id(self, id: str) -> bool:
try:
# Delete User
result = Users.delete_user_by_id(id)
with get_db() as db:
if result:
# Delete Auth
query = Auth.delete().where(Auth.id == id)
query.execute() # Remove the rows, return number of rows removed.
# Delete User
result = Users.delete_user_by_id(id)
return True
else:
return False
except:
if result:
db.query(Auth).filter_by(id=id).delete()
db.commit()
return True
else:
return False
except Exception:
return False
Auths = AuthsTable(DB)
Auths = AuthsTable()
+253 -201
View File
@@ -1,36 +1,38 @@
from pydantic import BaseModel
from typing import List, Union, Optional
from peewee import *
from playhouse.shortcuts import model_to_dict
from pydantic import BaseModel, ConfigDict
from typing import Union, Optional
import json
import uuid
import time
from apps.webui.internal.db import DB
from sqlalchemy import Column, String, BigInteger, Boolean, Text
from apps.webui.internal.db import Base, get_db
####################
# Chat DB Schema
####################
class Chat(Model):
id = CharField(unique=True)
user_id = CharField()
title = TextField()
chat = TextField() # Save Chat JSON as Text
class Chat(Base):
__tablename__ = "chat"
created_at = BigIntegerField()
updated_at = BigIntegerField()
id = Column(String, primary_key=True)
user_id = Column(String)
title = Column(Text)
chat = Column(Text) # Save Chat JSON as Text
share_id = CharField(null=True, unique=True)
archived = BooleanField(default=False)
created_at = Column(BigInteger)
updated_at = Column(BigInteger)
class Meta:
database = DB
share_id = Column(Text, unique=True, nullable=True)
archived = Column(Boolean, default=False)
class ChatModel(BaseModel):
model_config = ConfigDict(from_attributes=True)
id: str
user_id: str
title: str
@@ -75,148 +77,155 @@ class ChatTitleIdResponse(BaseModel):
class ChatTable:
def __init__(self, db):
self.db = db
db.create_tables([Chat])
def insert_new_chat(self, user_id: str, form_data: ChatForm) -> Optional[ChatModel]:
id = str(uuid.uuid4())
chat = ChatModel(
**{
"id": id,
"user_id": user_id,
"title": (
form_data.chat["title"] if "title" in form_data.chat else "New Chat"
),
"chat": json.dumps(form_data.chat),
"created_at": int(time.time()),
"updated_at": int(time.time()),
}
)
with get_db() as db:
result = Chat.create(**chat.model_dump())
return chat if result else None
id = str(uuid.uuid4())
chat = ChatModel(
**{
"id": id,
"user_id": user_id,
"title": (
form_data.chat["title"]
if "title" in form_data.chat
else "New Chat"
),
"chat": json.dumps(form_data.chat),
"created_at": int(time.time()),
"updated_at": int(time.time()),
}
)
result = Chat(**chat.model_dump())
db.add(result)
db.commit()
db.refresh(result)
return ChatModel.model_validate(result) if result else None
def update_chat_by_id(self, id: str, chat: dict) -> Optional[ChatModel]:
try:
query = Chat.update(
chat=json.dumps(chat),
title=chat["title"] if "title" in chat else "New Chat",
updated_at=int(time.time()),
).where(Chat.id == id)
query.execute()
with get_db() as db:
chat = Chat.get(Chat.id == id)
return ChatModel(**model_to_dict(chat))
except:
chat_obj = db.get(Chat, id)
chat_obj.chat = json.dumps(chat)
chat_obj.title = chat["title"] if "title" in chat else "New Chat"
chat_obj.updated_at = int(time.time())
db.commit()
db.refresh(chat_obj)
return ChatModel.model_validate(chat_obj)
except Exception as e:
return None
def insert_shared_chat_by_chat_id(self, chat_id: str) -> Optional[ChatModel]:
# Get the existing chat to share
chat = Chat.get(Chat.id == chat_id)
# Check if the chat is already shared
if chat.share_id:
return self.get_chat_by_id_and_user_id(chat.share_id, "shared")
# Create a new chat with the same data, but with a new ID
shared_chat = ChatModel(
**{
"id": str(uuid.uuid4()),
"user_id": f"shared-{chat_id}",
"title": chat.title,
"chat": chat.chat,
"created_at": chat.created_at,
"updated_at": int(time.time()),
}
)
shared_result = Chat.create(**shared_chat.model_dump())
# Update the original chat with the share_id
result = (
Chat.update(share_id=shared_chat.id).where(Chat.id == chat_id).execute()
)
with get_db() as db:
return shared_chat if (shared_result and result) else None
# Get the existing chat to share
chat = db.get(Chat, chat_id)
# Check if the chat is already shared
if chat.share_id:
return self.get_chat_by_id_and_user_id(chat.share_id, "shared")
# Create a new chat with the same data, but with a new ID
shared_chat = ChatModel(
**{
"id": str(uuid.uuid4()),
"user_id": f"shared-{chat_id}",
"title": chat.title,
"chat": chat.chat,
"created_at": chat.created_at,
"updated_at": int(time.time()),
}
)
shared_result = Chat(**shared_chat.model_dump())
db.add(shared_result)
db.commit()
db.refresh(shared_result)
# Update the original chat with the share_id
result = (
db.query(Chat)
.filter_by(id=chat_id)
.update({"share_id": shared_chat.id})
)
db.commit()
return shared_chat if (shared_result and result) else None
def update_shared_chat_by_chat_id(self, chat_id: str) -> Optional[ChatModel]:
try:
print("update_shared_chat_by_id")
chat = Chat.get(Chat.id == chat_id)
print(chat)
with get_db() as db:
query = Chat.update(
title=chat.title,
chat=chat.chat,
).where(Chat.id == chat.share_id)
print("update_shared_chat_by_id")
chat = db.get(Chat, chat_id)
print(chat)
chat.title = chat.title
chat.chat = chat.chat
db.commit()
db.refresh(chat)
query.execute()
chat = Chat.get(Chat.id == chat.share_id)
return ChatModel(**model_to_dict(chat))
except:
return self.get_chat_by_id(chat.share_id)
except Exception:
return None
def delete_shared_chat_by_chat_id(self, chat_id: str) -> bool:
try:
query = Chat.delete().where(Chat.user_id == f"shared-{chat_id}")
query.execute() # Remove the rows, return number of rows removed.
with get_db() as db:
return True
except:
db.query(Chat).filter_by(user_id=f"shared-{chat_id}").delete()
db.commit()
return True
except Exception:
return False
def update_chat_share_id_by_id(
self, id: str, share_id: Optional[str]
) -> Optional[ChatModel]:
try:
query = Chat.update(
share_id=share_id,
).where(Chat.id == id)
query.execute()
with get_db() as db:
chat = Chat.get(Chat.id == id)
return ChatModel(**model_to_dict(chat))
except:
chat = db.get(Chat, id)
chat.share_id = share_id
db.commit()
db.refresh(chat)
return ChatModel.model_validate(chat)
except Exception:
return None
def toggle_chat_archive_by_id(self, id: str) -> Optional[ChatModel]:
try:
chat = self.get_chat_by_id(id)
query = Chat.update(
archived=(not chat.archived),
).where(Chat.id == id)
with get_db() as db:
query.execute()
chat = Chat.get(Chat.id == id)
return ChatModel(**model_to_dict(chat))
except:
chat = db.get(Chat, id)
chat.archived = not chat.archived
db.commit()
db.refresh(chat)
return ChatModel.model_validate(chat)
except Exception:
return None
def archive_all_chats_by_user_id(self, user_id: str) -> bool:
try:
chats = self.get_chats_by_user_id(user_id)
for chat in chats:
query = Chat.update(
archived=True,
).where(Chat.id == chat.id)
query.execute()
return True
except:
with get_db() as db:
db.query(Chat).filter_by(user_id=user_id).update({"archived": True})
db.commit()
return True
except Exception:
return False
def get_archived_chat_list_by_user_id(
self, user_id: str, skip: int = 0, limit: int = 50
) -> List[ChatModel]:
return [
ChatModel(**model_to_dict(chat))
for chat in Chat.select()
.where(Chat.archived == True)
.where(Chat.user_id == user_id)
.order_by(Chat.updated_at.desc())
# .limit(limit)
# .offset(skip)
]
) -> list[ChatModel]:
with get_db() as db:
all_chats = (
db.query(Chat)
.filter_by(user_id=user_id, archived=True)
.order_by(Chat.updated_at.desc())
# .limit(limit).offset(skip)
.all()
)
return [ChatModel.model_validate(chat) for chat in all_chats]
def get_chat_list_by_user_id(
self,
@@ -224,132 +233,175 @@ class ChatTable:
include_archived: bool = False,
skip: int = 0,
limit: int = 50,
) -> List[ChatModel]:
if include_archived:
) -> list[ChatModel]:
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(limit).offset(skip)
.all()
)
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 [
ChatModel(**model_to_dict(chat))
for chat in Chat.select()
.where(Chat.user_id == user_id)
.order_by(Chat.updated_at.desc())
# .limit(limit)
# .offset(skip)
]
else:
return [
ChatModel(**model_to_dict(chat))
for chat in Chat.select()
.where(Chat.archived == False)
.where(Chat.user_id == user_id)
.order_by(Chat.updated_at.desc())
# .limit(limit)
# .offset(skip)
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]:
return [
ChatModel(**model_to_dict(chat))
for chat in Chat.select()
.where(Chat.archived == False)
.where(Chat.id.in_(chat_ids))
.order_by(Chat.updated_at.desc())
]
self, chat_ids: list[str], skip: int = 0, limit: int = 50
) -> list[ChatModel]:
with get_db() as db:
all_chats = (
db.query(Chat)
.filter(Chat.id.in_(chat_ids))
.filter_by(archived=False)
.order_by(Chat.updated_at.desc())
.all()
)
return [ChatModel.model_validate(chat) for chat in all_chats]
def get_chat_by_id(self, id: str) -> Optional[ChatModel]:
try:
chat = Chat.get(Chat.id == id)
return ChatModel(**model_to_dict(chat))
except:
with get_db() as db:
chat = db.get(Chat, id)
return ChatModel.model_validate(chat)
except Exception:
return None
def get_chat_by_share_id(self, id: str) -> Optional[ChatModel]:
try:
chat = Chat.get(Chat.share_id == id)
with get_db() as db:
if chat:
chat = Chat.get(Chat.id == id)
return ChatModel(**model_to_dict(chat))
else:
return None
except:
chat = db.query(Chat).filter_by(share_id=id).first()
if chat:
return self.get_chat_by_id(id)
else:
return None
except Exception as e:
return None
def get_chat_by_id_and_user_id(self, id: str, user_id: str) -> Optional[ChatModel]:
try:
chat = Chat.get(Chat.id == id, Chat.user_id == user_id)
return ChatModel(**model_to_dict(chat))
except:
with get_db() as db:
chat = db.query(Chat).filter_by(id=id, user_id=user_id).first()
return ChatModel.model_validate(chat)
except Exception:
return None
def get_chats(self, skip: int = 0, limit: int = 50) -> List[ChatModel]:
return [
ChatModel(**model_to_dict(chat))
for chat in Chat.select().order_by(Chat.updated_at.desc())
# .limit(limit).offset(skip)
]
def get_chats(self, skip: int = 0, limit: int = 50) -> list[ChatModel]:
with get_db() as db:
def get_chats_by_user_id(self, user_id: str) -> List[ChatModel]:
return [
ChatModel(**model_to_dict(chat))
for chat in Chat.select()
.where(Chat.user_id == user_id)
.order_by(Chat.updated_at.desc())
# .limit(limit).offset(skip)
]
all_chats = (
db.query(Chat)
# .limit(limit).offset(skip)
.order_by(Chat.updated_at.desc())
)
return [ChatModel.model_validate(chat) for chat in all_chats]
def get_archived_chats_by_user_id(self, user_id: str) -> List[ChatModel]:
return [
ChatModel(**model_to_dict(chat))
for chat in Chat.select()
.where(Chat.archived == True)
.where(Chat.user_id == user_id)
.order_by(Chat.updated_at.desc())
]
def get_chats_by_user_id(self, user_id: str) -> list[ChatModel]:
with get_db() as db:
all_chats = (
db.query(Chat)
.filter_by(user_id=user_id)
.order_by(Chat.updated_at.desc())
)
return [ChatModel.model_validate(chat) for chat in all_chats]
def get_archived_chats_by_user_id(self, user_id: str) -> list[ChatModel]:
with get_db() as db:
all_chats = (
db.query(Chat)
.filter_by(user_id=user_id, archived=True)
.order_by(Chat.updated_at.desc())
)
return [ChatModel.model_validate(chat) for chat in all_chats]
def delete_chat_by_id(self, id: str) -> bool:
try:
query = Chat.delete().where((Chat.id == id))
query.execute() # Remove the rows, return number of rows removed.
with get_db() as db:
return True and self.delete_shared_chat_by_chat_id(id)
except:
db.query(Chat).filter_by(id=id).delete()
db.commit()
return True and self.delete_shared_chat_by_chat_id(id)
except Exception:
return False
def delete_chat_by_id_and_user_id(self, id: str, user_id: str) -> bool:
try:
query = Chat.delete().where((Chat.id == id) & (Chat.user_id == user_id))
query.execute() # Remove the rows, return number of rows removed.
with get_db() as db:
return True and self.delete_shared_chat_by_chat_id(id)
except:
db.query(Chat).filter_by(id=id, user_id=user_id).delete()
db.commit()
return True and self.delete_shared_chat_by_chat_id(id)
except Exception:
return False
def delete_chats_by_user_id(self, user_id: str) -> bool:
try:
self.delete_shared_chats_by_user_id(user_id)
with get_db() as db:
query = Chat.delete().where(Chat.user_id == user_id)
query.execute() # Remove the rows, return number of rows removed.
self.delete_shared_chats_by_user_id(user_id)
return True
except:
db.query(Chat).filter_by(user_id=user_id).delete()
db.commit()
return True
except Exception:
return False
def delete_shared_chats_by_user_id(self, user_id: str) -> bool:
try:
shared_chat_ids = [
f"shared-{chat.id}"
for chat in Chat.select().where(Chat.user_id == user_id)
]
query = Chat.delete().where(Chat.user_id << shared_chat_ids)
query.execute() # Remove the rows, return number of rows removed.
with get_db() as db:
return True
except:
chats_by_user = db.query(Chat).filter_by(user_id=user_id).all()
shared_chat_ids = [f"shared-{chat.id}" for chat in chats_by_user]
db.query(Chat).filter(Chat.user_id.in_(shared_chat_ids)).delete()
db.commit()
return True
except Exception:
return False
Chats = ChatTable(DB)
Chats = ChatTable()
+70 -63
View File
@@ -1,14 +1,11 @@
from pydantic import BaseModel
from peewee import *
from playhouse.shortcuts import model_to_dict
from typing import List, Union, Optional
from pydantic import BaseModel, ConfigDict
from typing import Optional
import time
import logging
from utils.utils import decode_token
from utils.misc import get_gravatar_url
from sqlalchemy import String, Column, BigInteger, Text
from apps.webui.internal.db import DB
from apps.webui.internal.db import Base, get_db
import json
@@ -22,20 +19,21 @@ log.setLevel(SRC_LOG_LEVELS["MODELS"])
####################
class Document(Model):
collection_name = CharField(unique=True)
name = CharField(unique=True)
title = TextField()
filename = TextField()
content = TextField(null=True)
user_id = CharField()
timestamp = BigIntegerField()
class Document(Base):
__tablename__ = "document"
class Meta:
database = DB
collection_name = Column(String, primary_key=True)
name = Column(String, unique=True)
title = Column(Text)
filename = Column(Text)
content = Column(Text, nullable=True)
user_id = Column(String)
timestamp = Column(BigInteger)
class DocumentModel(BaseModel):
model_config = ConfigDict(from_attributes=True)
collection_name: str
name: str
title: str
@@ -72,57 +70,63 @@ class DocumentForm(DocumentUpdateForm):
class DocumentsTable:
def __init__(self, db):
self.db = db
self.db.create_tables([Document])
def insert_new_doc(
self, user_id: str, form_data: DocumentForm
) -> Optional[DocumentModel]:
document = DocumentModel(
**{
**form_data.model_dump(),
"user_id": user_id,
"timestamp": int(time.time()),
}
)
with get_db() as db:
try:
result = Document.create(**document.model_dump())
if result:
return document
else:
document = DocumentModel(
**{
**form_data.model_dump(),
"user_id": user_id,
"timestamp": int(time.time()),
}
)
try:
result = Document(**document.model_dump())
db.add(result)
db.commit()
db.refresh(result)
if result:
return DocumentModel.model_validate(result)
else:
return None
except Exception:
return None
except:
return None
def get_doc_by_name(self, name: str) -> Optional[DocumentModel]:
try:
document = Document.get(Document.name == name)
return DocumentModel(**model_to_dict(document))
except:
with get_db() as db:
document = db.query(Document).filter_by(name=name).first()
return DocumentModel.model_validate(document) if document else None
except Exception:
return None
def get_docs(self) -> List[DocumentModel]:
return [
DocumentModel(**model_to_dict(doc))
for doc in Document.select()
# .limit(limit).offset(skip)
]
def get_docs(self) -> list[DocumentModel]:
with get_db() as db:
return [
DocumentModel.model_validate(doc) for doc in db.query(Document).all()
]
def update_doc_by_name(
self, name: str, form_data: DocumentUpdateForm
) -> Optional[DocumentModel]:
try:
query = Document.update(
title=form_data.title,
name=form_data.name,
timestamp=int(time.time()),
).where(Document.name == name)
query.execute()
with get_db() as db:
doc = Document.get(Document.name == form_data.name)
return DocumentModel(**model_to_dict(doc))
db.query(Document).filter_by(name=name).update(
{
"title": form_data.title,
"name": form_data.name,
"timestamp": int(time.time()),
}
)
db.commit()
return self.get_doc_by_name(form_data.name)
except Exception as e:
log.exception(e)
return None
@@ -135,26 +139,29 @@ class DocumentsTable:
doc_content = json.loads(doc.content if doc.content else "{}")
doc_content = {**doc_content, **updated}
query = Document.update(
content=json.dumps(doc_content),
timestamp=int(time.time()),
).where(Document.name == name)
query.execute()
with get_db() as db:
doc = Document.get(Document.name == name)
return DocumentModel(**model_to_dict(doc))
db.query(Document).filter_by(name=name).update(
{
"content": json.dumps(doc_content),
"timestamp": int(time.time()),
}
)
db.commit()
return self.get_doc_by_name(name)
except Exception as e:
log.exception(e)
return None
def delete_doc_by_name(self, name: str) -> bool:
try:
query = Document.delete().where((Document.name == name))
query.execute() # Remove the rows, return number of rows removed.
with get_db() as db:
return True
except:
db.query(Document).filter_by(name=name).delete()
db.commit()
return True
except Exception:
return False
Documents = DocumentsTable(DB)
Documents = DocumentsTable()
+126
View File
@@ -0,0 +1,126 @@
from pydantic import BaseModel, ConfigDict
from typing import Union, Optional
import time
import logging
from sqlalchemy import Column, String, BigInteger, Text
from apps.webui.internal.db import JSONField, Base, get_db
import json
from config import SRC_LOG_LEVELS
log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["MODELS"])
####################
# Files DB Schema
####################
class File(Base):
__tablename__ = "file"
id = Column(String, primary_key=True)
user_id = Column(String)
filename = Column(Text)
meta = Column(JSONField)
created_at = Column(BigInteger)
class FileModel(BaseModel):
id: str
user_id: str
filename: str
meta: dict
created_at: int # timestamp in epoch
model_config = ConfigDict(from_attributes=True)
####################
# Forms
####################
class FileModelResponse(BaseModel):
id: str
user_id: str
filename: str
meta: dict
created_at: int # timestamp in epoch
class FileForm(BaseModel):
id: str
filename: str
meta: dict = {}
class FilesTable:
def insert_new_file(self, user_id: str, form_data: FileForm) -> Optional[FileModel]:
with get_db() as db:
file = FileModel(
**{
**form_data.model_dump(),
"user_id": user_id,
"created_at": int(time.time()),
}
)
try:
result = File(**file.model_dump())
db.add(result)
db.commit()
db.refresh(result)
if result:
return FileModel.model_validate(result)
else:
return None
except Exception as e:
print(f"Error creating tool: {e}")
return None
def get_file_by_id(self, id: str) -> Optional[FileModel]:
with get_db() as db:
try:
file = db.get(File, id)
return FileModel.model_validate(file)
except Exception:
return None
def get_files(self) -> list[FileModel]:
with get_db() as db:
return [FileModel.model_validate(file) for file in db.query(File).all()]
def delete_file_by_id(self, id: str) -> bool:
with get_db() as db:
try:
db.query(File).filter_by(id=id).delete()
db.commit()
return True
except Exception:
return False
def delete_all_files(self) -> bool:
with get_db() as db:
try:
db.query(File).delete()
db.commit()
return True
except Exception:
return False
Files = FilesTable()
+288
View File
@@ -0,0 +1,288 @@
from pydantic import BaseModel, ConfigDict
from typing import Union, Optional
import time
import logging
from sqlalchemy import Column, String, Text, BigInteger, Boolean
from apps.webui.internal.db import JSONField, Base, get_db
from apps.webui.models.users import Users
import json
import copy
from config import SRC_LOG_LEVELS
log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["MODELS"])
####################
# Functions DB Schema
####################
class Function(Base):
__tablename__ = "function"
id = Column(String, primary_key=True)
user_id = Column(String)
name = Column(Text)
type = Column(Text)
content = Column(Text)
meta = Column(JSONField)
valves = Column(JSONField)
is_active = Column(Boolean)
is_global = Column(Boolean)
updated_at = Column(BigInteger)
created_at = Column(BigInteger)
class FunctionMeta(BaseModel):
description: Optional[str] = None
manifest: Optional[dict] = {}
class FunctionModel(BaseModel):
id: str
user_id: str
name: str
type: str
content: str
meta: FunctionMeta
is_active: bool = False
is_global: bool = False
updated_at: int # timestamp in epoch
created_at: int # timestamp in epoch
model_config = ConfigDict(from_attributes=True)
####################
# Forms
####################
class FunctionResponse(BaseModel):
id: str
user_id: str
type: str
name: str
meta: FunctionMeta
is_active: bool
is_global: bool
updated_at: int # timestamp in epoch
created_at: int # timestamp in epoch
class FunctionForm(BaseModel):
id: str
name: str
content: str
meta: FunctionMeta
class FunctionValves(BaseModel):
valves: Optional[dict] = None
class FunctionsTable:
def insert_new_function(
self, user_id: str, type: str, form_data: FunctionForm
) -> Optional[FunctionModel]:
function = FunctionModel(
**{
**form_data.model_dump(),
"user_id": user_id,
"type": type,
"updated_at": int(time.time()),
"created_at": int(time.time()),
}
)
try:
with get_db() as db:
result = Function(**function.model_dump())
db.add(result)
db.commit()
db.refresh(result)
if result:
return FunctionModel.model_validate(result)
else:
return None
except Exception as e:
print(f"Error creating tool: {e}")
return None
def get_function_by_id(self, id: str) -> Optional[FunctionModel]:
try:
with get_db() as db:
function = db.get(Function, id)
return FunctionModel.model_validate(function)
except Exception:
return None
def get_functions(self, active_only=False) -> list[FunctionModel]:
with get_db() as db:
if active_only:
return [
FunctionModel.model_validate(function)
for function in db.query(Function).filter_by(is_active=True).all()
]
else:
return [
FunctionModel.model_validate(function)
for function in db.query(Function).all()
]
def get_functions_by_type(
self, type: str, active_only=False
) -> list[FunctionModel]:
with get_db() as db:
if active_only:
return [
FunctionModel.model_validate(function)
for function in db.query(Function)
.filter_by(type=type, is_active=True)
.all()
]
else:
return [
FunctionModel.model_validate(function)
for function in db.query(Function).filter_by(type=type).all()
]
def get_global_filter_functions(self) -> list[FunctionModel]:
with get_db() as db:
return [
FunctionModel.model_validate(function)
for function in db.query(Function)
.filter_by(type="filter", is_active=True, is_global=True)
.all()
]
def get_global_action_functions(self) -> list[FunctionModel]:
with get_db() as db:
return [
FunctionModel.model_validate(function)
for function in db.query(Function)
.filter_by(type="action", is_active=True, is_global=True)
.all()
]
def get_function_valves_by_id(self, id: str) -> Optional[dict]:
with get_db() as db:
try:
function = db.get(Function, id)
return function.valves if function.valves else {}
except Exception as e:
print(f"An error occurred: {e}")
return None
def update_function_valves_by_id(
self, id: str, valves: dict
) -> Optional[FunctionValves]:
with get_db() as db:
try:
function = db.get(Function, id)
function.valves = valves
function.updated_at = int(time.time())
db.commit()
db.refresh(function)
return self.get_function_by_id(id)
except Exception:
return None
def get_user_valves_by_id_and_user_id(
self, id: str, user_id: str
) -> Optional[dict]:
try:
user = Users.get_user_by_id(user_id)
user_settings = user.settings.model_dump() if user.settings else {}
# Check if user has "functions" and "valves" settings
if "functions" not in user_settings:
user_settings["functions"] = {}
if "valves" not in user_settings["functions"]:
user_settings["functions"]["valves"] = {}
return user_settings["functions"]["valves"].get(id, {})
except Exception as e:
print(f"An error occurred: {e}")
return None
def update_user_valves_by_id_and_user_id(
self, id: str, user_id: str, valves: dict
) -> Optional[dict]:
try:
user = Users.get_user_by_id(user_id)
user_settings = user.settings.model_dump() if user.settings else {}
# Check if user has "functions" and "valves" settings
if "functions" not in user_settings:
user_settings["functions"] = {}
if "valves" not in user_settings["functions"]:
user_settings["functions"]["valves"] = {}
user_settings["functions"]["valves"][id] = valves
# Update the user settings in the database
Users.update_user_by_id(user_id, {"settings": user_settings})
return user_settings["functions"]["valves"][id]
except Exception as e:
print(f"An error occurred: {e}")
return None
def update_function_by_id(self, id: str, updated: dict) -> Optional[FunctionModel]:
with get_db() as db:
try:
db.query(Function).filter_by(id=id).update(
{
**updated,
"updated_at": int(time.time()),
}
)
db.commit()
return self.get_function_by_id(id)
except Exception:
return None
def deactivate_all_functions(self) -> Optional[bool]:
with get_db() as db:
try:
db.query(Function).update(
{
"is_active": False,
"updated_at": int(time.time()),
}
)
db.commit()
return True
except Exception:
return None
def delete_function_by_id(self, id: str) -> bool:
with get_db() as db:
try:
db.query(Function).filter_by(id=id).delete()
db.commit()
return True
except Exception:
return False
Functions = FunctionsTable()
+99 -69
View File
@@ -1,10 +1,9 @@
from pydantic import BaseModel
from peewee import *
from playhouse.shortcuts import model_to_dict
from typing import List, Union, Optional
from pydantic import BaseModel, ConfigDict
from typing import Union, Optional
from apps.webui.internal.db import DB
from apps.webui.models.chats import Chats
from sqlalchemy import Column, String, BigInteger, Text
from apps.webui.internal.db import Base, get_db
import time
import uuid
@@ -14,15 +13,14 @@ import uuid
####################
class Memory(Model):
id = CharField(unique=True)
user_id = CharField()
content = TextField()
updated_at = BigIntegerField()
created_at = BigIntegerField()
class Memory(Base):
__tablename__ = "memory"
class Meta:
database = DB
id = Column(String, primary_key=True)
user_id = Column(String)
content = Column(Text)
updated_at = Column(BigInteger)
created_at = Column(BigInteger)
class MemoryModel(BaseModel):
@@ -32,6 +30,8 @@ class MemoryModel(BaseModel):
updated_at: int # timestamp in epoch
created_at: int # timestamp in epoch
model_config = ConfigDict(from_attributes=True)
####################
# Forms
@@ -39,80 +39,110 @@ class MemoryModel(BaseModel):
class MemoriesTable:
def __init__(self, db):
self.db = db
self.db.create_tables([Memory])
def insert_new_memory(
self,
user_id: str,
content: str,
) -> Optional[MemoryModel]:
id = str(uuid.uuid4())
memory = MemoryModel(
**{
"id": id,
"user_id": user_id,
"content": content,
"created_at": int(time.time()),
"updated_at": int(time.time()),
}
)
result = Memory.create(**memory.model_dump())
if result:
return memory
else:
return None
with get_db() as db:
id = str(uuid.uuid4())
def get_memories(self) -> List[MemoryModel]:
try:
memories = Memory.select()
return [MemoryModel(**model_to_dict(memory)) for memory in memories]
except:
return None
memory = MemoryModel(
**{
"id": id,
"user_id": user_id,
"content": content,
"created_at": int(time.time()),
"updated_at": int(time.time()),
}
)
result = Memory(**memory.model_dump())
db.add(result)
db.commit()
db.refresh(result)
if result:
return MemoryModel.model_validate(result)
else:
return None
def get_memories_by_user_id(self, user_id: str) -> List[MemoryModel]:
try:
memories = Memory.select().where(Memory.user_id == user_id)
return [MemoryModel(**model_to_dict(memory)) for memory in memories]
except:
return None
def update_memory_by_id(
self,
id: str,
content: str,
) -> Optional[MemoryModel]:
with get_db() as db:
def get_memory_by_id(self, id) -> Optional[MemoryModel]:
try:
memory = Memory.get(Memory.id == id)
return MemoryModel(**model_to_dict(memory))
except:
return None
try:
db.query(Memory).filter_by(id=id).update(
{"content": content, "updated_at": int(time.time())}
)
db.commit()
return self.get_memory_by_id(id)
except Exception:
return None
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 Exception:
return None
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 Exception:
return None
def get_memory_by_id(self, id: str) -> Optional[MemoryModel]:
with get_db() as db:
try:
memory = db.get(Memory, id)
return MemoryModel.model_validate(memory)
except Exception:
return None
def delete_memory_by_id(self, id: str) -> bool:
try:
query = Memory.delete().where(Memory.id == id)
query.execute() # Remove the rows, return number of rows removed.
with get_db() as db:
return True
try:
db.query(Memory).filter_by(id=id).delete()
db.commit()
except:
return False
return True
except Exception:
return False
def delete_memories_by_user_id(self, user_id: str) -> bool:
try:
query = Memory.delete().where(Memory.user_id == user_id)
query.execute()
with get_db() as db:
return True
except:
return False
try:
db.query(Memory).filter_by(user_id=user_id).delete()
db.commit()
return True
except Exception:
return False
def delete_memory_by_id_and_user_id(self, id: str, user_id: str) -> bool:
try:
query = Memory.delete().where(Memory.id == id, Memory.user_id == user_id)
query.execute()
with get_db() as db:
return True
except:
return False
try:
db.query(Memory).filter_by(id=id, user_id=user_id).delete()
db.commit()
return True
except Exception:
return False
Memories = MemoriesTable(DB)
Memories = MemoriesTable()
+51 -48
View File
@@ -1,16 +1,11 @@
import json
import logging
from typing import Optional
from typing import Optional, List
import peewee as pw
from peewee import *
from playhouse.shortcuts import model_to_dict
from pydantic import BaseModel, ConfigDict
from sqlalchemy import Column, BigInteger, Text
from apps.webui.internal.db import DB, JSONField
from apps.webui.internal.db import Base, JSONField, get_db
from typing import List, Union, Optional
from config import SRC_LOG_LEVELS
import time
@@ -32,7 +27,7 @@ class ModelParams(BaseModel):
# ModelMeta is a model for the data stored in the meta field of the Model table
class ModelMeta(BaseModel):
profile_image_url: Optional[str] = "/favicon.png"
profile_image_url: Optional[str] = "/static/favicon.png"
description: Optional[str] = None
"""
@@ -46,38 +41,37 @@ class ModelMeta(BaseModel):
pass
class Model(pw.Model):
id = pw.TextField(unique=True)
class Model(Base):
__tablename__ = "model"
id = Column(Text, primary_key=True)
"""
The model's id as used in the API. If set to an existing model, it will override the model.
"""
user_id = pw.TextField()
user_id = Column(Text)
base_model_id = pw.TextField(null=True)
base_model_id = Column(Text, nullable=True)
"""
An optional pointer to the actual model that should be used when proxying requests.
"""
name = pw.TextField()
name = Column(Text)
"""
The human-readable display name of the model.
"""
params = JSONField()
params = Column(JSONField)
"""
Holds a JSON encoded blob of parameters, see `ModelParams`.
"""
meta = JSONField()
meta = Column(JSONField)
"""
Holds a JSON encoded blob of metadata, see `ModelMeta`.
"""
updated_at = BigIntegerField()
created_at = BigIntegerField()
class Meta:
database = DB
updated_at = Column(BigInteger)
created_at = Column(BigInteger)
class ModelModel(BaseModel):
@@ -92,6 +86,8 @@ class ModelModel(BaseModel):
updated_at: int # timestamp in epoch
created_at: int # timestamp in epoch
model_config = ConfigDict(from_attributes=True)
####################
# Forms
@@ -115,13 +111,6 @@ class ModelForm(BaseModel):
class ModelsTable:
def __init__(
self,
db: pw.SqliteDatabase | pw.PostgresqlDatabase,
):
self.db = db
self.db.create_tables([Model])
def insert_new_model(
self, form_data: ModelForm, user_id: str
) -> Optional[ModelModel]:
@@ -134,34 +123,46 @@ class ModelsTable:
}
)
try:
result = Model.create(**model.model_dump())
with get_db() as db:
result = Model(**model.model_dump())
db.add(result)
db.commit()
db.refresh(result)
if result:
return model
else:
return None
if result:
return ModelModel.model_validate(result)
else:
return None
except Exception as e:
print(e)
return None
def get_all_models(self) -> List[ModelModel]:
return [ModelModel(**model_to_dict(model)) for model in Model.select()]
def get_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:
model = Model.get(Model.id == id)
return ModelModel(**model_to_dict(model))
except:
with get_db() as db:
model = db.get(Model, id)
return ModelModel.model_validate(model)
except Exception:
return None
def update_model_by_id(self, id: str, model: ModelForm) -> Optional[ModelModel]:
try:
# update only the fields that are present in the model
query = Model.update(**model.model_dump()).where(Model.id == id)
query.execute()
with get_db() as db:
# update only the fields that are present in the model
result = (
db.query(Model)
.filter_by(id=id)
.update(model.model_dump(exclude={"id"}, exclude_none=True))
)
db.commit()
model = Model.get(Model.id == id)
return ModelModel(**model_to_dict(model))
model = db.get(Model, id)
db.refresh(model)
return ModelModel.model_validate(model)
except Exception as e:
print(e)
@@ -169,11 +170,13 @@ class ModelsTable:
def delete_model_by_id(self, id: str) -> bool:
try:
query = Model.delete().where(Model.id == id)
query.execute()
return True
except:
with get_db() as db:
db.query(Model).filter_by(id=id).delete()
db.commit()
return True
except Exception:
return False
Models = ModelsTable(DB)
Models = ModelsTable()
+50 -49
View File
@@ -1,13 +1,10 @@
from pydantic import BaseModel
from peewee import *
from playhouse.shortcuts import model_to_dict
from typing import List, Union, Optional
from pydantic import BaseModel, ConfigDict
from typing import Optional
import time
from utils.utils import decode_token
from utils.misc import get_gravatar_url
from sqlalchemy import String, Column, BigInteger, Text
from apps.webui.internal.db import DB
from apps.webui.internal.db import Base, get_db
import json
@@ -16,15 +13,14 @@ import json
####################
class Prompt(Model):
command = CharField(unique=True)
user_id = CharField()
title = TextField()
content = TextField()
timestamp = BigIntegerField()
class Prompt(Base):
__tablename__ = "prompt"
class Meta:
database = DB
command = Column(String, primary_key=True)
user_id = Column(String)
title = Column(Text)
content = Column(Text)
timestamp = Column(BigInteger)
class PromptModel(BaseModel):
@@ -34,6 +30,8 @@ class PromptModel(BaseModel):
content: str
timestamp: int # timestamp in epoch
model_config = ConfigDict(from_attributes=True)
####################
# Forms
@@ -48,10 +46,6 @@ class PromptForm(BaseModel):
class PromptsTable:
def __init__(self, db):
self.db = db
self.db.create_tables([Prompt])
def insert_new_prompt(
self, user_id: str, form_data: PromptForm
) -> Optional[PromptModel]:
@@ -66,53 +60,60 @@ class PromptsTable:
)
try:
result = Prompt.create(**prompt.model_dump())
if result:
return prompt
else:
return None
except:
with get_db() as db:
result = Prompt(**prompt.dict())
db.add(result)
db.commit()
db.refresh(result)
if result:
return PromptModel.model_validate(result)
else:
return None
except Exception as e:
return None
def get_prompt_by_command(self, command: str) -> Optional[PromptModel]:
try:
prompt = Prompt.get(Prompt.command == command)
return PromptModel(**model_to_dict(prompt))
except:
with get_db() as db:
prompt = db.query(Prompt).filter_by(command=command).first()
return PromptModel.model_validate(prompt)
except Exception:
return None
def get_prompts(self) -> List[PromptModel]:
return [
PromptModel(**model_to_dict(prompt))
for prompt in Prompt.select()
# .limit(limit).offset(skip)
]
def get_prompts(self) -> list[PromptModel]:
with get_db() as db:
return [
PromptModel.model_validate(prompt) for prompt in db.query(Prompt).all()
]
def update_prompt_by_command(
self, command: str, form_data: PromptForm
) -> Optional[PromptModel]:
try:
query = Prompt.update(
title=form_data.title,
content=form_data.content,
timestamp=int(time.time()),
).where(Prompt.command == command)
with get_db() as db:
query.execute()
prompt = Prompt.get(Prompt.command == command)
return PromptModel(**model_to_dict(prompt))
except:
prompt = db.query(Prompt).filter_by(command=command).first()
prompt.title = form_data.title
prompt.content = form_data.content
prompt.timestamp = int(time.time())
db.commit()
return PromptModel.model_validate(prompt)
except Exception:
return None
def delete_prompt_by_command(self, command: str) -> bool:
try:
query = Prompt.delete().where((Prompt.command == command))
query.execute() # Remove the rows, return number of rows removed.
with get_db() as db:
return True
except:
db.query(Prompt).filter_by(command=command).delete()
db.commit()
return True
except Exception:
return False
Prompts = PromptsTable(DB)
Prompts = PromptsTable()
+144 -109
View File
@@ -1,14 +1,14 @@
from pydantic import BaseModel
from typing import List, Union, Optional
from peewee import *
from playhouse.shortcuts import model_to_dict
from pydantic import BaseModel, ConfigDict
from typing import Optional
import json
import uuid
import time
import logging
from apps.webui.internal.db import DB
from sqlalchemy import String, Column, BigInteger, Text
from apps.webui.internal.db import Base, get_db
from config import SRC_LOG_LEVELS
@@ -20,25 +20,23 @@ log.setLevel(SRC_LOG_LEVELS["MODELS"])
####################
class Tag(Model):
id = CharField(unique=True)
name = CharField()
user_id = CharField()
data = TextField(null=True)
class Tag(Base):
__tablename__ = "tag"
class Meta:
database = DB
id = Column(String, primary_key=True)
name = Column(String)
user_id = Column(String)
data = Column(Text, nullable=True)
class ChatIdTag(Model):
id = CharField(unique=True)
tag_name = CharField()
chat_id = CharField()
user_id = CharField()
timestamp = BigIntegerField()
class ChatIdTag(Base):
__tablename__ = "chatidtag"
class Meta:
database = DB
id = Column(String, primary_key=True)
tag_name = Column(String)
chat_id = Column(String)
user_id = Column(String)
timestamp = Column(BigInteger)
class TagModel(BaseModel):
@@ -47,6 +45,8 @@ class TagModel(BaseModel):
user_id: str
data: Optional[str] = None
model_config = ConfigDict(from_attributes=True)
class ChatIdTagModel(BaseModel):
id: str
@@ -55,6 +55,8 @@ class ChatIdTagModel(BaseModel):
user_id: str
timestamp: int
model_config = ConfigDict(from_attributes=True)
####################
# Forms
@@ -67,36 +69,39 @@ 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:
def __init__(self, db):
self.db = db
db.create_tables([Tag, ChatIdTag])
def insert_new_tag(self, name: str, user_id: str) -> Optional[TagModel]:
id = str(uuid.uuid4())
tag = TagModel(**{"id": id, "user_id": user_id, "name": name})
try:
result = Tag.create(**tag.model_dump())
if result:
return tag
else:
with get_db() as db:
id = str(uuid.uuid4())
tag = TagModel(**{"id": id, "user_id": user_id, "name": name})
try:
result = Tag(**tag.model_dump())
db.add(result)
db.commit()
db.refresh(result)
if result:
return TagModel.model_validate(result)
else:
return None
except Exception as e:
return None
except Exception as e:
return None
def get_tag_by_name_and_user_id(
self, name: str, user_id: str
) -> Optional[TagModel]:
try:
tag = Tag.get(Tag.name == name, Tag.user_id == user_id)
return TagModel(**model_to_dict(tag))
with get_db() as db:
tag = db.query(Tag).filter_by(name=name, user_id=user_id).first()
return TagModel.model_validate(tag)
except Exception as e:
return None
@@ -104,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())
@@ -118,82 +123,110 @@ class TagTable:
}
)
try:
result = ChatIdTag.create(**chatIdTag.model_dump())
if result:
return chatIdTag
else:
return None
except:
with get_db() as db:
result = ChatIdTag(**chatIdTag.model_dump())
db.add(result)
db.commit()
db.refresh(result)
if result:
return ChatIdTagModel.model_validate(result)
else:
return None
except Exception:
return None
def get_tags_by_user_id(self, user_id: str) -> List[TagModel]:
tag_names = [
ChatIdTagModel(**model_to_dict(chat_id_tag)).tag_name
for chat_id_tag in ChatIdTag.select()
.where(ChatIdTag.user_id == user_id)
.order_by(ChatIdTag.timestamp.desc())
]
def get_tags_by_user_id(self, user_id: str) -> list[TagModel]:
with get_db() as db:
tag_names = [
chat_id_tag.tag_name
for chat_id_tag in (
db.query(ChatIdTag)
.filter_by(user_id=user_id)
.order_by(ChatIdTag.timestamp.desc())
.all()
)
]
return [
TagModel(**model_to_dict(tag))
for tag in Tag.select()
.where(Tag.user_id == user_id)
.where(Tag.name.in_(tag_names))
]
return [
TagModel.model_validate(tag)
for tag in (
db.query(Tag)
.filter_by(user_id=user_id)
.filter(Tag.name.in_(tag_names))
.all()
)
]
def get_tags_by_chat_id_and_user_id(
self, chat_id: str, user_id: str
) -> List[TagModel]:
tag_names = [
ChatIdTagModel(**model_to_dict(chat_id_tag)).tag_name
for chat_id_tag in ChatIdTag.select()
.where((ChatIdTag.user_id == user_id) & (ChatIdTag.chat_id == chat_id))
.order_by(ChatIdTag.timestamp.desc())
]
) -> list[TagModel]:
with get_db() as db:
return [
TagModel(**model_to_dict(tag))
for tag in Tag.select()
.where(Tag.user_id == user_id)
.where(Tag.name.in_(tag_names))
]
tag_names = [
chat_id_tag.tag_name
for chat_id_tag in (
db.query(ChatIdTag)
.filter_by(user_id=user_id, chat_id=chat_id)
.order_by(ChatIdTag.timestamp.desc())
.all()
)
]
return [
TagModel.model_validate(tag)
for tag in (
db.query(Tag)
.filter_by(user_id=user_id)
.filter(Tag.name.in_(tag_names))
.all()
)
]
def get_chat_ids_by_tag_name_and_user_id(
self, tag_name: str, user_id: str
) -> Optional[ChatIdTagModel]:
return [
ChatIdTagModel(**model_to_dict(chat_id_tag))
for chat_id_tag in ChatIdTag.select()
.where((ChatIdTag.user_id == user_id) & (ChatIdTag.tag_name == tag_name))
.order_by(ChatIdTag.timestamp.desc())
]
) -> list[ChatIdTagModel]:
with get_db() as db:
return [
ChatIdTagModel.model_validate(chat_id_tag)
for chat_id_tag in (
db.query(ChatIdTag)
.filter_by(user_id=user_id, tag_name=tag_name)
.order_by(ChatIdTag.timestamp.desc())
.all()
)
]
def count_chat_ids_by_tag_name_and_user_id(
self, tag_name: str, user_id: str
) -> int:
return (
ChatIdTag.select()
.where((ChatIdTag.tag_name == tag_name) & (ChatIdTag.user_id == user_id))
.count()
)
with get_db() as db:
return (
db.query(ChatIdTag)
.filter_by(tag_name=tag_name, user_id=user_id)
.count()
)
def delete_tag_by_tag_name_and_user_id(self, tag_name: str, user_id: str) -> bool:
try:
query = ChatIdTag.delete().where(
(ChatIdTag.tag_name == tag_name) & (ChatIdTag.user_id == user_id)
)
res = query.execute() # Remove the rows, return number of rows removed.
log.debug(f"res: {res}")
tag_count = self.count_chat_ids_by_tag_name_and_user_id(tag_name, user_id)
if tag_count == 0:
# Remove tag item from Tag col as well
query = Tag.delete().where(
(Tag.name == tag_name) & (Tag.user_id == user_id)
with get_db() as db:
res = (
db.query(ChatIdTag)
.filter_by(tag_name=tag_name, user_id=user_id)
.delete()
)
query.execute() # Remove the rows, return number of rows removed.
log.debug(f"res: {res}")
db.commit()
return True
tag_count = self.count_chat_ids_by_tag_name_and_user_id(
tag_name, user_id
)
if tag_count == 0:
# Remove tag item from Tag col as well
db.query(Tag).filter_by(name=tag_name, user_id=user_id).delete()
db.commit()
return True
except Exception as e:
log.error(f"delete_tag: {e}")
return False
@@ -202,23 +235,25 @@ class TagTable:
self, tag_name: str, chat_id: str, user_id: str
) -> bool:
try:
query = ChatIdTag.delete().where(
(ChatIdTag.tag_name == tag_name)
& (ChatIdTag.chat_id == chat_id)
& (ChatIdTag.user_id == user_id)
)
res = query.execute() # Remove the rows, return number of rows removed.
log.debug(f"res: {res}")
with get_db() as db:
tag_count = self.count_chat_ids_by_tag_name_and_user_id(tag_name, user_id)
if tag_count == 0:
# Remove tag item from Tag col as well
query = Tag.delete().where(
(Tag.name == tag_name) & (Tag.user_id == user_id)
res = (
db.query(ChatIdTag)
.filter_by(tag_name=tag_name, chat_id=chat_id, user_id=user_id)
.delete()
)
query.execute() # Remove the rows, return number of rows removed.
log.debug(f"res: {res}")
db.commit()
return True
tag_count = self.count_chat_ids_by_tag_name_and_user_id(
tag_name, user_id
)
if tag_count == 0:
# Remove tag item from Tag col as well
db.query(Tag).filter_by(name=tag_name, user_id=user_id).delete()
db.commit()
return True
except Exception as e:
log.error(f"delete_tag: {e}")
return False
@@ -234,4 +269,4 @@ class TagTable:
return True
Tags = TagTable(DB)
Tags = TagTable()
+213
View File
@@ -0,0 +1,213 @@
from pydantic import BaseModel, ConfigDict
from typing import Optional
import time
import logging
from sqlalchemy import String, Column, BigInteger, Text
from apps.webui.internal.db import Base, JSONField, get_db
from apps.webui.models.users import Users
import json
import copy
from config import SRC_LOG_LEVELS
log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["MODELS"])
####################
# Tools DB Schema
####################
class Tool(Base):
__tablename__ = "tool"
id = Column(String, primary_key=True)
user_id = Column(String)
name = Column(Text)
content = Column(Text)
specs = Column(JSONField)
meta = Column(JSONField)
valves = Column(JSONField)
updated_at = Column(BigInteger)
created_at = Column(BigInteger)
class ToolMeta(BaseModel):
description: Optional[str] = None
manifest: Optional[dict] = {}
class ToolModel(BaseModel):
id: str
user_id: str
name: str
content: str
specs: list[dict]
meta: ToolMeta
updated_at: int # timestamp in epoch
created_at: int # timestamp in epoch
model_config = ConfigDict(from_attributes=True)
####################
# Forms
####################
class ToolResponse(BaseModel):
id: str
user_id: str
name: str
meta: ToolMeta
updated_at: int # timestamp in epoch
created_at: int # timestamp in epoch
class ToolForm(BaseModel):
id: str
name: str
content: str
meta: ToolMeta
class ToolValves(BaseModel):
valves: Optional[dict] = None
class ToolsTable:
def insert_new_tool(
self, user_id: str, form_data: ToolForm, specs: list[dict]
) -> Optional[ToolModel]:
with get_db() as db:
tool = ToolModel(
**{
**form_data.model_dump(),
"specs": specs,
"user_id": user_id,
"updated_at": int(time.time()),
"created_at": int(time.time()),
}
)
try:
result = Tool(**tool.model_dump())
db.add(result)
db.commit()
db.refresh(result)
if result:
return ToolModel.model_validate(result)
else:
return None
except Exception as e:
print(f"Error creating tool: {e}")
return None
def get_tool_by_id(self, id: str) -> Optional[ToolModel]:
try:
with get_db() as db:
tool = db.get(Tool, id)
return ToolModel.model_validate(tool)
except Exception:
return None
def get_tools(self) -> list[ToolModel]:
with get_db() as db:
return [ToolModel.model_validate(tool) for tool in db.query(Tool).all()]
def get_tool_valves_by_id(self, id: str) -> Optional[dict]:
try:
with get_db() as db:
tool = db.get(Tool, id)
return tool.valves if tool.valves else {}
except Exception as e:
print(f"An error occurred: {e}")
return None
def update_tool_valves_by_id(self, id: str, valves: dict) -> Optional[ToolValves]:
try:
with get_db() as db:
db.query(Tool).filter_by(id=id).update(
{"valves": valves, "updated_at": int(time.time())}
)
db.commit()
return self.get_tool_by_id(id)
except Exception:
return None
def get_user_valves_by_id_and_user_id(
self, id: str, user_id: str
) -> Optional[dict]:
try:
user = Users.get_user_by_id(user_id)
user_settings = user.settings.model_dump() if user.settings else {}
# Check if user has "tools" and "valves" settings
if "tools" not in user_settings:
user_settings["tools"] = {}
if "valves" not in user_settings["tools"]:
user_settings["tools"]["valves"] = {}
return user_settings["tools"]["valves"].get(id, {})
except Exception as e:
print(f"An error occurred: {e}")
return None
def update_user_valves_by_id_and_user_id(
self, id: str, user_id: str, valves: dict
) -> Optional[dict]:
try:
user = Users.get_user_by_id(user_id)
user_settings = user.settings.model_dump() if user.settings else {}
# Check if user has "tools" and "valves" settings
if "tools" not in user_settings:
user_settings["tools"] = {}
if "valves" not in user_settings["tools"]:
user_settings["tools"]["valves"] = {}
user_settings["tools"]["valves"][id] = valves
# Update the user settings in the database
Users.update_user_by_id(user_id, {"settings": user_settings})
return user_settings["tools"]["valves"][id]
except Exception as e:
print(f"An error occurred: {e}")
return None
def update_tool_by_id(self, id: str, updated: dict) -> Optional[ToolModel]:
try:
with get_db() as db:
db.query(Tool).filter_by(id=id).update(
{**updated, "updated_at": int(time.time())}
)
db.commit()
tool = db.query(Tool).get(id)
db.refresh(tool)
return ToolModel.model_validate(tool)
except Exception:
return None
def delete_tool_by_id(self, id: str) -> bool:
try:
with get_db() as db:
db.query(Tool).filter_by(id=id).delete()
db.commit()
return True
except Exception:
return False
Tools = ToolsTable()
+138 -94
View File
@@ -1,11 +1,10 @@
from pydantic import BaseModel, ConfigDict
from peewee import *
from playhouse.shortcuts import model_to_dict
from typing import List, Union, Optional
from typing import Optional
import time
from utils.misc import get_gravatar_url
from apps.webui.internal.db import DB, JSONField
from sqlalchemy import String, Column, BigInteger, Text
from apps.webui.internal.db import Base, JSONField, get_db
from apps.webui.models.chats import Chats
####################
@@ -13,22 +12,24 @@ from apps.webui.models.chats import Chats
####################
class User(Model):
id = CharField(unique=True)
name = CharField()
email = CharField()
role = CharField()
profile_image_url = TextField()
class User(Base):
__tablename__ = "user"
last_active_at = BigIntegerField()
updated_at = BigIntegerField()
created_at = BigIntegerField()
id = Column(String, primary_key=True)
name = Column(String)
email = Column(String)
role = Column(String)
profile_image_url = Column(Text)
api_key = CharField(null=True, unique=True)
settings = JSONField(null=True)
last_active_at = Column(BigInteger)
updated_at = Column(BigInteger)
created_at = Column(BigInteger)
class Meta:
database = DB
api_key = Column(String, nullable=True, unique=True)
settings = Column(JSONField, nullable=True)
info = Column(JSONField, nullable=True)
oauth_sub = Column(Text, unique=True)
class UserSettings(BaseModel):
@@ -50,6 +51,11 @@ class UserModel(BaseModel):
api_key: Optional[str] = None
settings: Optional[UserSettings] = None
info: Optional[dict] = None
oauth_sub: Optional[str] = None
model_config = ConfigDict(from_attributes=True)
####################
@@ -70,10 +76,6 @@ class UserUpdateForm(BaseModel):
class UsersTable:
def __init__(self, db):
self.db = db
self.db.create_tables([User])
def insert_new_user(
self,
id: str,
@@ -81,105 +83,145 @@ class UsersTable:
email: str,
profile_image_url: str = "/user.png",
role: str = "pending",
oauth_sub: Optional[str] = None,
) -> Optional[UserModel]:
user = UserModel(
**{
"id": id,
"name": name,
"email": email,
"role": role,
"profile_image_url": profile_image_url,
"last_active_at": int(time.time()),
"created_at": int(time.time()),
"updated_at": int(time.time()),
}
)
result = User.create(**user.model_dump())
if result:
return user
else:
return None
with get_db() as db:
user = UserModel(
**{
"id": id,
"name": name,
"email": email,
"role": role,
"profile_image_url": profile_image_url,
"last_active_at": int(time.time()),
"created_at": int(time.time()),
"updated_at": int(time.time()),
"oauth_sub": oauth_sub,
}
)
result = User(**user.model_dump())
db.add(result)
db.commit()
db.refresh(result)
if result:
return user
else:
return None
def get_user_by_id(self, id: str) -> Optional[UserModel]:
try:
user = User.get(User.id == id)
return UserModel(**model_to_dict(user))
except:
with get_db() as db:
user = db.query(User).filter_by(id=id).first()
return UserModel.model_validate(user)
except Exception as e:
return None
def get_user_by_api_key(self, api_key: str) -> Optional[UserModel]:
try:
user = User.get(User.api_key == api_key)
return UserModel(**model_to_dict(user))
except:
with get_db() as db:
user = db.query(User).filter_by(api_key=api_key).first()
return UserModel.model_validate(user)
except Exception:
return None
def get_user_by_email(self, email: str) -> Optional[UserModel]:
try:
user = User.get(User.email == email)
return UserModel(**model_to_dict(user))
except:
with get_db() as db:
user = db.query(User).filter_by(email=email).first()
return UserModel.model_validate(user)
except Exception:
return None
def get_users(self, skip: int = 0, limit: int = 50) -> List[UserModel]:
return [
UserModel(**model_to_dict(user))
for user in User.select()
# .limit(limit).offset(skip)
]
def get_user_by_oauth_sub(self, sub: str) -> Optional[UserModel]:
try:
with get_db() as db:
user = db.query(User).filter_by(oauth_sub=sub).first()
return UserModel.model_validate(user)
except Exception:
return None
def get_users(self, skip: int = 0, limit: int = 50) -> list[UserModel]:
with get_db() as db:
users = (
db.query(User)
# .offset(skip).limit(limit)
.all()
)
return [UserModel.model_validate(user) for user in users]
def get_num_users(self) -> Optional[int]:
return User.select().count()
with get_db() as db:
return db.query(User).count()
def get_first_user(self) -> UserModel:
try:
user = User.select().order_by(User.created_at).first()
return UserModel(**model_to_dict(user))
except:
with get_db() as db:
user = db.query(User).order_by(User.created_at).first()
return UserModel.model_validate(user)
except Exception:
return None
def update_user_role_by_id(self, id: str, role: str) -> Optional[UserModel]:
try:
query = User.update(role=role).where(User.id == id)
query.execute()
user = User.get(User.id == id)
return UserModel(**model_to_dict(user))
except:
with get_db() as db:
db.query(User).filter_by(id=id).update({"role": role})
db.commit()
user = db.query(User).filter_by(id=id).first()
return UserModel.model_validate(user)
except Exception:
return None
def update_user_profile_image_url_by_id(
self, id: str, profile_image_url: str
) -> Optional[UserModel]:
try:
query = User.update(profile_image_url=profile_image_url).where(
User.id == id
)
query.execute()
with get_db() as db:
db.query(User).filter_by(id=id).update(
{"profile_image_url": profile_image_url}
)
db.commit()
user = User.get(User.id == id)
return UserModel(**model_to_dict(user))
except:
user = db.query(User).filter_by(id=id).first()
return UserModel.model_validate(user)
except Exception:
return None
def update_user_last_active_by_id(self, id: str) -> Optional[UserModel]:
try:
query = User.update(last_active_at=int(time.time())).where(User.id == id)
query.execute()
with get_db() as db:
db.query(User).filter_by(id=id).update(
{"last_active_at": int(time.time())}
)
db.commit()
user = User.get(User.id == id)
return UserModel(**model_to_dict(user))
except:
user = db.query(User).filter_by(id=id).first()
return UserModel.model_validate(user)
except Exception:
return None
def update_user_oauth_sub_by_id(
self, id: str, oauth_sub: str
) -> Optional[UserModel]:
try:
with get_db() as db:
db.query(User).filter_by(id=id).update({"oauth_sub": oauth_sub})
db.commit()
user = db.query(User).filter_by(id=id).first()
return UserModel.model_validate(user)
except Exception:
return None
def update_user_by_id(self, id: str, updated: dict) -> Optional[UserModel]:
try:
query = User.update(**updated).where(User.id == id)
query.execute()
with get_db() as db:
db.query(User).filter_by(id=id).update(updated)
db.commit()
user = User.get(User.id == id)
return UserModel(**model_to_dict(user))
except:
user = db.query(User).filter_by(id=id).first()
return UserModel.model_validate(user)
# return UserModel(**user.dict())
except Exception as e:
return None
def delete_user_by_id(self, id: str) -> bool:
@@ -188,31 +230,33 @@ class UsersTable:
result = Chats.delete_chats_by_user_id(id)
if result:
# Delete User
query = User.delete().where(User.id == id)
query.execute() # Remove the rows, return number of rows removed.
with get_db() as db:
# Delete User
db.query(User).filter_by(id=id).delete()
db.commit()
return True
else:
return False
except:
except Exception:
return False
def update_user_api_key_by_id(self, id: str, api_key: str) -> str:
try:
query = User.update(api_key=api_key).where(User.id == id)
result = query.execute()
return True if result == 1 else False
except:
with get_db() as db:
result = db.query(User).filter_by(id=id).update({"api_key": api_key})
db.commit()
return True if result == 1 else False
except Exception:
return False
def get_user_api_key_by_id(self, id: str) -> Optional[str]:
try:
user = User.get(User.id == id)
return user.api_key
except:
with get_db() as db:
user = db.query(User).filter_by(id=id).first()
return user.api_key
except Exception as e:
return None
Users = UsersTable(DB)
Users = UsersTable()
+49 -6
View File
@@ -2,6 +2,7 @@ import logging
from fastapi import Request, UploadFile, File
from fastapi import Depends, HTTPException, status
from fastapi.responses import Response
from fastapi import APIRouter
from pydantic import BaseModel
@@ -9,7 +10,6 @@ import re
import uuid
import csv
from apps.webui.models.auths import (
SigninForm,
SignupForm,
@@ -33,7 +33,11 @@ from utils.utils import (
from utils.misc import parse_duration, validate_email_format
from utils.webhook import post_webhook
from constants import ERROR_MESSAGES, WEBHOOK_MESSAGES
from config import WEBUI_AUTH, WEBUI_AUTH_TRUSTED_EMAIL_HEADER
from config import (
WEBUI_AUTH,
WEBUI_AUTH_TRUSTED_EMAIL_HEADER,
WEBUI_AUTH_TRUSTED_NAME_HEADER,
)
router = APIRouter()
@@ -43,7 +47,21 @@ router = APIRouter()
@router.get("/", response_model=UserResponse)
async def get_session_user(user=Depends(get_current_user)):
async def get_session_user(
request: Request, response: Response, user=Depends(get_current_user)
):
token = create_token(
data={"id": user.id},
expires_delta=parse_duration(request.app.state.config.JWT_EXPIRES_IN),
)
# Set the cookie token
response.set_cookie(
key="token",
value=token,
httponly=True, # Ensures the cookie is not accessible via JavaScript
)
return {
"id": user.id,
"email": user.email,
@@ -104,17 +122,23 @@ async def update_password(
@router.post("/signin", response_model=SigninResponse)
async def signin(request: Request, form_data: SigninForm):
async def signin(request: Request, response: Response, form_data: SigninForm):
if WEBUI_AUTH_TRUSTED_EMAIL_HEADER:
if WEBUI_AUTH_TRUSTED_EMAIL_HEADER not in request.headers:
raise HTTPException(400, detail=ERROR_MESSAGES.INVALID_TRUSTED_HEADER)
trusted_email = request.headers[WEBUI_AUTH_TRUSTED_EMAIL_HEADER].lower()
trusted_name = trusted_email
if WEBUI_AUTH_TRUSTED_NAME_HEADER:
trusted_name = request.headers.get(
WEBUI_AUTH_TRUSTED_NAME_HEADER, trusted_email
)
if not Users.get_user_by_email(trusted_email.lower()):
await signup(
request,
response,
SignupForm(
email=trusted_email, password=str(uuid.uuid4()), name=trusted_email
email=trusted_email, password=str(uuid.uuid4()), name=trusted_name
),
)
user = Auths.authenticate_user_by_trusted_header(trusted_email)
@@ -130,6 +154,7 @@ async def signin(request: Request, form_data: SigninForm):
await signup(
request,
response,
SignupForm(email=admin_email, password=admin_password, name="User"),
)
@@ -143,6 +168,13 @@ async def signin(request: Request, form_data: SigninForm):
expires_delta=parse_duration(request.app.state.config.JWT_EXPIRES_IN),
)
# Set the cookie token
response.set_cookie(
key="token",
value=token,
httponly=True, # Ensures the cookie is not accessible via JavaScript
)
return {
"token": token,
"token_type": "Bearer",
@@ -162,7 +194,7 @@ async def signin(request: Request, form_data: SigninForm):
@router.post("/signup", response_model=SigninResponse)
async def signup(request: Request, form_data: SignupForm):
async def signup(request: Request, response: Response, form_data: SignupForm):
if not request.app.state.config.ENABLE_SIGNUP and WEBUI_AUTH:
raise HTTPException(
status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.ACCESS_PROHIBITED
@@ -198,6 +230,13 @@ async def signup(request: Request, form_data: SignupForm):
)
# response.set_cookie(key='token', value=token, httponly=True)
# Set the cookie token
response.set_cookie(
key="token",
value=token,
httponly=True, # Ensures the cookie is not accessible via JavaScript
)
if request.app.state.config.WEBHOOK_URL:
post_webhook(
request.app.state.config.WEBHOOK_URL,
@@ -313,6 +352,7 @@ async def get_admin_config(request: Request, user=Depends(get_admin_user)):
"DEFAULT_USER_ROLE": request.app.state.config.DEFAULT_USER_ROLE,
"JWT_EXPIRES_IN": request.app.state.config.JWT_EXPIRES_IN,
"ENABLE_COMMUNITY_SHARING": request.app.state.config.ENABLE_COMMUNITY_SHARING,
"ENABLE_MESSAGE_RATING": request.app.state.config.ENABLE_MESSAGE_RATING,
}
@@ -322,6 +362,7 @@ class AdminConfig(BaseModel):
DEFAULT_USER_ROLE: str
JWT_EXPIRES_IN: str
ENABLE_COMMUNITY_SHARING: bool
ENABLE_MESSAGE_RATING: bool
@router.post("/admin/config")
@@ -343,6 +384,7 @@ async def update_admin_config(
request.app.state.config.ENABLE_COMMUNITY_SHARING = (
form_data.ENABLE_COMMUNITY_SHARING
)
request.app.state.config.ENABLE_MESSAGE_RATING = form_data.ENABLE_MESSAGE_RATING
return {
"SHOW_ADMIN_DETAILS": request.app.state.config.SHOW_ADMIN_DETAILS,
@@ -350,6 +392,7 @@ async def update_admin_config(
"DEFAULT_USER_ROLE": request.app.state.config.DEFAULT_USER_ROLE,
"JWT_EXPIRES_IN": request.app.state.config.JWT_EXPIRES_IN,
"ENABLE_COMMUNITY_SHARING": request.app.state.config.ENABLE_COMMUNITY_SHARING,
"ENABLE_MESSAGE_RATING": request.app.state.config.ENABLE_MESSAGE_RATING,
}
+53 -40
View File
@@ -1,7 +1,7 @@
from fastapi import Depends, Request, HTTPException, status
from datetime import datetime, timedelta
from typing import List, Union, Optional
from utils.utils import get_current_user, get_admin_user
from typing import Union, Optional
from utils.utils import get_verified_user, get_admin_user
from fastapi import APIRouter
from pydantic import BaseModel
import json
@@ -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_current_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)
############################
@@ -54,7 +60,7 @@ async def get_session_user_chat_list(
@router.delete("/", response_model=bool)
async def delete_all_user_chats(request: Request, user=Depends(get_current_user)):
async def delete_all_user_chats(request: Request, user=Depends(get_verified_user)):
if (
user.role == "user"
@@ -74,10 +80,18 @@ async def delete_all_user_chats(request: Request, user=Depends(get_current_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
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
)
@@ -89,7 +103,7 @@ async def get_user_chat_list_by_user_id(
@router.post("/new", response_model=Optional[ChatResponse])
async def create_new_chat(form_data: ChatForm, user=Depends(get_current_user)):
async def create_new_chat(form_data: ChatForm, user=Depends(get_verified_user)):
try:
chat = Chats.insert_new_chat(user.id, form_data)
return ChatResponse(**{**chat.model_dump(), "chat": json.loads(chat.chat)})
@@ -105,8 +119,8 @@ async def create_new_chat(form_data: ChatForm, user=Depends(get_current_user)):
############################
@router.get("/all", response_model=List[ChatResponse])
async def get_user_chats(user=Depends(get_current_user)):
@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)})
for chat in Chats.get_chats_by_user_id(user.id)
@@ -118,8 +132,8 @@ async def get_user_chats(user=Depends(get_current_user)):
############################
@router.get("/all/archived", response_model=List[ChatResponse])
async def get_user_chats(user=Depends(get_current_user)):
@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)})
for chat in Chats.get_archived_chats_by_user_id(user.id)
@@ -131,7 +145,7 @@ async def get_user_chats(user=Depends(get_current_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(
@@ -149,9 +163,9 @@ 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_current_user), skip: int = 0, limit: int = 50
user=Depends(get_verified_user), skip: int = 0, limit: int = 50
):
return Chats.get_archived_chat_list_by_user_id(user.id, skip, limit)
@@ -161,8 +175,8 @@ async def get_archived_session_user_chat_list(
############################
@router.post("/archive/all", response_model=List[ChatTitleIdResponse])
async def archive_all_chats(user=Depends(get_current_user)):
@router.post("/archive/all", response_model=bool)
async def archive_all_chats(user=Depends(get_verified_user)):
return Chats.archive_all_chats_by_user_id(user.id)
@@ -172,15 +186,15 @@ async def archive_all_chats(user=Depends(get_current_user)):
@router.get("/share/{share_id}", response_model=Optional[ChatResponse])
async def get_shared_chat_by_id(share_id: str, user=Depends(get_current_user)):
async def get_shared_chat_by_id(share_id: str, user=Depends(get_verified_user)):
if user.role == "pending":
raise HTTPException(
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:
@@ -202,12 +216,11 @@ 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_current_user)
form_data: TagNameForm, user=Depends(get_verified_user)
):
print(form_data)
chat_ids = [
chat_id_tag.chat_id
for chat_id_tag in Tags.get_chat_ids_by_tag_name_and_user_id(
@@ -228,8 +241,8 @@ async def get_user_chat_list_by_tag_name(
############################
@router.get("/tags/all", response_model=List[TagModel])
async def get_all_tags(user=Depends(get_current_user)):
@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)
return tags
@@ -246,7 +259,7 @@ async def get_all_tags(user=Depends(get_current_user)):
@router.get("/{id}", response_model=Optional[ChatResponse])
async def get_chat_by_id(id: str, user=Depends(get_current_user)):
async def get_chat_by_id(id: str, user=Depends(get_verified_user)):
chat = Chats.get_chat_by_id_and_user_id(id, user.id)
if chat:
@@ -264,7 +277,7 @@ async def get_chat_by_id(id: str, user=Depends(get_current_user)):
@router.post("/{id}", response_model=Optional[ChatResponse])
async def update_chat_by_id(
id: str, form_data: ChatForm, user=Depends(get_current_user)
id: str, form_data: ChatForm, user=Depends(get_verified_user)
):
chat = Chats.get_chat_by_id_and_user_id(id, user.id)
if chat:
@@ -285,7 +298,7 @@ async def update_chat_by_id(
@router.delete("/{id}", response_model=bool)
async def delete_chat_by_id(request: Request, id: str, user=Depends(get_current_user)):
async def delete_chat_by_id(request: Request, id: str, user=Depends(get_verified_user)):
if user.role == "admin":
result = Chats.delete_chat_by_id(id)
@@ -307,7 +320,7 @@ async def delete_chat_by_id(request: Request, id: str, user=Depends(get_current_
@router.get("/{id}/clone", response_model=Optional[ChatResponse])
async def clone_chat_by_id(id: str, user=Depends(get_current_user)):
async def clone_chat_by_id(id: str, user=Depends(get_verified_user)):
chat = Chats.get_chat_by_id_and_user_id(id, user.id)
if chat:
@@ -333,7 +346,7 @@ async def clone_chat_by_id(id: str, user=Depends(get_current_user)):
@router.get("/{id}/archive", response_model=Optional[ChatResponse])
async def archive_chat_by_id(id: str, user=Depends(get_current_user)):
async def archive_chat_by_id(id: str, user=Depends(get_verified_user)):
chat = Chats.get_chat_by_id_and_user_id(id, user.id)
if chat:
chat = Chats.toggle_chat_archive_by_id(id)
@@ -350,7 +363,7 @@ async def archive_chat_by_id(id: str, user=Depends(get_current_user)):
@router.post("/{id}/share", response_model=Optional[ChatResponse])
async def share_chat_by_id(id: str, user=Depends(get_current_user)):
async def share_chat_by_id(id: str, user=Depends(get_verified_user)):
chat = Chats.get_chat_by_id_and_user_id(id, user.id)
if chat:
if chat.share_id:
@@ -382,7 +395,7 @@ async def share_chat_by_id(id: str, user=Depends(get_current_user)):
@router.delete("/{id}/share", response_model=Optional[bool])
async def delete_shared_chat_by_id(id: str, user=Depends(get_current_user)):
async def delete_shared_chat_by_id(id: str, user=Depends(get_verified_user)):
chat = Chats.get_chat_by_id_and_user_id(id, user.id)
if chat:
if not chat.share_id:
@@ -404,8 +417,8 @@ async def delete_shared_chat_by_id(id: str, user=Depends(get_current_user)):
############################
@router.get("/{id}/tags", response_model=List[TagModel])
async def get_chat_tags_by_id(id: str, user=Depends(get_current_user)):
@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)
if tags != None:
@@ -423,7 +436,7 @@ async def get_chat_tags_by_id(id: str, user=Depends(get_current_user)):
@router.post("/{id}/tags", response_model=Optional[ChatIdTagModel])
async def add_chat_tag_by_id(
id: str, form_data: ChatIdTagForm, user=Depends(get_current_user)
id: str, form_data: ChatIdTagForm, user=Depends(get_verified_user)
):
tags = Tags.get_tags_by_chat_id_and_user_id(id, user.id)
@@ -450,7 +463,7 @@ async def add_chat_tag_by_id(
@router.delete("/{id}/tags", response_model=Optional[bool])
async def delete_chat_tag_by_id(
id: str, form_data: ChatIdTagForm, user=Depends(get_current_user)
id: str, form_data: ChatIdTagForm, user=Depends(get_verified_user)
):
result = Tags.delete_tag_by_tag_name_and_chat_id_and_user_id(
form_data.tag_name, id, user.id
@@ -470,7 +483,7 @@ async def delete_chat_tag_by_id(
@router.delete("/{id}/tags/all", response_model=Optional[bool])
async def delete_all_chat_tags_by_id(id: str, user=Depends(get_current_user)):
async def delete_all_chat_tags_by_id(id: str, user=Depends(get_verified_user)):
result = Tags.delete_tags_by_chat_id_and_user_id(id, user.id)
if result:
+9 -9
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
@@ -14,7 +14,7 @@ from apps.webui.models.users import Users
from utils.utils import (
get_password_hash,
get_current_user,
get_verified_user,
get_admin_user,
create_token,
)
@@ -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,9 +81,9 @@ 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_current_user),
user=Depends(get_verified_user),
):
return request.app.state.config.BANNERS
+15 -13
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
@@ -14,7 +14,7 @@ from apps.webui.models.documents import (
DocumentResponse,
)
from utils.utils import get_current_user, get_admin_user
from utils.utils import get_verified_user, get_admin_user
from constants import ERROR_MESSAGES
router = APIRouter()
@@ -24,8 +24,8 @@ router = APIRouter()
############################
@router.get("/", response_model=List[DocumentResponse])
async def get_documents(user=Depends(get_current_user)):
@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_current_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:
@@ -73,8 +73,8 @@ async def create_new_doc(form_data: DocumentForm, user=Depends(get_admin_user)):
############################
@router.get("/name/{name}", response_model=Optional[DocumentResponse])
async def get_doc_by_name(name: str, user=Depends(get_current_user)):
@router.get("/doc", response_model=Optional[DocumentResponse])
async def get_doc_by_name(name: str, user=Depends(get_verified_user)):
doc = Documents.get_doc_by_name(name)
if doc:
@@ -102,11 +102,11 @@ class TagItem(BaseModel):
class TagDocumentForm(BaseModel):
name: str
tags: List[dict]
tags: list[dict]
@router.post("/name/{name}/tags", response_model=Optional[DocumentResponse])
async def tag_doc_by_name(form_data: TagDocumentForm, user=Depends(get_current_user)):
@router.post("/doc/tags", response_model=Optional[DocumentResponse])
async def tag_doc_by_name(form_data: TagDocumentForm, user=Depends(get_verified_user)):
doc = Documents.update_doc_content_by_name(form_data.name, {"tags": form_data.tags})
if doc:
@@ -128,9 +128,11 @@ async def tag_doc_by_name(form_data: TagDocumentForm, user=Depends(get_current_u
############################
@router.post("/name/{name}/update", response_model=Optional[DocumentResponse])
@router.post("/doc/update", response_model=Optional[DocumentResponse])
async def update_doc_by_name(
name: str, form_data: DocumentUpdateForm, user=Depends(get_admin_user)
name: str,
form_data: DocumentUpdateForm,
user=Depends(get_admin_user),
):
doc = Documents.update_doc_by_name(name, form_data)
if doc:
@@ -152,7 +154,7 @@ async def update_doc_by_name(
############################
@router.delete("/name/{name}/delete", response_model=bool)
@router.delete("/doc/delete", response_model=bool)
async def delete_doc_by_name(name: str, user=Depends(get_admin_user)):
result = Documents.delete_doc_by_name(name)
return result
+241
View File
@@ -0,0 +1,241 @@
from fastapi import (
Depends,
FastAPI,
HTTPException,
status,
Request,
UploadFile,
File,
Form,
)
from datetime import datetime, timedelta
from typing import Union, Optional
from pathlib import Path
from fastapi import APIRouter
from fastapi.responses import StreamingResponse, JSONResponse, FileResponse
from pydantic import BaseModel
import json
from apps.webui.models.files import (
Files,
FileForm,
FileModel,
FileModelResponse,
)
from utils.utils import get_verified_user, get_admin_user
from constants import ERROR_MESSAGES
from importlib import util
import os
import uuid
import os, shutil, logging, re
from config import SRC_LOG_LEVELS, UPLOAD_DIR
log = logging.getLogger(__name__)
log.setLevel(SRC_LOG_LEVELS["MODELS"])
router = APIRouter()
############################
# Upload File
############################
@router.post("/")
def upload_file(file: UploadFile = File(...), user=Depends(get_verified_user)):
log.info(f"file.content_type: {file.content_type}")
try:
unsanitized_filename = file.filename
filename = os.path.basename(unsanitized_filename)
# replace filename with uuid
id = str(uuid.uuid4())
name = filename
filename = f"{id}_{filename}"
file_path = f"{UPLOAD_DIR}/{filename}"
contents = file.file.read()
with open(file_path, "wb") as f:
f.write(contents)
f.close()
file = Files.insert_new_file(
user.id,
FileForm(
**{
"id": id,
"filename": filename,
"meta": {
"name": name,
"content_type": file.content_type,
"size": len(contents),
"path": file_path,
},
}
),
)
if file:
return file
else:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.DEFAULT("Error uploading file"),
)
except Exception as e:
log.exception(e)
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.DEFAULT(e),
)
############################
# List Files
############################
@router.get("/", response_model=list[FileModel])
async def list_files(user=Depends(get_verified_user)):
files = Files.get_files()
return files
############################
# Delete All Files
############################
@router.delete("/all")
async def delete_all_files(user=Depends(get_admin_user)):
result = Files.delete_all_files()
if result:
folder = f"{UPLOAD_DIR}"
try:
# Check if the directory exists
if os.path.exists(folder):
# Iterate over all the files and directories in the specified directory
for filename in os.listdir(folder):
file_path = os.path.join(folder, filename)
try:
if os.path.isfile(file_path) or os.path.islink(file_path):
os.unlink(file_path) # Remove the file or link
elif os.path.isdir(file_path):
shutil.rmtree(file_path) # Remove the directory
except Exception as e:
print(f"Failed to delete {file_path}. Reason: {e}")
else:
print(f"The directory {folder} does not exist")
except Exception as e:
print(f"Failed to process the directory {folder}. Reason: {e}")
return {"message": "All files deleted successfully"}
else:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.DEFAULT("Error deleting files"),
)
############################
# Get File By Id
############################
@router.get("/{id}", response_model=Optional[FileModel])
async def get_file_by_id(id: str, user=Depends(get_verified_user)):
file = Files.get_file_by_id(id)
if file:
return file
else:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=ERROR_MESSAGES.NOT_FOUND,
)
############################
# Get File Content By Id
############################
@router.get("/{id}/content", response_model=Optional[FileModel])
async def get_file_content_by_id(id: str, user=Depends(get_verified_user)):
file = Files.get_file_by_id(id)
if file:
file_path = Path(file.meta["path"])
# Check if the file already exists in the cache
if file_path.is_file():
print(f"file_path: {file_path}")
return FileResponse(file_path)
else:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=ERROR_MESSAGES.NOT_FOUND,
)
else:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=ERROR_MESSAGES.NOT_FOUND,
)
@router.get("/{id}/content/{file_name}", response_model=Optional[FileModel])
async def get_file_content_by_id(id: str, user=Depends(get_verified_user)):
file = Files.get_file_by_id(id)
if file:
file_path = Path(file.meta["path"])
# Check if the file already exists in the cache
if file_path.is_file():
print(f"file_path: {file_path}")
return FileResponse(file_path)
else:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=ERROR_MESSAGES.NOT_FOUND,
)
else:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=ERROR_MESSAGES.NOT_FOUND,
)
############################
# Delete File By Id
############################
@router.delete("/{id}")
async def delete_file_by_id(id: str, user=Depends(get_verified_user)):
file = Files.get_file_by_id(id)
if file:
result = Files.delete_file_by_id(id)
if result:
return {"message": "File deleted successfully"}
else:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.DEFAULT("Error deleting file"),
)
else:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=ERROR_MESSAGES.NOT_FOUND,
)
+426
View File
@@ -0,0 +1,426 @@
from fastapi import Depends, FastAPI, HTTPException, status, Request
from datetime import datetime, timedelta
from typing import Union, Optional
from fastapi import APIRouter
from pydantic import BaseModel
import json
from apps.webui.models.functions import (
Functions,
FunctionForm,
FunctionModel,
FunctionResponse,
)
from apps.webui.utils import load_function_module_by_id
from utils.utils import get_verified_user, get_admin_user
from constants import ERROR_MESSAGES
from importlib import util
import os
from pathlib import Path
from config import DATA_DIR, CACHE_DIR, FUNCTIONS_DIR
router = APIRouter()
############################
# GetFunctions
############################
@router.get("/", response_model=list[FunctionResponse])
async def get_functions(user=Depends(get_verified_user)):
return Functions.get_functions()
############################
# ExportFunctions
############################
@router.get("/export", response_model=list[FunctionModel])
async def get_functions(user=Depends(get_admin_user)):
return Functions.get_functions()
############################
# CreateNewFunction
############################
@router.post("/create", response_model=Optional[FunctionResponse])
async def create_new_function(
request: Request, form_data: FunctionForm, user=Depends(get_admin_user)
):
if not form_data.id.isidentifier():
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Only alphanumeric characters and underscores are allowed in the id",
)
form_data.id = form_data.id.lower()
function = Functions.get_function_by_id(form_data.id)
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:
function_file.write(form_data.content)
function_module, function_type, frontmatter = load_function_module_by_id(
form_data.id
)
form_data.meta.manifest = frontmatter
FUNCTIONS = request.app.state.FUNCTIONS
FUNCTIONS[form_data.id] = function_module
function = Functions.insert_new_function(user.id, function_type, form_data)
function_cache_dir = Path(CACHE_DIR) / "functions" / form_data.id
function_cache_dir.mkdir(parents=True, exist_ok=True)
if function:
return function
else:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.DEFAULT("Error creating function"),
)
except Exception as e:
print(e)
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.DEFAULT(e),
)
else:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.ID_TAKEN,
)
############################
# GetFunctionById
############################
@router.get("/id/{id}", response_model=Optional[FunctionModel])
async def get_function_by_id(id: str, user=Depends(get_admin_user)):
function = Functions.get_function_by_id(id)
if function:
return function
else:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=ERROR_MESSAGES.NOT_FOUND,
)
############################
# ToggleFunctionById
############################
@router.post("/id/{id}/toggle", response_model=Optional[FunctionModel])
async def toggle_function_by_id(id: str, user=Depends(get_admin_user)):
function = Functions.get_function_by_id(id)
if function:
function = Functions.update_function_by_id(
id, {"is_active": not function.is_active}
)
if function:
return function
else:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.DEFAULT("Error updating function"),
)
else:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=ERROR_MESSAGES.NOT_FOUND,
)
############################
# ToggleGlobalById
############################
@router.post("/id/{id}/toggle/global", response_model=Optional[FunctionModel])
async def toggle_global_by_id(id: str, user=Depends(get_admin_user)):
function = Functions.get_function_by_id(id)
if function:
function = Functions.update_function_by_id(
id, {"is_global": not function.is_global}
)
if function:
return function
else:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.DEFAULT("Error updating function"),
)
else:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=ERROR_MESSAGES.NOT_FOUND,
)
############################
# UpdateFunctionById
############################
@router.post("/id/{id}/update", response_model=Optional[FunctionModel])
async def update_function_by_id(
request: Request, id: str, form_data: FunctionForm, user=Depends(get_admin_user)
):
function_path = os.path.join(FUNCTIONS_DIR, f"{id}.py")
try:
with open(function_path, "w") as function_file:
function_file.write(form_data.content)
function_module, function_type, frontmatter = load_function_module_by_id(id)
form_data.meta.manifest = frontmatter
FUNCTIONS = request.app.state.FUNCTIONS
FUNCTIONS[id] = function_module
updated = {**form_data.model_dump(exclude={"id"}), "type": function_type}
print(updated)
function = Functions.update_function_by_id(id, updated)
if function:
return function
else:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.DEFAULT("Error updating function"),
)
except Exception as e:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.DEFAULT(e),
)
############################
# DeleteFunctionById
############################
@router.delete("/id/{id}/delete", response_model=bool)
async def delete_function_by_id(
request: Request, id: str, user=Depends(get_admin_user)
):
result = Functions.delete_function_by_id(id)
if result:
FUNCTIONS = request.app.state.FUNCTIONS
if id in FUNCTIONS:
del FUNCTIONS[id]
# delete the function file
function_path = os.path.join(FUNCTIONS_DIR, f"{id}.py")
try:
os.remove(function_path)
except Exception:
pass
return result
############################
# GetFunctionValves
############################
@router.get("/id/{id}/valves", response_model=Optional[dict])
async def get_function_valves_by_id(id: str, user=Depends(get_admin_user)):
function = Functions.get_function_by_id(id)
if function:
try:
valves = Functions.get_function_valves_by_id(id)
return valves
except Exception as e:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.DEFAULT(e),
)
else:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=ERROR_MESSAGES.NOT_FOUND,
)
############################
# GetFunctionValvesSpec
############################
@router.get("/id/{id}/valves/spec", response_model=Optional[dict])
async def get_function_valves_spec_by_id(
request: Request, id: str, user=Depends(get_admin_user)
):
function = Functions.get_function_by_id(id)
if function:
if id in request.app.state.FUNCTIONS:
function_module = request.app.state.FUNCTIONS[id]
else:
function_module, function_type, frontmatter = load_function_module_by_id(id)
request.app.state.FUNCTIONS[id] = function_module
if hasattr(function_module, "Valves"):
Valves = function_module.Valves
return Valves.schema()
return None
else:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=ERROR_MESSAGES.NOT_FOUND,
)
############################
# UpdateFunctionValves
############################
@router.post("/id/{id}/valves/update", response_model=Optional[dict])
async def update_function_valves_by_id(
request: Request, id: str, form_data: dict, user=Depends(get_admin_user)
):
function = Functions.get_function_by_id(id)
if function:
if id in request.app.state.FUNCTIONS:
function_module = request.app.state.FUNCTIONS[id]
else:
function_module, function_type, frontmatter = load_function_module_by_id(id)
request.app.state.FUNCTIONS[id] = function_module
if hasattr(function_module, "Valves"):
Valves = function_module.Valves
try:
form_data = {k: v for k, v in form_data.items() if v is not None}
valves = Valves(**form_data)
Functions.update_function_valves_by_id(id, valves.model_dump())
return valves.model_dump()
except Exception as e:
print(e)
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.DEFAULT(e),
)
else:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=ERROR_MESSAGES.NOT_FOUND,
)
else:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=ERROR_MESSAGES.NOT_FOUND,
)
############################
# FunctionUserValves
############################
@router.get("/id/{id}/valves/user", response_model=Optional[dict])
async def get_function_user_valves_by_id(id: str, user=Depends(get_verified_user)):
function = Functions.get_function_by_id(id)
if function:
try:
user_valves = Functions.get_user_valves_by_id_and_user_id(id, user.id)
return user_valves
except Exception as e:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.DEFAULT(e),
)
else:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=ERROR_MESSAGES.NOT_FOUND,
)
@router.get("/id/{id}/valves/user/spec", response_model=Optional[dict])
async def get_function_user_valves_spec_by_id(
request: Request, id: str, user=Depends(get_verified_user)
):
function = Functions.get_function_by_id(id)
if function:
if id in request.app.state.FUNCTIONS:
function_module = request.app.state.FUNCTIONS[id]
else:
function_module, function_type, frontmatter = load_function_module_by_id(id)
request.app.state.FUNCTIONS[id] = function_module
if hasattr(function_module, "UserValves"):
UserValves = function_module.UserValves
return UserValves.schema()
return None
else:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=ERROR_MESSAGES.NOT_FOUND,
)
@router.post("/id/{id}/valves/user/update", response_model=Optional[dict])
async def update_function_user_valves_by_id(
request: Request, id: str, form_data: dict, user=Depends(get_verified_user)
):
function = Functions.get_function_by_id(id)
if function:
if id in request.app.state.FUNCTIONS:
function_module = request.app.state.FUNCTIONS[id]
else:
function_module, function_type, frontmatter = load_function_module_by_id(id)
request.app.state.FUNCTIONS[id] = function_module
if hasattr(function_module, "UserValves"):
UserValves = function_module.UserValves
try:
form_data = {k: v for k, v in form_data.items() if v is not None}
user_valves = UserValves(**form_data)
Functions.update_user_valves_by_id_and_user_id(
id, user.id, user_valves.model_dump()
)
return user_valves.model_dump()
except Exception as e:
print(e)
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.DEFAULT(e),
)
else:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=ERROR_MESSAGES.NOT_FOUND,
)
else:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=ERROR_MESSAGES.NOT_FOUND,
)
+39 -4
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)
@@ -44,9 +44,15 @@ class AddMemoryForm(BaseModel):
content: str
class MemoryUpdateModel(BaseModel):
content: Optional[str] = None
@router.post("/add", response_model=Optional[MemoryModel])
async def add_memory(
request: Request, form_data: AddMemoryForm, user=Depends(get_verified_user)
request: Request,
form_data: AddMemoryForm,
user=Depends(get_verified_user),
):
memory = Memories.insert_new_memory(user.id, form_data.content)
memory_embedding = request.app.state.EMBEDDING_FUNCTION(memory.content)
@@ -62,6 +68,34 @@ async def add_memory(
return memory
@router.post("/{memory_id}/update", response_model=Optional[MemoryModel])
async def update_memory_by_id(
memory_id: str,
request: Request,
form_data: MemoryUpdateModel,
user=Depends(get_verified_user),
):
memory = Memories.update_memory_by_id(memory_id, form_data.content)
if memory is None:
raise HTTPException(status_code=404, detail="Memory not found")
if form_data.content is not None:
memory_embedding = request.app.state.EMBEDDING_FUNCTION(form_data.content)
collection = CHROMA_CLIENT.get_or_create_collection(
name=f"user-memory-{user.id}"
)
collection.upsert(
documents=[form_data.content],
ids=[memory.id],
embeddings=[memory_embedding],
metadatas=[
{"created_at": memory.created_at, "updated_at": memory.updated_at}
],
)
return memory
############################
# QueryMemory
############################
@@ -69,6 +103,7 @@ async def add_memory(
class QueryMemoryForm(BaseModel):
content: str
k: Optional[int] = 1
@router.post("/query")
@@ -80,7 +115,7 @@ async def query_memory(
results = collection.query(
query_embeddings=[query_embedding],
n_results=1, # how many results to return
n_results=form_data.k, # how many results to return
)
return results
+10 -4
View File
@@ -1,10 +1,11 @@
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
import json
from apps.webui.models.models import Models, ModelModel, ModelForm, ModelResponse
from utils.utils import get_verified_user, get_admin_user
@@ -17,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()
@@ -29,7 +30,9 @@ async def get_models(user=Depends(get_verified_user)):
@router.post("/add", response_model=Optional[ModelModel])
async def add_new_model(
request: Request, form_data: ModelForm, user=Depends(get_admin_user)
request: Request,
form_data: ModelForm,
user=Depends(get_admin_user),
):
if form_data.id in request.app.state.MODELS:
raise HTTPException(
@@ -73,7 +76,10 @@ async def get_model_by_id(id: str, user=Depends(get_verified_user)):
@router.post("/update", response_model=Optional[ModelModel])
async def update_model_by_id(
request: Request, id: str, form_data: ModelForm, user=Depends(get_admin_user)
request: Request,
id: str,
form_data: ModelForm,
user=Depends(get_admin_user),
):
model = Models.get_model_by_id(id)
if model:
+9 -7
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
@@ -8,7 +8,7 @@ import json
from apps.webui.models.prompts import Prompts, PromptForm, PromptModel
from utils.utils import get_current_user, get_admin_user
from utils.utils import get_verified_user, get_admin_user
from constants import ERROR_MESSAGES
router = APIRouter()
@@ -18,8 +18,8 @@ router = APIRouter()
############################
@router.get("/", response_model=List[PromptModel])
async def get_prompts(user=Depends(get_current_user)):
@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_current_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:
@@ -52,7 +52,7 @@ async def create_new_prompt(form_data: PromptForm, user=Depends(get_admin_user))
@router.get("/command/{command}", response_model=Optional[PromptModel])
async def get_prompt_by_command(command: str, user=Depends(get_current_user)):
async def get_prompt_by_command(command: str, user=Depends(get_verified_user)):
prompt = Prompts.get_prompt_by_command(f"/{command}")
if prompt:
@@ -71,7 +71,9 @@ async def get_prompt_by_command(command: str, user=Depends(get_current_user)):
@router.post("/command/{command}/update", response_model=Optional[PromptModel])
async def update_prompt_by_command(
command: str, form_data: PromptForm, user=Depends(get_admin_user)
command: str,
form_data: PromptForm,
user=Depends(get_admin_user),
):
prompt = Prompts.update_prompt_by_command(f"/{command}", form_data)
if prompt:
+374
View File
@@ -0,0 +1,374 @@
from fastapi import Depends, HTTPException, status, Request
from typing import Optional
from fastapi import APIRouter
from apps.webui.models.tools import Tools, ToolForm, ToolModel, ToolResponse
from apps.webui.utils import load_toolkit_module_by_id
from utils.utils import get_admin_user, get_verified_user
from utils.tools import get_tools_specs
from constants import ERROR_MESSAGES
import os
from pathlib import Path
from config import DATA_DIR, CACHE_DIR
TOOLS_DIR = f"{DATA_DIR}/tools"
os.makedirs(TOOLS_DIR, exist_ok=True)
router = APIRouter()
############################
# GetToolkits
############################
@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
############################
# ExportToolKits
############################
@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
############################
# CreateNewToolKit
############################
@router.post("/create", response_model=Optional[ToolResponse])
async def create_new_toolkit(
request: Request,
form_data: ToolForm,
user=Depends(get_admin_user),
):
if not form_data.id.isidentifier():
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Only alphanumeric characters and underscores are allowed in the id",
)
form_data.id = form_data.id.lower()
toolkit = Tools.get_tool_by_id(form_data.id)
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:
tool_file.write(form_data.content)
toolkit_module, frontmatter = load_toolkit_module_by_id(form_data.id)
form_data.meta.manifest = frontmatter
TOOLS = request.app.state.TOOLS
TOOLS[form_data.id] = toolkit_module
specs = get_tools_specs(TOOLS[form_data.id])
toolkit = Tools.insert_new_tool(user.id, form_data, specs)
tool_cache_dir = Path(CACHE_DIR) / "tools" / form_data.id
tool_cache_dir.mkdir(parents=True, exist_ok=True)
if toolkit:
return toolkit
else:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.DEFAULT("Error creating toolkit"),
)
except Exception as e:
print(e)
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.DEFAULT(str(e)),
)
else:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.ID_TAKEN,
)
############################
# GetToolkitById
############################
@router.get("/id/{id}", response_model=Optional[ToolModel])
async def get_toolkit_by_id(id: str, user=Depends(get_admin_user)):
toolkit = Tools.get_tool_by_id(id)
if toolkit:
return toolkit
else:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=ERROR_MESSAGES.NOT_FOUND,
)
############################
# UpdateToolkitById
############################
@router.post("/id/{id}/update", response_model=Optional[ToolModel])
async def update_toolkit_by_id(
request: Request,
id: str,
form_data: ToolForm,
user=Depends(get_admin_user),
):
toolkit_path = os.path.join(TOOLS_DIR, f"{id}.py")
try:
with open(toolkit_path, "w") as tool_file:
tool_file.write(form_data.content)
toolkit_module, frontmatter = load_toolkit_module_by_id(id)
form_data.meta.manifest = frontmatter
TOOLS = request.app.state.TOOLS
TOOLS[id] = toolkit_module
specs = get_tools_specs(TOOLS[id])
updated = {
**form_data.model_dump(exclude={"id"}),
"specs": specs,
}
print(updated)
toolkit = Tools.update_tool_by_id(id, updated)
if toolkit:
return toolkit
else:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.DEFAULT("Error updating toolkit"),
)
except Exception as e:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.DEFAULT(str(e)),
)
############################
# DeleteToolkitById
############################
@router.delete("/id/{id}/delete", response_model=bool)
async def delete_toolkit_by_id(request: Request, id: str, user=Depends(get_admin_user)):
result = Tools.delete_tool_by_id(id)
if result:
TOOLS = request.app.state.TOOLS
if id in TOOLS:
del TOOLS[id]
# delete the toolkit file
toolkit_path = os.path.join(TOOLS_DIR, f"{id}.py")
os.remove(toolkit_path)
return result
############################
# GetToolValves
############################
@router.get("/id/{id}/valves", response_model=Optional[dict])
async def get_toolkit_valves_by_id(id: str, user=Depends(get_admin_user)):
toolkit = Tools.get_tool_by_id(id)
if toolkit:
try:
valves = Tools.get_tool_valves_by_id(id)
return valves
except Exception as e:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.DEFAULT(str(e)),
)
else:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=ERROR_MESSAGES.NOT_FOUND,
)
############################
# GetToolValvesSpec
############################
@router.get("/id/{id}/valves/spec", response_model=Optional[dict])
async def get_toolkit_valves_spec_by_id(
request: Request, id: str, user=Depends(get_admin_user)
):
toolkit = Tools.get_tool_by_id(id)
if toolkit:
if id in request.app.state.TOOLS:
toolkit_module = request.app.state.TOOLS[id]
else:
toolkit_module, _ = load_toolkit_module_by_id(id)
request.app.state.TOOLS[id] = toolkit_module
if hasattr(toolkit_module, "Valves"):
Valves = toolkit_module.Valves
return Valves.schema()
return None
else:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=ERROR_MESSAGES.NOT_FOUND,
)
############################
# UpdateToolValves
############################
@router.post("/id/{id}/valves/update", response_model=Optional[dict])
async def update_toolkit_valves_by_id(
request: Request, id: str, form_data: dict, user=Depends(get_admin_user)
):
toolkit = Tools.get_tool_by_id(id)
if toolkit:
if id in request.app.state.TOOLS:
toolkit_module = request.app.state.TOOLS[id]
else:
toolkit_module, _ = load_toolkit_module_by_id(id)
request.app.state.TOOLS[id] = toolkit_module
if hasattr(toolkit_module, "Valves"):
Valves = toolkit_module.Valves
try:
form_data = {k: v for k, v in form_data.items() if v is not None}
valves = Valves(**form_data)
Tools.update_tool_valves_by_id(id, valves.model_dump())
return valves.model_dump()
except Exception as e:
print(e)
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.DEFAULT(str(e)),
)
else:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=ERROR_MESSAGES.NOT_FOUND,
)
else:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=ERROR_MESSAGES.NOT_FOUND,
)
############################
# ToolUserValves
############################
@router.get("/id/{id}/valves/user", response_model=Optional[dict])
async def get_toolkit_user_valves_by_id(id: str, user=Depends(get_verified_user)):
toolkit = Tools.get_tool_by_id(id)
if toolkit:
try:
user_valves = Tools.get_user_valves_by_id_and_user_id(id, user.id)
return user_valves
except Exception as e:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.DEFAULT(str(e)),
)
else:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=ERROR_MESSAGES.NOT_FOUND,
)
@router.get("/id/{id}/valves/user/spec", response_model=Optional[dict])
async def get_toolkit_user_valves_spec_by_id(
request: Request, id: str, user=Depends(get_verified_user)
):
toolkit = Tools.get_tool_by_id(id)
if toolkit:
if id in request.app.state.TOOLS:
toolkit_module = request.app.state.TOOLS[id]
else:
toolkit_module, _ = load_toolkit_module_by_id(id)
request.app.state.TOOLS[id] = toolkit_module
if hasattr(toolkit_module, "UserValves"):
UserValves = toolkit_module.UserValves
return UserValves.schema()
return None
else:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=ERROR_MESSAGES.NOT_FOUND,
)
@router.post("/id/{id}/valves/user/update", response_model=Optional[dict])
async def update_toolkit_user_valves_by_id(
request: Request, id: str, form_data: dict, user=Depends(get_verified_user)
):
toolkit = Tools.get_tool_by_id(id)
if toolkit:
if id in request.app.state.TOOLS:
toolkit_module = request.app.state.TOOLS[id]
else:
toolkit_module, _ = load_toolkit_module_by_id(id)
request.app.state.TOOLS[id] = toolkit_module
if hasattr(toolkit_module, "UserValves"):
UserValves = toolkit_module.UserValves
try:
form_data = {k: v for k, v in form_data.items() if v is not None}
user_valves = UserValves(**form_data)
Tools.update_user_valves_by_id_and_user_id(
id, user.id, user_valves.model_dump()
)
return user_valves.model_dump()
except Exception as e:
print(e)
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.DEFAULT(str(e)),
)
else:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=ERROR_MESSAGES.NOT_FOUND,
)
else:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=ERROR_MESSAGES.NOT_FOUND,
)
+51 -3
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)
@@ -115,6 +115,52 @@ async def update_user_settings_by_session_user(
)
############################
# GetUserInfoBySessionUser
############################
@router.get("/user/info", response_model=Optional[dict])
async def get_user_info_by_session_user(user=Depends(get_verified_user)):
user = Users.get_user_by_id(user.id)
if user:
return user.info
else:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.USER_NOT_FOUND,
)
############################
# UpdateUserInfoBySessionUser
############################
@router.post("/user/info/update", response_model=Optional[dict])
async def update_user_info_by_session_user(
form_data: dict, user=Depends(get_verified_user)
):
user = Users.get_user_by_id(user.id)
if user:
if user.info is None:
user.info = {}
user = Users.update_user_by_id(user.id, {"info": {**user.info, **form_data}})
if user:
return user.info
else:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.USER_NOT_FOUND,
)
else:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.USER_NOT_FOUND,
)
############################
# GetUserById
############################
@@ -159,7 +205,9 @@ async def get_user_by_id(user_id: str, user=Depends(get_verified_user)):
@router.post("/{user_id}/update", response_model=Optional[UserModel])
async def update_user_by_id(
user_id: str, form_data: UserUpdateForm, session_user=Depends(get_admin_user)
user_id: str,
form_data: UserUpdateForm,
session_user=Depends(get_admin_user),
):
user = Users.get_user_by_id(user_id)
+40 -9
View File
@@ -1,20 +1,23 @@
from pathlib import Path
import site
from fastapi import APIRouter, UploadFile, File, Response
from fastapi import Depends, HTTPException, status
from peewee import SqliteDatabase
from starlette.responses import StreamingResponse, FileResponse
from pydantic import BaseModel
from fpdf import FPDF
import markdown
import black
from apps.webui.internal.db import DB
from utils.utils import get_admin_user
from utils.misc import calculate_sha256, get_gravatar_url
from config import OLLAMA_BASE_URLS, DATA_DIR, UPLOAD_DIR, ENABLE_ADMIN_EXPORT
from constants import ERROR_MESSAGES
from typing import List
router = APIRouter()
@@ -26,6 +29,21 @@ async def get_gravatar(
return get_gravatar_url(email)
class CodeFormatRequest(BaseModel):
code: str
@router.post("/code/format")
async def format_code(request: CodeFormatRequest):
try:
formatted_code = black.format_str(request.code, mode=black.Mode())
return {"code": formatted_code}
except black.NothingChanged:
return {"code": request.code}
except Exception as e:
raise HTTPException(status_code=400, detail=str(e))
class MarkdownForm(BaseModel):
md: str
@@ -39,7 +57,7 @@ async def get_html_from_markdown(
class ChatForm(BaseModel):
title: str
messages: List[dict]
messages: list[dict]
@router.post("/pdf")
@@ -49,17 +67,28 @@ 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")
pdf.add_font("NotoSans", "i", f"{FONTS_DIR}/NotoSans-Italic.ttf")
pdf.add_font("NotoSansKR", "", f"{FONTS_DIR}/NotoSansKR-Regular.ttf")
pdf.add_font("NotoSansJP", "", f"{FONTS_DIR}/NotoSansJP-Regular.ttf")
pdf.add_font("NotoSansSC", "", f"{FONTS_DIR}/NotoSansSC-Regular.ttf")
pdf.set_font("NotoSans", size=12)
pdf.set_fallback_fonts(["NotoSansKR", "NotoSansJP"])
pdf.set_fallback_fonts(["NotoSansKR", "NotoSansJP", "NotoSansSC"])
pdf.set_auto_page_break(auto=True, margin=15)
@@ -97,13 +126,15 @@ async def download_db(user=Depends(get_admin_user)):
status_code=status.HTTP_401_UNAUTHORIZED,
detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
)
if not isinstance(DB, SqliteDatabase):
from apps.webui.internal.db import engine
if engine.name != "sqlite":
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ERROR_MESSAGES.DB_NOT_SQLITE,
)
return FileResponse(
DB.database,
engine.url.database,
media_type="application/octet-stream",
filename="webui.db",
)
+104
View File
@@ -0,0 +1,104 @@
from importlib import util
import os
import re
import sys
import subprocess
from config import TOOLS_DIR, FUNCTIONS_DIR
def extract_frontmatter(file_path):
"""
Extract frontmatter as a dictionary from the specified file path.
"""
frontmatter = {}
frontmatter_started = False
frontmatter_ended = False
frontmatter_pattern = re.compile(r"^\s*([a-z_]+):\s*(.*)\s*$", re.IGNORECASE)
try:
with open(file_path, "r", encoding="utf-8") as file:
first_line = file.readline()
if first_line.strip() != '"""':
# The file doesn't start with triple quotes
return {}
frontmatter_started = True
for line in file:
if '"""' in line:
if frontmatter_started:
frontmatter_ended = True
break
if frontmatter_started and not frontmatter_ended:
match = frontmatter_pattern.match(line)
if match:
key, value = match.groups()
frontmatter[key.strip()] = value.strip()
except FileNotFoundError:
print(f"Error: The file {file_path} does not exist.")
return {}
except Exception as e:
print(f"An error occurred: {e}")
return {}
return frontmatter
def load_toolkit_module_by_id(toolkit_id):
toolkit_path = os.path.join(TOOLS_DIR, f"{toolkit_id}.py")
spec = util.spec_from_file_location(toolkit_id, toolkit_path)
module = util.module_from_spec(spec)
frontmatter = extract_frontmatter(toolkit_path)
try:
install_frontmatter_requirements(frontmatter.get("requirements", ""))
spec.loader.exec_module(module)
print(f"Loaded module: {module.__name__}")
if hasattr(module, "Tools"):
return module.Tools(), frontmatter
else:
raise Exception("No Tools class found")
except Exception as e:
print(f"Error loading module: {toolkit_id}")
# Move the file to the error folder
os.rename(toolkit_path, f"{toolkit_path}.error")
raise e
def load_function_module_by_id(function_id):
function_path = os.path.join(FUNCTIONS_DIR, f"{function_id}.py")
spec = util.spec_from_file_location(function_id, function_path)
module = util.module_from_spec(spec)
frontmatter = extract_frontmatter(function_path)
try:
install_frontmatter_requirements(frontmatter.get("requirements", ""))
spec.loader.exec_module(module)
print(f"Loaded module: {module.__name__}")
if hasattr(module, "Pipe"):
return module.Pipe(), "pipe", frontmatter
elif hasattr(module, "Filter"):
return module.Filter(), "filter", frontmatter
elif hasattr(module, "Action"):
return module.Action(), "action", frontmatter
else:
raise Exception("No Function class found")
except Exception as e:
print(f"Error loading module: {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.")
+640 -36
View File
@@ -3,11 +3,12 @@ import sys
import logging
import importlib.metadata
import pkgutil
from urllib.parse import urlparse
import chromadb
from chromadb import Settings
from base64 import b64encode
from bs4 import BeautifulSoup
from typing import TypeVar, Generic, Union
from typing import TypeVar, Generic
from pydantic import BaseModel
from typing import Optional
@@ -19,7 +20,6 @@ import markdown
import requests
import shutil
from secrets import token_bytes
from constants import ERROR_MESSAGES
####################################
@@ -79,6 +79,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)"
@@ -96,7 +106,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:
@@ -129,7 +139,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()
@@ -166,6 +176,11 @@ for version in soup.find_all("h2"):
CHANGELOG = changelog_json
####################################
# SAFE_MODE
####################################
SAFE_MODE = os.environ.get("SAFE_MODE", "false").lower() == "true"
####################################
# WEBUI_BUILD_HASH
@@ -188,12 +203,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 = {}
@@ -294,22 +309,206 @@ WEBUI_AUTH = os.environ.get("WEBUI_AUTH", "True").lower() == "true"
WEBUI_AUTH_TRUSTED_EMAIL_HEADER = os.environ.get(
"WEBUI_AUTH_TRUSTED_EMAIL_HEADER", None
)
WEBUI_AUTH_TRUSTED_NAME_HEADER = os.environ.get("WEBUI_AUTH_TRUSTED_NAME_HEADER", None)
JWT_EXPIRES_IN = PersistentConfig(
"JWT_EXPIRES_IN", "auth.jwt_expiry", os.environ.get("JWT_EXPIRES_IN", "-1")
)
####################################
# OAuth config
####################################
ENABLE_OAUTH_SIGNUP = PersistentConfig(
"ENABLE_OAUTH_SIGNUP",
"oauth.enable_signup",
os.environ.get("ENABLE_OAUTH_SIGNUP", "False").lower() == "true",
)
OAUTH_MERGE_ACCOUNTS_BY_EMAIL = PersistentConfig(
"OAUTH_MERGE_ACCOUNTS_BY_EMAIL",
"oauth.merge_accounts_by_email",
os.environ.get("OAUTH_MERGE_ACCOUNTS_BY_EMAIL", "False").lower() == "true",
)
OAUTH_PROVIDERS = {}
GOOGLE_CLIENT_ID = PersistentConfig(
"GOOGLE_CLIENT_ID",
"oauth.google.client_id",
os.environ.get("GOOGLE_CLIENT_ID", ""),
)
GOOGLE_CLIENT_SECRET = PersistentConfig(
"GOOGLE_CLIENT_SECRET",
"oauth.google.client_secret",
os.environ.get("GOOGLE_CLIENT_SECRET", ""),
)
GOOGLE_OAUTH_SCOPE = PersistentConfig(
"GOOGLE_OAUTH_SCOPE",
"oauth.google.scope",
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",
os.environ.get("MICROSOFT_CLIENT_ID", ""),
)
MICROSOFT_CLIENT_SECRET = PersistentConfig(
"MICROSOFT_CLIENT_SECRET",
"oauth.microsoft.client_secret",
os.environ.get("MICROSOFT_CLIENT_SECRET", ""),
)
MICROSOFT_CLIENT_TENANT_ID = PersistentConfig(
"MICROSOFT_CLIENT_TENANT_ID",
"oauth.microsoft.tenant_id",
os.environ.get("MICROSOFT_CLIENT_TENANT_ID", ""),
)
MICROSOFT_OAUTH_SCOPE = PersistentConfig(
"MICROSOFT_OAUTH_SCOPE",
"oauth.microsoft.scope",
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",
os.environ.get("OAUTH_CLIENT_ID", ""),
)
OAUTH_CLIENT_SECRET = PersistentConfig(
"OAUTH_CLIENT_SECRET",
"oauth.oidc.client_secret",
os.environ.get("OAUTH_CLIENT_SECRET", ""),
)
OPENID_PROVIDER_URL = PersistentConfig(
"OPENID_PROVIDER_URL",
"oauth.oidc.provider_url",
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",
os.environ.get("OAUTH_SCOPES", "openid email profile"),
)
OAUTH_PROVIDER_NAME = PersistentConfig(
"OAUTH_PROVIDER_NAME",
"oauth.oidc.provider_name",
os.environ.get("OAUTH_PROVIDER_NAME", "SSO"),
)
OAUTH_USERNAME_CLAIM = PersistentConfig(
"OAUTH_USERNAME_CLAIM",
"oauth.oidc.username_claim",
os.environ.get("OAUTH_USERNAME_CLAIM", "name"),
)
OAUTH_PICTURE_CLAIM = PersistentConfig(
"OAUTH_USERNAME_CLAIM",
"oauth.oidc.avatar_claim",
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()
if GOOGLE_CLIENT_ID.value and GOOGLE_CLIENT_SECRET.value:
OAUTH_PROVIDERS["google"] = {
"client_id": GOOGLE_CLIENT_ID.value,
"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 (
MICROSOFT_CLIENT_ID.value
and MICROSOFT_CLIENT_SECRET.value
and MICROSOFT_CLIENT_TENANT_ID.value
):
OAUTH_PROVIDERS["microsoft"] = {
"client_id": MICROSOFT_CLIENT_ID.value,
"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 (
OAUTH_CLIENT_ID.value
and OAUTH_CLIENT_SECRET.value
and OPENID_PROVIDER_URL.value
):
OAUTH_PROVIDERS["oidc"] = {
"client_id": OAUTH_CLIENT_ID.value,
"client_secret": OAUTH_CLIENT_SECRET.value,
"server_metadata_url": OPENID_PROVIDER_URL.value,
"scope": OAUTH_SCOPES.value,
"name": OAUTH_PROVIDER_NAME.value,
"redirect_uri": OPENID_REDIRECT_URI.value,
}
load_oauth_providers()
####################################
# Static DIR
####################################
STATIC_DIR = Path(os.getenv("STATIC_DIR", BACKEND_DIR / "static")).resolve()
frontend_favicon = FRONTEND_BUILD_DIR / "favicon.png"
frontend_favicon = FRONTEND_BUILD_DIR / "static" / "favicon.png"
if frontend_favicon.exists():
shutil.copyfile(frontend_favicon, STATIC_DIR / "favicon.png")
try:
shutil.copyfile(frontend_favicon, STATIC_DIR / "favicon.png")
except Exception as e:
logging.error(f"An error occurred: {e}")
else:
logging.warning(f"Frontend favicon not found at {frontend_favicon}")
frontend_splash = FRONTEND_BUILD_DIR / "static" / "splash.png"
if frontend_splash.exists():
try:
shutil.copyfile(frontend_splash, STATIC_DIR / "splash.png")
except Exception as e:
logging.error(f"An error occurred: {e}")
else:
logging.warning(f"Frontend splash not found at {frontend_splash}")
####################################
# CUSTOM_NAME
####################################
@@ -334,6 +533,19 @@ if CUSTOM_NAME:
r.raw.decode_content = True
shutil.copyfileobj(r.raw, f)
if "splash" in data:
url = (
f"https://api.openwebui.com{data['splash']}"
if data["splash"][0] == "/"
else data["splash"]
)
r = requests.get(url, stream=True)
if r.status_code == 200:
with open(f"{STATIC_DIR}/splash.png", "wb") as f:
r.raw.decode_content = True
shutil.copyfileobj(r.raw, f)
WEBUI_NAME = data["name"]
except Exception as e:
log.exception(e)
@@ -364,6 +576,22 @@ DOCS_DIR = os.getenv("DOCS_DIR", f"{DATA_DIR}/docs")
Path(DOCS_DIR).mkdir(parents=True, exist_ok=True)
####################################
# Tools DIR
####################################
TOOLS_DIR = os.getenv("TOOLS_DIR", f"{DATA_DIR}/tools")
Path(TOOLS_DIR).mkdir(parents=True, exist_ok=True)
####################################
# Functions DIR
####################################
FUNCTIONS_DIR = os.getenv("FUNCTIONS_DIR", f"{DATA_DIR}/functions")
Path(FUNCTIONS_DIR).mkdir(parents=True, exist_ok=True)
####################################
# LITELLM_CONFIG
####################################
@@ -413,6 +641,17 @@ OLLAMA_API_BASE_URL = os.environ.get(
)
OLLAMA_BASE_URL = os.environ.get("OLLAMA_BASE_URL", "")
AIOHTTP_CLIENT_TIMEOUT = os.environ.get("AIOHTTP_CLIENT_TIMEOUT", "")
if AIOHTTP_CLIENT_TIMEOUT == "":
AIOHTTP_CLIENT_TIMEOUT = None
else:
try:
AIOHTTP_CLIENT_TIMEOUT = int(AIOHTTP_CLIENT_TIMEOUT)
except Exception:
AIOHTTP_CLIENT_TIMEOUT = 300
K8S_FLAG = os.environ.get("K8S_FLAG", "")
USE_OLLAMA_DOCKER = os.environ.get("USE_OLLAMA_DOCKER", "false")
@@ -489,7 +728,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"
@@ -507,6 +746,19 @@ ENABLE_SIGNUP = PersistentConfig(
else os.environ.get("ENABLE_SIGNUP", "True").lower() == "true"
),
)
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",
os.environ.get("DEFAULT_LOCALE", ""),
)
DEFAULT_MODELS = PersistentConfig(
"DEFAULT_MODELS", "ui.default_models", os.environ.get("DEFAULT_MODELS", None)
)
@@ -555,10 +807,24 @@ USER_PERMISSIONS_CHAT_DELETION = (
os.environ.get("USER_PERMISSIONS_CHAT_DELETION", "True").lower() == "true"
)
USER_PERMISSIONS_CHAT_EDITING = (
os.environ.get("USER_PERMISSIONS_CHAT_EDITING", "True").lower() == "true"
)
USER_PERMISSIONS_CHAT_TEMPORARY = (
os.environ.get("USER_PERMISSIONS_CHAT_TEMPORARY", "True").lower() == "true"
)
USER_PERMISSIONS = PersistentConfig(
"USER_PERMISSIONS",
"ui.user_permissions",
{"chat": {"deletion": USER_PERMISSIONS_CHAT_DELETION}},
{
"chat": {
"deletion": USER_PERMISSIONS_CHAT_DELETION,
"editing": USER_PERMISSIONS_CHAT_EDITING,
"temporary": USER_PERMISSIONS_CHAT_TEMPORARY,
}
},
)
ENABLE_MODEL_FILTER = PersistentConfig(
@@ -579,12 +845,57 @@ 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",
os.environ.get("ENABLE_COMMUNITY_SHARING", "True").lower() == "true",
)
ENABLE_MESSAGE_RATING = PersistentConfig(
"ENABLE_MESSAGE_RATING",
"ui.enable_message_rating",
os.environ.get("ENABLE_MESSAGE_RATING", "True").lower() == "true",
)
def validate_cors_origins(origins):
for origin in origins:
if origin != "*":
validate_cors_origin(origin)
def validate_cors_origin(origin):
parsed_url = urlparse(origin)
# Check if the scheme is either http or https
if parsed_url.scheme not in ["http", "https"]:
raise ValueError(
f"Invalid scheme in CORS_ALLOW_ORIGIN: '{origin}'. Only 'http' and 'https' are allowed."
)
# Ensure that the netloc (domain + port) is present, indicating it's a valid URL
if not parsed_url.netloc:
raise ValueError(f"Invalid URL structure in CORS_ALLOW_ORIGIN: '{origin}'.")
# For production, you should only need one host as
# fastapi serves the svelte-kit built frontend and backend from the same host and port.
# To test CORS_ALLOW_ORIGIN locally, you can set something like
# CORS_ALLOW_ORIGIN=http://localhost:5173;http://localhost:8080
# in your .env file depending on your frontend port, 5173 in this case.
CORS_ALLOW_ORIGIN = os.environ.get("CORS_ALLOW_ORIGIN", "*").split(";")
if "*" in CORS_ALLOW_ORIGIN:
log.warning(
"\n\nWARNING: CORS_ALLOW_ORIGIN IS SET TO '*' - NOT RECOMMENDED FOR PRODUCTION DEPLOYMENTS.\n"
)
validate_cors_origins(CORS_ALLOW_ORIGIN)
class BannerModel(BaseModel):
id: str
@@ -595,11 +906,14 @@ class BannerModel(BaseModel):
timestamp: int
WEBUI_BANNERS = PersistentConfig(
"WEBUI_BANNERS",
"ui.banners",
[BannerModel(**banner) for banner in json.loads("[]")],
)
try:
banners = json.loads(os.environ.get("WEBUI_BANNERS", "[]"))
banners = [BannerModel(**banner) for banner in banners]
except Exception as e:
print(f"Error loading WEBUI_BANNERS: {e}")
banners = []
WEBUI_BANNERS = PersistentConfig("WEBUI_BANNERS", "ui.banners", banners)
SHOW_ADMIN_DETAILS = PersistentConfig(
@@ -615,6 +929,76 @@ ADMIN_EMAIL = PersistentConfig(
)
####################################
# TASKS
####################################
TASK_MODEL = PersistentConfig(
"TASK_MODEL",
"task.model.default",
os.environ.get("TASK_MODEL", ""),
)
TASK_MODEL_EXTERNAL = PersistentConfig(
"TASK_MODEL_EXTERNAL",
"task.model.external",
os.environ.get("TASK_MODEL_EXTERNAL", ""),
)
TITLE_GENERATION_PROMPT_TEMPLATE = PersistentConfig(
"TITLE_GENERATION_PROMPT_TEMPLATE",
"task.title.prompt_template",
os.environ.get(
"TITLE_GENERATION_PROMPT_TEMPLATE",
"""Create a concise, 3-5 word title with an emoji as a title for the prompt in the given language. Suitable Emojis for the summary can be used to enhance understanding but avoid quotation marks or special formatting. RESPOND ONLY WITH THE TITLE TEXT.
Examples of titles:
📉 Stock Market Trends
🍪 Perfect Chocolate Chip Recipe
Evolution of Music Streaming
Remote Work Productivity Tips
Artificial Intelligence in Healthcare
🎮 Video Game Development Insights
Prompt: {{prompt:middletruncate:8000}}""",
),
)
SEARCH_QUERY_GENERATION_PROMPT_TEMPLATE = PersistentConfig(
"SEARCH_QUERY_GENERATION_PROMPT_TEMPLATE",
"task.search.prompt_template",
os.environ.get(
"SEARCH_QUERY_GENERATION_PROMPT_TEMPLATE",
"""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 {{CURRENT_DATE}}.
Question:
{{prompt:end:4000}}""",
),
)
SEARCH_QUERY_PROMPT_LENGTH_THRESHOLD = PersistentConfig(
"SEARCH_QUERY_PROMPT_LENGTH_THRESHOLD",
"task.search.prompt_length_threshold",
int(
os.environ.get(
"SEARCH_QUERY_PROMPT_LENGTH_THRESHOLD",
100,
)
),
)
TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE = PersistentConfig(
"TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE",
"task.tools.prompt_template",
os.environ.get(
"TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE",
"""Available Tools: {{TOOLS}}\nReturn an empty string if no tools match the query. If a function tool matches, construct and return a JSON object in the format {\"name\": \"functionName\", \"parameters\": {\"requiredFunctionParamKey\": \"requiredFunctionParamValue\"}} using the appropriate tool and its parameters. Only return the object and limit the response to the JSON object without additional text.""",
),
)
####################################
# WEBUI_SECRET_KEY
####################################
@@ -626,9 +1010,35 @@ WEBUI_SECRET_KEY = os.environ.get(
), # DEPRECATED: remove at next major version
)
WEBUI_SESSION_COOKIE_SAME_SITE = os.environ.get(
"WEBUI_SESSION_COOKIE_SAME_SITE",
os.environ.get("WEBUI_SESSION_COOKIE_SAME_SITE", "lax"),
)
WEBUI_SESSION_COOKIE_SECURE = os.environ.get(
"WEBUI_SESSION_COOKIE_SECURE",
os.environ.get("WEBUI_SESSION_COOKIE_SECURE", "false").lower() == "true",
)
if WEBUI_AUTH and WEBUI_SECRET_KEY == "":
raise ValueError(ERROR_MESSAGES.ENV_VAR_NOT_FOUND)
####################################
# RAG document content extraction
####################################
CONTENT_EXTRACTION_ENGINE = PersistentConfig(
"CONTENT_EXTRACTION_ENGINE",
"rag.CONTENT_EXTRACTION_ENGINE",
os.environ.get("CONTENT_EXTRACTION_ENGINE", "").lower(),
)
TIKA_SERVER_URL = PersistentConfig(
"TIKA_SERVER_URL",
"rag.tika_server_url",
os.getenv("TIKA_SERVER_URL", "http://tika:9998"), # Default for sidecar deployment
)
####################################
# RAG
####################################
@@ -687,7 +1097,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"
@@ -700,7 +1110,7 @@ RAG_EMBEDDING_MODEL_TRUST_REMOTE_CODE = (
RAG_EMBEDDING_OPENAI_BATCH_SIZE = PersistentConfig(
"RAG_EMBEDDING_OPENAI_BATCH_SIZE",
"rag.embedding_openai_batch_size",
os.environ.get("RAG_EMBEDDING_OPENAI_BATCH_SIZE", 1),
int(os.environ.get("RAG_EMBEDDING_OPENAI_BATCH_SIZE", "1")),
)
RAG_RERANKING_MODEL = PersistentConfig(
@@ -709,7 +1119,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"
@@ -810,6 +1220,18 @@ RAG_WEB_SEARCH_ENGINE = PersistentConfig(
os.getenv("RAG_WEB_SEARCH_ENGINE", ""),
)
# You can provide a list of your own websites to filter after performing a web search.
# This ensures the highest level of safety and reliability of the information sources.
RAG_WEB_SEARCH_DOMAIN_FILTER_LIST = PersistentConfig(
"RAG_WEB_SEARCH_DOMAIN_FILTER_LIST",
"rag.rag.web.search.domain.filter_list",
[
# "wikipedia.com",
# "wikimedia.org",
# "wikidata.org",
],
)
SEARXNG_QUERY_URL = PersistentConfig(
"SEARXNG_QUERY_URL",
"rag.web.search.searxng_query_url",
@@ -852,6 +1274,17 @@ SERPER_API_KEY = PersistentConfig(
os.getenv("SERPER_API_KEY", ""),
)
SERPLY_API_KEY = PersistentConfig(
"SERPLY_API_KEY",
"rag.web.search.serply_api_key",
os.getenv("SERPLY_API_KEY", ""),
)
TAVILY_API_KEY = PersistentConfig(
"TAVILY_API_KEY",
"rag.web.search.tavily_api_key",
os.getenv("TAVILY_API_KEY", ""),
)
RAG_WEB_SEARCH_RESULT_COUNT = PersistentConfig(
"RAG_WEB_SEARCH_RESULT_COUNT",
@@ -884,7 +1317,7 @@ WHISPER_MODEL_AUTO_UPDATE = (
IMAGE_GENERATION_ENGINE = PersistentConfig(
"IMAGE_GENERATION_ENGINE",
"image_generation.engine",
os.getenv("IMAGE_GENERATION_ENGINE", ""),
os.getenv("IMAGE_GENERATION_ENGINE", "openai"),
)
ENABLE_IMAGE_GENERATION = PersistentConfig(
@@ -897,6 +1330,11 @@ AUTOMATIC1111_BASE_URL = PersistentConfig(
"image_generation.automatic1111.base_url",
os.getenv("AUTOMATIC1111_BASE_URL", ""),
)
AUTOMATIC1111_API_AUTH = PersistentConfig(
"AUTOMATIC1111_API_AUTH",
"image_generation.automatic1111.api_auth",
os.getenv("AUTOMATIC1111_API_AUTH", ""),
)
COMFYUI_BASE_URL = PersistentConfig(
"COMFYUI_BASE_URL",
@@ -904,6 +1342,129 @@ COMFYUI_BASE_URL = PersistentConfig(
os.getenv("COMFYUI_BASE_URL", ""),
)
COMFYUI_DEFAULT_WORKFLOW = """
{
"3": {
"inputs": {
"seed": 0,
"steps": 20,
"cfg": 8,
"sampler_name": "euler",
"scheduler": "normal",
"denoise": 1,
"model": [
"4",
0
],
"positive": [
"6",
0
],
"negative": [
"7",
0
],
"latent_image": [
"5",
0
]
},
"class_type": "KSampler",
"_meta": {
"title": "KSampler"
}
},
"4": {
"inputs": {
"ckpt_name": "model.safetensors"
},
"class_type": "CheckpointLoaderSimple",
"_meta": {
"title": "Load Checkpoint"
}
},
"5": {
"inputs": {
"width": 512,
"height": 512,
"batch_size": 1
},
"class_type": "EmptyLatentImage",
"_meta": {
"title": "Empty Latent Image"
}
},
"6": {
"inputs": {
"text": "Prompt",
"clip": [
"4",
1
]
},
"class_type": "CLIPTextEncode",
"_meta": {
"title": "CLIP Text Encode (Prompt)"
}
},
"7": {
"inputs": {
"text": "",
"clip": [
"4",
1
]
},
"class_type": "CLIPTextEncode",
"_meta": {
"title": "CLIP Text Encode (Prompt)"
}
},
"8": {
"inputs": {
"samples": [
"3",
0
],
"vae": [
"4",
2
]
},
"class_type": "VAEDecode",
"_meta": {
"title": "VAE Decode"
}
},
"9": {
"inputs": {
"filename_prefix": "ComfyUI",
"images": [
"8",
0
]
},
"class_type": "SaveImage",
"_meta": {
"title": "Save Image"
}
}
}
"""
COMFYUI_WORKFLOW = PersistentConfig(
"COMFYUI_WORKFLOW",
"image_generation.comfyui.workflow",
os.getenv("COMFYUI_WORKFLOW", COMFYUI_DEFAULT_WORKFLOW),
)
COMFYUI_WORKFLOW_NODES = PersistentConfig(
"COMFYUI_WORKFLOW",
"image_generation.comfyui.nodes",
[],
)
IMAGES_OPENAI_API_BASE_URL = PersistentConfig(
"IMAGES_OPENAI_API_BASE_URL",
"image_generation.openai.api_base_url",
@@ -933,25 +1494,64 @@ IMAGE_GENERATION_MODEL = PersistentConfig(
# Audio
####################################
AUDIO_OPENAI_API_BASE_URL = PersistentConfig(
"AUDIO_OPENAI_API_BASE_URL",
"audio.openai.api_base_url",
os.getenv("AUDIO_OPENAI_API_BASE_URL", OPENAI_API_BASE_URL),
AUDIO_STT_OPENAI_API_BASE_URL = PersistentConfig(
"AUDIO_STT_OPENAI_API_BASE_URL",
"audio.stt.openai.api_base_url",
os.getenv("AUDIO_STT_OPENAI_API_BASE_URL", OPENAI_API_BASE_URL),
)
AUDIO_OPENAI_API_KEY = PersistentConfig(
"AUDIO_OPENAI_API_KEY",
"audio.openai.api_key",
os.getenv("AUDIO_OPENAI_API_KEY", OPENAI_API_KEY),
AUDIO_STT_OPENAI_API_KEY = PersistentConfig(
"AUDIO_STT_OPENAI_API_KEY",
"audio.stt.openai.api_key",
os.getenv("AUDIO_STT_OPENAI_API_KEY", OPENAI_API_KEY),
)
AUDIO_OPENAI_API_MODEL = PersistentConfig(
"AUDIO_OPENAI_API_MODEL",
"audio.openai.api_model",
os.getenv("AUDIO_OPENAI_API_MODEL", "tts-1"),
AUDIO_STT_ENGINE = PersistentConfig(
"AUDIO_STT_ENGINE",
"audio.stt.engine",
os.getenv("AUDIO_STT_ENGINE", ""),
)
AUDIO_OPENAI_API_VOICE = PersistentConfig(
"AUDIO_OPENAI_API_VOICE",
"audio.openai.api_voice",
os.getenv("AUDIO_OPENAI_API_VOICE", "alloy"),
AUDIO_STT_MODEL = PersistentConfig(
"AUDIO_STT_MODEL",
"audio.stt.model",
os.getenv("AUDIO_STT_MODEL", "whisper-1"),
)
AUDIO_TTS_OPENAI_API_BASE_URL = PersistentConfig(
"AUDIO_TTS_OPENAI_API_BASE_URL",
"audio.tts.openai.api_base_url",
os.getenv("AUDIO_TTS_OPENAI_API_BASE_URL", OPENAI_API_BASE_URL),
)
AUDIO_TTS_OPENAI_API_KEY = PersistentConfig(
"AUDIO_TTS_OPENAI_API_KEY",
"audio.tts.openai.api_key",
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",
"audio.tts.engine",
os.getenv("AUDIO_TTS_ENGINE", ""),
)
AUDIO_TTS_MODEL = PersistentConfig(
"AUDIO_TTS_MODEL",
"audio.tts.model",
os.getenv("AUDIO_TTS_MODEL", "tts-1"), # OpenAI default model
)
AUDIO_TTS_VOICE = PersistentConfig(
"AUDIO_TTS_VOICE",
"audio.tts.voice",
os.getenv("AUDIO_TTS_VOICE", "alloy"), # OpenAI default voice
)
@@ -960,3 +1560,7 @@ AUDIO_OPENAI_API_VOICE = PersistentConfig(
####################################
DATABASE_URL = os.environ.get("DATABASE_URL", f"sqlite:///{DATA_DIR}/webui.db")
# Replace the postgres:// with postgresql://
if "postgres://" in DATABASE_URL:
DATABASE_URL = DATABASE_URL.replace("postgres://", "postgresql://")
+13
View File
@@ -32,6 +32,7 @@ class ERROR_MESSAGES(str, Enum):
COMMAND_TAKEN = "Uh-oh! This command is already registered. Please choose another command string."
FILE_EXISTS = "Uh-oh! This file is already registered. Please choose another file."
ID_TAKEN = "Uh-oh! This id is already registered. Please choose another id string."
MODEL_ID_TAKEN = "Uh-oh! This model id is already registered. Please choose another model id string."
NAME_TAG_TAKEN = "Uh-oh! This name tag is already registered. Please choose another name tag string."
@@ -88,3 +89,15 @@ class ERROR_MESSAGES(str, Enum):
OLLAMA_API_DISABLED = (
"The Ollama API is disabled. Please enable it to use this feature."
)
class TASKS(str, Enum):
def __str__(self) -> str:
return super().__str__()
DEFAULT = lambda task="": f"{task if task else 'generation'}"
TITLE_GENERATION = "title_generation"
EMOJI_GENERATION = "emoji_generation"
QUERY_GENERATION = "query_generation"
FUNCTION_CALLING = "function_calling"
MOA_RESPONSE_GENERATION = "moa_response_generation"
+1 -1
View File
@@ -1,7 +1,7 @@
{
"version": 0,
"ui": {
"default_locale": "en-US",
"default_locale": "",
"prompt_suggestions": [
{
"title": ["Help me study", "vocabulary for a college entrance exam"],
+1549 -269
View File
File diff suppressed because it is too large Load Diff
+4
View File
@@ -0,0 +1,4 @@
Generic single-database configuration.
Create new migrations with
DATABASE_URL=<replace with actual url> alembic revision --autogenerate -m "a description"
+96
View File
@@ -0,0 +1,96 @@
import os
from logging.config import fileConfig
from sqlalchemy import engine_from_config
from sqlalchemy import pool
from alembic import context
from apps.webui.models.auths import Auth
from apps.webui.models.chats import Chat
from apps.webui.models.documents import Document
from apps.webui.models.memories import Memory
from apps.webui.models.models import Model
from apps.webui.models.prompts import Prompt
from apps.webui.models.tags import Tag, ChatIdTag
from apps.webui.models.tools import Tool
from apps.webui.models.users import User
from apps.webui.models.files import File
from apps.webui.models.functions import Function
from config import DATABASE_URL
# this is the Alembic Config object, which provides
# access to the values within the .ini file in use.
config = context.config
# Interpret the config file for Python logging.
# This line sets up loggers basically.
if config.config_file_name is not None:
fileConfig(config.config_file_name, disable_existing_loggers=False)
# add your model's MetaData object here
# for 'autogenerate' support
# from myapp import mymodel
# target_metadata = mymodel.Base.metadata
target_metadata = Auth.metadata
# other values from the config, defined by the needs of env.py,
# can be acquired:
# my_important_option = config.get_main_option("my_important_option")
# ... etc.
DB_URL = DATABASE_URL
if DB_URL:
config.set_main_option("sqlalchemy.url", DB_URL.replace("%", "%%"))
def run_migrations_offline() -> None:
"""Run migrations in 'offline' mode.
This configures the context with just a URL
and not an Engine, though an Engine is acceptable
here as well. By skipping the Engine creation
we don't even need a DBAPI to be available.
Calls to context.execute() here emit the given string to the
script output.
"""
url = config.get_main_option("sqlalchemy.url")
context.configure(
url=url,
target_metadata=target_metadata,
literal_binds=True,
dialect_opts={"paramstyle": "named"},
)
with context.begin_transaction():
context.run_migrations()
def run_migrations_online() -> None:
"""Run migrations in 'online' mode.
In this scenario we need to create an Engine
and associate a connection with the context.
"""
connectable = engine_from_config(
config.get_section(config.config_ini_section, {}),
prefix="sqlalchemy.",
poolclass=pool.NullPool,
)
with connectable.connect() as connection:
context.configure(connection=connection, target_metadata=target_metadata)
with context.begin_transaction():
context.run_migrations()
if context.is_offline_mode():
run_migrations_offline()
else:
run_migrations_online()
+27
View File
@@ -0,0 +1,27 @@
"""${message}
Revision ID: ${up_revision}
Revises: ${down_revision | comma,n}
Create Date: ${create_date}
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
import apps.webui.internal.db
${imports if imports else ""}
# revision identifiers, used by Alembic.
revision: str = ${repr(up_revision)}
down_revision: Union[str, None] = ${repr(down_revision)}
branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)}
depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)}
def upgrade() -> None:
${upgrades if upgrades else "pass"}
def downgrade() -> None:
${downgrades if downgrades else "pass"}
+9
View File
@@ -0,0 +1,9 @@
from alembic import op
from sqlalchemy import Inspector
def get_existing_tables():
con = op.get_bind()
inspector = Inspector.from_engine(con)
tables = set(inspector.get_table_names())
return tables
@@ -0,0 +1,202 @@
"""init
Revision ID: 7e5b5dc7342b
Revises:
Create Date: 2024-06-24 13:15:33.808998
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
import apps.webui.internal.db
from migrations.util import get_existing_tables
# revision identifiers, used by Alembic.
revision: str = "7e5b5dc7342b"
down_revision: Union[str, None] = None
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
existing_tables = set(get_existing_tables())
# ### commands auto generated by Alembic - please adjust! ###
if "auth" not in existing_tables:
op.create_table(
"auth",
sa.Column("id", sa.String(), nullable=False),
sa.Column("email", sa.String(), nullable=True),
sa.Column("password", sa.Text(), nullable=True),
sa.Column("active", sa.Boolean(), nullable=True),
sa.PrimaryKeyConstraint("id"),
)
if "chat" not in existing_tables:
op.create_table(
"chat",
sa.Column("id", sa.String(), nullable=False),
sa.Column("user_id", sa.String(), nullable=True),
sa.Column("title", sa.Text(), nullable=True),
sa.Column("chat", sa.Text(), nullable=True),
sa.Column("created_at", sa.BigInteger(), nullable=True),
sa.Column("updated_at", sa.BigInteger(), nullable=True),
sa.Column("share_id", sa.Text(), nullable=True),
sa.Column("archived", sa.Boolean(), nullable=True),
sa.PrimaryKeyConstraint("id"),
sa.UniqueConstraint("share_id"),
)
if "chatidtag" not in existing_tables:
op.create_table(
"chatidtag",
sa.Column("id", sa.String(), nullable=False),
sa.Column("tag_name", sa.String(), nullable=True),
sa.Column("chat_id", sa.String(), nullable=True),
sa.Column("user_id", sa.String(), nullable=True),
sa.Column("timestamp", sa.BigInteger(), nullable=True),
sa.PrimaryKeyConstraint("id"),
)
if "document" not in existing_tables:
op.create_table(
"document",
sa.Column("collection_name", sa.String(), nullable=False),
sa.Column("name", sa.String(), nullable=True),
sa.Column("title", sa.Text(), nullable=True),
sa.Column("filename", sa.Text(), nullable=True),
sa.Column("content", sa.Text(), nullable=True),
sa.Column("user_id", sa.String(), nullable=True),
sa.Column("timestamp", sa.BigInteger(), nullable=True),
sa.PrimaryKeyConstraint("collection_name"),
sa.UniqueConstraint("name"),
)
if "file" not in existing_tables:
op.create_table(
"file",
sa.Column("id", sa.String(), nullable=False),
sa.Column("user_id", sa.String(), nullable=True),
sa.Column("filename", sa.Text(), nullable=True),
sa.Column("meta", apps.webui.internal.db.JSONField(), nullable=True),
sa.Column("created_at", sa.BigInteger(), nullable=True),
sa.PrimaryKeyConstraint("id"),
)
if "function" not in existing_tables:
op.create_table(
"function",
sa.Column("id", sa.String(), nullable=False),
sa.Column("user_id", sa.String(), nullable=True),
sa.Column("name", sa.Text(), nullable=True),
sa.Column("type", sa.Text(), nullable=True),
sa.Column("content", sa.Text(), nullable=True),
sa.Column("meta", apps.webui.internal.db.JSONField(), nullable=True),
sa.Column("valves", apps.webui.internal.db.JSONField(), nullable=True),
sa.Column("is_active", sa.Boolean(), nullable=True),
sa.Column("is_global", sa.Boolean(), nullable=True),
sa.Column("updated_at", sa.BigInteger(), nullable=True),
sa.Column("created_at", sa.BigInteger(), nullable=True),
sa.PrimaryKeyConstraint("id"),
)
if "memory" not in existing_tables:
op.create_table(
"memory",
sa.Column("id", sa.String(), nullable=False),
sa.Column("user_id", sa.String(), nullable=True),
sa.Column("content", sa.Text(), nullable=True),
sa.Column("updated_at", sa.BigInteger(), nullable=True),
sa.Column("created_at", sa.BigInteger(), nullable=True),
sa.PrimaryKeyConstraint("id"),
)
if "model" not in existing_tables:
op.create_table(
"model",
sa.Column("id", sa.Text(), nullable=False),
sa.Column("user_id", sa.Text(), nullable=True),
sa.Column("base_model_id", sa.Text(), nullable=True),
sa.Column("name", sa.Text(), nullable=True),
sa.Column("params", apps.webui.internal.db.JSONField(), nullable=True),
sa.Column("meta", apps.webui.internal.db.JSONField(), nullable=True),
sa.Column("updated_at", sa.BigInteger(), nullable=True),
sa.Column("created_at", sa.BigInteger(), nullable=True),
sa.PrimaryKeyConstraint("id"),
)
if "prompt" not in existing_tables:
op.create_table(
"prompt",
sa.Column("command", sa.String(), nullable=False),
sa.Column("user_id", sa.String(), nullable=True),
sa.Column("title", sa.Text(), nullable=True),
sa.Column("content", sa.Text(), nullable=True),
sa.Column("timestamp", sa.BigInteger(), nullable=True),
sa.PrimaryKeyConstraint("command"),
)
if "tag" not in existing_tables:
op.create_table(
"tag",
sa.Column("id", sa.String(), nullable=False),
sa.Column("name", sa.String(), nullable=True),
sa.Column("user_id", sa.String(), nullable=True),
sa.Column("data", sa.Text(), nullable=True),
sa.PrimaryKeyConstraint("id"),
)
if "tool" not in existing_tables:
op.create_table(
"tool",
sa.Column("id", sa.String(), nullable=False),
sa.Column("user_id", sa.String(), nullable=True),
sa.Column("name", sa.Text(), nullable=True),
sa.Column("content", sa.Text(), nullable=True),
sa.Column("specs", apps.webui.internal.db.JSONField(), nullable=True),
sa.Column("meta", apps.webui.internal.db.JSONField(), nullable=True),
sa.Column("valves", apps.webui.internal.db.JSONField(), nullable=True),
sa.Column("updated_at", sa.BigInteger(), nullable=True),
sa.Column("created_at", sa.BigInteger(), nullable=True),
sa.PrimaryKeyConstraint("id"),
)
if "user" not in existing_tables:
op.create_table(
"user",
sa.Column("id", sa.String(), nullable=False),
sa.Column("name", sa.String(), nullable=True),
sa.Column("email", sa.String(), nullable=True),
sa.Column("role", sa.String(), nullable=True),
sa.Column("profile_image_url", sa.Text(), nullable=True),
sa.Column("last_active_at", sa.BigInteger(), nullable=True),
sa.Column("updated_at", sa.BigInteger(), nullable=True),
sa.Column("created_at", sa.BigInteger(), nullable=True),
sa.Column("api_key", sa.String(), nullable=True),
sa.Column("settings", apps.webui.internal.db.JSONField(), nullable=True),
sa.Column("info", apps.webui.internal.db.JSONField(), nullable=True),
sa.Column("oauth_sub", sa.Text(), nullable=True),
sa.PrimaryKeyConstraint("id"),
sa.UniqueConstraint("api_key"),
sa.UniqueConstraint("oauth_sub"),
)
# ### end Alembic commands ###
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table("user")
op.drop_table("tool")
op.drop_table("tag")
op.drop_table("prompt")
op.drop_table("model")
op.drop_table("memory")
op.drop_table("function")
op.drop_table("file")
op.drop_table("document")
op.drop_table("chatidtag")
op.drop_table("chat")
op.drop_table("auth")
# ### end Alembic commands ###
+47 -26
View File
@@ -1,59 +1,80 @@
fastapi==0.111.0
uvicorn[standard]==0.22.0
pydantic==2.7.1
uvicorn[standard]==0.30.6
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.32
alembic==1.13.2
peewee==3.17.6
peewee-migrate==1.12.2
psycopg2-binary==2.9.9
PyMySQL==1.1.1
bcrypt==4.1.3
bcrypt==4.2.0
boto3==1.34.110
pymongo
redis
boto3==1.35.0
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
# AI libraries
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
python-pptx==0.6.23
unstructured==0.14.0
python-pptx==1.0.0
unstructured==0.15.5
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
faster-whisper==1.0.3
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
pytube==15.0.0
extract_msg
pydub
duckduckgo-search~=6.2.1
## Tests
docker~=7.1.0
pytest~=8.3.2
pytest-docker~=3.1.1
+2 -3
View File
@@ -20,17 +20,16 @@ if test "$WEBUI_SECRET_KEY $WEBUI_JWT_SECRET_KEY" = " "; then
WEBUI_SECRET_KEY=$(cat "$KEY_FILE")
fi
if [ "$USE_OLLAMA_DOCKER" = "true" ]; then
if [[ "${USE_OLLAMA_DOCKER,,}" == "true" ]]; then
echo "USE_OLLAMA is set to true, starting ollama serve."
ollama serve &
fi
if [ "$USE_CUDA_DOCKER" = "true" ]; then
if [[ "${USE_CUDA_DOCKER,,}" == "true" ]]; then
echo "CUDA is enabled, appending LD_LIBRARY_PATH to include torch/cudnn & cublas libraries."
export LD_LIBRARY_PATH="$LD_LIBRARY_PATH:/usr/local/lib/python3.11/site-packages/torch/lib:/usr/local/lib/python3.11/site-packages/nvidia/cudnn/lib"
fi
# Check if SPACE_ID is set, if so, configure for space
if [ -n "$SPACE_ID" ]; then
echo "Configuring for HuggingFace Space deployment"
+2 -1
View File
@@ -8,6 +8,7 @@ cd /d "%SCRIPT_DIR%" || exit /b
SET "KEY_FILE=.webui_secret_key"
IF "%PORT%"=="" SET PORT=8080
IF "%HOST%"=="" SET HOST=0.0.0.0
SET "WEBUI_SECRET_KEY=%WEBUI_SECRET_KEY%"
SET "WEBUI_JWT_SECRET_KEY=%WEBUI_JWT_SECRET_KEY%"
@@ -29,4 +30,4 @@ IF "%WEBUI_SECRET_KEY%%WEBUI_JWT_SECRET_KEY%" == " " (
:: Execute uvicorn
SET "WEBUI_SECRET_KEY=%WEBUI_SECRET_KEY%"
uvicorn main:app --host 0.0.0.0 --port "%PORT%" --forwarded-allow-ips '*'
uvicorn main:app --host "%HOST%" --port "%PORT%" --forwarded-allow-ips '*'
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

View File
@@ -0,0 +1,202 @@
import pytest
from test.util.abstract_integration_test import AbstractPostgresTest
from test.util.mock_user import mock_webui_user
class TestAuths(AbstractPostgresTest):
BASE_PATH = "/api/v1/auths"
def setup_class(cls):
super().setup_class()
from apps.webui.models.users import Users
from apps.webui.models.auths import Auths
cls.users = Users
cls.auths = Auths
def test_get_session_user(self):
with mock_webui_user():
response = self.fast_api_client.get(self.create_url(""))
assert response.status_code == 200
assert response.json() == {
"id": "1",
"name": "John Doe",
"email": "john.doe@openwebui.com",
"role": "user",
"profile_image_url": "/user.png",
}
def test_update_profile(self):
from utils.utils import get_password_hash
user = self.auths.insert_new_auth(
email="john.doe@openwebui.com",
password=get_password_hash("old_password"),
name="John Doe",
profile_image_url="/user.png",
role="user",
)
with mock_webui_user(id=user.id):
response = self.fast_api_client.post(
self.create_url("/update/profile"),
json={"name": "John Doe 2", "profile_image_url": "/user2.png"},
)
assert response.status_code == 200
db_user = self.users.get_user_by_id(user.id)
assert db_user.name == "John Doe 2"
assert db_user.profile_image_url == "/user2.png"
def test_update_password(self):
from utils.utils import get_password_hash
user = self.auths.insert_new_auth(
email="john.doe@openwebui.com",
password=get_password_hash("old_password"),
name="John Doe",
profile_image_url="/user.png",
role="user",
)
with mock_webui_user(id=user.id):
response = self.fast_api_client.post(
self.create_url("/update/password"),
json={"password": "old_password", "new_password": "new_password"},
)
assert response.status_code == 200
old_auth = self.auths.authenticate_user(
"john.doe@openwebui.com", "old_password"
)
assert old_auth is None
new_auth = self.auths.authenticate_user(
"john.doe@openwebui.com", "new_password"
)
assert new_auth is not None
def test_signin(self):
from utils.utils import get_password_hash
user = self.auths.insert_new_auth(
email="john.doe@openwebui.com",
password=get_password_hash("password"),
name="John Doe",
profile_image_url="/user.png",
role="user",
)
response = self.fast_api_client.post(
self.create_url("/signin"),
json={"email": "john.doe@openwebui.com", "password": "password"},
)
assert response.status_code == 200
data = response.json()
assert data["id"] == user.id
assert data["name"] == "John Doe"
assert data["email"] == "john.doe@openwebui.com"
assert data["role"] == "user"
assert data["profile_image_url"] == "/user.png"
assert data["token"] is not None and len(data["token"]) > 0
assert data["token_type"] == "Bearer"
def test_signup(self):
response = self.fast_api_client.post(
self.create_url("/signup"),
json={
"name": "John Doe",
"email": "john.doe@openwebui.com",
"password": "password",
},
)
assert response.status_code == 200
data = response.json()
assert data["id"] is not None and len(data["id"]) > 0
assert data["name"] == "John Doe"
assert data["email"] == "john.doe@openwebui.com"
assert data["role"] in ["admin", "user", "pending"]
assert data["profile_image_url"] == "/user.png"
assert data["token"] is not None and len(data["token"]) > 0
assert data["token_type"] == "Bearer"
def test_add_user(self):
with mock_webui_user():
response = self.fast_api_client.post(
self.create_url("/add"),
json={
"name": "John Doe 2",
"email": "john.doe2@openwebui.com",
"password": "password2",
"role": "admin",
},
)
assert response.status_code == 200
data = response.json()
assert data["id"] is not None and len(data["id"]) > 0
assert data["name"] == "John Doe 2"
assert data["email"] == "john.doe2@openwebui.com"
assert data["role"] == "admin"
assert data["profile_image_url"] == "/user.png"
assert data["token"] is not None and len(data["token"]) > 0
assert data["token_type"] == "Bearer"
def test_get_admin_details(self):
self.auths.insert_new_auth(
email="john.doe@openwebui.com",
password="password",
name="John Doe",
profile_image_url="/user.png",
role="admin",
)
with mock_webui_user():
response = self.fast_api_client.get(self.create_url("/admin/details"))
assert response.status_code == 200
assert response.json() == {
"name": "John Doe",
"email": "john.doe@openwebui.com",
}
def test_create_api_key_(self):
user = self.auths.insert_new_auth(
email="john.doe@openwebui.com",
password="password",
name="John Doe",
profile_image_url="/user.png",
role="admin",
)
with mock_webui_user(id=user.id):
response = self.fast_api_client.post(self.create_url("/api_key"))
assert response.status_code == 200
data = response.json()
assert data["api_key"] is not None
assert len(data["api_key"]) > 0
def test_delete_api_key(self):
user = self.auths.insert_new_auth(
email="john.doe@openwebui.com",
password="password",
name="John Doe",
profile_image_url="/user.png",
role="admin",
)
self.users.update_user_api_key_by_id(user.id, "abc")
with mock_webui_user(id=user.id):
response = self.fast_api_client.delete(self.create_url("/api_key"))
assert response.status_code == 200
assert response.json() == True
db_user = self.users.get_user_by_id(user.id)
assert db_user.api_key is None
def test_get_api_key(self):
user = self.auths.insert_new_auth(
email="john.doe@openwebui.com",
password="password",
name="John Doe",
profile_image_url="/user.png",
role="admin",
)
self.users.update_user_api_key_by_id(user.id, "abc")
with mock_webui_user(id=user.id):
response = self.fast_api_client.get(self.create_url("/api_key"))
assert response.status_code == 200
assert response.json() == {"api_key": "abc"}
@@ -0,0 +1,238 @@
import uuid
from test.util.abstract_integration_test import AbstractPostgresTest
from test.util.mock_user import mock_webui_user
class TestChats(AbstractPostgresTest):
BASE_PATH = "/api/v1/chats"
def setup_class(cls):
super().setup_class()
def setup_method(self):
super().setup_method()
from apps.webui.models.chats import ChatForm
from apps.webui.models.chats import Chats
self.chats = Chats
self.chats.insert_new_chat(
"2",
ChatForm(
**{
"chat": {
"name": "chat1",
"description": "chat1 description",
"tags": ["tag1", "tag2"],
"history": {"currentId": "1", "messages": []},
}
}
),
)
def test_get_session_user_chat_list(self):
with mock_webui_user(id="2"):
response = self.fast_api_client.get(self.create_url("/"))
assert response.status_code == 200
first_chat = response.json()[0]
assert first_chat["id"] is not None
assert first_chat["title"] == "New Chat"
assert first_chat["created_at"] is not None
assert first_chat["updated_at"] is not None
def test_delete_all_user_chats(self):
with mock_webui_user(id="2"):
response = self.fast_api_client.delete(self.create_url("/"))
assert response.status_code == 200
assert len(self.chats.get_chats()) == 0
def test_get_user_chat_list_by_user_id(self):
with mock_webui_user(id="3"):
response = self.fast_api_client.get(self.create_url("/list/user/2"))
assert response.status_code == 200
first_chat = response.json()[0]
assert first_chat["id"] is not None
assert first_chat["title"] == "New Chat"
assert first_chat["created_at"] is not None
assert first_chat["updated_at"] is not None
def test_create_new_chat(self):
with mock_webui_user(id="2"):
response = self.fast_api_client.post(
self.create_url("/new"),
json={
"chat": {
"name": "chat2",
"description": "chat2 description",
"tags": ["tag1", "tag2"],
}
},
)
assert response.status_code == 200
data = response.json()
assert data["archived"] is False
assert data["chat"] == {
"name": "chat2",
"description": "chat2 description",
"tags": ["tag1", "tag2"],
}
assert data["user_id"] == "2"
assert data["id"] is not None
assert data["share_id"] is None
assert data["title"] == "New Chat"
assert data["updated_at"] is not None
assert data["created_at"] is not None
assert len(self.chats.get_chats()) == 2
def test_get_user_chats(self):
self.test_get_session_user_chat_list()
def test_get_user_archived_chats(self):
self.chats.archive_all_chats_by_user_id("2")
from apps.webui.internal.db import Session
Session.commit()
with mock_webui_user(id="2"):
response = self.fast_api_client.get(self.create_url("/all/archived"))
assert response.status_code == 200
first_chat = response.json()[0]
assert first_chat["id"] is not None
assert first_chat["title"] == "New Chat"
assert first_chat["created_at"] is not None
assert first_chat["updated_at"] is not None
def test_get_all_user_chats_in_db(self):
with mock_webui_user(id="4"):
response = self.fast_api_client.get(self.create_url("/all/db"))
assert response.status_code == 200
assert len(response.json()) == 1
def test_get_archived_session_user_chat_list(self):
self.test_get_user_archived_chats()
def test_archive_all_chats(self):
with mock_webui_user(id="2"):
response = self.fast_api_client.post(self.create_url("/archive/all"))
assert response.status_code == 200
assert len(self.chats.get_archived_chats_by_user_id("2")) == 1
def test_get_shared_chat_by_id(self):
chat_id = self.chats.get_chats()[0].id
self.chats.update_chat_share_id_by_id(chat_id, chat_id)
with mock_webui_user(id="2"):
response = self.fast_api_client.get(self.create_url(f"/share/{chat_id}"))
assert response.status_code == 200
data = response.json()
assert data["id"] == chat_id
assert data["chat"] == {
"name": "chat1",
"description": "chat1 description",
"tags": ["tag1", "tag2"],
"history": {"currentId": "1", "messages": []},
}
assert data["id"] == chat_id
assert data["share_id"] == chat_id
assert data["title"] == "New Chat"
def test_get_chat_by_id(self):
chat_id = self.chats.get_chats()[0].id
with mock_webui_user(id="2"):
response = self.fast_api_client.get(self.create_url(f"/{chat_id}"))
assert response.status_code == 200
data = response.json()
assert data["id"] == chat_id
assert data["chat"] == {
"name": "chat1",
"description": "chat1 description",
"tags": ["tag1", "tag2"],
"history": {"currentId": "1", "messages": []},
}
assert data["share_id"] is None
assert data["title"] == "New Chat"
assert data["user_id"] == "2"
def test_update_chat_by_id(self):
chat_id = self.chats.get_chats()[0].id
with mock_webui_user(id="2"):
response = self.fast_api_client.post(
self.create_url(f"/{chat_id}"),
json={
"chat": {
"name": "chat2",
"description": "chat2 description",
"tags": ["tag2", "tag4"],
"title": "Just another title",
}
},
)
assert response.status_code == 200
data = response.json()
assert data["id"] == chat_id
assert data["chat"] == {
"name": "chat2",
"title": "Just another title",
"description": "chat2 description",
"tags": ["tag2", "tag4"],
"history": {"currentId": "1", "messages": []},
}
assert data["share_id"] is None
assert data["title"] == "Just another title"
assert data["user_id"] == "2"
def test_delete_chat_by_id(self):
chat_id = self.chats.get_chats()[0].id
with mock_webui_user(id="2"):
response = self.fast_api_client.delete(self.create_url(f"/{chat_id}"))
assert response.status_code == 200
assert response.json() is True
def test_clone_chat_by_id(self):
chat_id = self.chats.get_chats()[0].id
with mock_webui_user(id="2"):
response = self.fast_api_client.get(self.create_url(f"/{chat_id}/clone"))
assert response.status_code == 200
data = response.json()
assert data["id"] != chat_id
assert data["chat"] == {
"branchPointMessageId": "1",
"description": "chat1 description",
"history": {"currentId": "1", "messages": []},
"name": "chat1",
"originalChatId": chat_id,
"tags": ["tag1", "tag2"],
"title": "Clone of New Chat",
}
assert data["share_id"] is None
assert data["title"] == "Clone of New Chat"
assert data["user_id"] == "2"
def test_archive_chat_by_id(self):
chat_id = self.chats.get_chats()[0].id
with mock_webui_user(id="2"):
response = self.fast_api_client.get(self.create_url(f"/{chat_id}/archive"))
assert response.status_code == 200
chat = self.chats.get_chat_by_id(chat_id)
assert chat.archived is True
def test_share_chat_by_id(self):
chat_id = self.chats.get_chats()[0].id
with mock_webui_user(id="2"):
response = self.fast_api_client.post(self.create_url(f"/{chat_id}/share"))
assert response.status_code == 200
chat = self.chats.get_chat_by_id(chat_id)
assert chat.share_id is not None
def test_delete_shared_chat_by_id(self):
chat_id = self.chats.get_chats()[0].id
share_id = str(uuid.uuid4())
self.chats.update_chat_share_id_by_id(chat_id, share_id)
with mock_webui_user(id="2"):
response = self.fast_api_client.delete(self.create_url(f"/{chat_id}/share"))
assert response.status_code
chat = self.chats.get_chat_by_id(chat_id)
assert chat.share_id is None
@@ -0,0 +1,106 @@
from test.util.abstract_integration_test import AbstractPostgresTest
from test.util.mock_user import mock_webui_user
class TestDocuments(AbstractPostgresTest):
BASE_PATH = "/api/v1/documents"
def setup_class(cls):
super().setup_class()
from apps.webui.models.documents import Documents
cls.documents = Documents
def test_documents(self):
# Empty database
assert len(self.documents.get_docs()) == 0
with mock_webui_user(id="2"):
response = self.fast_api_client.get(self.create_url("/"))
assert response.status_code == 200
assert len(response.json()) == 0
# Create a new document
with mock_webui_user(id="2"):
response = self.fast_api_client.post(
self.create_url("/create"),
json={
"name": "doc_name",
"title": "doc title",
"collection_name": "custom collection",
"filename": "doc_name.pdf",
"content": "",
},
)
assert response.status_code == 200
assert response.json()["name"] == "doc_name"
assert len(self.documents.get_docs()) == 1
# Get the document
with mock_webui_user(id="2"):
response = self.fast_api_client.get(self.create_url("/doc?name=doc_name"))
assert response.status_code == 200
data = response.json()
assert data["collection_name"] == "custom collection"
assert data["name"] == "doc_name"
assert data["title"] == "doc title"
assert data["filename"] == "doc_name.pdf"
assert data["content"] == {}
# Create another document
with mock_webui_user(id="2"):
response = self.fast_api_client.post(
self.create_url("/create"),
json={
"name": "doc_name 2",
"title": "doc title 2",
"collection_name": "custom collection 2",
"filename": "doc_name2.pdf",
"content": "",
},
)
assert response.status_code == 200
assert response.json()["name"] == "doc_name 2"
assert len(self.documents.get_docs()) == 2
# Get all documents
with mock_webui_user(id="2"):
response = self.fast_api_client.get(self.create_url("/"))
assert response.status_code == 200
assert len(response.json()) == 2
# Update the first document
with mock_webui_user(id="2"):
response = self.fast_api_client.post(
self.create_url("/doc/update?name=doc_name"),
json={"name": "doc_name rework", "title": "updated title"},
)
assert response.status_code == 200
data = response.json()
assert data["name"] == "doc_name rework"
assert data["title"] == "updated title"
# Tag the first document
with mock_webui_user(id="2"):
response = self.fast_api_client.post(
self.create_url("/doc/tags"),
json={
"name": "doc_name rework",
"tags": [{"name": "testing-tag"}, {"name": "another-tag"}],
},
)
assert response.status_code == 200
data = response.json()
assert data["name"] == "doc_name rework"
assert data["content"] == {
"tags": [{"name": "testing-tag"}, {"name": "another-tag"}]
}
assert len(self.documents.get_docs()) == 2
# Delete the first document
with mock_webui_user(id="2"):
response = self.fast_api_client.delete(
self.create_url("/doc/delete?name=doc_name rework")
)
assert response.status_code == 200
assert len(self.documents.get_docs()) == 1
@@ -0,0 +1,62 @@
from test.util.abstract_integration_test import AbstractPostgresTest
from test.util.mock_user import mock_webui_user
class TestModels(AbstractPostgresTest):
BASE_PATH = "/api/v1/models"
def setup_class(cls):
super().setup_class()
from apps.webui.models.models import Model
cls.models = Model
def test_models(self):
with mock_webui_user(id="2"):
response = self.fast_api_client.get(self.create_url("/"))
assert response.status_code == 200
assert len(response.json()) == 0
with mock_webui_user(id="2"):
response = self.fast_api_client.post(
self.create_url("/add"),
json={
"id": "my-model",
"base_model_id": "base-model-id",
"name": "Hello World",
"meta": {
"profile_image_url": "/static/favicon.png",
"description": "description",
"capabilities": None,
"model_config": {},
},
"params": {},
},
)
assert response.status_code == 200
with mock_webui_user(id="2"):
response = self.fast_api_client.get(self.create_url("/"))
assert response.status_code == 200
assert len(response.json()) == 1
with mock_webui_user(id="2"):
response = self.fast_api_client.get(
self.create_url(query_params={"id": "my-model"})
)
assert response.status_code == 200
data = response.json()[0]
assert data["id"] == "my-model"
assert data["name"] == "Hello World"
with mock_webui_user(id="2"):
response = self.fast_api_client.delete(
self.create_url("/delete?id=my-model")
)
assert response.status_code == 200
with mock_webui_user(id="2"):
response = self.fast_api_client.get(self.create_url("/"))
assert response.status_code == 200
assert len(response.json()) == 0
@@ -0,0 +1,92 @@
from test.util.abstract_integration_test import AbstractPostgresTest
from test.util.mock_user import mock_webui_user
class TestPrompts(AbstractPostgresTest):
BASE_PATH = "/api/v1/prompts"
def test_prompts(self):
# Get all prompts
with mock_webui_user(id="2"):
response = self.fast_api_client.get(self.create_url("/"))
assert response.status_code == 200
assert len(response.json()) == 0
# Create a two new prompts
with mock_webui_user(id="2"):
response = self.fast_api_client.post(
self.create_url("/create"),
json={
"command": "/my-command",
"title": "Hello World",
"content": "description",
},
)
assert response.status_code == 200
with mock_webui_user(id="3"):
response = self.fast_api_client.post(
self.create_url("/create"),
json={
"command": "/my-command2",
"title": "Hello World 2",
"content": "description 2",
},
)
assert response.status_code == 200
# Get all prompts
with mock_webui_user(id="2"):
response = self.fast_api_client.get(self.create_url("/"))
assert response.status_code == 200
assert len(response.json()) == 2
# Get prompt by command
with mock_webui_user(id="2"):
response = self.fast_api_client.get(self.create_url("/command/my-command"))
assert response.status_code == 200
data = response.json()
assert data["command"] == "/my-command"
assert data["title"] == "Hello World"
assert data["content"] == "description"
assert data["user_id"] == "2"
# Update prompt
with mock_webui_user(id="2"):
response = self.fast_api_client.post(
self.create_url("/command/my-command2/update"),
json={
"command": "irrelevant for request",
"title": "Hello World Updated",
"content": "description Updated",
},
)
assert response.status_code == 200
data = response.json()
assert data["command"] == "/my-command2"
assert data["title"] == "Hello World Updated"
assert data["content"] == "description Updated"
assert data["user_id"] == "3"
# Get prompt by command
with mock_webui_user(id="2"):
response = self.fast_api_client.get(self.create_url("/command/my-command2"))
assert response.status_code == 200
data = response.json()
assert data["command"] == "/my-command2"
assert data["title"] == "Hello World Updated"
assert data["content"] == "description Updated"
assert data["user_id"] == "3"
# Delete prompt
with mock_webui_user(id="2"):
response = self.fast_api_client.delete(
self.create_url("/command/my-command/delete")
)
assert response.status_code == 200
# Get all prompts
with mock_webui_user(id="2"):
response = self.fast_api_client.get(self.create_url("/"))
assert response.status_code == 200
assert len(response.json()) == 1
@@ -0,0 +1,168 @@
from test.util.abstract_integration_test import AbstractPostgresTest
from test.util.mock_user import mock_webui_user
def _get_user_by_id(data, param):
return next((item for item in data if item["id"] == param), None)
def _assert_user(data, id, **kwargs):
user = _get_user_by_id(data, id)
assert user is not None
comparison_data = {
"name": f"user {id}",
"email": f"user{id}@openwebui.com",
"profile_image_url": f"/user{id}.png",
"role": "user",
**kwargs,
}
for key, value in comparison_data.items():
assert user[key] == value
class TestUsers(AbstractPostgresTest):
BASE_PATH = "/api/v1/users"
def setup_class(cls):
super().setup_class()
from apps.webui.models.users import Users
cls.users = Users
def setup_method(self):
super().setup_method()
self.users.insert_new_user(
id="1",
name="user 1",
email="user1@openwebui.com",
profile_image_url="/user1.png",
role="user",
)
self.users.insert_new_user(
id="2",
name="user 2",
email="user2@openwebui.com",
profile_image_url="/user2.png",
role="user",
)
def test_users(self):
# Get all users
with mock_webui_user(id="3"):
response = self.fast_api_client.get(self.create_url(""))
assert response.status_code == 200
assert len(response.json()) == 2
data = response.json()
_assert_user(data, "1")
_assert_user(data, "2")
# update role
with mock_webui_user(id="3"):
response = self.fast_api_client.post(
self.create_url("/update/role"), json={"id": "2", "role": "admin"}
)
assert response.status_code == 200
_assert_user([response.json()], "2", role="admin")
# Get all users
with mock_webui_user(id="3"):
response = self.fast_api_client.get(self.create_url(""))
assert response.status_code == 200
assert len(response.json()) == 2
data = response.json()
_assert_user(data, "1")
_assert_user(data, "2", role="admin")
# Get (empty) user settings
with mock_webui_user(id="2"):
response = self.fast_api_client.get(self.create_url("/user/settings"))
assert response.status_code == 200
assert response.json() is None
# Update user settings
with mock_webui_user(id="2"):
response = self.fast_api_client.post(
self.create_url("/user/settings/update"),
json={
"ui": {"attr1": "value1", "attr2": "value2"},
"model_config": {"attr3": "value3", "attr4": "value4"},
},
)
assert response.status_code == 200
# Get user settings
with mock_webui_user(id="2"):
response = self.fast_api_client.get(self.create_url("/user/settings"))
assert response.status_code == 200
assert response.json() == {
"ui": {"attr1": "value1", "attr2": "value2"},
"model_config": {"attr3": "value3", "attr4": "value4"},
}
# Get (empty) user info
with mock_webui_user(id="1"):
response = self.fast_api_client.get(self.create_url("/user/info"))
assert response.status_code == 200
assert response.json() is None
# Update user info
with mock_webui_user(id="1"):
response = self.fast_api_client.post(
self.create_url("/user/info/update"),
json={"attr1": "value1", "attr2": "value2"},
)
assert response.status_code == 200
# Get user info
with mock_webui_user(id="1"):
response = self.fast_api_client.get(self.create_url("/user/info"))
assert response.status_code == 200
assert response.json() == {"attr1": "value1", "attr2": "value2"}
# Get user by id
with mock_webui_user(id="1"):
response = self.fast_api_client.get(self.create_url("/2"))
assert response.status_code == 200
assert response.json() == {"name": "user 2", "profile_image_url": "/user2.png"}
# Update user by id
with mock_webui_user(id="1"):
response = self.fast_api_client.post(
self.create_url("/2/update"),
json={
"name": "user 2 updated",
"email": "user2-updated@openwebui.com",
"profile_image_url": "/user2-updated.png",
},
)
assert response.status_code == 200
# Get all users
with mock_webui_user(id="3"):
response = self.fast_api_client.get(self.create_url(""))
assert response.status_code == 200
assert len(response.json()) == 2
data = response.json()
_assert_user(data, "1")
_assert_user(
data,
"2",
role="admin",
name="user 2 updated",
email="user2-updated@openwebui.com",
profile_image_url="/user2-updated.png",
)
# Delete user by id
with mock_webui_user(id="1"):
response = self.fast_api_client.delete(self.create_url("/2"))
assert response.status_code == 200
# Get all users
with mock_webui_user(id="3"):
response = self.fast_api_client.get(self.create_url(""))
assert response.status_code == 200
assert len(response.json()) == 1
data = response.json()
_assert_user(data, "1")
@@ -0,0 +1,161 @@
import logging
import os
import time
import docker
import pytest
from docker import DockerClient
from pytest_docker.plugin import get_docker_ip
from fastapi.testclient import TestClient
from sqlalchemy import text, create_engine
log = logging.getLogger(__name__)
def get_fast_api_client():
from main import app
with TestClient(app) as c:
return c
class AbstractIntegrationTest:
BASE_PATH = None
def create_url(self, path="", query_params=None):
if self.BASE_PATH is None:
raise Exception("BASE_PATH is not set")
parts = self.BASE_PATH.split("/")
parts = [part.strip() for part in parts if part.strip() != ""]
path_parts = path.split("/")
path_parts = [part.strip() for part in path_parts if part.strip() != ""]
query_parts = ""
if query_params:
query_parts = "&".join(
[f"{key}={value}" for key, value in query_params.items()]
)
query_parts = f"?{query_parts}"
return "/".join(parts + path_parts) + query_parts
@classmethod
def setup_class(cls):
pass
def setup_method(self):
pass
@classmethod
def teardown_class(cls):
pass
def teardown_method(self):
pass
class AbstractPostgresTest(AbstractIntegrationTest):
DOCKER_CONTAINER_NAME = "postgres-test-container-will-get-deleted"
docker_client: DockerClient
@classmethod
def _create_db_url(cls, env_vars_postgres: dict) -> str:
host = get_docker_ip()
user = env_vars_postgres["POSTGRES_USER"]
pw = env_vars_postgres["POSTGRES_PASSWORD"]
port = 8081
db = env_vars_postgres["POSTGRES_DB"]
return f"postgresql://{user}:{pw}@{host}:{port}/{db}"
@classmethod
def setup_class(cls):
super().setup_class()
try:
env_vars_postgres = {
"POSTGRES_USER": "user",
"POSTGRES_PASSWORD": "example",
"POSTGRES_DB": "openwebui",
}
cls.docker_client = docker.from_env()
cls.docker_client.containers.run(
"postgres:16.2",
detach=True,
environment=env_vars_postgres,
name=cls.DOCKER_CONTAINER_NAME,
ports={5432: ("0.0.0.0", 8081)},
command="postgres -c log_statement=all",
)
time.sleep(0.5)
database_url = cls._create_db_url(env_vars_postgres)
os.environ["DATABASE_URL"] = database_url
retries = 10
db = None
while retries > 0:
try:
from config import BACKEND_DIR
db = create_engine(database_url, pool_pre_ping=True)
db = db.connect()
log.info("postgres is ready!")
break
except Exception as e:
log.warning(e)
time.sleep(3)
retries -= 1
if db:
# import must be after setting env!
cls.fast_api_client = get_fast_api_client()
db.close()
else:
raise Exception("Could not connect to Postgres")
except Exception as ex:
log.error(ex)
cls.teardown_class()
pytest.fail(f"Could not setup test environment: {ex}")
def _check_db_connection(self):
from apps.webui.internal.db import Session
retries = 10
while retries > 0:
try:
Session.execute(text("SELECT 1"))
Session.commit()
break
except Exception as e:
Session.rollback()
log.warning(e)
time.sleep(3)
retries -= 1
def setup_method(self):
super().setup_method()
self._check_db_connection()
@classmethod
def teardown_class(cls) -> None:
super().teardown_class()
cls.docker_client.containers.get(cls.DOCKER_CONTAINER_NAME).remove(force=True)
def teardown_method(self):
from apps.webui.internal.db import Session
# rollback everything not yet committed
Session.commit()
# truncate all tables
tables = [
"auth",
"chat",
"chatidtag",
"document",
"memory",
"model",
"prompt",
"tag",
'"user"',
]
for table in tables:
Session.execute(text(f"TRUNCATE TABLE {table}"))
Session.commit()
+45
View File
@@ -0,0 +1,45 @@
from contextlib import contextmanager
from fastapi import FastAPI
@contextmanager
def mock_webui_user(**kwargs):
from apps.webui.main import app
with mock_user(app, **kwargs):
yield
@contextmanager
def mock_user(app: FastAPI, **kwargs):
from utils.utils import (
get_current_user,
get_verified_user,
get_admin_user,
get_current_user_by_api_key,
)
from apps.webui.models.users import User
def create_user():
user_parameters = {
"id": "1",
"name": "John Doe",
"email": "john.doe@openwebui.com",
"role": "user",
"profile_image_url": "/user.png",
"last_active_at": 1627351200,
"updated_at": 1627351200,
"created_at": 162735120,
**kwargs,
}
return User(**user_parameters)
app.dependency_overrides = {
get_current_user: create_user,
get_verified_user: create_user,
get_admin_user: create_user,
get_current_user_by_api_key: create_user,
}
yield
app.dependency_overrides = {}
+202 -6
View File
@@ -1,9 +1,199 @@
from pathlib import Path
import hashlib
import json
import re
from datetime import timedelta
from typing import Optional
from typing import Optional, Callable
import uuid
import time
from utils.task import prompt_template
def get_last_user_message_item(messages: list[dict]) -> Optional[dict]:
for message in reversed(messages):
if message["role"] == "user":
return message
return None
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_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":
return get_content_from_message(message)
return None
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]:
return [message for message in messages if message["role"] != "system"]
def pop_system_message(messages: list[dict]) -> tuple[Optional[dict], list[dict]]:
return get_system_message(messages), remove_system_message(messages)
def prepend_to_first_user_message_content(
content: str, messages: list[dict]
) -> list[dict]:
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.
:param msg: The message to be added or appended.
:param messages: The list of message dictionaries.
:return: The updated list of message dictionaries.
"""
if messages and messages[0].get("role") == "system":
messages[0]["content"] += f"{content}\n{messages[0]['content']}"
else:
# Insert at the beginning
messages.insert(0, {"role": "system", "content": content})
return messages
def openai_chat_message_template(model: str):
return {
"id": f"{model}-{str(uuid.uuid4())}",
"created": int(time.time()),
"model": model,
"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):
@@ -76,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
@@ -172,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)
@@ -193,8 +383,14 @@ def parse_ollama_modelfile(model_text):
system_desc_match = re.search(
r'SYSTEM\s+"""(.+?)"""', model_text, re.DOTALL | re.IGNORECASE
)
system_desc_match_single = re.search(
r"SYSTEM\s+([^\n]+)", model_text, re.IGNORECASE
)
if system_desc_match:
data["params"]["system"] = system_desc_match.group(1).strip()
elif system_desc_match_single:
data["params"]["system"] = system_desc_match_single.group(1).strip()
# Parse messages
messages = []
-10
View File
@@ -1,10 +0,0 @@
from apps.webui.models.models import Models, ModelModel, ModelForm, ModelResponse
def get_model_id_from_custom_model_id(id: str):
model = Models.get_model_by_id(id)
if model:
return model.id
else:
return id
+104
View File
@@ -0,0 +1,104 @@
from pydantic import BaseModel, Field, create_model
from typing import Any, Optional, Type
def json_schema_to_model(tool_dict: dict[str, Any]) -> Type[BaseModel]:
"""
Converts a JSON schema to a Pydantic BaseModel class.
Args:
json_schema: The JSON schema to convert.
Returns:
A Pydantic BaseModel class.
"""
# Extract the model name from the schema title.
model_name = tool_dict["name"]
schema = tool_dict["parameters"]
# Extract the field definitions from the schema properties.
field_definitions = {
name: json_schema_to_pydantic_field(name, prop, schema.get("required", []))
for name, prop in schema.get("properties", {}).items()
}
# Create the BaseModel class using create_model().
return create_model(model_name, **field_definitions)
def json_schema_to_pydantic_field(
name: str, json_schema: dict[str, Any], required: list[str]
) -> Any:
"""
Converts a JSON schema property to a Pydantic field definition.
Args:
name: The field name.
json_schema: The JSON schema property.
Returns:
A Pydantic field definition.
"""
# Get the field type.
type_ = json_schema_to_pydantic_type(json_schema)
# Get the field description.
description = json_schema.get("description")
# Get the field examples.
examples = json_schema.get("examples")
# Create a Field object with the type, description, and examples.
# The 'required' flag will be set later when creating the model.
return (
type_,
Field(
description=description,
examples=examples,
default=... if name in required else None,
),
)
def json_schema_to_pydantic_type(json_schema: dict[str, Any]) -> Any:
"""
Converts a JSON schema type to a Pydantic type.
Args:
json_schema: The JSON schema to convert.
Returns:
A Pydantic type.
"""
type_ = json_schema.get("type")
if type_ == "string" or type_ == "str":
return str
elif type_ == "integer" or type_ == "int":
return int
elif type_ == "number" or type_ == "float":
return float
elif type_ == "boolean" or type_ == "bool":
return bool
elif type_ == "array":
items_schema = json_schema.get("items")
if items_schema:
item_type = json_schema_to_pydantic_type(items_schema)
return list[item_type]
else:
return list
elif type_ == "object":
# Handle nested models.
properties = json_schema.get("properties")
if properties:
nested_model = json_schema_to_model(json_schema)
return nested_model
else:
return dict
elif type_ == "null":
return Optional[Any] # Use Optional[Any] for nullable fields
else:
raise ValueError(f"Unsupported JSON schema type: {type_}")
+163
View File
@@ -0,0 +1,163 @@
import re
import math
from datetime import datetime
from typing import Optional
def prompt_template(
template: str, user_name: Optional[str] = None, user_location: Optional[str] = None
) -> str:
# Get the current date
current_date = datetime.now()
# Format the date to YYYY-MM-DD
formatted_date = current_date.strftime("%Y-%m-%d")
formatted_time = current_date.strftime("%I:%M:%S %p")
template = template.replace("{{CURRENT_DATE}}", formatted_date)
template = template.replace("{{CURRENT_TIME}}", formatted_time)
template = template.replace(
"{{CURRENT_DATETIME}}", f"{formatted_date} {formatted_time}"
)
if user_name:
# Replace {{USER_NAME}} in the template with the user's name
template = template.replace("{{USER_NAME}}", user_name)
else:
# Replace {{USER_NAME}} in the template with "Unknown"
template = template.replace("{{USER_NAME}}", "Unknown")
if user_location:
# Replace {{USER_LOCATION}} in the template with the current location
template = template.replace("{{USER_LOCATION}}", user_location)
else:
# Replace {{USER_LOCATION}} in the template with "Unknown"
template = template.replace("{{USER_LOCATION}}", "Unknown")
return template
def title_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)
end_length = match.group(2)
middle_length = match.group(3)
if full_match == "{{prompt}}":
return prompt
elif start_length is not None:
return prompt[: int(start_length)]
elif end_length is not None:
return prompt[-int(end_length) :]
elif middle_length is not None:
middle_length = int(middle_length)
if len(prompt) <= middle_length:
return prompt
start = prompt[: math.ceil(middle_length / 2)]
end = prompt[-math.floor(middle_length / 2) :]
return f"{start}...{end}"
return ""
template = re.sub(
r"{{prompt}}|{{prompt:start:(\d+)}}|{{prompt:end:(\d+)}}|{{prompt:middletruncate:(\d+)}}",
replacement_function,
template,
)
template = prompt_template(
template,
**(
{"user_name": user.get("name"), "user_location": user.get("location")}
if user
else {}
),
)
return 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)
end_length = match.group(2)
middle_length = match.group(3)
if full_match == "{{prompt}}":
return prompt
elif start_length is not None:
return prompt[: int(start_length)]
elif end_length is not None:
return prompt[-int(end_length) :]
elif middle_length is not None:
middle_length = int(middle_length)
if len(prompt) <= middle_length:
return prompt
start = prompt[: math.ceil(middle_length / 2)]
end = prompt[-math.floor(middle_length / 2) :]
return f"{start}...{end}"
return ""
template = re.sub(
r"{{prompt}}|{{prompt:start:(\d+)}}|{{prompt:end:(\d+)}}|{{prompt:middletruncate:(\d+)}}",
replacement_function,
template,
)
template = prompt_template(
template,
**(
{"user_name": user.get("name"), "user_location": user.get("location")}
if user
else {}
),
)
return template
def moa_response_generation_template(
template: str, prompt: str, responses: list[str]
) -> str:
def replacement_function(match):
full_match = match.group(0)
start_length = match.group(1)
end_length = match.group(2)
middle_length = match.group(3)
if full_match == "{{prompt}}":
return prompt
elif start_length is not None:
return prompt[: int(start_length)]
elif end_length is not None:
return prompt[-int(end_length) :]
elif middle_length is not None:
middle_length = int(middle_length)
if len(prompt) <= middle_length:
return prompt
start = prompt[: math.ceil(middle_length / 2)]
end = prompt[-math.floor(middle_length / 2) :]
return f"{start}...{end}"
return ""
template = re.sub(
r"{{prompt}}|{{prompt:start:(\d+)}}|{{prompt:end:(\d+)}}|{{prompt:middletruncate:(\d+)}}",
replacement_function,
template,
)
responses = [f'"""{response}"""' for response in responses]
responses = "\n\n".join(responses)
template = template.replace("{{responses}}", responses)
return template
def tools_function_calling_generation_template(template: str, tools_specs: str) -> str:
template = template.replace("{{TOOLS}}", tools_specs)
return template
+164
View File
@@ -0,0 +1,164 @@
import inspect
import logging
from typing import Awaitable, Callable, get_type_hints
from apps.webui.models.tools import Tools
from apps.webui.models.users import UserModel
from apps.webui.utils import load_toolkit_module_by_id
from utils.schemas import json_schema_to_model
log = logging.getLogger(__name__)
def apply_extra_params_to_tool_function(
function: Callable, extra_params: dict
) -> Callable[..., Awaitable]:
sig = inspect.signature(function)
extra_params = {
key: value for key, value in extra_params.items() if key in sig.parameters
}
is_coroutine = inspect.iscoroutinefunction(function)
async def new_function(**kwargs):
extra_kwargs = kwargs | extra_params
if is_coroutine:
return await function(**extra_kwargs)
return function(**extra_kwargs)
return new_function
# Mutation on extra_params
def get_tools(
webui_app, tool_ids: list[str], user: UserModel, extra_params: dict
) -> dict[str, dict]:
tools = {}
for tool_id in tool_ids:
toolkit = Tools.get_tool_by_id(tool_id)
if toolkit is None:
continue
module = webui_app.state.TOOLS.get(tool_id, None)
if module is None:
module, _ = load_toolkit_module_by_id(tool_id)
webui_app.state.TOOLS[tool_id] = module
extra_params["__id__"] = tool_id
if hasattr(module, "valves") and hasattr(module, "Valves"):
valves = Tools.get_tool_valves_by_id(tool_id) or {}
module.valves = module.Valves(**valves)
if hasattr(module, "UserValves"):
extra_params["__user__"]["valves"] = module.UserValves( # type: ignore
**Tools.get_user_valves_by_id_and_user_id(tool_id, user.id)
)
for spec in toolkit.specs:
# TODO: Fix hack for OpenAI API
for val in spec.get("parameters", {}).get("properties", {}).values():
if val["type"] == "str":
val["type"] = "string"
function_name = spec["name"]
# convert to function that takes only model params and inserts custom params
original_func = getattr(module, function_name)
callable = apply_extra_params_to_tool_function(original_func, extra_params)
if hasattr(original_func, "__doc__"):
callable.__doc__ = original_func.__doc__
# TODO: This needs to be a pydantic model
tool_dict = {
"toolkit_id": tool_id,
"callable": callable,
"spec": spec,
"pydantic_model": json_schema_to_model(spec),
"file_handler": hasattr(module, "file_handler") and module.file_handler,
"citation": hasattr(module, "citation") and module.citation,
}
# TODO: if collision, prepend toolkit name
if function_name in tools:
log.warning(f"Tool {function_name} already exists in another toolkit!")
log.warning(f"Collision between {toolkit} and {tool_id}.")
log.warning(f"Discarding {toolkit}.{function_name}")
else:
tools[function_name] = tool_dict
return tools
def doc_to_dict(docstring):
lines = docstring.split("\n")
description = lines[1].strip()
param_dict = {}
for line in lines:
if ":param" in line:
line = line.replace(":param", "").strip()
param, desc = line.split(":", 1)
param_dict[param.strip()] = desc.strip()
ret_dict = {"description": description, "params": param_dict}
return ret_dict
def get_tools_specs(tools) -> list[dict]:
function_list = [
{"name": func, "function": getattr(tools, func)}
for func in dir(tools)
if callable(getattr(tools, func))
and not func.startswith("__")
and not inspect.isclass(getattr(tools, func))
]
specs = []
for function_item in function_list:
function_name = function_item["name"]
function = function_item["function"]
function_doc = doc_to_dict(function.__doc__ or function_name)
specs.append(
{
"name": function_name,
# TODO: multi-line desc?
"description": function_doc.get("description", function_name),
"parameters": {
"type": "object",
"properties": {
param_name: {
"type": param_annotation.__name__.lower(),
**(
{
"enum": (
str(param_annotation.__args__)
if hasattr(param_annotation, "__args__")
else None
)
}
if hasattr(param_annotation, "__args__")
else {}
),
"description": function_doc.get("params", {}).get(
param_name, param_name
),
}
for param_name, param_annotation in get_type_hints(
function
).items()
if param_name != "return"
and not (
param_name.startswith("__") and param_name.endswith("__")
)
},
"required": [
name
for name, param in inspect.signature(
function
).parameters.items()
if param.default is param.empty
and not (name.startswith("__") and name.endswith("__"))
],
},
}
)
return specs
+21 -10
View File
@@ -1,14 +1,12 @@
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from fastapi import HTTPException, status, Depends
from fastapi import HTTPException, status, Depends, Request
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
@@ -24,7 +22,7 @@ ALGORITHM = "HS256"
# Auth Utils
##############
bearer_security = HTTPBearer()
bearer_security = HTTPBearer(auto_error=False)
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
@@ -53,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
@@ -70,19 +68,32 @@ 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)
def get_current_user(
request: Request,
auth_token: HTTPAuthorizationCredentials = Depends(bearer_security),
):
token = None
if auth_token is not None:
token = auth_token.credentials
if token is None and "token" in request.cookies:
token = request.cookies.get("token")
if token is None:
raise HTTPException(status_code=403, detail="Not authenticated")
# auth by api key
if auth_token.credentials.startswith("sk-"):
return get_current_user_by_api_key(auth_token.credentials)
if token.startswith("sk-"):
return get_current_user_by_api_key(token)
# auth by jwt token
data = decode_token(auth_token.credentials)
if data != None and "id" in data:
data = decode_token(token)
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');
});
});
});
-21
View File
@@ -28,19 +28,6 @@ describe('Settings', () => {
});
});
context('Connections', () => {
it('user can open the Connections modal and hit save', () => {
cy.get('button').contains('Connections').click();
cy.get('button').contains('Save').click();
});
});
context('Models', () => {
it('user can open the Models modal', () => {
cy.get('button').contains('Models').click();
});
});
context('Interface', () => {
it('user can open the Interface modal and hit save', () => {
cy.get('button').contains('Interface').click();
@@ -55,14 +42,6 @@ describe('Settings', () => {
});
});
context('Images', () => {
it('user can open the Images modal and hit save', () => {
cy.get('button').contains('Images').click();
// Currently fails because the backend requires a valid URL
// cy.get('button').contains('Save').click();
});
});
context('Chats', () => {
it('user can open the Chats modal', () => {
cy.get('button').contains('Chats').click();
+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();
});

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