Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3b70cd64d7 | ||
|
|
1173459eee | ||
|
|
c0ecff9d70 | ||
|
|
d4fca9dabf | ||
|
|
aaaebfabbe | ||
|
|
6eccf8668e | ||
|
|
5e78a5c510 | ||
|
|
dce24225e0 | ||
|
|
f9b44c6bf2 | ||
|
|
f573a90a56 | ||
|
|
23bb0d927f | ||
|
|
6b0af942db | ||
|
|
f95a482c24 | ||
|
|
edf94d732b | ||
|
|
73a77abd20 | ||
|
|
a8f205213c | ||
|
|
4770285c04 | ||
|
|
1639fbb544 | ||
|
|
2dc5f3c30c | ||
|
|
1a51584fe0 | ||
|
|
976c94ba5a | ||
|
|
4df74690e1 | ||
|
|
e2d60bd92e | ||
|
|
c2eada36f1 | ||
|
|
0ce6975ec8 | ||
|
|
1ddc56fcf8 | ||
|
|
7f7365db4f | ||
|
|
c9e2ba56c6 | ||
|
|
b01c9184c2 | ||
|
|
3ee1ae8c95 | ||
|
|
8ce6c8cf78 | ||
|
|
fbf4f5eb30 | ||
|
|
6c77e6fdb3 | ||
|
|
3b74431ea3 | ||
|
|
53fe7c460b | ||
|
|
f39556b933 | ||
|
|
cffd02defe | ||
|
|
bdc6088159 | ||
|
|
2a75a85556 | ||
|
|
94c025c6d7 | ||
|
|
8961bc2453 | ||
|
|
cb82c7062a | ||
|
|
b15814c42f | ||
|
|
023c8bb5b4 | ||
|
|
500440ebec | ||
|
|
b94de4f002 | ||
|
|
039a1e1f14 | ||
|
|
14a6c1f496 | ||
|
|
0a295ff56d | ||
|
|
36ffa9824b | ||
|
|
e3769c1073 | ||
|
|
d78fe4decc | ||
|
|
7b442e4be0 | ||
|
|
d17ac95f95 | ||
|
|
59c3203990 | ||
|
|
a4747c88e0 | ||
|
|
39ea59edc8 | ||
|
|
67ed61d022 | ||
|
|
b8f3abda5a | ||
|
|
8697f72068 | ||
|
|
3e7077bb73 | ||
|
|
5f89c1b137 | ||
|
|
811e1f92b1 | ||
|
|
15cbccc8f5 | ||
|
|
1cfa491179 | ||
|
|
35e7e07821 | ||
|
|
9ff54288ef | ||
|
|
baca6db680 | ||
|
|
6bd2bc5683 | ||
|
|
029625ec4e | ||
|
|
7e65f7fa4d | ||
|
|
80f5573cf3 | ||
|
|
1236157800 | ||
|
|
e628bfe6ff | ||
|
|
bb2bd7d721 | ||
|
|
9c107f9d2d | ||
|
|
fafd847f18 | ||
|
|
3a7e5a5f31 | ||
|
|
613d278afc | ||
|
|
96e0c4d7b8 | ||
|
|
46cea1f904 | ||
|
|
d844fc7edb | ||
|
|
3468dda556 | ||
|
|
e6ff4169e1 | ||
|
|
6f8c1a8f0d | ||
|
|
d529be86fe | ||
|
|
3ad4b10470 | ||
|
|
376fce273e | ||
|
|
c19960002b | ||
|
|
dc2b8fd327 | ||
|
|
737dfd2763 | ||
|
|
3fa323d1a1 | ||
|
|
a01ef6cfa0 | ||
|
|
c7d4d2e41f | ||
|
|
744ffbb1fb | ||
|
|
4d6132b9de | ||
|
|
7a31ff6a2f | ||
|
|
516f1cf67b | ||
|
|
3d6e48b05e | ||
|
|
3dd492627e | ||
|
|
66210b95fc | ||
|
|
6e58d99920 | ||
|
|
6471f12668 | ||
|
|
f3c4c2b8e3 | ||
|
|
a73b6448c5 | ||
|
|
3238981202 | ||
|
|
59ddce1ea3 | ||
|
|
2b025b267a | ||
|
|
efe8c4ca69 | ||
|
|
d7088efe73 | ||
|
|
05c5e73304 | ||
|
|
f00f518abe | ||
|
|
639f4a44e0 | ||
|
|
92e1c654ce | ||
|
|
81d2df4df9 | ||
|
|
65949dbcbc | ||
|
|
eb97e30b0a | ||
|
|
84518490c5 | ||
|
|
dd73ecbdc9 | ||
|
|
588a179b7f | ||
|
|
244915cd1e | ||
|
|
094d777e55 | ||
|
|
b430deab15 | ||
|
|
fe44e4d344 | ||
|
|
88c02d5a14 | ||
|
|
b75b891de1 | ||
|
|
f1ce653830 | ||
|
|
c06484297b | ||
|
|
331caad5b8 | ||
|
|
8b3fcfea13 | ||
|
|
05599e1bb5 | ||
|
|
bcdbc4fb8a | ||
|
|
41a2b86e70 | ||
|
|
f9292ab4d2 | ||
|
|
f4bd094128 | ||
|
|
90aa29528c | ||
|
|
34e3cb6881 | ||
|
|
d0ddb0637e | ||
|
|
2405809f92 | ||
|
|
77579f5509 | ||
|
|
5e33955566 | ||
|
|
73c0262345 | ||
|
|
a3ea924f0a | ||
|
|
01435997b9 | ||
|
|
90fe437e29 | ||
|
|
43077b82b7 | ||
|
|
1b56a8f3cb | ||
|
|
1d27402dd0 | ||
|
|
6f7161acf8 | ||
|
|
54e3ca8e6c | ||
|
|
95cadaca72 | ||
|
|
e26c443815 | ||
|
|
046f1fb30a | ||
|
|
63d825692e | ||
|
|
70667d5cc8 | ||
|
|
2ac0eae99e | ||
|
|
1dd442f38b | ||
|
|
94c19f73b0 | ||
|
|
ed0b9be4e3 | ||
|
|
866d02bbae | ||
|
|
f7c5cd486b | ||
|
|
660a44c918 | ||
|
|
b6873153c1 | ||
|
|
24a2e84692 | ||
|
|
5e873bc643 | ||
|
|
f643182eb0 | ||
|
|
c5cfcc1229 | ||
|
|
203dfdedf1 | ||
|
|
febc9ab885 | ||
|
|
38fb9d5fd8 | ||
|
|
15485e7c5d | ||
|
|
8241fa2e12 | ||
|
|
37a7dd6d33 | ||
|
|
85b7ec9387 | ||
|
|
b838a1df99 | ||
|
|
d7b18b662b | ||
|
|
b061775932 | ||
|
|
8c4d967ef4 | ||
|
|
ce7cf62a55 | ||
|
|
0a4dbf7cf0 | ||
|
|
fcbdfbd744 | ||
|
|
1ae702b8a6 | ||
|
|
2d379cb35f | ||
|
|
76e90d9f3f | ||
|
|
da8c42ee49 | ||
|
|
ddb30589e3 | ||
|
|
ef734833a5 | ||
|
|
40ddd172e0 | ||
|
|
57010901e6 | ||
|
|
1c2e36f1b7 | ||
|
|
34aeaaf020 | ||
|
|
fa91d83ac3 | ||
|
|
78a8ef8e66 | ||
|
|
46ac6f2b29 | ||
|
|
33d3558ca9 | ||
|
|
c1c92b0c03 | ||
|
|
618307f35b | ||
|
|
c5a09cdd21 | ||
|
|
661711164a | ||
|
|
80efaaffdd | ||
|
|
46f2dc0d11 | ||
|
|
674d6e08fc | ||
|
|
76891f4760 | ||
|
|
20f0b02b38 | ||
|
|
bef7a532cc | ||
|
|
ced508c912 | ||
|
|
c883368842 | ||
|
|
8097b6a3ff | ||
|
|
7cfbf6489a | ||
|
|
11967a780d | ||
|
|
6cf0ceedba | ||
|
|
ec72dcba75 | ||
|
|
54d7bf8f28 | ||
|
|
3be5e3129b | ||
|
|
2298bfbd7e | ||
|
|
0e5d5ecb81 | ||
|
|
b52a7e3543 | ||
|
|
df1238229c | ||
|
|
eafefcf838 | ||
|
|
9f27d7710b | ||
|
|
7c68a424f8 | ||
|
|
5d3249848d | ||
|
|
7e2b2ec761 | ||
|
|
b964b175d4 | ||
|
|
46c4da4864 | ||
|
|
126045318f | ||
|
|
205013da2f | ||
|
|
abf36c0645 | ||
|
|
e06111a362 | ||
|
|
cd3904046d | ||
|
|
89aaf64209 | ||
|
|
95ae2aeef3 | ||
|
|
cefe274f74 | ||
|
|
d8bc3098db | ||
|
|
628b89009c | ||
|
|
23ff2988bf | ||
|
|
e0c199255f | ||
|
|
b2422b3c8f | ||
|
|
4cc3102758 | ||
|
|
8c020488dd | ||
|
|
0335d479f9 | ||
|
|
dcaab9d6b5 | ||
|
|
0ba45ad82b | ||
|
|
802f8809dd | ||
|
|
7a63947b94 | ||
|
|
737b1723c7 | ||
|
|
e90e254575 | ||
|
|
f8ad74fc42 | ||
|
|
6c862b8fbf | ||
|
|
4b908ea626 | ||
|
|
b77937dfad | ||
|
|
b00e14eb3f | ||
|
|
9748a030f7 | ||
|
|
1060d62add | ||
|
|
4c3dd84b32 | ||
|
|
82657acdce | ||
|
|
c96cc831d5 | ||
|
|
e41a5278f7 | ||
|
|
b5aa562039 | ||
|
|
5281aeb07b | ||
|
|
39717812ee | ||
|
|
9f6e182400 | ||
|
|
e4ba8fea8a | ||
|
|
f80aea60c7 | ||
|
|
a407558375 | ||
|
|
65aa41ab81 | ||
|
|
6331963e66 | ||
|
|
32fa8d5143 | ||
|
|
8f3fcdcf5c | ||
|
|
794919e91d | ||
|
|
667d26ca12 | ||
|
|
50dec12072 | ||
|
|
8fb6b03ddc | ||
|
|
7bfa29fa81 | ||
|
|
9bada6421e | ||
|
|
b14e75dd6c | ||
|
|
6623583004 | ||
|
|
d50098b622 | ||
|
|
642dcd4b70 | ||
|
|
a7d8ed0c6d | ||
|
|
ab1b910d80 | ||
|
|
a3c2ff61ed | ||
|
|
b111434fa9 | ||
|
|
613a087387 | ||
|
|
b0a19a9801 | ||
|
|
086a78f863 | ||
|
|
b4c64a21b3 | ||
|
|
fb3886cf04 | ||
|
|
c9079d4b4d | ||
|
|
c4b441de65 | ||
|
|
f789ad59a9 | ||
|
|
cdf620e6ee | ||
|
|
2993332b38 | ||
|
|
ab1f2ae914 | ||
|
|
68e33b5386 | ||
|
|
e31f680788 | ||
|
|
1bf16ec23d | ||
|
|
2697a4d7f8 | ||
|
|
8629f6b0f3 | ||
|
|
1332a0d381 | ||
|
|
a8859a8145 | ||
|
|
08df5f6d7c | ||
|
|
39c6ecb50d | ||
|
|
f865a23a18 | ||
|
|
8aa6415500 | ||
|
|
4a2a12fd21 | ||
|
|
a381ffc585 | ||
|
|
cdc13e9960 | ||
|
|
1a2d81bc01 | ||
|
|
06062568c7 | ||
|
|
6d62e71c34 | ||
|
|
35f3824932 |
@@ -1,80 +0,0 @@
|
|||||||
---
|
|
||||||
name: Bug report
|
|
||||||
about: Create a report to help us improve
|
|
||||||
title: ''
|
|
||||||
labels: ''
|
|
||||||
assignees: ''
|
|
||||||
---
|
|
||||||
|
|
||||||
# Bug Report
|
|
||||||
|
|
||||||
## Important Notes
|
|
||||||
|
|
||||||
- **Before submitting a bug report**: Please check the Issues or Discussions section to see if a similar issue or feature request has already been posted. It's likely we're already tracking it! If you’re unsure, start a discussion post first. This will help us efficiently focus on improving the project.
|
|
||||||
|
|
||||||
- **Collaborate respectfully**: We value a constructive attitude, so please be mindful of your communication. If negativity is part of your approach, our capacity to engage may be limited. We’re here to help if you’re open to learning and communicating positively. Remember, Open WebUI is a volunteer-driven project managed by a single maintainer and supported by contributors who also have full-time jobs. We appreciate your time and ask that you respect ours.
|
|
||||||
|
|
||||||
- **Contributing**: If you encounter an issue, we highly encourage you to submit a pull request or fork the project. We actively work to prevent contributor burnout to maintain the quality and continuity of Open WebUI.
|
|
||||||
|
|
||||||
- **Bug reproducibility**: If a bug cannot be reproduced with a `:main` or `:dev` Docker setup, or a pip install with Python 3.11, it may require additional help from the community. In such cases, we will move it to the "issues" Discussions section due to our limited resources. We encourage the community to assist with these issues. Remember, it’s not that the issue doesn’t exist; we need your help!
|
|
||||||
|
|
||||||
Note: Please remove the notes above when submitting your post. Thank you for your understanding and support!
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Installation Method
|
|
||||||
|
|
||||||
[Describe the method you used to install the project, e.g., git clone, Docker, pip, etc.]
|
|
||||||
|
|
||||||
## Environment
|
|
||||||
|
|
||||||
- **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]
|
|
||||||
|
|
||||||
**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
|
|
||||||
|
|
||||||
**Browser Console Logs:**
|
|
||||||
[Include relevant browser console logs, if applicable]
|
|
||||||
|
|
||||||
**Docker Container Logs:**
|
|
||||||
[Include relevant Docker container logs, if applicable]
|
|
||||||
|
|
||||||
**Screenshots/Screen Recordings (if applicable):**
|
|
||||||
[Attach any relevant screenshots to help illustrate the issue]
|
|
||||||
|
|
||||||
## 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.]
|
|
||||||
|
|
||||||
## Note
|
|
||||||
|
|
||||||
If the bug report is incomplete or does not follow the provided instructions, it may not be addressed. Please ensure that you have followed the steps outlined in the README.md and troubleshooting.md documents, and provide all necessary information for us to reproduce and address the issue. Thank you!
|
|
||||||
@@ -0,0 +1,144 @@
|
|||||||
|
name: Bug Report
|
||||||
|
description: Create a detailed bug report to help us improve Open WebUI.
|
||||||
|
title: 'issue: '
|
||||||
|
labels: ['bug', 'triage']
|
||||||
|
assignees: []
|
||||||
|
body:
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
# Bug Report
|
||||||
|
|
||||||
|
## Important Notes
|
||||||
|
|
||||||
|
- **Before submitting a bug report**: Please check the [Issues](https://github.com/open-webui/open-webui/issues) or [Discussions](https://github.com/open-webui/open-webui/discussions) sections to see if a similar issue has already been reported. If unsure, start a discussion first, as this helps us efficiently focus on improving the project.
|
||||||
|
|
||||||
|
- **Respectful collaboration**: Open WebUI is a volunteer-driven project with a single maintainer and contributors who also have full-time jobs. Please be constructive and respectful in your communication.
|
||||||
|
|
||||||
|
- **Contributing**: If you encounter an issue, consider submitting a pull request or forking the project. We prioritize preventing contributor burnout to maintain Open WebUI's quality.
|
||||||
|
|
||||||
|
- **Bug Reproducibility**: If a bug cannot be reproduced using a `:main` or `:dev` Docker setup or with `pip install` on Python 3.11, community assistance may be required. In such cases, we will move it to the "[Issues](https://github.com/open-webui/open-webui/discussions/categories/issues)" Discussions section. Your help is appreciated!
|
||||||
|
|
||||||
|
- type: checkboxes
|
||||||
|
id: issue-check
|
||||||
|
attributes:
|
||||||
|
label: Check Existing Issues
|
||||||
|
description: Confirm that you’ve checked for existing reports before submitting a new one.
|
||||||
|
options:
|
||||||
|
- label: I have searched the existing issues and discussions.
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: dropdown
|
||||||
|
id: installation-method
|
||||||
|
attributes:
|
||||||
|
label: Installation Method
|
||||||
|
description: How did you install Open WebUI?
|
||||||
|
options:
|
||||||
|
- Git Clone
|
||||||
|
- Pip Install
|
||||||
|
- Docker
|
||||||
|
- Other
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: input
|
||||||
|
id: open-webui-version
|
||||||
|
attributes:
|
||||||
|
label: Open WebUI Version
|
||||||
|
description: Specify the version (e.g., v0.3.11)
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: input
|
||||||
|
id: ollama-version
|
||||||
|
attributes:
|
||||||
|
label: Ollama Version (if applicable)
|
||||||
|
description: Specify the version (e.g., v0.2.0, or v0.1.32-rc1)
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
|
||||||
|
- type: input
|
||||||
|
id: operating-system
|
||||||
|
attributes:
|
||||||
|
label: Operating System
|
||||||
|
description: Specify the OS (e.g., Windows 10, macOS Sonoma, Ubuntu 22.04)
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: input
|
||||||
|
id: browser
|
||||||
|
attributes:
|
||||||
|
label: Browser (if applicable)
|
||||||
|
description: Specify the browser/version (e.g., Chrome 100.0, Firefox 98.0)
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
|
||||||
|
- type: checkboxes
|
||||||
|
id: confirmation
|
||||||
|
attributes:
|
||||||
|
label: Confirmation
|
||||||
|
description: Ensure the following prerequisites have been met.
|
||||||
|
options:
|
||||||
|
- label: I have read and followed all instructions in `README.md`.
|
||||||
|
required: true
|
||||||
|
- label: I am using the latest version of **both** Open WebUI and Ollama.
|
||||||
|
required: true
|
||||||
|
- label: I have checked the browser console logs.
|
||||||
|
required: true
|
||||||
|
- label: I have checked the Docker container logs.
|
||||||
|
required: true
|
||||||
|
- label: I have listed steps to reproduce the bug in detail.
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: expected-behavior
|
||||||
|
attributes:
|
||||||
|
label: Expected Behavior
|
||||||
|
description: Describe what should have happened.
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: actual-behavior
|
||||||
|
attributes:
|
||||||
|
label: Actual Behavior
|
||||||
|
description: Describe what actually happened.
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: reproduction-steps
|
||||||
|
attributes:
|
||||||
|
label: Steps to Reproduce
|
||||||
|
description: Provide step-by-step instructions to reproduce the issue.
|
||||||
|
placeholder: |
|
||||||
|
1. Go to '...'
|
||||||
|
2. Click on '...'
|
||||||
|
3. Scroll down to '...'
|
||||||
|
4. See the error message '...'
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: logs-screenshots
|
||||||
|
attributes:
|
||||||
|
label: Logs & Screenshots
|
||||||
|
description: Include relevant logs, errors, or screenshots to help diagnose the issue.
|
||||||
|
placeholder: 'Attach logs from the browser console, Docker logs, or error messages.'
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: additional-info
|
||||||
|
attributes:
|
||||||
|
label: Additional Information
|
||||||
|
description: Provide any extra details that may assist in understanding the issue.
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
## Note
|
||||||
|
If the bug report is incomplete or does not follow instructions, it may not be addressed. Ensure that you've followed all the **README.md** and **troubleshooting.md** guidelines, and provide all necessary information for us to reproduce the issue.
|
||||||
|
Thank you for contributing to Open WebUI!
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
---
|
|
||||||
name: Feature request
|
|
||||||
about: Suggest an idea for this project
|
|
||||||
title: ''
|
|
||||||
labels: ''
|
|
||||||
assignees: ''
|
|
||||||
---
|
|
||||||
|
|
||||||
# Feature Request
|
|
||||||
|
|
||||||
## Important Notes
|
|
||||||
|
|
||||||
- **Before submitting a report**: Please check the Issues or Discussions section to see if a similar issue or feature request has already been posted. It's likely we're already tracking it! If you’re unsure, start a discussion post first. This will help us efficiently focus on improving the project.
|
|
||||||
|
|
||||||
- **Collaborate respectfully**: We value a constructive attitude, so please be mindful of your communication. If negativity is part of your approach, our capacity to engage may be limited. We’re here to help if you’re open to learning and communicating positively. Remember, Open WebUI is a volunteer-driven project managed by a single maintainer and supported by contributors who also have full-time jobs. We appreciate your time and ask that you respect ours.
|
|
||||||
|
|
||||||
- **Contributing**: If you encounter an issue, we highly encourage you to submit a pull request or fork the project. We actively work to prevent contributor burnout to maintain the quality and continuity of Open WebUI.
|
|
||||||
|
|
||||||
- **Bug reproducibility**: If a bug cannot be reproduced with a `:main` or `:dev` Docker setup, or a pip install with Python 3.11, it may require additional help from the community. In such cases, we will move it to the "issues" Discussions section due to our limited resources. We encourage the community to assist with these issues. Remember, it’s not that the issue doesn’t exist; we need your help!
|
|
||||||
|
|
||||||
Note: Please remove the notes above when submitting your post. Thank you for your understanding and support!
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**Is your feature request related to a problem? Please describe.**
|
|
||||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
|
||||||
|
|
||||||
**Describe the solution you'd like**
|
|
||||||
A clear and concise description of what you want to happen.
|
|
||||||
|
|
||||||
**Describe alternatives you've considered**
|
|
||||||
A clear and concise description of any alternative solutions or features you've considered.
|
|
||||||
|
|
||||||
**Additional context**
|
|
||||||
Add any other context or screenshots about the feature request here.
|
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
name: Feature Request
|
||||||
|
description: Suggest an idea for this project
|
||||||
|
title: 'feat: '
|
||||||
|
labels: ['triage']
|
||||||
|
body:
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
## Important Notes
|
||||||
|
### Before submitting
|
||||||
|
Please check the [Issues](https://github.com/open-webui/open-webui/issues) or [Discussions](https://github.com/open-webui/open-webui/discussions) to see if a similar request has been posted.
|
||||||
|
It's likely we're already tracking it! If you’re unsure, start a discussion post first.
|
||||||
|
This will help us efficiently focus on improving the project.
|
||||||
|
|
||||||
|
### Collaborate respectfully
|
||||||
|
We value a **constructive attitude**, so please be mindful of your communication. If negativity is part of your approach, our capacity to engage may be limited. We're here to help if you're **open to learning** and **communicating positively**.
|
||||||
|
|
||||||
|
Remember:
|
||||||
|
- Open WebUI is a **volunteer-driven project**
|
||||||
|
- It's managed by a **single maintainer**
|
||||||
|
- It's supported by contributors who also have **full-time jobs**
|
||||||
|
|
||||||
|
We appreciate your time and ask that you **respect ours**.
|
||||||
|
|
||||||
|
|
||||||
|
### Contributing
|
||||||
|
If you encounter an issue, we highly encourage you to submit a pull request or fork the project. We actively work to prevent contributor burnout to maintain the quality and continuity of Open WebUI.
|
||||||
|
|
||||||
|
### Bug reproducibility
|
||||||
|
If a bug cannot be reproduced with a `:main` or `:dev` Docker setup, or a `pip install` with Python 3.11, it may require additional help from the community. In such cases, we will move it to the "[issues](https://github.com/open-webui/open-webui/discussions/categories/issues)" Discussions section due to our limited resources. We encourage the community to assist with these issues. Remember, it’s not that the issue doesn’t exist; we need your help!
|
||||||
|
|
||||||
|
- type: checkboxes
|
||||||
|
id: existing-issue
|
||||||
|
attributes:
|
||||||
|
label: Check Existing Issues
|
||||||
|
description: Please confirm that you've checked for existing similar requests
|
||||||
|
options:
|
||||||
|
- label: I have searched the existing issues and discussions.
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
id: problem-description
|
||||||
|
attributes:
|
||||||
|
label: Problem Description
|
||||||
|
description: Is your feature request related to a problem? Please provide a clear and concise description of what the problem is.
|
||||||
|
placeholder: "Ex. I'm always frustrated when..."
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
id: solution-description
|
||||||
|
attributes:
|
||||||
|
label: Desired Solution you'd like
|
||||||
|
description: Clearly describe what you want to happen.
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
id: alternatives-considered
|
||||||
|
attributes:
|
||||||
|
label: Alternatives Considered
|
||||||
|
description: A clear and concise description of any alternative solutions or features you've considered.
|
||||||
|
- type: textarea
|
||||||
|
id: additional-context
|
||||||
|
attributes:
|
||||||
|
label: Additional Context
|
||||||
|
description: Add any other context or screenshots about the feature request here.
|
||||||
@@ -14,7 +14,7 @@ env:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build-main-image:
|
build-main-image:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ${{ matrix.platform == 'linux/arm64' && 'ubuntu-24.04-arm' || 'ubuntu-latest' }}
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
packages: write
|
packages: write
|
||||||
@@ -111,7 +111,7 @@ jobs:
|
|||||||
retention-days: 1
|
retention-days: 1
|
||||||
|
|
||||||
build-cuda-image:
|
build-cuda-image:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ${{ matrix.platform == 'linux/arm64' && 'ubuntu-24.04-arm' || 'ubuntu-latest' }}
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
packages: write
|
packages: write
|
||||||
@@ -211,7 +211,7 @@ jobs:
|
|||||||
retention-days: 1
|
retention-days: 1
|
||||||
|
|
||||||
build-ollama-image:
|
build-ollama-image:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ${{ matrix.platform == 'linux/arm64' && 'ubuntu-24.04-arm' || 'ubuntu-latest' }}
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
packages: write
|
packages: write
|
||||||
|
|||||||
@@ -5,6 +5,65 @@ 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/),
|
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).
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
## [0.5.20] - 2025-03-05
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- **⚡ Toggle Code Execution On/Off**: You can now enable or disable code execution, providing more control over security, ensuring a safer and more customizable experience.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- **📜 Pinyin Keyboard Enter Key Now Works Properly**: Resolved an issue where the Enter key for Pinyin keyboards was not functioning as expected, ensuring seamless input for Chinese users.
|
||||||
|
- **🖼️ Web Manifest Loading Issue Fixed**: Addressed inconsistencies with 'site.webmanifest', guaranteeing proper loading and representation of the app across different browsers and devices.
|
||||||
|
- **📦 Non-Root Container Issue Resolved**: Fixed a critical issue where the UI failed to load correctly in non-root containers, ensuring reliable deployment in various environments.
|
||||||
|
|
||||||
|
## [0.5.19] - 2025-03-04
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- **📊 Logit Bias Parameter Support**: Fine-tune conversation dynamics by adjusting the Logit Bias parameter directly in chat settings, giving you more control over model responses.
|
||||||
|
- **⌨️ Customizable Enter Behavior**: You can now configure Enter to send messages only when combined with Ctrl (Ctrl+Enter) via Settings > Interface, preventing accidental message sends.
|
||||||
|
- **📝 Collapsible Code Blocks**: Easily collapse long code blocks to declutter your chat, making it easier to focus on important details.
|
||||||
|
- **🏷️ Tag Selector in Model Selector**: Quickly find and categorize models with the new tag filtering system in the Model Selector, streamlining model discovery.
|
||||||
|
- **📈 Experimental Elasticsearch Vector DB Support**: Now supports Elasticsearch as a vector database, offering more flexibility for data retrieval in Retrieval-Augmented Generation (RAG) workflows.
|
||||||
|
- **⚙️ General Reliability Enhancements**: Various stability improvements across the WebUI, ensuring a smoother, more consistent experience.
|
||||||
|
- **🌍 Updated Translations**: Refined multilingual support for better localization and accuracy across various languages.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- **🔄 "Stream" Hook Activation**: Fixed an issue where the "Stream" hook only worked when globally enabled, ensuring reliable real-time filtering.
|
||||||
|
- **📧 LDAP Email Case Sensitivity**: Resolved an issue where LDAP login failed due to email case sensitivity mismatches, improving authentication reliability.
|
||||||
|
- **💬 WebSocket Chat Event Registration**: Fixed a bug preventing chat event listeners from being registered upon sign-in, ensuring real-time updates work properly.
|
||||||
|
|
||||||
|
## [0.5.18] - 2025-02-27
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- **🌐 Open WebUI Now Works Over LAN in Insecure Context**: Resolved an issue preventing Open WebUI from functioning when accessed over a local network in an insecure context, ensuring seamless connectivity.
|
||||||
|
- **🔄 UI Now Reflects Deleted Connections Instantly**: Fixed an issue where deleting a connection did not update the UI in real time, ensuring accurate system state visibility.
|
||||||
|
- **🛠️ Models Now Display Correctly with ENABLE_FORWARD_USER_INFO_HEADERS**: Addressed a bug where models were not visible when ENABLE_FORWARD_USER_INFO_HEADERS was set, restoring proper model listing.
|
||||||
|
|
||||||
|
## [0.5.17] - 2025-02-27
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- **🚀 Instant Document Upload with Bypass Embedding & Retrieval**: Admins can now enable "Bypass Embedding & Retrieval" in Admin Settings > Documents, significantly speeding up document uploads and ensuring full document context is retained without chunking.
|
||||||
|
- **🔎 "Stream" Hook for Real-Time Filtering**: The new "stream" hook allows dynamic real-time message filtering. Learn more in our documentation (https://docs.openwebui.com/features/plugin/functions/filter).
|
||||||
|
- **☁️ OneDrive Integration**: Early support for OneDrive storage integration has been introduced, expanding file import options.
|
||||||
|
- **📈 Enhanced Logging with Loguru**: Backend logging has been improved with Loguru, making debugging and issue tracking far more efficient.
|
||||||
|
- **⚙️ General Stability Enhancements**: Backend and frontend refactoring improves performance, ensuring a smoother and more reliable user experience.
|
||||||
|
- **🌍 Updated Translations**: Refined multilingual support for better localization and accuracy across various languages.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- **🔄 Reliable Model Imports from the Community Platform**: Resolved import failures, allowing seamless integration of community-shared models without errors.
|
||||||
|
- **📊 OpenAI Usage Statistics Restored**: Fixed an issue where OpenAI usage metrics were not displaying correctly, ensuring accurate tracking of usage data.
|
||||||
|
- **🗂️ Deduplication for Retrieved Documents**: Documents retrieved during searches are now intelligently deduplicated, meaning no more redundant results—helping to keep information concise and relevant.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- **📝 "Full Context Mode" Renamed for Clarity**: The "Full Context Mode" toggle in Web Search settings is now labeled "Bypass Embedding & Retrieval" for consistency across the UI.
|
||||||
|
|
||||||
## [0.5.16] - 2025-02-20
|
## [0.5.16] - 2025-02-20
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ from pathlib import Path
|
|||||||
from typing import Generic, Optional, TypeVar
|
from typing import Generic, Optional, TypeVar
|
||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
import chromadb
|
|
||||||
import requests
|
import requests
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
from sqlalchemy import JSON, Column, DateTime, Integer, func
|
from sqlalchemy import JSON, Column, DateTime, Integer, func
|
||||||
@@ -44,7 +43,7 @@ logging.getLogger("uvicorn.access").addFilter(EndpointFilter())
|
|||||||
|
|
||||||
# Function to run the alembic migrations
|
# Function to run the alembic migrations
|
||||||
def run_migrations():
|
def run_migrations():
|
||||||
print("Running migrations")
|
log.info("Running migrations")
|
||||||
try:
|
try:
|
||||||
from alembic import command
|
from alembic import command
|
||||||
from alembic.config import Config
|
from alembic.config import Config
|
||||||
@@ -57,7 +56,7 @@ def run_migrations():
|
|||||||
|
|
||||||
command.upgrade(alembic_cfg, "head")
|
command.upgrade(alembic_cfg, "head")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error: {e}")
|
log.exception(f"Error running migrations: {e}")
|
||||||
|
|
||||||
|
|
||||||
run_migrations()
|
run_migrations()
|
||||||
@@ -588,6 +587,17 @@ load_oauth_providers()
|
|||||||
|
|
||||||
STATIC_DIR = Path(os.getenv("STATIC_DIR", OPEN_WEBUI_DIR / "static")).resolve()
|
STATIC_DIR = Path(os.getenv("STATIC_DIR", OPEN_WEBUI_DIR / "static")).resolve()
|
||||||
|
|
||||||
|
for file_path in (FRONTEND_BUILD_DIR / "static").glob("**/*"):
|
||||||
|
if file_path.is_file():
|
||||||
|
target_path = STATIC_DIR / file_path.relative_to(
|
||||||
|
(FRONTEND_BUILD_DIR / "static")
|
||||||
|
)
|
||||||
|
target_path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
try:
|
||||||
|
shutil.copyfile(file_path, target_path)
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"An error occurred: {e}")
|
||||||
|
|
||||||
frontend_favicon = FRONTEND_BUILD_DIR / "static" / "favicon.png"
|
frontend_favicon = FRONTEND_BUILD_DIR / "static" / "favicon.png"
|
||||||
|
|
||||||
if frontend_favicon.exists():
|
if frontend_favicon.exists():
|
||||||
@@ -660,11 +670,7 @@ if CUSTOM_NAME:
|
|||||||
# LICENSE_KEY
|
# LICENSE_KEY
|
||||||
####################################
|
####################################
|
||||||
|
|
||||||
LICENSE_KEY = PersistentConfig(
|
LICENSE_KEY = os.environ.get("LICENSE_KEY", "")
|
||||||
"LICENSE_KEY",
|
|
||||||
"license.key",
|
|
||||||
os.environ.get("LICENSE_KEY", ""),
|
|
||||||
)
|
|
||||||
|
|
||||||
####################################
|
####################################
|
||||||
# STORAGE PROVIDER
|
# STORAGE PROVIDER
|
||||||
@@ -678,6 +684,10 @@ S3_REGION_NAME = os.environ.get("S3_REGION_NAME", None)
|
|||||||
S3_BUCKET_NAME = os.environ.get("S3_BUCKET_NAME", None)
|
S3_BUCKET_NAME = os.environ.get("S3_BUCKET_NAME", None)
|
||||||
S3_KEY_PREFIX = os.environ.get("S3_KEY_PREFIX", None)
|
S3_KEY_PREFIX = os.environ.get("S3_KEY_PREFIX", None)
|
||||||
S3_ENDPOINT_URL = os.environ.get("S3_ENDPOINT_URL", None)
|
S3_ENDPOINT_URL = os.environ.get("S3_ENDPOINT_URL", None)
|
||||||
|
S3_USE_ACCELERATE_ENDPOINT = (
|
||||||
|
os.environ.get("S3_USE_ACCELERATE_ENDPOINT", "False").lower() == "true"
|
||||||
|
)
|
||||||
|
S3_ADDRESSING_STYLE = os.environ.get("S3_ADDRESSING_STYLE", None)
|
||||||
|
|
||||||
GCS_BUCKET_NAME = os.environ.get("GCS_BUCKET_NAME", None)
|
GCS_BUCKET_NAME = os.environ.get("GCS_BUCKET_NAME", None)
|
||||||
GOOGLE_APPLICATION_CREDENTIALS_JSON = os.environ.get(
|
GOOGLE_APPLICATION_CREDENTIALS_JSON = os.environ.get(
|
||||||
@@ -692,16 +702,16 @@ AZURE_STORAGE_KEY = os.environ.get("AZURE_STORAGE_KEY", None)
|
|||||||
# File Upload DIR
|
# File Upload DIR
|
||||||
####################################
|
####################################
|
||||||
|
|
||||||
UPLOAD_DIR = f"{DATA_DIR}/uploads"
|
UPLOAD_DIR = DATA_DIR / "uploads"
|
||||||
Path(UPLOAD_DIR).mkdir(parents=True, exist_ok=True)
|
UPLOAD_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
|
||||||
####################################
|
####################################
|
||||||
# Cache DIR
|
# Cache DIR
|
||||||
####################################
|
####################################
|
||||||
|
|
||||||
CACHE_DIR = f"{DATA_DIR}/cache"
|
CACHE_DIR = DATA_DIR / "cache"
|
||||||
Path(CACHE_DIR).mkdir(parents=True, exist_ok=True)
|
CACHE_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
|
||||||
####################################
|
####################################
|
||||||
@@ -1094,7 +1104,7 @@ try:
|
|||||||
banners = json.loads(os.environ.get("WEBUI_BANNERS", "[]"))
|
banners = json.loads(os.environ.get("WEBUI_BANNERS", "[]"))
|
||||||
banners = [BannerModel(**banner) for banner in banners]
|
banners = [BannerModel(**banner) for banner in banners]
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error loading WEBUI_BANNERS: {e}")
|
log.exception(f"Error loading WEBUI_BANNERS: {e}")
|
||||||
banners = []
|
banners = []
|
||||||
|
|
||||||
WEBUI_BANNERS = PersistentConfig("WEBUI_BANNERS", "ui.banners", banners)
|
WEBUI_BANNERS = PersistentConfig("WEBUI_BANNERS", "ui.banners", banners)
|
||||||
@@ -1370,6 +1380,11 @@ Responses from models: {{responses}}"""
|
|||||||
# Code Interpreter
|
# Code Interpreter
|
||||||
####################################
|
####################################
|
||||||
|
|
||||||
|
ENABLE_CODE_EXECUTION = PersistentConfig(
|
||||||
|
"ENABLE_CODE_EXECUTION",
|
||||||
|
"code_execution.enable",
|
||||||
|
os.environ.get("ENABLE_CODE_EXECUTION", "True").lower() == "true",
|
||||||
|
)
|
||||||
|
|
||||||
CODE_EXECUTION_ENGINE = PersistentConfig(
|
CODE_EXECUTION_ENGINE = PersistentConfig(
|
||||||
"CODE_EXECUTION_ENGINE",
|
"CODE_EXECUTION_ENGINE",
|
||||||
@@ -1498,21 +1513,27 @@ VECTOR_DB = os.environ.get("VECTOR_DB", "chroma")
|
|||||||
|
|
||||||
# Chroma
|
# Chroma
|
||||||
CHROMA_DATA_PATH = f"{DATA_DIR}/vector_db"
|
CHROMA_DATA_PATH = f"{DATA_DIR}/vector_db"
|
||||||
CHROMA_TENANT = os.environ.get("CHROMA_TENANT", chromadb.DEFAULT_TENANT)
|
|
||||||
CHROMA_DATABASE = os.environ.get("CHROMA_DATABASE", chromadb.DEFAULT_DATABASE)
|
if VECTOR_DB == "chroma":
|
||||||
CHROMA_HTTP_HOST = os.environ.get("CHROMA_HTTP_HOST", "")
|
import chromadb
|
||||||
CHROMA_HTTP_PORT = int(os.environ.get("CHROMA_HTTP_PORT", "8000"))
|
|
||||||
CHROMA_CLIENT_AUTH_PROVIDER = os.environ.get("CHROMA_CLIENT_AUTH_PROVIDER", "")
|
CHROMA_TENANT = os.environ.get("CHROMA_TENANT", chromadb.DEFAULT_TENANT)
|
||||||
CHROMA_CLIENT_AUTH_CREDENTIALS = os.environ.get("CHROMA_CLIENT_AUTH_CREDENTIALS", "")
|
CHROMA_DATABASE = os.environ.get("CHROMA_DATABASE", chromadb.DEFAULT_DATABASE)
|
||||||
# Comma-separated list of header=value pairs
|
CHROMA_HTTP_HOST = os.environ.get("CHROMA_HTTP_HOST", "")
|
||||||
CHROMA_HTTP_HEADERS = os.environ.get("CHROMA_HTTP_HEADERS", "")
|
CHROMA_HTTP_PORT = int(os.environ.get("CHROMA_HTTP_PORT", "8000"))
|
||||||
if CHROMA_HTTP_HEADERS:
|
CHROMA_CLIENT_AUTH_PROVIDER = os.environ.get("CHROMA_CLIENT_AUTH_PROVIDER", "")
|
||||||
CHROMA_HTTP_HEADERS = dict(
|
CHROMA_CLIENT_AUTH_CREDENTIALS = os.environ.get(
|
||||||
[pair.split("=") for pair in CHROMA_HTTP_HEADERS.split(",")]
|
"CHROMA_CLIENT_AUTH_CREDENTIALS", ""
|
||||||
)
|
)
|
||||||
else:
|
# Comma-separated list of header=value pairs
|
||||||
CHROMA_HTTP_HEADERS = None
|
CHROMA_HTTP_HEADERS = os.environ.get("CHROMA_HTTP_HEADERS", "")
|
||||||
CHROMA_HTTP_SSL = os.environ.get("CHROMA_HTTP_SSL", "false").lower() == "true"
|
if CHROMA_HTTP_HEADERS:
|
||||||
|
CHROMA_HTTP_HEADERS = dict(
|
||||||
|
[pair.split("=") for pair in CHROMA_HTTP_HEADERS.split(",")]
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
CHROMA_HTTP_HEADERS = None
|
||||||
|
CHROMA_HTTP_SSL = os.environ.get("CHROMA_HTTP_SSL", "false").lower() == "true"
|
||||||
# this uses the model defined in the Dockerfile ENV variable. If you dont use docker or docker based deployments such as k8s, the default embedding model will be used (sentence-transformers/all-MiniLM-L6-v2)
|
# this uses the model defined in the Dockerfile ENV variable. If you dont use docker or docker based deployments such as k8s, the default embedding model will be used (sentence-transformers/all-MiniLM-L6-v2)
|
||||||
|
|
||||||
# Milvus
|
# Milvus
|
||||||
@@ -1532,6 +1553,17 @@ OPENSEARCH_CERT_VERIFY = os.environ.get("OPENSEARCH_CERT_VERIFY", False)
|
|||||||
OPENSEARCH_USERNAME = os.environ.get("OPENSEARCH_USERNAME", None)
|
OPENSEARCH_USERNAME = os.environ.get("OPENSEARCH_USERNAME", None)
|
||||||
OPENSEARCH_PASSWORD = os.environ.get("OPENSEARCH_PASSWORD", None)
|
OPENSEARCH_PASSWORD = os.environ.get("OPENSEARCH_PASSWORD", None)
|
||||||
|
|
||||||
|
# ElasticSearch
|
||||||
|
ELASTICSEARCH_URL = os.environ.get("ELASTICSEARCH_URL", "https://localhost:9200")
|
||||||
|
ELASTICSEARCH_CA_CERTS = os.environ.get("ELASTICSEARCH_CA_CERTS", None)
|
||||||
|
ELASTICSEARCH_API_KEY = os.environ.get("ELASTICSEARCH_API_KEY", None)
|
||||||
|
ELASTICSEARCH_USERNAME = os.environ.get("ELASTICSEARCH_USERNAME", None)
|
||||||
|
ELASTICSEARCH_PASSWORD = os.environ.get("ELASTICSEARCH_PASSWORD", None)
|
||||||
|
ELASTICSEARCH_CLOUD_ID = os.environ.get("ELASTICSEARCH_CLOUD_ID", None)
|
||||||
|
SSL_ASSERT_FINGERPRINT = os.environ.get("SSL_ASSERT_FINGERPRINT", None)
|
||||||
|
ELASTICSEARCH_INDEX_PREFIX = os.environ.get(
|
||||||
|
"ELASTICSEARCH_INDEX_PREFIX", "open_webui_collections"
|
||||||
|
)
|
||||||
# Pgvector
|
# Pgvector
|
||||||
PGVECTOR_DB_URL = os.environ.get("PGVECTOR_DB_URL", DATABASE_URL)
|
PGVECTOR_DB_URL = os.environ.get("PGVECTOR_DB_URL", DATABASE_URL)
|
||||||
if VECTOR_DB == "pgvector" and not PGVECTOR_DB_URL.startswith("postgres"):
|
if VECTOR_DB == "pgvector" and not PGVECTOR_DB_URL.startswith("postgres"):
|
||||||
@@ -1566,6 +1598,18 @@ GOOGLE_DRIVE_API_KEY = PersistentConfig(
|
|||||||
os.environ.get("GOOGLE_DRIVE_API_KEY", ""),
|
os.environ.get("GOOGLE_DRIVE_API_KEY", ""),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
ENABLE_ONEDRIVE_INTEGRATION = PersistentConfig(
|
||||||
|
"ENABLE_ONEDRIVE_INTEGRATION",
|
||||||
|
"onedrive.enable",
|
||||||
|
os.getenv("ENABLE_ONEDRIVE_INTEGRATION", "False").lower() == "true",
|
||||||
|
)
|
||||||
|
|
||||||
|
ONEDRIVE_CLIENT_ID = PersistentConfig(
|
||||||
|
"ONEDRIVE_CLIENT_ID",
|
||||||
|
"onedrive.client_id",
|
||||||
|
os.environ.get("ONEDRIVE_CLIENT_ID", ""),
|
||||||
|
)
|
||||||
|
|
||||||
# RAG Content Extraction
|
# RAG Content Extraction
|
||||||
CONTENT_EXTRACTION_ENGINE = PersistentConfig(
|
CONTENT_EXTRACTION_ENGINE = PersistentConfig(
|
||||||
"CONTENT_EXTRACTION_ENGINE",
|
"CONTENT_EXTRACTION_ENGINE",
|
||||||
@@ -1579,6 +1623,26 @@ TIKA_SERVER_URL = PersistentConfig(
|
|||||||
os.getenv("TIKA_SERVER_URL", "http://tika:9998"), # Default for sidecar deployment
|
os.getenv("TIKA_SERVER_URL", "http://tika:9998"), # Default for sidecar deployment
|
||||||
)
|
)
|
||||||
|
|
||||||
|
DOCUMENT_INTELLIGENCE_ENDPOINT = PersistentConfig(
|
||||||
|
"DOCUMENT_INTELLIGENCE_ENDPOINT",
|
||||||
|
"rag.document_intelligence_endpoint",
|
||||||
|
os.getenv("DOCUMENT_INTELLIGENCE_ENDPOINT", ""),
|
||||||
|
)
|
||||||
|
|
||||||
|
DOCUMENT_INTELLIGENCE_KEY = PersistentConfig(
|
||||||
|
"DOCUMENT_INTELLIGENCE_KEY",
|
||||||
|
"rag.document_intelligence_key",
|
||||||
|
os.getenv("DOCUMENT_INTELLIGENCE_KEY", ""),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
BYPASS_EMBEDDING_AND_RETRIEVAL = PersistentConfig(
|
||||||
|
"BYPASS_EMBEDDING_AND_RETRIEVAL",
|
||||||
|
"rag.bypass_embedding_and_retrieval",
|
||||||
|
os.environ.get("BYPASS_EMBEDDING_AND_RETRIEVAL", "False").lower() == "true",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
RAG_TOP_K = PersistentConfig(
|
RAG_TOP_K = PersistentConfig(
|
||||||
"RAG_TOP_K", "rag.top_k", int(os.environ.get("RAG_TOP_K", "3"))
|
"RAG_TOP_K", "rag.top_k", int(os.environ.get("RAG_TOP_K", "3"))
|
||||||
)
|
)
|
||||||
@@ -1795,10 +1859,10 @@ RAG_WEB_SEARCH_ENGINE = PersistentConfig(
|
|||||||
os.getenv("RAG_WEB_SEARCH_ENGINE", ""),
|
os.getenv("RAG_WEB_SEARCH_ENGINE", ""),
|
||||||
)
|
)
|
||||||
|
|
||||||
RAG_WEB_SEARCH_FULL_CONTEXT = PersistentConfig(
|
BYPASS_WEB_SEARCH_EMBEDDING_AND_RETRIEVAL = PersistentConfig(
|
||||||
"RAG_WEB_SEARCH_FULL_CONTEXT",
|
"BYPASS_WEB_SEARCH_EMBEDDING_AND_RETRIEVAL",
|
||||||
"rag.web.search.full_context",
|
"rag.web.search.bypass_embedding_and_retrieval",
|
||||||
os.getenv("RAG_WEB_SEARCH_FULL_CONTEXT", "False").lower() == "true",
|
os.getenv("BYPASS_WEB_SEARCH_EMBEDDING_AND_RETRIEVAL", "False").lower() == "true",
|
||||||
)
|
)
|
||||||
|
|
||||||
# You can provide a list of your own websites to filter after performing a web search.
|
# You can provide a list of your own websites to filter after performing a web search.
|
||||||
@@ -1936,6 +2000,12 @@ EXA_API_KEY = PersistentConfig(
|
|||||||
os.getenv("EXA_API_KEY", ""),
|
os.getenv("EXA_API_KEY", ""),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
PERPLEXITY_API_KEY = PersistentConfig(
|
||||||
|
"PERPLEXITY_API_KEY",
|
||||||
|
"rag.web.search.perplexity_api_key",
|
||||||
|
os.getenv("PERPLEXITY_API_KEY", ""),
|
||||||
|
)
|
||||||
|
|
||||||
RAG_WEB_SEARCH_RESULT_COUNT = PersistentConfig(
|
RAG_WEB_SEARCH_RESULT_COUNT = PersistentConfig(
|
||||||
"RAG_WEB_SEARCH_RESULT_COUNT",
|
"RAG_WEB_SEARCH_RESULT_COUNT",
|
||||||
"rag.web.search.result_count",
|
"rag.web.search.result_count",
|
||||||
@@ -2375,7 +2445,7 @@ LDAP_SEARCH_BASE = PersistentConfig(
|
|||||||
LDAP_SEARCH_FILTERS = PersistentConfig(
|
LDAP_SEARCH_FILTERS = PersistentConfig(
|
||||||
"LDAP_SEARCH_FILTER",
|
"LDAP_SEARCH_FILTER",
|
||||||
"ldap.server.search_filter",
|
"ldap.server.search_filter",
|
||||||
os.environ.get("LDAP_SEARCH_FILTER", ""),
|
os.environ.get("LDAP_SEARCH_FILTER", os.environ.get("LDAP_SEARCH_FILTERS", "")),
|
||||||
)
|
)
|
||||||
|
|
||||||
LDAP_USE_TLS = PersistentConfig(
|
LDAP_USE_TLS = PersistentConfig(
|
||||||
|
|||||||
@@ -65,10 +65,8 @@ except Exception:
|
|||||||
# LOGGING
|
# LOGGING
|
||||||
####################################
|
####################################
|
||||||
|
|
||||||
log_levels = ["CRITICAL", "ERROR", "WARNING", "INFO", "DEBUG"]
|
|
||||||
|
|
||||||
GLOBAL_LOG_LEVEL = os.environ.get("GLOBAL_LOG_LEVEL", "").upper()
|
GLOBAL_LOG_LEVEL = os.environ.get("GLOBAL_LOG_LEVEL", "").upper()
|
||||||
if GLOBAL_LOG_LEVEL in log_levels:
|
if GLOBAL_LOG_LEVEL in logging.getLevelNamesMapping():
|
||||||
logging.basicConfig(stream=sys.stdout, level=GLOBAL_LOG_LEVEL, force=True)
|
logging.basicConfig(stream=sys.stdout, level=GLOBAL_LOG_LEVEL, force=True)
|
||||||
else:
|
else:
|
||||||
GLOBAL_LOG_LEVEL = "INFO"
|
GLOBAL_LOG_LEVEL = "INFO"
|
||||||
@@ -78,6 +76,7 @@ log.info(f"GLOBAL_LOG_LEVEL: {GLOBAL_LOG_LEVEL}")
|
|||||||
|
|
||||||
if "cuda_error" in locals():
|
if "cuda_error" in locals():
|
||||||
log.exception(cuda_error)
|
log.exception(cuda_error)
|
||||||
|
del cuda_error
|
||||||
|
|
||||||
log_sources = [
|
log_sources = [
|
||||||
"AUDIO",
|
"AUDIO",
|
||||||
@@ -100,7 +99,7 @@ SRC_LOG_LEVELS = {}
|
|||||||
for source in log_sources:
|
for source in log_sources:
|
||||||
log_env_var = source + "_LOG_LEVEL"
|
log_env_var = source + "_LOG_LEVEL"
|
||||||
SRC_LOG_LEVELS[source] = os.environ.get(log_env_var, "").upper()
|
SRC_LOG_LEVELS[source] = os.environ.get(log_env_var, "").upper()
|
||||||
if SRC_LOG_LEVELS[source] not in log_levels:
|
if SRC_LOG_LEVELS[source] not in logging.getLevelNamesMapping():
|
||||||
SRC_LOG_LEVELS[source] = GLOBAL_LOG_LEVEL
|
SRC_LOG_LEVELS[source] = GLOBAL_LOG_LEVEL
|
||||||
log.info(f"{log_env_var}: {SRC_LOG_LEVELS[source]}")
|
log.info(f"{log_env_var}: {SRC_LOG_LEVELS[source]}")
|
||||||
|
|
||||||
@@ -386,6 +385,7 @@ ENABLE_WEBSOCKET_SUPPORT = (
|
|||||||
WEBSOCKET_MANAGER = os.environ.get("WEBSOCKET_MANAGER", "")
|
WEBSOCKET_MANAGER = os.environ.get("WEBSOCKET_MANAGER", "")
|
||||||
|
|
||||||
WEBSOCKET_REDIS_URL = os.environ.get("WEBSOCKET_REDIS_URL", REDIS_URL)
|
WEBSOCKET_REDIS_URL = os.environ.get("WEBSOCKET_REDIS_URL", REDIS_URL)
|
||||||
|
WEBSOCKET_REDIS_LOCK_TIMEOUT = os.environ.get("WEBSOCKET_REDIS_LOCK_TIMEOUT", 60)
|
||||||
|
|
||||||
AIOHTTP_CLIENT_TIMEOUT = os.environ.get("AIOHTTP_CLIENT_TIMEOUT", "")
|
AIOHTTP_CLIENT_TIMEOUT = os.environ.get("AIOHTTP_CLIENT_TIMEOUT", "")
|
||||||
|
|
||||||
@@ -397,19 +397,20 @@ else:
|
|||||||
except Exception:
|
except Exception:
|
||||||
AIOHTTP_CLIENT_TIMEOUT = 300
|
AIOHTTP_CLIENT_TIMEOUT = 300
|
||||||
|
|
||||||
AIOHTTP_CLIENT_TIMEOUT_OPENAI_MODEL_LIST = os.environ.get(
|
AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST = os.environ.get(
|
||||||
"AIOHTTP_CLIENT_TIMEOUT_OPENAI_MODEL_LIST", ""
|
"AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST",
|
||||||
|
os.environ.get("AIOHTTP_CLIENT_TIMEOUT_OPENAI_MODEL_LIST", ""),
|
||||||
)
|
)
|
||||||
|
|
||||||
if AIOHTTP_CLIENT_TIMEOUT_OPENAI_MODEL_LIST == "":
|
|
||||||
AIOHTTP_CLIENT_TIMEOUT_OPENAI_MODEL_LIST = None
|
if AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST == "":
|
||||||
|
AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST = None
|
||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
AIOHTTP_CLIENT_TIMEOUT_OPENAI_MODEL_LIST = int(
|
AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST = int(AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST)
|
||||||
AIOHTTP_CLIENT_TIMEOUT_OPENAI_MODEL_LIST
|
|
||||||
)
|
|
||||||
except Exception:
|
except Exception:
|
||||||
AIOHTTP_CLIENT_TIMEOUT_OPENAI_MODEL_LIST = 5
|
AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST = 5
|
||||||
|
|
||||||
|
|
||||||
####################################
|
####################################
|
||||||
# OFFLINE_MODE
|
# OFFLINE_MODE
|
||||||
@@ -419,3 +420,25 @@ OFFLINE_MODE = os.environ.get("OFFLINE_MODE", "false").lower() == "true"
|
|||||||
|
|
||||||
if OFFLINE_MODE:
|
if OFFLINE_MODE:
|
||||||
os.environ["HF_HUB_OFFLINE"] = "1"
|
os.environ["HF_HUB_OFFLINE"] = "1"
|
||||||
|
|
||||||
|
####################################
|
||||||
|
# AUDIT LOGGING
|
||||||
|
####################################
|
||||||
|
ENABLE_AUDIT_LOGS = os.getenv("ENABLE_AUDIT_LOGS", "false").lower() == "true"
|
||||||
|
# Where to store log file
|
||||||
|
AUDIT_LOGS_FILE_PATH = f"{DATA_DIR}/audit.log"
|
||||||
|
# Maximum size of a file before rotating into a new log file
|
||||||
|
AUDIT_LOG_FILE_ROTATION_SIZE = os.getenv("AUDIT_LOG_FILE_ROTATION_SIZE", "10MB")
|
||||||
|
# METADATA | REQUEST | REQUEST_RESPONSE
|
||||||
|
AUDIT_LOG_LEVEL = os.getenv("AUDIT_LOG_LEVEL", "REQUEST_RESPONSE").upper()
|
||||||
|
try:
|
||||||
|
MAX_BODY_LOG_SIZE = int(os.environ.get("MAX_BODY_LOG_SIZE") or 2048)
|
||||||
|
except ValueError:
|
||||||
|
MAX_BODY_LOG_SIZE = 2048
|
||||||
|
|
||||||
|
# Comma separated list for urls to exclude from audit
|
||||||
|
AUDIT_EXCLUDED_PATHS = os.getenv("AUDIT_EXCLUDED_PATHS", "/chats,/chat,/folders").split(
|
||||||
|
","
|
||||||
|
)
|
||||||
|
AUDIT_EXCLUDED_PATHS = [path.strip() for path in AUDIT_EXCLUDED_PATHS]
|
||||||
|
AUDIT_EXCLUDED_PATHS = [path.lstrip("/") for path in AUDIT_EXCLUDED_PATHS]
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import logging
|
|||||||
import sys
|
import sys
|
||||||
import inspect
|
import inspect
|
||||||
import json
|
import json
|
||||||
|
import asyncio
|
||||||
|
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
from typing import AsyncGenerator, Generator, Iterator
|
from typing import AsyncGenerator, Generator, Iterator
|
||||||
@@ -76,11 +77,13 @@ async def get_function_models(request):
|
|||||||
if hasattr(function_module, "pipes"):
|
if hasattr(function_module, "pipes"):
|
||||||
sub_pipes = []
|
sub_pipes = []
|
||||||
|
|
||||||
# Check if pipes is a function or a list
|
# Handle pipes being a list, sync function, or async function
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if callable(function_module.pipes):
|
if callable(function_module.pipes):
|
||||||
sub_pipes = function_module.pipes()
|
if asyncio.iscoroutinefunction(function_module.pipes):
|
||||||
|
sub_pipes = await function_module.pipes()
|
||||||
|
else:
|
||||||
|
sub_pipes = function_module.pipes()
|
||||||
else:
|
else:
|
||||||
sub_pipes = function_module.pipes
|
sub_pipes = function_module.pipes
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|||||||
@@ -45,6 +45,9 @@ from starlette.middleware.sessions import SessionMiddleware
|
|||||||
from starlette.responses import Response, StreamingResponse
|
from starlette.responses import Response, StreamingResponse
|
||||||
|
|
||||||
|
|
||||||
|
from open_webui.utils import logger
|
||||||
|
from open_webui.utils.audit import AuditLevel, AuditLoggingMiddleware
|
||||||
|
from open_webui.utils.logger import start_logger
|
||||||
from open_webui.socket.main import (
|
from open_webui.socket.main import (
|
||||||
app as socket_app,
|
app as socket_app,
|
||||||
periodic_usage_pool_cleanup,
|
periodic_usage_pool_cleanup,
|
||||||
@@ -95,12 +98,14 @@ from open_webui.config import (
|
|||||||
OLLAMA_API_CONFIGS,
|
OLLAMA_API_CONFIGS,
|
||||||
# OpenAI
|
# OpenAI
|
||||||
ENABLE_OPENAI_API,
|
ENABLE_OPENAI_API,
|
||||||
|
ONEDRIVE_CLIENT_ID,
|
||||||
OPENAI_API_BASE_URLS,
|
OPENAI_API_BASE_URLS,
|
||||||
OPENAI_API_KEYS,
|
OPENAI_API_KEYS,
|
||||||
OPENAI_API_CONFIGS,
|
OPENAI_API_CONFIGS,
|
||||||
# Direct Connections
|
# Direct Connections
|
||||||
ENABLE_DIRECT_CONNECTIONS,
|
ENABLE_DIRECT_CONNECTIONS,
|
||||||
# Code Execution
|
# Code Execution
|
||||||
|
ENABLE_CODE_EXECUTION,
|
||||||
CODE_EXECUTION_ENGINE,
|
CODE_EXECUTION_ENGINE,
|
||||||
CODE_EXECUTION_JUPYTER_URL,
|
CODE_EXECUTION_JUPYTER_URL,
|
||||||
CODE_EXECUTION_JUPYTER_AUTH,
|
CODE_EXECUTION_JUPYTER_AUTH,
|
||||||
@@ -161,6 +166,7 @@ from open_webui.config import (
|
|||||||
RAG_TEMPLATE,
|
RAG_TEMPLATE,
|
||||||
DEFAULT_RAG_TEMPLATE,
|
DEFAULT_RAG_TEMPLATE,
|
||||||
RAG_FULL_CONTEXT,
|
RAG_FULL_CONTEXT,
|
||||||
|
BYPASS_EMBEDDING_AND_RETRIEVAL,
|
||||||
RAG_EMBEDDING_MODEL,
|
RAG_EMBEDDING_MODEL,
|
||||||
RAG_EMBEDDING_MODEL_AUTO_UPDATE,
|
RAG_EMBEDDING_MODEL_AUTO_UPDATE,
|
||||||
RAG_EMBEDDING_MODEL_TRUST_REMOTE_CODE,
|
RAG_EMBEDDING_MODEL_TRUST_REMOTE_CODE,
|
||||||
@@ -180,6 +186,8 @@ from open_webui.config import (
|
|||||||
CHUNK_SIZE,
|
CHUNK_SIZE,
|
||||||
CONTENT_EXTRACTION_ENGINE,
|
CONTENT_EXTRACTION_ENGINE,
|
||||||
TIKA_SERVER_URL,
|
TIKA_SERVER_URL,
|
||||||
|
DOCUMENT_INTELLIGENCE_ENDPOINT,
|
||||||
|
DOCUMENT_INTELLIGENCE_KEY,
|
||||||
RAG_TOP_K,
|
RAG_TOP_K,
|
||||||
RAG_TEXT_SPLITTER,
|
RAG_TEXT_SPLITTER,
|
||||||
TIKTOKEN_ENCODING_NAME,
|
TIKTOKEN_ENCODING_NAME,
|
||||||
@@ -188,7 +196,7 @@ from open_webui.config import (
|
|||||||
YOUTUBE_LOADER_PROXY_URL,
|
YOUTUBE_LOADER_PROXY_URL,
|
||||||
# Retrieval (Web Search)
|
# Retrieval (Web Search)
|
||||||
RAG_WEB_SEARCH_ENGINE,
|
RAG_WEB_SEARCH_ENGINE,
|
||||||
RAG_WEB_SEARCH_FULL_CONTEXT,
|
BYPASS_WEB_SEARCH_EMBEDDING_AND_RETRIEVAL,
|
||||||
RAG_WEB_SEARCH_RESULT_COUNT,
|
RAG_WEB_SEARCH_RESULT_COUNT,
|
||||||
RAG_WEB_SEARCH_CONCURRENT_REQUESTS,
|
RAG_WEB_SEARCH_CONCURRENT_REQUESTS,
|
||||||
RAG_WEB_SEARCH_TRUST_ENV,
|
RAG_WEB_SEARCH_TRUST_ENV,
|
||||||
@@ -208,6 +216,7 @@ from open_webui.config import (
|
|||||||
BING_SEARCH_V7_SUBSCRIPTION_KEY,
|
BING_SEARCH_V7_SUBSCRIPTION_KEY,
|
||||||
BRAVE_SEARCH_API_KEY,
|
BRAVE_SEARCH_API_KEY,
|
||||||
EXA_API_KEY,
|
EXA_API_KEY,
|
||||||
|
PERPLEXITY_API_KEY,
|
||||||
KAGI_SEARCH_API_KEY,
|
KAGI_SEARCH_API_KEY,
|
||||||
MOJEEK_SEARCH_API_KEY,
|
MOJEEK_SEARCH_API_KEY,
|
||||||
BOCHA_SEARCH_API_KEY,
|
BOCHA_SEARCH_API_KEY,
|
||||||
@@ -215,11 +224,13 @@ from open_webui.config import (
|
|||||||
GOOGLE_PSE_ENGINE_ID,
|
GOOGLE_PSE_ENGINE_ID,
|
||||||
GOOGLE_DRIVE_CLIENT_ID,
|
GOOGLE_DRIVE_CLIENT_ID,
|
||||||
GOOGLE_DRIVE_API_KEY,
|
GOOGLE_DRIVE_API_KEY,
|
||||||
|
ONEDRIVE_CLIENT_ID,
|
||||||
ENABLE_RAG_HYBRID_SEARCH,
|
ENABLE_RAG_HYBRID_SEARCH,
|
||||||
ENABLE_RAG_LOCAL_WEB_FETCH,
|
ENABLE_RAG_LOCAL_WEB_FETCH,
|
||||||
ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION,
|
ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION,
|
||||||
ENABLE_RAG_WEB_SEARCH,
|
ENABLE_RAG_WEB_SEARCH,
|
||||||
ENABLE_GOOGLE_DRIVE_INTEGRATION,
|
ENABLE_GOOGLE_DRIVE_INTEGRATION,
|
||||||
|
ENABLE_ONEDRIVE_INTEGRATION,
|
||||||
UPLOAD_DIR,
|
UPLOAD_DIR,
|
||||||
# WebUI
|
# WebUI
|
||||||
WEBUI_AUTH,
|
WEBUI_AUTH,
|
||||||
@@ -298,8 +309,11 @@ from open_webui.config import (
|
|||||||
reset_config,
|
reset_config,
|
||||||
)
|
)
|
||||||
from open_webui.env import (
|
from open_webui.env import (
|
||||||
|
AUDIT_EXCLUDED_PATHS,
|
||||||
|
AUDIT_LOG_LEVEL,
|
||||||
CHANGELOG,
|
CHANGELOG,
|
||||||
GLOBAL_LOG_LEVEL,
|
GLOBAL_LOG_LEVEL,
|
||||||
|
MAX_BODY_LOG_SIZE,
|
||||||
SAFE_MODE,
|
SAFE_MODE,
|
||||||
SRC_LOG_LEVELS,
|
SRC_LOG_LEVELS,
|
||||||
VERSION,
|
VERSION,
|
||||||
@@ -384,11 +398,12 @@ https://github.com/open-webui/open-webui
|
|||||||
|
|
||||||
@asynccontextmanager
|
@asynccontextmanager
|
||||||
async def lifespan(app: FastAPI):
|
async def lifespan(app: FastAPI):
|
||||||
|
start_logger()
|
||||||
if RESET_CONFIG_ON_START:
|
if RESET_CONFIG_ON_START:
|
||||||
reset_config()
|
reset_config()
|
||||||
|
|
||||||
if app.state.config.LICENSE_KEY:
|
if LICENSE_KEY:
|
||||||
get_license_data(app, app.state.config.LICENSE_KEY)
|
get_license_data(app, LICENSE_KEY)
|
||||||
|
|
||||||
asyncio.create_task(periodic_usage_pool_cleanup())
|
asyncio.create_task(periodic_usage_pool_cleanup())
|
||||||
yield
|
yield
|
||||||
@@ -406,7 +421,7 @@ oauth_manager = OAuthManager(app)
|
|||||||
app.state.config = AppConfig()
|
app.state.config = AppConfig()
|
||||||
|
|
||||||
app.state.WEBUI_NAME = WEBUI_NAME
|
app.state.WEBUI_NAME = WEBUI_NAME
|
||||||
app.state.config.LICENSE_KEY = LICENSE_KEY
|
app.state.LICENSE_METADATA = None
|
||||||
|
|
||||||
########################################
|
########################################
|
||||||
#
|
#
|
||||||
@@ -526,6 +541,7 @@ app.state.config.FILE_MAX_COUNT = RAG_FILE_MAX_COUNT
|
|||||||
|
|
||||||
|
|
||||||
app.state.config.RAG_FULL_CONTEXT = RAG_FULL_CONTEXT
|
app.state.config.RAG_FULL_CONTEXT = RAG_FULL_CONTEXT
|
||||||
|
app.state.config.BYPASS_EMBEDDING_AND_RETRIEVAL = BYPASS_EMBEDDING_AND_RETRIEVAL
|
||||||
app.state.config.ENABLE_RAG_HYBRID_SEARCH = ENABLE_RAG_HYBRID_SEARCH
|
app.state.config.ENABLE_RAG_HYBRID_SEARCH = ENABLE_RAG_HYBRID_SEARCH
|
||||||
app.state.config.ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION = (
|
app.state.config.ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION = (
|
||||||
ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION
|
ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION
|
||||||
@@ -533,6 +549,8 @@ app.state.config.ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION = (
|
|||||||
|
|
||||||
app.state.config.CONTENT_EXTRACTION_ENGINE = CONTENT_EXTRACTION_ENGINE
|
app.state.config.CONTENT_EXTRACTION_ENGINE = CONTENT_EXTRACTION_ENGINE
|
||||||
app.state.config.TIKA_SERVER_URL = TIKA_SERVER_URL
|
app.state.config.TIKA_SERVER_URL = TIKA_SERVER_URL
|
||||||
|
app.state.config.DOCUMENT_INTELLIGENCE_ENDPOINT = DOCUMENT_INTELLIGENCE_ENDPOINT
|
||||||
|
app.state.config.DOCUMENT_INTELLIGENCE_KEY = DOCUMENT_INTELLIGENCE_KEY
|
||||||
|
|
||||||
app.state.config.TEXT_SPLITTER = RAG_TEXT_SPLITTER
|
app.state.config.TEXT_SPLITTER = RAG_TEXT_SPLITTER
|
||||||
app.state.config.TIKTOKEN_ENCODING_NAME = TIKTOKEN_ENCODING_NAME
|
app.state.config.TIKTOKEN_ENCODING_NAME = TIKTOKEN_ENCODING_NAME
|
||||||
@@ -560,10 +578,13 @@ app.state.config.YOUTUBE_LOADER_PROXY_URL = YOUTUBE_LOADER_PROXY_URL
|
|||||||
|
|
||||||
app.state.config.ENABLE_RAG_WEB_SEARCH = ENABLE_RAG_WEB_SEARCH
|
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_ENGINE = RAG_WEB_SEARCH_ENGINE
|
||||||
app.state.config.RAG_WEB_SEARCH_FULL_CONTEXT = RAG_WEB_SEARCH_FULL_CONTEXT
|
app.state.config.BYPASS_WEB_SEARCH_EMBEDDING_AND_RETRIEVAL = (
|
||||||
|
BYPASS_WEB_SEARCH_EMBEDDING_AND_RETRIEVAL
|
||||||
|
)
|
||||||
app.state.config.RAG_WEB_SEARCH_DOMAIN_FILTER_LIST = RAG_WEB_SEARCH_DOMAIN_FILTER_LIST
|
app.state.config.RAG_WEB_SEARCH_DOMAIN_FILTER_LIST = RAG_WEB_SEARCH_DOMAIN_FILTER_LIST
|
||||||
|
|
||||||
app.state.config.ENABLE_GOOGLE_DRIVE_INTEGRATION = ENABLE_GOOGLE_DRIVE_INTEGRATION
|
app.state.config.ENABLE_GOOGLE_DRIVE_INTEGRATION = ENABLE_GOOGLE_DRIVE_INTEGRATION
|
||||||
|
app.state.config.ENABLE_ONEDRIVE_INTEGRATION = ENABLE_ONEDRIVE_INTEGRATION
|
||||||
app.state.config.SEARXNG_QUERY_URL = SEARXNG_QUERY_URL
|
app.state.config.SEARXNG_QUERY_URL = SEARXNG_QUERY_URL
|
||||||
app.state.config.GOOGLE_PSE_API_KEY = GOOGLE_PSE_API_KEY
|
app.state.config.GOOGLE_PSE_API_KEY = GOOGLE_PSE_API_KEY
|
||||||
app.state.config.GOOGLE_PSE_ENGINE_ID = GOOGLE_PSE_ENGINE_ID
|
app.state.config.GOOGLE_PSE_ENGINE_ID = GOOGLE_PSE_ENGINE_ID
|
||||||
@@ -584,6 +605,7 @@ app.state.config.JINA_API_KEY = JINA_API_KEY
|
|||||||
app.state.config.BING_SEARCH_V7_ENDPOINT = BING_SEARCH_V7_ENDPOINT
|
app.state.config.BING_SEARCH_V7_ENDPOINT = BING_SEARCH_V7_ENDPOINT
|
||||||
app.state.config.BING_SEARCH_V7_SUBSCRIPTION_KEY = BING_SEARCH_V7_SUBSCRIPTION_KEY
|
app.state.config.BING_SEARCH_V7_SUBSCRIPTION_KEY = BING_SEARCH_V7_SUBSCRIPTION_KEY
|
||||||
app.state.config.EXA_API_KEY = EXA_API_KEY
|
app.state.config.EXA_API_KEY = EXA_API_KEY
|
||||||
|
app.state.config.PERPLEXITY_API_KEY = PERPLEXITY_API_KEY
|
||||||
|
|
||||||
app.state.config.RAG_WEB_SEARCH_RESULT_COUNT = RAG_WEB_SEARCH_RESULT_COUNT
|
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
|
app.state.config.RAG_WEB_SEARCH_CONCURRENT_REQUESTS = RAG_WEB_SEARCH_CONCURRENT_REQUESTS
|
||||||
@@ -639,6 +661,7 @@ app.state.EMBEDDING_FUNCTION = get_embedding_function(
|
|||||||
#
|
#
|
||||||
########################################
|
########################################
|
||||||
|
|
||||||
|
app.state.config.ENABLE_CODE_EXECUTION = ENABLE_CODE_EXECUTION
|
||||||
app.state.config.CODE_EXECUTION_ENGINE = CODE_EXECUTION_ENGINE
|
app.state.config.CODE_EXECUTION_ENGINE = CODE_EXECUTION_ENGINE
|
||||||
app.state.config.CODE_EXECUTION_JUPYTER_URL = CODE_EXECUTION_JUPYTER_URL
|
app.state.config.CODE_EXECUTION_JUPYTER_URL = CODE_EXECUTION_JUPYTER_URL
|
||||||
app.state.config.CODE_EXECUTION_JUPYTER_AUTH = CODE_EXECUTION_JUPYTER_AUTH
|
app.state.config.CODE_EXECUTION_JUPYTER_AUTH = CODE_EXECUTION_JUPYTER_AUTH
|
||||||
@@ -879,6 +902,19 @@ app.include_router(
|
|||||||
app.include_router(utils.router, prefix="/api/v1/utils", tags=["utils"])
|
app.include_router(utils.router, prefix="/api/v1/utils", tags=["utils"])
|
||||||
|
|
||||||
|
|
||||||
|
try:
|
||||||
|
audit_level = AuditLevel(AUDIT_LOG_LEVEL)
|
||||||
|
except ValueError as e:
|
||||||
|
logger.error(f"Invalid audit level: {AUDIT_LOG_LEVEL}. Error: {e}")
|
||||||
|
audit_level = AuditLevel.NONE
|
||||||
|
|
||||||
|
if audit_level != AuditLevel.NONE:
|
||||||
|
app.add_middleware(
|
||||||
|
AuditLoggingMiddleware,
|
||||||
|
audit_level=audit_level,
|
||||||
|
excluded_paths=AUDIT_EXCLUDED_PATHS,
|
||||||
|
max_body_size=MAX_BODY_LOG_SIZE,
|
||||||
|
)
|
||||||
##################################
|
##################################
|
||||||
#
|
#
|
||||||
# Chat Endpoints
|
# Chat Endpoints
|
||||||
@@ -911,7 +947,7 @@ async def get_models(request: Request, user=Depends(get_verified_user)):
|
|||||||
|
|
||||||
return filtered_models
|
return filtered_models
|
||||||
|
|
||||||
models = await get_all_models(request)
|
models = await get_all_models(request, user=user)
|
||||||
|
|
||||||
# Filter out filter pipelines
|
# Filter out filter pipelines
|
||||||
models = [
|
models = [
|
||||||
@@ -940,7 +976,7 @@ async def get_models(request: Request, user=Depends(get_verified_user)):
|
|||||||
|
|
||||||
@app.get("/api/models/base")
|
@app.get("/api/models/base")
|
||||||
async def get_base_models(request: Request, user=Depends(get_admin_user)):
|
async def get_base_models(request: Request, user=Depends(get_admin_user)):
|
||||||
models = await get_all_base_models(request)
|
models = await get_all_base_models(request, user=user)
|
||||||
return {"data": models}
|
return {"data": models}
|
||||||
|
|
||||||
|
|
||||||
@@ -951,7 +987,7 @@ async def chat_completion(
|
|||||||
user=Depends(get_verified_user),
|
user=Depends(get_verified_user),
|
||||||
):
|
):
|
||||||
if not request.app.state.MODELS:
|
if not request.app.state.MODELS:
|
||||||
await get_all_models(request)
|
await get_all_models(request, user=user)
|
||||||
|
|
||||||
model_item = form_data.pop("model_item", {})
|
model_item = form_data.pop("model_item", {})
|
||||||
tasks = form_data.pop("background_tasks", None)
|
tasks = form_data.pop("background_tasks", None)
|
||||||
@@ -987,7 +1023,7 @@ async def chat_completion(
|
|||||||
"files": form_data.get("files", None),
|
"files": form_data.get("files", None),
|
||||||
"features": form_data.get("features", None),
|
"features": form_data.get("features", None),
|
||||||
"variables": form_data.get("variables", None),
|
"variables": form_data.get("variables", None),
|
||||||
"model": model_info.model_dump() if model_info else model,
|
"model": model,
|
||||||
"direct": model_item.get("direct", False),
|
"direct": model_item.get("direct", False),
|
||||||
**(
|
**(
|
||||||
{"function_calling": "native"}
|
{"function_calling": "native"}
|
||||||
@@ -1005,7 +1041,7 @@ async def chat_completion(
|
|||||||
form_data["metadata"] = metadata
|
form_data["metadata"] = metadata
|
||||||
|
|
||||||
form_data, metadata, events = await process_chat_payload(
|
form_data, metadata, events = await process_chat_payload(
|
||||||
request, form_data, metadata, user, model
|
request, form_data, user, metadata, model
|
||||||
)
|
)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -1019,7 +1055,7 @@ async def chat_completion(
|
|||||||
response = await chat_completion_handler(request, form_data, user)
|
response = await chat_completion_handler(request, form_data, user)
|
||||||
|
|
||||||
return await process_chat_response(
|
return await process_chat_response(
|
||||||
request, response, form_data, user, events, metadata, tasks
|
request, response, form_data, user, metadata, model, events, tasks
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
@@ -1108,9 +1144,10 @@ async def get_app_config(request: Request):
|
|||||||
if data is not None and "id" in data:
|
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"])
|
||||||
|
|
||||||
|
user_count = Users.get_num_users()
|
||||||
onboarding = False
|
onboarding = False
|
||||||
|
|
||||||
if user is None:
|
if user is None:
|
||||||
user_count = Users.get_num_users()
|
|
||||||
onboarding = user_count == 0
|
onboarding = user_count == 0
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -1138,6 +1175,7 @@ async def get_app_config(request: Request):
|
|||||||
"enable_direct_connections": app.state.config.ENABLE_DIRECT_CONNECTIONS,
|
"enable_direct_connections": app.state.config.ENABLE_DIRECT_CONNECTIONS,
|
||||||
"enable_channels": app.state.config.ENABLE_CHANNELS,
|
"enable_channels": app.state.config.ENABLE_CHANNELS,
|
||||||
"enable_web_search": app.state.config.ENABLE_RAG_WEB_SEARCH,
|
"enable_web_search": app.state.config.ENABLE_RAG_WEB_SEARCH,
|
||||||
|
"enable_code_execution": app.state.config.ENABLE_CODE_EXECUTION,
|
||||||
"enable_code_interpreter": app.state.config.ENABLE_CODE_INTERPRETER,
|
"enable_code_interpreter": app.state.config.ENABLE_CODE_INTERPRETER,
|
||||||
"enable_image_generation": app.state.config.ENABLE_IMAGE_GENERATION,
|
"enable_image_generation": app.state.config.ENABLE_IMAGE_GENERATION,
|
||||||
"enable_autocomplete_generation": app.state.config.ENABLE_AUTOCOMPLETE_GENERATION,
|
"enable_autocomplete_generation": app.state.config.ENABLE_AUTOCOMPLETE_GENERATION,
|
||||||
@@ -1146,6 +1184,7 @@ async def get_app_config(request: Request):
|
|||||||
"enable_admin_export": ENABLE_ADMIN_EXPORT,
|
"enable_admin_export": ENABLE_ADMIN_EXPORT,
|
||||||
"enable_admin_chat_access": ENABLE_ADMIN_CHAT_ACCESS,
|
"enable_admin_chat_access": ENABLE_ADMIN_CHAT_ACCESS,
|
||||||
"enable_google_drive_integration": app.state.config.ENABLE_GOOGLE_DRIVE_INTEGRATION,
|
"enable_google_drive_integration": app.state.config.ENABLE_GOOGLE_DRIVE_INTEGRATION,
|
||||||
|
"enable_onedrive_integration": app.state.config.ENABLE_ONEDRIVE_INTEGRATION,
|
||||||
}
|
}
|
||||||
if user is not None
|
if user is not None
|
||||||
else {}
|
else {}
|
||||||
@@ -1155,6 +1194,7 @@ async def get_app_config(request: Request):
|
|||||||
{
|
{
|
||||||
"default_models": app.state.config.DEFAULT_MODELS,
|
"default_models": app.state.config.DEFAULT_MODELS,
|
||||||
"default_prompt_suggestions": app.state.config.DEFAULT_PROMPT_SUGGESTIONS,
|
"default_prompt_suggestions": app.state.config.DEFAULT_PROMPT_SUGGESTIONS,
|
||||||
|
"user_count": user_count,
|
||||||
"code": {
|
"code": {
|
||||||
"engine": app.state.config.CODE_EXECUTION_ENGINE,
|
"engine": app.state.config.CODE_EXECUTION_ENGINE,
|
||||||
},
|
},
|
||||||
@@ -1177,6 +1217,15 @@ async def get_app_config(request: Request):
|
|||||||
"client_id": GOOGLE_DRIVE_CLIENT_ID.value,
|
"client_id": GOOGLE_DRIVE_CLIENT_ID.value,
|
||||||
"api_key": GOOGLE_DRIVE_API_KEY.value,
|
"api_key": GOOGLE_DRIVE_API_KEY.value,
|
||||||
},
|
},
|
||||||
|
"onedrive": {"client_id": ONEDRIVE_CLIENT_ID.value},
|
||||||
|
"license_metadata": app.state.LICENSE_METADATA,
|
||||||
|
**(
|
||||||
|
{
|
||||||
|
"active_entries": app.state.USER_COUNT,
|
||||||
|
}
|
||||||
|
if user.role == "admin"
|
||||||
|
else {}
|
||||||
|
),
|
||||||
}
|
}
|
||||||
if user is not None
|
if user is not None
|
||||||
else {}
|
else {}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import logging
|
||||||
import json
|
import json
|
||||||
import time
|
import time
|
||||||
import uuid
|
import uuid
|
||||||
@@ -5,7 +6,7 @@ from typing import Optional
|
|||||||
|
|
||||||
from open_webui.internal.db import Base, get_db
|
from open_webui.internal.db import Base, get_db
|
||||||
from open_webui.models.tags import TagModel, Tag, Tags
|
from open_webui.models.tags import TagModel, Tag, Tags
|
||||||
|
from open_webui.env import SRC_LOG_LEVELS
|
||||||
|
|
||||||
from pydantic import BaseModel, ConfigDict
|
from pydantic import BaseModel, ConfigDict
|
||||||
from sqlalchemy import BigInteger, Boolean, Column, String, Text, JSON
|
from sqlalchemy import BigInteger, Boolean, Column, String, Text, JSON
|
||||||
@@ -16,6 +17,9 @@ from sqlalchemy.sql import exists
|
|||||||
# Chat DB Schema
|
# Chat DB Schema
|
||||||
####################
|
####################
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
log.setLevel(SRC_LOG_LEVELS["MODELS"])
|
||||||
|
|
||||||
|
|
||||||
class Chat(Base):
|
class Chat(Base):
|
||||||
__tablename__ = "chat"
|
__tablename__ = "chat"
|
||||||
@@ -670,7 +674,7 @@ class ChatTable:
|
|||||||
# Perform pagination at the SQL level
|
# Perform pagination at the SQL level
|
||||||
all_chats = query.offset(skip).limit(limit).all()
|
all_chats = query.offset(skip).limit(limit).all()
|
||||||
|
|
||||||
print(len(all_chats))
|
log.info(f"The number of chats: {len(all_chats)}")
|
||||||
|
|
||||||
# Validate and return chats
|
# Validate and return chats
|
||||||
return [ChatModel.model_validate(chat) for chat in all_chats]
|
return [ChatModel.model_validate(chat) for chat in all_chats]
|
||||||
@@ -731,7 +735,7 @@ class ChatTable:
|
|||||||
query = db.query(Chat).filter_by(user_id=user_id)
|
query = db.query(Chat).filter_by(user_id=user_id)
|
||||||
tag_id = tag_name.replace(" ", "_").lower()
|
tag_id = tag_name.replace(" ", "_").lower()
|
||||||
|
|
||||||
print(db.bind.dialect.name)
|
log.info(f"DB dialect name: {db.bind.dialect.name}")
|
||||||
if db.bind.dialect.name == "sqlite":
|
if db.bind.dialect.name == "sqlite":
|
||||||
# SQLite JSON1 querying for tags within the meta JSON field
|
# SQLite JSON1 querying for tags within the meta JSON field
|
||||||
query = query.filter(
|
query = query.filter(
|
||||||
@@ -752,7 +756,7 @@ class ChatTable:
|
|||||||
)
|
)
|
||||||
|
|
||||||
all_chats = query.all()
|
all_chats = query.all()
|
||||||
print("all_chats", all_chats)
|
log.debug(f"all_chats: {all_chats}")
|
||||||
return [ChatModel.model_validate(chat) for chat in all_chats]
|
return [ChatModel.model_validate(chat) for chat in all_chats]
|
||||||
|
|
||||||
def add_chat_tag_by_id_and_user_id_and_tag_name(
|
def add_chat_tag_by_id_and_user_id_and_tag_name(
|
||||||
@@ -810,7 +814,7 @@ class ChatTable:
|
|||||||
count = query.count()
|
count = query.count()
|
||||||
|
|
||||||
# Debugging output for inspection
|
# Debugging output for inspection
|
||||||
print(f"Count of chats for tag '{tag_name}':", count)
|
log.info(f"Count of chats for tag '{tag_name}': {count}")
|
||||||
|
|
||||||
return count
|
return count
|
||||||
|
|
||||||
|
|||||||
@@ -118,7 +118,7 @@ class FeedbackTable:
|
|||||||
else:
|
else:
|
||||||
return None
|
return None
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(e)
|
log.exception(f"Error creating a new feedback: {e}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def get_feedback_by_id(self, id: str) -> Optional[FeedbackModel]:
|
def get_feedback_by_id(self, id: str) -> Optional[FeedbackModel]:
|
||||||
|
|||||||
@@ -119,7 +119,7 @@ class FilesTable:
|
|||||||
else:
|
else:
|
||||||
return None
|
return None
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error creating tool: {e}")
|
log.exception(f"Error inserting a new file: {e}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def get_file_by_id(self, id: str) -> Optional[FileModel]:
|
def get_file_by_id(self, id: str) -> Optional[FileModel]:
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ class FolderTable:
|
|||||||
else:
|
else:
|
||||||
return None
|
return None
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(e)
|
log.exception(f"Error inserting a new folder: {e}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def get_folder_by_id_and_user_id(
|
def get_folder_by_id_and_user_id(
|
||||||
|
|||||||
@@ -105,7 +105,7 @@ class FunctionsTable:
|
|||||||
else:
|
else:
|
||||||
return None
|
return None
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error creating tool: {e}")
|
log.exception(f"Error creating a new function: {e}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def get_function_by_id(self, id: str) -> Optional[FunctionModel]:
|
def get_function_by_id(self, id: str) -> Optional[FunctionModel]:
|
||||||
@@ -170,7 +170,7 @@ class FunctionsTable:
|
|||||||
function = db.get(Function, id)
|
function = db.get(Function, id)
|
||||||
return function.valves if function.valves else {}
|
return function.valves if function.valves else {}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"An error occurred: {e}")
|
log.exception(f"Error getting function valves by id {id}: {e}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def update_function_valves_by_id(
|
def update_function_valves_by_id(
|
||||||
@@ -202,7 +202,9 @@ class FunctionsTable:
|
|||||||
|
|
||||||
return user_settings["functions"]["valves"].get(id, {})
|
return user_settings["functions"]["valves"].get(id, {})
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"An error occurred: {e}")
|
log.exception(
|
||||||
|
f"Error getting user values by id {id} and user id {user_id}: {e}"
|
||||||
|
)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def update_user_valves_by_id_and_user_id(
|
def update_user_valves_by_id_and_user_id(
|
||||||
@@ -225,7 +227,9 @@ class FunctionsTable:
|
|||||||
|
|
||||||
return user_settings["functions"]["valves"][id]
|
return user_settings["functions"]["valves"][id]
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"An error occurred: {e}")
|
log.exception(
|
||||||
|
f"Error updating user valves by id {id} and user_id {user_id}: {e}"
|
||||||
|
)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def update_function_by_id(self, id: str, updated: dict) -> Optional[FunctionModel]:
|
def update_function_by_id(self, id: str, updated: dict) -> Optional[FunctionModel]:
|
||||||
|
|||||||
@@ -166,7 +166,7 @@ class ModelsTable:
|
|||||||
else:
|
else:
|
||||||
return None
|
return None
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(e)
|
log.exception(f"Failed to insert a new model: {e}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def get_all_models(self) -> list[ModelModel]:
|
def get_all_models(self) -> list[ModelModel]:
|
||||||
@@ -246,8 +246,7 @@ class ModelsTable:
|
|||||||
db.refresh(model)
|
db.refresh(model)
|
||||||
return ModelModel.model_validate(model)
|
return ModelModel.model_validate(model)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(e)
|
log.exception(f"Failed to update the model by id {id}: {e}")
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def delete_model_by_id(self, id: str) -> bool:
|
def delete_model_by_id(self, id: str) -> bool:
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ class TagTable:
|
|||||||
else:
|
else:
|
||||||
return None
|
return None
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(e)
|
log.exception(f"Error inserting a new tag: {e}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def get_tag_by_name_and_user_id(
|
def get_tag_by_name_and_user_id(
|
||||||
|
|||||||
@@ -131,7 +131,7 @@ class ToolsTable:
|
|||||||
else:
|
else:
|
||||||
return None
|
return None
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error creating tool: {e}")
|
log.exception(f"Error creating a new tool: {e}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def get_tool_by_id(self, id: str) -> Optional[ToolModel]:
|
def get_tool_by_id(self, id: str) -> Optional[ToolModel]:
|
||||||
@@ -175,7 +175,7 @@ class ToolsTable:
|
|||||||
tool = db.get(Tool, id)
|
tool = db.get(Tool, id)
|
||||||
return tool.valves if tool.valves else {}
|
return tool.valves if tool.valves else {}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"An error occurred: {e}")
|
log.exception(f"Error getting tool valves by id {id}: {e}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def update_tool_valves_by_id(self, id: str, valves: dict) -> Optional[ToolValves]:
|
def update_tool_valves_by_id(self, id: str, valves: dict) -> Optional[ToolValves]:
|
||||||
@@ -204,7 +204,9 @@ class ToolsTable:
|
|||||||
|
|
||||||
return user_settings["tools"]["valves"].get(id, {})
|
return user_settings["tools"]["valves"].get(id, {})
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"An error occurred: {e}")
|
log.exception(
|
||||||
|
f"Error getting user values by id {id} and user_id {user_id}: {e}"
|
||||||
|
)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def update_user_valves_by_id_and_user_id(
|
def update_user_valves_by_id_and_user_id(
|
||||||
@@ -227,7 +229,9 @@ class ToolsTable:
|
|||||||
|
|
||||||
return user_settings["tools"]["valves"][id]
|
return user_settings["tools"]["valves"][id]
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"An error occurred: {e}")
|
log.exception(
|
||||||
|
f"Error updating user valves by id {id} and user_id {user_id}: {e}"
|
||||||
|
)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def update_tool_by_id(self, id: str, updated: dict) -> Optional[ToolModel]:
|
def update_tool_by_id(self, id: str, updated: dict) -> Optional[ToolModel]:
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import ftfy
|
|||||||
import sys
|
import sys
|
||||||
|
|
||||||
from langchain_community.document_loaders import (
|
from langchain_community.document_loaders import (
|
||||||
|
AzureAIDocumentIntelligenceLoader,
|
||||||
BSHTMLLoader,
|
BSHTMLLoader,
|
||||||
CSVLoader,
|
CSVLoader,
|
||||||
Docx2txtLoader,
|
Docx2txtLoader,
|
||||||
@@ -76,6 +77,7 @@ known_source_ext = [
|
|||||||
"jsx",
|
"jsx",
|
||||||
"hs",
|
"hs",
|
||||||
"lhs",
|
"lhs",
|
||||||
|
"json",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@@ -147,6 +149,27 @@ class Loader:
|
|||||||
file_path=file_path,
|
file_path=file_path,
|
||||||
mime_type=file_content_type,
|
mime_type=file_content_type,
|
||||||
)
|
)
|
||||||
|
elif (
|
||||||
|
self.engine == "document_intelligence"
|
||||||
|
and self.kwargs.get("DOCUMENT_INTELLIGENCE_ENDPOINT") != ""
|
||||||
|
and self.kwargs.get("DOCUMENT_INTELLIGENCE_KEY") != ""
|
||||||
|
and (
|
||||||
|
file_ext in ["pdf", "xls", "xlsx", "docx", "ppt", "pptx"]
|
||||||
|
or file_content_type
|
||||||
|
in [
|
||||||
|
"application/vnd.ms-excel",
|
||||||
|
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||||
|
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
||||||
|
"application/vnd.ms-powerpoint",
|
||||||
|
"application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
||||||
|
]
|
||||||
|
)
|
||||||
|
):
|
||||||
|
loader = AzureAIDocumentIntelligenceLoader(
|
||||||
|
file_path=file_path,
|
||||||
|
api_endpoint=self.kwargs.get("DOCUMENT_INTELLIGENCE_ENDPOINT"),
|
||||||
|
api_key=self.kwargs.get("DOCUMENT_INTELLIGENCE_KEY"),
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
if file_ext == "pdf":
|
if file_ext == "pdf":
|
||||||
loader = PyPDFLoader(
|
loader = PyPDFLoader(
|
||||||
|
|||||||
@@ -1,13 +1,19 @@
|
|||||||
import os
|
import os
|
||||||
|
import logging
|
||||||
import torch
|
import torch
|
||||||
import numpy as np
|
import numpy as np
|
||||||
from colbert.infra import ColBERTConfig
|
from colbert.infra import ColBERTConfig
|
||||||
from colbert.modeling.checkpoint import Checkpoint
|
from colbert.modeling.checkpoint import Checkpoint
|
||||||
|
|
||||||
|
from open_webui.env import SRC_LOG_LEVELS
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
log.setLevel(SRC_LOG_LEVELS["RAG"])
|
||||||
|
|
||||||
|
|
||||||
class ColBERT:
|
class ColBERT:
|
||||||
def __init__(self, name, **kwargs) -> None:
|
def __init__(self, name, **kwargs) -> None:
|
||||||
print("ColBERT: Loading model", name)
|
log.info("ColBERT: Loading model", name)
|
||||||
self.device = "cuda" if torch.cuda.is_available() else "cpu"
|
self.device = "cuda" if torch.cuda.is_available() else "cpu"
|
||||||
|
|
||||||
DOCKER = kwargs.get("env") == "docker"
|
DOCKER = kwargs.get("env") == "docker"
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ from typing import Optional, Union
|
|||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import requests
|
import requests
|
||||||
|
import hashlib
|
||||||
|
|
||||||
from huggingface_hub import snapshot_download
|
from huggingface_hub import snapshot_download
|
||||||
from langchain.retrievers import ContextualCompressionRetriever, EnsembleRetriever
|
from langchain.retrievers import ContextualCompressionRetriever, EnsembleRetriever
|
||||||
@@ -17,6 +18,7 @@ from open_webui.retrieval.vector.connector import VECTOR_DB_CLIENT
|
|||||||
from open_webui.utils.misc import get_last_user_message, calculate_sha256_string
|
from open_webui.utils.misc import get_last_user_message, calculate_sha256_string
|
||||||
|
|
||||||
from open_webui.models.users import UserModel
|
from open_webui.models.users import UserModel
|
||||||
|
from open_webui.models.files import Files
|
||||||
|
|
||||||
from open_webui.env import (
|
from open_webui.env import (
|
||||||
SRC_LOG_LEVELS,
|
SRC_LOG_LEVELS,
|
||||||
@@ -81,7 +83,7 @@ def query_doc(
|
|||||||
|
|
||||||
return result
|
return result
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(e)
|
log.exception(f"Error querying doc {collection_name} with limit {k}: {e}")
|
||||||
raise e
|
raise e
|
||||||
|
|
||||||
|
|
||||||
@@ -94,7 +96,7 @@ def get_doc(collection_name: str, user: UserModel = None):
|
|||||||
|
|
||||||
return result
|
return result
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(e)
|
log.exception(f"Error getting doc {collection_name}: {e}")
|
||||||
raise e
|
raise e
|
||||||
|
|
||||||
|
|
||||||
@@ -174,46 +176,41 @@ def merge_get_results(get_results: list[dict]) -> dict:
|
|||||||
|
|
||||||
def merge_and_sort_query_results(
|
def merge_and_sort_query_results(
|
||||||
query_results: list[dict], k: int, reverse: bool = False
|
query_results: list[dict], k: int, reverse: bool = False
|
||||||
) -> list[dict]:
|
) -> dict:
|
||||||
# Initialize lists to store combined data
|
# Initialize lists to store combined data
|
||||||
combined_distances = []
|
combined = []
|
||||||
combined_documents = []
|
seen_hashes = set() # To store unique document hashes
|
||||||
combined_metadatas = []
|
|
||||||
|
|
||||||
for data in query_results:
|
for data in query_results:
|
||||||
combined_distances.extend(data["distances"][0])
|
distances = data["distances"][0]
|
||||||
combined_documents.extend(data["documents"][0])
|
documents = data["documents"][0]
|
||||||
combined_metadatas.extend(data["metadatas"][0])
|
metadatas = data["metadatas"][0]
|
||||||
|
|
||||||
# Create a list of tuples (distance, document, metadata)
|
for distance, document, metadata in zip(distances, documents, metadatas):
|
||||||
combined = list(zip(combined_distances, combined_documents, combined_metadatas))
|
if isinstance(document, str):
|
||||||
|
doc_hash = hashlib.md5(
|
||||||
|
document.encode()
|
||||||
|
).hexdigest() # Compute a hash for uniqueness
|
||||||
|
|
||||||
|
if doc_hash not in seen_hashes:
|
||||||
|
seen_hashes.add(doc_hash)
|
||||||
|
combined.append((distance, document, metadata))
|
||||||
|
|
||||||
# Sort the list based on distances
|
# Sort the list based on distances
|
||||||
combined.sort(key=lambda x: x[0], reverse=reverse)
|
combined.sort(key=lambda x: x[0], reverse=reverse)
|
||||||
|
|
||||||
# We don't have anything :-(
|
# Slice to keep only the top k elements
|
||||||
if not combined:
|
sorted_distances, sorted_documents, sorted_metadatas = (
|
||||||
sorted_distances = []
|
zip(*combined[:k]) if combined else ([], [], [])
|
||||||
sorted_documents = []
|
)
|
||||||
sorted_metadatas = []
|
|
||||||
else:
|
|
||||||
# Unzip the sorted list
|
|
||||||
sorted_distances, sorted_documents, sorted_metadatas = zip(*combined)
|
|
||||||
|
|
||||||
# Slicing the lists to include only k elements
|
# Create and return the output dictionary
|
||||||
sorted_distances = list(sorted_distances)[:k]
|
return {
|
||||||
sorted_documents = list(sorted_documents)[:k]
|
"distances": [list(sorted_distances)],
|
||||||
sorted_metadatas = list(sorted_metadatas)[:k]
|
"documents": [list(sorted_documents)],
|
||||||
|
"metadatas": [list(sorted_metadatas)],
|
||||||
# Create the output dictionary
|
|
||||||
result = {
|
|
||||||
"distances": [sorted_distances],
|
|
||||||
"documents": [sorted_documents],
|
|
||||||
"metadatas": [sorted_metadatas],
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return result
|
|
||||||
|
|
||||||
|
|
||||||
def get_all_items_from_collections(collection_names: list[str]) -> dict:
|
def get_all_items_from_collections(collection_names: list[str]) -> dict:
|
||||||
results = []
|
results = []
|
||||||
@@ -342,6 +339,7 @@ def get_embedding_function(
|
|||||||
|
|
||||||
|
|
||||||
def get_sources_from_files(
|
def get_sources_from_files(
|
||||||
|
request,
|
||||||
files,
|
files,
|
||||||
queries,
|
queries,
|
||||||
embedding_function,
|
embedding_function,
|
||||||
@@ -359,19 +357,71 @@ def get_sources_from_files(
|
|||||||
relevant_contexts = []
|
relevant_contexts = []
|
||||||
|
|
||||||
for file in files:
|
for file in files:
|
||||||
|
|
||||||
|
context = None
|
||||||
if file.get("docs"):
|
if file.get("docs"):
|
||||||
|
# BYPASS_WEB_SEARCH_EMBEDDING_AND_RETRIEVAL
|
||||||
context = {
|
context = {
|
||||||
"documents": [[doc.get("content") for doc in file.get("docs")]],
|
"documents": [[doc.get("content") for doc in file.get("docs")]],
|
||||||
"metadatas": [[doc.get("metadata") for doc in file.get("docs")]],
|
"metadatas": [[doc.get("metadata") for doc in file.get("docs")]],
|
||||||
}
|
}
|
||||||
elif file.get("context") == "full":
|
elif file.get("context") == "full":
|
||||||
|
# Manual Full Mode Toggle
|
||||||
context = {
|
context = {
|
||||||
"documents": [[file.get("file").get("data", {}).get("content")]],
|
"documents": [[file.get("file").get("data", {}).get("content")]],
|
||||||
"metadatas": [[{"file_id": file.get("id"), "name": file.get("name")}]],
|
"metadatas": [[{"file_id": file.get("id"), "name": file.get("name")}]],
|
||||||
}
|
}
|
||||||
else:
|
elif (
|
||||||
context = None
|
file.get("type") != "web_search"
|
||||||
|
and request.app.state.config.BYPASS_EMBEDDING_AND_RETRIEVAL
|
||||||
|
):
|
||||||
|
# BYPASS_EMBEDDING_AND_RETRIEVAL
|
||||||
|
if file.get("type") == "collection":
|
||||||
|
file_ids = file.get("data", {}).get("file_ids", [])
|
||||||
|
|
||||||
|
documents = []
|
||||||
|
metadatas = []
|
||||||
|
for file_id in file_ids:
|
||||||
|
file_object = Files.get_file_by_id(file_id)
|
||||||
|
|
||||||
|
if file_object:
|
||||||
|
documents.append(file_object.data.get("content", ""))
|
||||||
|
metadatas.append(
|
||||||
|
{
|
||||||
|
"file_id": file_id,
|
||||||
|
"name": file_object.filename,
|
||||||
|
"source": file_object.filename,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
context = {
|
||||||
|
"documents": [documents],
|
||||||
|
"metadatas": [metadatas],
|
||||||
|
}
|
||||||
|
|
||||||
|
elif file.get("id"):
|
||||||
|
file_object = Files.get_file_by_id(file.get("id"))
|
||||||
|
if file_object:
|
||||||
|
context = {
|
||||||
|
"documents": [[file_object.data.get("content", "")]],
|
||||||
|
"metadatas": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"file_id": file.get("id"),
|
||||||
|
"name": file_object.filename,
|
||||||
|
"source": file_object.filename,
|
||||||
|
}
|
||||||
|
]
|
||||||
|
],
|
||||||
|
}
|
||||||
|
elif file.get("file").get("data"):
|
||||||
|
context = {
|
||||||
|
"documents": [[file.get("file").get("data", {}).get("content")]],
|
||||||
|
"metadatas": [
|
||||||
|
[file.get("file").get("data", {}).get("metadata", {})]
|
||||||
|
],
|
||||||
|
}
|
||||||
|
else:
|
||||||
collection_names = []
|
collection_names = []
|
||||||
if file.get("type") == "collection":
|
if file.get("type") == "collection":
|
||||||
if file.get("legacy"):
|
if file.get("legacy"):
|
||||||
@@ -434,6 +484,7 @@ def get_sources_from_files(
|
|||||||
if context:
|
if context:
|
||||||
if "data" in file:
|
if "data" in file:
|
||||||
del file["data"]
|
del file["data"]
|
||||||
|
|
||||||
relevant_contexts.append({**context, "file": file})
|
relevant_contexts.append({**context, "file": file})
|
||||||
|
|
||||||
sources = []
|
sources = []
|
||||||
@@ -530,7 +581,7 @@ def generate_openai_batch_embeddings(
|
|||||||
else:
|
else:
|
||||||
raise "Something went wrong :/"
|
raise "Something went wrong :/"
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(e)
|
log.exception(f"Error generating openai batch embeddings: {e}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
@@ -564,7 +615,7 @@ def generate_ollama_batch_embeddings(
|
|||||||
else:
|
else:
|
||||||
raise "Something went wrong :/"
|
raise "Something went wrong :/"
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(e)
|
log.exception(f"Error generating ollama batch embeddings: {e}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,10 @@ elif VECTOR_DB == "pgvector":
|
|||||||
from open_webui.retrieval.vector.dbs.pgvector import PgvectorClient
|
from open_webui.retrieval.vector.dbs.pgvector import PgvectorClient
|
||||||
|
|
||||||
VECTOR_DB_CLIENT = PgvectorClient()
|
VECTOR_DB_CLIENT = PgvectorClient()
|
||||||
|
elif VECTOR_DB == "elasticsearch":
|
||||||
|
from open_webui.retrieval.vector.dbs.elasticsearch import ElasticsearchClient
|
||||||
|
|
||||||
|
VECTOR_DB_CLIENT = ElasticsearchClient()
|
||||||
else:
|
else:
|
||||||
from open_webui.retrieval.vector.dbs.chroma import ChromaClient
|
from open_webui.retrieval.vector.dbs.chroma import ChromaClient
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import chromadb
|
import chromadb
|
||||||
|
import logging
|
||||||
from chromadb import Settings
|
from chromadb import Settings
|
||||||
from chromadb.utils.batch_utils import create_batches
|
from chromadb.utils.batch_utils import create_batches
|
||||||
|
|
||||||
@@ -16,6 +17,10 @@ from open_webui.config import (
|
|||||||
CHROMA_CLIENT_AUTH_PROVIDER,
|
CHROMA_CLIENT_AUTH_PROVIDER,
|
||||||
CHROMA_CLIENT_AUTH_CREDENTIALS,
|
CHROMA_CLIENT_AUTH_CREDENTIALS,
|
||||||
)
|
)
|
||||||
|
from open_webui.env import SRC_LOG_LEVELS
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
log.setLevel(SRC_LOG_LEVELS["RAG"])
|
||||||
|
|
||||||
|
|
||||||
class ChromaClient:
|
class ChromaClient:
|
||||||
@@ -102,8 +107,7 @@ class ChromaClient:
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
return None
|
return None
|
||||||
except Exception as e:
|
except:
|
||||||
print(e)
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def get(self, collection_name: str) -> Optional[GetResult]:
|
def get(self, collection_name: str) -> Optional[GetResult]:
|
||||||
|
|||||||
@@ -0,0 +1,295 @@
|
|||||||
|
from elasticsearch import Elasticsearch, BadRequestError
|
||||||
|
from typing import Optional
|
||||||
|
import ssl
|
||||||
|
from elasticsearch.helpers import bulk, scan
|
||||||
|
from open_webui.retrieval.vector.main import VectorItem, SearchResult, GetResult
|
||||||
|
from open_webui.config import (
|
||||||
|
ELASTICSEARCH_URL,
|
||||||
|
ELASTICSEARCH_CA_CERTS,
|
||||||
|
ELASTICSEARCH_API_KEY,
|
||||||
|
ELASTICSEARCH_USERNAME,
|
||||||
|
ELASTICSEARCH_PASSWORD,
|
||||||
|
ELASTICSEARCH_CLOUD_ID,
|
||||||
|
ELASTICSEARCH_INDEX_PREFIX,
|
||||||
|
SSL_ASSERT_FINGERPRINT,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ElasticsearchClient:
|
||||||
|
"""
|
||||||
|
Important:
|
||||||
|
in order to reduce the number of indexes and since the embedding vector length is fixed, we avoid creating
|
||||||
|
an index for each file but store it as a text field, while seperating to different index
|
||||||
|
baesd on the embedding length.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.index_prefix = ELASTICSEARCH_INDEX_PREFIX
|
||||||
|
self.client = Elasticsearch(
|
||||||
|
hosts=[ELASTICSEARCH_URL],
|
||||||
|
ca_certs=ELASTICSEARCH_CA_CERTS,
|
||||||
|
api_key=ELASTICSEARCH_API_KEY,
|
||||||
|
cloud_id=ELASTICSEARCH_CLOUD_ID,
|
||||||
|
basic_auth=(
|
||||||
|
(ELASTICSEARCH_USERNAME, ELASTICSEARCH_PASSWORD)
|
||||||
|
if ELASTICSEARCH_USERNAME and ELASTICSEARCH_PASSWORD
|
||||||
|
else None
|
||||||
|
),
|
||||||
|
ssl_assert_fingerprint=SSL_ASSERT_FINGERPRINT,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Status: works
|
||||||
|
def _get_index_name(self, dimension: int) -> str:
|
||||||
|
return f"{self.index_prefix}_d{str(dimension)}"
|
||||||
|
|
||||||
|
# Status: works
|
||||||
|
def _scan_result_to_get_result(self, result) -> GetResult:
|
||||||
|
if not result:
|
||||||
|
return None
|
||||||
|
ids = []
|
||||||
|
documents = []
|
||||||
|
metadatas = []
|
||||||
|
|
||||||
|
for hit in result:
|
||||||
|
ids.append(hit["_id"])
|
||||||
|
documents.append(hit["_source"].get("text"))
|
||||||
|
metadatas.append(hit["_source"].get("metadata"))
|
||||||
|
|
||||||
|
return GetResult(ids=[ids], documents=[documents], metadatas=[metadatas])
|
||||||
|
|
||||||
|
# Status: works
|
||||||
|
def _result_to_get_result(self, result) -> GetResult:
|
||||||
|
if not result["hits"]["hits"]:
|
||||||
|
return None
|
||||||
|
ids = []
|
||||||
|
documents = []
|
||||||
|
metadatas = []
|
||||||
|
|
||||||
|
for hit in result["hits"]["hits"]:
|
||||||
|
ids.append(hit["_id"])
|
||||||
|
documents.append(hit["_source"].get("text"))
|
||||||
|
metadatas.append(hit["_source"].get("metadata"))
|
||||||
|
|
||||||
|
return GetResult(ids=[ids], documents=[documents], metadatas=[metadatas])
|
||||||
|
|
||||||
|
# Status: works
|
||||||
|
def _result_to_search_result(self, result) -> SearchResult:
|
||||||
|
ids = []
|
||||||
|
distances = []
|
||||||
|
documents = []
|
||||||
|
metadatas = []
|
||||||
|
|
||||||
|
for hit in result["hits"]["hits"]:
|
||||||
|
ids.append(hit["_id"])
|
||||||
|
distances.append(hit["_score"])
|
||||||
|
documents.append(hit["_source"].get("text"))
|
||||||
|
metadatas.append(hit["_source"].get("metadata"))
|
||||||
|
|
||||||
|
return SearchResult(
|
||||||
|
ids=[ids],
|
||||||
|
distances=[distances],
|
||||||
|
documents=[documents],
|
||||||
|
metadatas=[metadatas],
|
||||||
|
)
|
||||||
|
|
||||||
|
# Status: works
|
||||||
|
def _create_index(self, dimension: int):
|
||||||
|
body = {
|
||||||
|
"mappings": {
|
||||||
|
"dynamic_templates": [
|
||||||
|
{
|
||||||
|
"strings": {
|
||||||
|
"match_mapping_type": "string",
|
||||||
|
"mapping": {"type": "keyword"},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"collection": {"type": "keyword"},
|
||||||
|
"id": {"type": "keyword"},
|
||||||
|
"vector": {
|
||||||
|
"type": "dense_vector",
|
||||||
|
"dims": dimension, # Adjust based on your vector dimensions
|
||||||
|
"index": True,
|
||||||
|
"similarity": "cosine",
|
||||||
|
},
|
||||||
|
"text": {"type": "text"},
|
||||||
|
"metadata": {"type": "object"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.client.indices.create(index=self._get_index_name(dimension), body=body)
|
||||||
|
|
||||||
|
# Status: works
|
||||||
|
|
||||||
|
def _create_batches(self, items: list[VectorItem], batch_size=100):
|
||||||
|
for i in range(0, len(items), batch_size):
|
||||||
|
yield items[i : min(i + batch_size, len(items))]
|
||||||
|
|
||||||
|
# Status: works
|
||||||
|
def has_collection(self, collection_name) -> bool:
|
||||||
|
query_body = {"query": {"bool": {"filter": []}}}
|
||||||
|
query_body["query"]["bool"]["filter"].append(
|
||||||
|
{"term": {"collection": collection_name}}
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = self.client.count(index=f"{self.index_prefix}*", body=query_body)
|
||||||
|
|
||||||
|
return result.body["count"] > 0
|
||||||
|
except Exception as e:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def delete_collection(self, collection_name: str):
|
||||||
|
query = {"query": {"term": {"collection": collection_name}}}
|
||||||
|
self.client.delete_by_query(index=f"{self.index_prefix}*", body=query)
|
||||||
|
|
||||||
|
# Status: works
|
||||||
|
def search(
|
||||||
|
self, collection_name: str, vectors: list[list[float]], limit: int
|
||||||
|
) -> Optional[SearchResult]:
|
||||||
|
query = {
|
||||||
|
"size": limit,
|
||||||
|
"_source": ["text", "metadata"],
|
||||||
|
"query": {
|
||||||
|
"script_score": {
|
||||||
|
"query": {
|
||||||
|
"bool": {"filter": [{"term": {"collection": collection_name}}]}
|
||||||
|
},
|
||||||
|
"script": {
|
||||||
|
"source": "cosineSimilarity(params.vector, 'vector') + 1.0",
|
||||||
|
"params": {
|
||||||
|
"vector": vectors[0]
|
||||||
|
}, # Assuming single query vector
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
result = self.client.search(
|
||||||
|
index=self._get_index_name(len(vectors[0])), body=query
|
||||||
|
)
|
||||||
|
|
||||||
|
return self._result_to_search_result(result)
|
||||||
|
|
||||||
|
# Status: only tested halfwat
|
||||||
|
def query(
|
||||||
|
self, collection_name: str, filter: dict, limit: Optional[int] = None
|
||||||
|
) -> Optional[GetResult]:
|
||||||
|
if not self.has_collection(collection_name):
|
||||||
|
return None
|
||||||
|
|
||||||
|
query_body = {
|
||||||
|
"query": {"bool": {"filter": []}},
|
||||||
|
"_source": ["text", "metadata"],
|
||||||
|
}
|
||||||
|
|
||||||
|
for field, value in filter.items():
|
||||||
|
query_body["query"]["bool"]["filter"].append({"term": {field: value}})
|
||||||
|
query_body["query"]["bool"]["filter"].append(
|
||||||
|
{"term": {"collection": collection_name}}
|
||||||
|
)
|
||||||
|
size = limit if limit else 10
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = self.client.search(
|
||||||
|
index=f"{self.index_prefix}*",
|
||||||
|
body=query_body,
|
||||||
|
size=size,
|
||||||
|
)
|
||||||
|
|
||||||
|
return self._result_to_get_result(result)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Status: works
|
||||||
|
def _has_index(self, dimension: int):
|
||||||
|
return self.client.indices.exists(
|
||||||
|
index=self._get_index_name(dimension=dimension)
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_or_create_index(self, dimension: int):
|
||||||
|
if not self._has_index(dimension=dimension):
|
||||||
|
self._create_index(dimension=dimension)
|
||||||
|
|
||||||
|
# Status: works
|
||||||
|
def get(self, collection_name: str) -> Optional[GetResult]:
|
||||||
|
# Get all the items in the collection.
|
||||||
|
query = {
|
||||||
|
"query": {"bool": {"filter": [{"term": {"collection": collection_name}}]}},
|
||||||
|
"_source": ["text", "metadata"],
|
||||||
|
}
|
||||||
|
results = list(scan(self.client, index=f"{self.index_prefix}*", query=query))
|
||||||
|
|
||||||
|
return self._scan_result_to_get_result(results)
|
||||||
|
|
||||||
|
# Status: works
|
||||||
|
def insert(self, collection_name: str, items: list[VectorItem]):
|
||||||
|
if not self._has_index(dimension=len(items[0]["vector"])):
|
||||||
|
self._create_index(dimension=len(items[0]["vector"]))
|
||||||
|
|
||||||
|
for batch in self._create_batches(items):
|
||||||
|
actions = [
|
||||||
|
{
|
||||||
|
"_index": self._get_index_name(dimension=len(items[0]["vector"])),
|
||||||
|
"_id": item["id"],
|
||||||
|
"_source": {
|
||||||
|
"collection": collection_name,
|
||||||
|
"vector": item["vector"],
|
||||||
|
"text": item["text"],
|
||||||
|
"metadata": item["metadata"],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for item in batch
|
||||||
|
]
|
||||||
|
bulk(self.client, actions)
|
||||||
|
|
||||||
|
# Upsert documents using the update API with doc_as_upsert=True.
|
||||||
|
def upsert(self, collection_name: str, items: list[VectorItem]):
|
||||||
|
if not self._has_index(dimension=len(items[0]["vector"])):
|
||||||
|
self._create_index(dimension=len(items[0]["vector"]))
|
||||||
|
for batch in self._create_batches(items):
|
||||||
|
actions = [
|
||||||
|
{
|
||||||
|
"_op_type": "update",
|
||||||
|
"_index": self._get_index_name(dimension=len(item["vector"])),
|
||||||
|
"_id": item["id"],
|
||||||
|
"doc": {
|
||||||
|
"collection": collection_name,
|
||||||
|
"vector": item["vector"],
|
||||||
|
"text": item["text"],
|
||||||
|
"metadata": item["metadata"],
|
||||||
|
},
|
||||||
|
"doc_as_upsert": True,
|
||||||
|
}
|
||||||
|
for item in batch
|
||||||
|
]
|
||||||
|
bulk(self.client, actions)
|
||||||
|
|
||||||
|
# Delete specific documents from a collection by filtering on both collection and document IDs.
|
||||||
|
def delete(
|
||||||
|
self,
|
||||||
|
collection_name: str,
|
||||||
|
ids: Optional[list[str]] = None,
|
||||||
|
filter: Optional[dict] = None,
|
||||||
|
):
|
||||||
|
|
||||||
|
query = {
|
||||||
|
"query": {"bool": {"filter": [{"term": {"collection": collection_name}}]}}
|
||||||
|
}
|
||||||
|
# logic based on chromaDB
|
||||||
|
if ids:
|
||||||
|
query["query"]["bool"]["filter"].append({"terms": {"_id": ids}})
|
||||||
|
elif filter:
|
||||||
|
for field, value in filter.items():
|
||||||
|
query["query"]["bool"]["filter"].append(
|
||||||
|
{"term": {f"metadata.{field}": value}}
|
||||||
|
)
|
||||||
|
|
||||||
|
self.client.delete_by_query(index=f"{self.index_prefix}*", body=query)
|
||||||
|
|
||||||
|
def reset(self):
|
||||||
|
indices = self.client.indices.get(index=f"{self.index_prefix}*")
|
||||||
|
for index in indices:
|
||||||
|
self.client.indices.delete(index=index)
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
from pymilvus import MilvusClient as Client
|
from pymilvus import MilvusClient as Client
|
||||||
from pymilvus import FieldSchema, DataType
|
from pymilvus import FieldSchema, DataType
|
||||||
import json
|
import json
|
||||||
|
import logging
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from open_webui.retrieval.vector.main import VectorItem, SearchResult, GetResult
|
from open_webui.retrieval.vector.main import VectorItem, SearchResult, GetResult
|
||||||
@@ -10,15 +10,19 @@ from open_webui.config import (
|
|||||||
MILVUS_DB,
|
MILVUS_DB,
|
||||||
MILVUS_TOKEN,
|
MILVUS_TOKEN,
|
||||||
)
|
)
|
||||||
|
from open_webui.env import SRC_LOG_LEVELS
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
log.setLevel(SRC_LOG_LEVELS["RAG"])
|
||||||
|
|
||||||
|
|
||||||
class MilvusClient:
|
class MilvusClient:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.collection_prefix = "open_webui"
|
self.collection_prefix = "open_webui"
|
||||||
if MILVUS_TOKEN is None:
|
if MILVUS_TOKEN is None:
|
||||||
self.client = Client(uri=MILVUS_URI, database=MILVUS_DB)
|
self.client = Client(uri=MILVUS_URI, db_name=MILVUS_DB)
|
||||||
else:
|
else:
|
||||||
self.client = Client(uri=MILVUS_URI, database=MILVUS_DB, token=MILVUS_TOKEN)
|
self.client = Client(uri=MILVUS_URI, db_name=MILVUS_DB, token=MILVUS_TOKEN)
|
||||||
|
|
||||||
def _result_to_get_result(self, result) -> GetResult:
|
def _result_to_get_result(self, result) -> GetResult:
|
||||||
ids = []
|
ids = []
|
||||||
@@ -168,7 +172,7 @@ class MilvusClient:
|
|||||||
try:
|
try:
|
||||||
# Loop until there are no more items to fetch or the desired limit is reached
|
# Loop until there are no more items to fetch or the desired limit is reached
|
||||||
while remaining > 0:
|
while remaining > 0:
|
||||||
print("remaining", remaining)
|
log.info(f"remaining: {remaining}")
|
||||||
current_fetch = min(
|
current_fetch = min(
|
||||||
max_limit, remaining
|
max_limit, remaining
|
||||||
) # Determine how many items to fetch in this iteration
|
) # Determine how many items to fetch in this iteration
|
||||||
@@ -195,10 +199,12 @@ class MilvusClient:
|
|||||||
if results_count < current_fetch:
|
if results_count < current_fetch:
|
||||||
break
|
break
|
||||||
|
|
||||||
print(all_results)
|
log.debug(all_results)
|
||||||
return self._result_to_get_result([all_results])
|
return self._result_to_get_result([all_results])
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(e)
|
log.exception(
|
||||||
|
f"Error querying collection {collection_name} with limit {limit}: {e}"
|
||||||
|
)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def get(self, collection_name: str) -> Optional[GetResult]:
|
def get(self, collection_name: str) -> Optional[GetResult]:
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ class OpenSearchClient:
|
|||||||
ids=ids, distances=distances, documents=documents, metadatas=metadatas
|
ids=ids, distances=distances, documents=documents, metadatas=metadatas
|
||||||
)
|
)
|
||||||
|
|
||||||
def _create_index(self, index_name: str, dimension: int):
|
def _create_index(self, collection_name: str, dimension: int):
|
||||||
body = {
|
body = {
|
||||||
"mappings": {
|
"mappings": {
|
||||||
"properties": {
|
"properties": {
|
||||||
@@ -72,24 +72,28 @@ class OpenSearchClient:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
self.client.indices.create(index=f"{self.index_prefix}_{index_name}", body=body)
|
self.client.indices.create(
|
||||||
|
index=f"{self.index_prefix}_{collection_name}", body=body
|
||||||
|
)
|
||||||
|
|
||||||
def _create_batches(self, items: list[VectorItem], batch_size=100):
|
def _create_batches(self, items: list[VectorItem], batch_size=100):
|
||||||
for i in range(0, len(items), batch_size):
|
for i in range(0, len(items), batch_size):
|
||||||
yield items[i : i + batch_size]
|
yield items[i : i + batch_size]
|
||||||
|
|
||||||
def has_collection(self, index_name: str) -> bool:
|
def has_collection(self, collection_name: str) -> bool:
|
||||||
# has_collection here means has index.
|
# has_collection here means has index.
|
||||||
# We are simply adapting to the norms of the other DBs.
|
# We are simply adapting to the norms of the other DBs.
|
||||||
return self.client.indices.exists(index=f"{self.index_prefix}_{index_name}")
|
return self.client.indices.exists(
|
||||||
|
index=f"{self.index_prefix}_{collection_name}"
|
||||||
|
)
|
||||||
|
|
||||||
def delete_colleciton(self, index_name: str):
|
def delete_colleciton(self, collection_name: str):
|
||||||
# delete_collection here means delete index.
|
# delete_collection here means delete index.
|
||||||
# We are simply adapting to the norms of the other DBs.
|
# We are simply adapting to the norms of the other DBs.
|
||||||
self.client.indices.delete(index=f"{self.index_prefix}_{index_name}")
|
self.client.indices.delete(index=f"{self.index_prefix}_{collection_name}")
|
||||||
|
|
||||||
def search(
|
def search(
|
||||||
self, index_name: str, vectors: list[list[float]], limit: int
|
self, collection_name: str, vectors: list[list[float]], limit: int
|
||||||
) -> Optional[SearchResult]:
|
) -> Optional[SearchResult]:
|
||||||
query = {
|
query = {
|
||||||
"size": limit,
|
"size": limit,
|
||||||
@@ -108,7 +112,7 @@ class OpenSearchClient:
|
|||||||
}
|
}
|
||||||
|
|
||||||
result = self.client.search(
|
result = self.client.search(
|
||||||
index=f"{self.index_prefix}_{index_name}", body=query
|
index=f"{self.index_prefix}_{collection_name}", body=query
|
||||||
)
|
)
|
||||||
|
|
||||||
return self._result_to_search_result(result)
|
return self._result_to_search_result(result)
|
||||||
@@ -141,21 +145,22 @@ class OpenSearchClient:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def get_or_create_index(self, index_name: str, dimension: int):
|
def _create_index_if_not_exists(self, collection_name: str, dimension: int):
|
||||||
if not self.has_index(index_name):
|
if not self.has_index(collection_name):
|
||||||
self._create_index(index_name, dimension)
|
self._create_index(collection_name, dimension)
|
||||||
|
|
||||||
def get(self, index_name: str) -> Optional[GetResult]:
|
def get(self, collection_name: str) -> Optional[GetResult]:
|
||||||
query = {"query": {"match_all": {}}, "_source": ["text", "metadata"]}
|
query = {"query": {"match_all": {}}, "_source": ["text", "metadata"]}
|
||||||
|
|
||||||
result = self.client.search(
|
result = self.client.search(
|
||||||
index=f"{self.index_prefix}_{index_name}", body=query
|
index=f"{self.index_prefix}_{collection_name}", body=query
|
||||||
)
|
)
|
||||||
return self._result_to_get_result(result)
|
return self._result_to_get_result(result)
|
||||||
|
|
||||||
def insert(self, index_name: str, items: list[VectorItem]):
|
def insert(self, collection_name: str, items: list[VectorItem]):
|
||||||
if not self.has_index(index_name):
|
self._create_index_if_not_exists(
|
||||||
self._create_index(index_name, dimension=len(items[0]["vector"]))
|
collection_name=collection_name, dimension=len(items[0]["vector"])
|
||||||
|
)
|
||||||
|
|
||||||
for batch in self._create_batches(items):
|
for batch in self._create_batches(items):
|
||||||
actions = [
|
actions = [
|
||||||
@@ -173,15 +178,17 @@ class OpenSearchClient:
|
|||||||
]
|
]
|
||||||
self.client.bulk(actions)
|
self.client.bulk(actions)
|
||||||
|
|
||||||
def upsert(self, index_name: str, items: list[VectorItem]):
|
def upsert(self, collection_name: str, items: list[VectorItem]):
|
||||||
if not self.has_index(index_name):
|
self._create_index_if_not_exists(
|
||||||
self._create_index(index_name, dimension=len(items[0]["vector"]))
|
collection_name=collection_name, dimension=len(items[0]["vector"])
|
||||||
|
)
|
||||||
|
|
||||||
for batch in self._create_batches(items):
|
for batch in self._create_batches(items):
|
||||||
actions = [
|
actions = [
|
||||||
{
|
{
|
||||||
"index": {
|
"index": {
|
||||||
"_id": item["id"],
|
"_id": item["id"],
|
||||||
|
"_index": f"{self.index_prefix}_{collection_name}",
|
||||||
"_source": {
|
"_source": {
|
||||||
"vector": item["vector"],
|
"vector": item["vector"],
|
||||||
"text": item["text"],
|
"text": item["text"],
|
||||||
@@ -193,9 +200,9 @@ class OpenSearchClient:
|
|||||||
]
|
]
|
||||||
self.client.bulk(actions)
|
self.client.bulk(actions)
|
||||||
|
|
||||||
def delete(self, index_name: str, ids: list[str]):
|
def delete(self, collection_name: str, ids: list[str]):
|
||||||
actions = [
|
actions = [
|
||||||
{"delete": {"_index": f"{self.index_prefix}_{index_name}", "_id": id}}
|
{"delete": {"_index": f"{self.index_prefix}_{collection_name}", "_id": id}}
|
||||||
for id in ids
|
for id in ids
|
||||||
]
|
]
|
||||||
self.client.bulk(body=actions)
|
self.client.bulk(body=actions)
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
from typing import Optional, List, Dict, Any
|
from typing import Optional, List, Dict, Any
|
||||||
|
import logging
|
||||||
from sqlalchemy import (
|
from sqlalchemy import (
|
||||||
cast,
|
cast,
|
||||||
column,
|
column,
|
||||||
@@ -24,9 +25,14 @@ from sqlalchemy.exc import NoSuchTableError
|
|||||||
from open_webui.retrieval.vector.main import VectorItem, SearchResult, GetResult
|
from open_webui.retrieval.vector.main import VectorItem, SearchResult, GetResult
|
||||||
from open_webui.config import PGVECTOR_DB_URL, PGVECTOR_INITIALIZE_MAX_VECTOR_LENGTH
|
from open_webui.config import PGVECTOR_DB_URL, PGVECTOR_INITIALIZE_MAX_VECTOR_LENGTH
|
||||||
|
|
||||||
|
from open_webui.env import SRC_LOG_LEVELS
|
||||||
|
|
||||||
VECTOR_LENGTH = PGVECTOR_INITIALIZE_MAX_VECTOR_LENGTH
|
VECTOR_LENGTH = PGVECTOR_INITIALIZE_MAX_VECTOR_LENGTH
|
||||||
Base = declarative_base()
|
Base = declarative_base()
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
log.setLevel(SRC_LOG_LEVELS["RAG"])
|
||||||
|
|
||||||
|
|
||||||
class DocumentChunk(Base):
|
class DocumentChunk(Base):
|
||||||
__tablename__ = "document_chunk"
|
__tablename__ = "document_chunk"
|
||||||
@@ -82,10 +88,10 @@ class PgvectorClient:
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
self.session.commit()
|
self.session.commit()
|
||||||
print("Initialization complete.")
|
log.info("Initialization complete.")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.session.rollback()
|
self.session.rollback()
|
||||||
print(f"Error during initialization: {e}")
|
log.exception(f"Error during initialization: {e}")
|
||||||
raise
|
raise
|
||||||
|
|
||||||
def check_vector_length(self) -> None:
|
def check_vector_length(self) -> None:
|
||||||
@@ -150,12 +156,12 @@ class PgvectorClient:
|
|||||||
new_items.append(new_chunk)
|
new_items.append(new_chunk)
|
||||||
self.session.bulk_save_objects(new_items)
|
self.session.bulk_save_objects(new_items)
|
||||||
self.session.commit()
|
self.session.commit()
|
||||||
print(
|
log.info(
|
||||||
f"Inserted {len(new_items)} items into collection '{collection_name}'."
|
f"Inserted {len(new_items)} items into collection '{collection_name}'."
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.session.rollback()
|
self.session.rollback()
|
||||||
print(f"Error during insert: {e}")
|
log.exception(f"Error during insert: {e}")
|
||||||
raise
|
raise
|
||||||
|
|
||||||
def upsert(self, collection_name: str, items: List[VectorItem]) -> None:
|
def upsert(self, collection_name: str, items: List[VectorItem]) -> None:
|
||||||
@@ -184,10 +190,12 @@ class PgvectorClient:
|
|||||||
)
|
)
|
||||||
self.session.add(new_chunk)
|
self.session.add(new_chunk)
|
||||||
self.session.commit()
|
self.session.commit()
|
||||||
print(f"Upserted {len(items)} items into collection '{collection_name}'.")
|
log.info(
|
||||||
|
f"Upserted {len(items)} items into collection '{collection_name}'."
|
||||||
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.session.rollback()
|
self.session.rollback()
|
||||||
print(f"Error during upsert: {e}")
|
log.exception(f"Error during upsert: {e}")
|
||||||
raise
|
raise
|
||||||
|
|
||||||
def search(
|
def search(
|
||||||
@@ -278,7 +286,7 @@ class PgvectorClient:
|
|||||||
ids=ids, distances=distances, documents=documents, metadatas=metadatas
|
ids=ids, distances=distances, documents=documents, metadatas=metadatas
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error during search: {e}")
|
log.exception(f"Error during search: {e}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def query(
|
def query(
|
||||||
@@ -310,7 +318,7 @@ class PgvectorClient:
|
|||||||
metadatas=metadatas,
|
metadatas=metadatas,
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error during query: {e}")
|
log.exception(f"Error during query: {e}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def get(
|
def get(
|
||||||
@@ -334,7 +342,7 @@ class PgvectorClient:
|
|||||||
|
|
||||||
return GetResult(ids=ids, documents=documents, metadatas=metadatas)
|
return GetResult(ids=ids, documents=documents, metadatas=metadatas)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error during get: {e}")
|
log.exception(f"Error during get: {e}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def delete(
|
def delete(
|
||||||
@@ -356,22 +364,22 @@ class PgvectorClient:
|
|||||||
)
|
)
|
||||||
deleted = query.delete(synchronize_session=False)
|
deleted = query.delete(synchronize_session=False)
|
||||||
self.session.commit()
|
self.session.commit()
|
||||||
print(f"Deleted {deleted} items from collection '{collection_name}'.")
|
log.info(f"Deleted {deleted} items from collection '{collection_name}'.")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.session.rollback()
|
self.session.rollback()
|
||||||
print(f"Error during delete: {e}")
|
log.exception(f"Error during delete: {e}")
|
||||||
raise
|
raise
|
||||||
|
|
||||||
def reset(self) -> None:
|
def reset(self) -> None:
|
||||||
try:
|
try:
|
||||||
deleted = self.session.query(DocumentChunk).delete()
|
deleted = self.session.query(DocumentChunk).delete()
|
||||||
self.session.commit()
|
self.session.commit()
|
||||||
print(
|
log.info(
|
||||||
f"Reset complete. Deleted {deleted} items from 'document_chunk' table."
|
f"Reset complete. Deleted {deleted} items from 'document_chunk' table."
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.session.rollback()
|
self.session.rollback()
|
||||||
print(f"Error during reset: {e}")
|
log.exception(f"Error during reset: {e}")
|
||||||
raise
|
raise
|
||||||
|
|
||||||
def close(self) -> None:
|
def close(self) -> None:
|
||||||
@@ -387,9 +395,9 @@ class PgvectorClient:
|
|||||||
)
|
)
|
||||||
return exists
|
return exists
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error checking collection existence: {e}")
|
log.exception(f"Error checking collection existence: {e}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def delete_collection(self, collection_name: str) -> None:
|
def delete_collection(self, collection_name: str) -> None:
|
||||||
self.delete(collection_name)
|
self.delete(collection_name)
|
||||||
print(f"Collection '{collection_name}' deleted.")
|
log.info(f"Collection '{collection_name}' deleted.")
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
import logging
|
||||||
|
|
||||||
from qdrant_client import QdrantClient as Qclient
|
from qdrant_client import QdrantClient as Qclient
|
||||||
from qdrant_client.http.models import PointStruct
|
from qdrant_client.http.models import PointStruct
|
||||||
@@ -6,9 +7,13 @@ from qdrant_client.models import models
|
|||||||
|
|
||||||
from open_webui.retrieval.vector.main import VectorItem, SearchResult, GetResult
|
from open_webui.retrieval.vector.main import VectorItem, SearchResult, GetResult
|
||||||
from open_webui.config import QDRANT_URI, QDRANT_API_KEY
|
from open_webui.config import QDRANT_URI, QDRANT_API_KEY
|
||||||
|
from open_webui.env import SRC_LOG_LEVELS
|
||||||
|
|
||||||
NO_LIMIT = 999999999
|
NO_LIMIT = 999999999
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
log.setLevel(SRC_LOG_LEVELS["RAG"])
|
||||||
|
|
||||||
|
|
||||||
class QdrantClient:
|
class QdrantClient:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
@@ -49,7 +54,7 @@ class QdrantClient:
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
print(f"collection {collection_name_with_prefix} successfully created!")
|
log.info(f"collection {collection_name_with_prefix} successfully created!")
|
||||||
|
|
||||||
def _create_collection_if_not_exists(self, collection_name, dimension):
|
def _create_collection_if_not_exists(self, collection_name, dimension):
|
||||||
if not self.has_collection(collection_name=collection_name):
|
if not self.has_collection(collection_name=collection_name):
|
||||||
@@ -120,7 +125,7 @@ class QdrantClient:
|
|||||||
)
|
)
|
||||||
return self._result_to_get_result(points.points)
|
return self._result_to_get_result(points.points)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(e)
|
log.exception(f"Error querying a collection '{collection_name}': {e}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def get(self, collection_name: str) -> Optional[GetResult]:
|
def get(self, collection_name: str) -> Optional[GetResult]:
|
||||||
|
|||||||
@@ -0,0 +1,87 @@
|
|||||||
|
import logging
|
||||||
|
from typing import Optional, List
|
||||||
|
import requests
|
||||||
|
|
||||||
|
from open_webui.retrieval.web.main import SearchResult, get_filtered_results
|
||||||
|
from open_webui.env import SRC_LOG_LEVELS
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
log.setLevel(SRC_LOG_LEVELS["RAG"])
|
||||||
|
|
||||||
|
|
||||||
|
def search_perplexity(
|
||||||
|
api_key: str,
|
||||||
|
query: str,
|
||||||
|
count: int,
|
||||||
|
filter_list: Optional[list[str]] = None,
|
||||||
|
) -> list[SearchResult]:
|
||||||
|
"""Search using Perplexity API and return the results as a list of SearchResult objects.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
api_key (str): A Perplexity API key
|
||||||
|
query (str): The query to search for
|
||||||
|
count (int): Maximum number of results to return
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Handle PersistentConfig object
|
||||||
|
if hasattr(api_key, "__str__"):
|
||||||
|
api_key = str(api_key)
|
||||||
|
|
||||||
|
try:
|
||||||
|
url = "https://api.perplexity.ai/chat/completions"
|
||||||
|
|
||||||
|
# Create payload for the API call
|
||||||
|
payload = {
|
||||||
|
"model": "sonar",
|
||||||
|
"messages": [
|
||||||
|
{
|
||||||
|
"role": "system",
|
||||||
|
"content": "You are a search assistant. Provide factual information with citations.",
|
||||||
|
},
|
||||||
|
{"role": "user", "content": query},
|
||||||
|
],
|
||||||
|
"temperature": 0.2, # Lower temperature for more factual responses
|
||||||
|
"stream": False,
|
||||||
|
}
|
||||||
|
|
||||||
|
headers = {
|
||||||
|
"Authorization": f"Bearer {api_key}",
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
}
|
||||||
|
|
||||||
|
# Make the API request
|
||||||
|
response = requests.request("POST", url, json=payload, headers=headers)
|
||||||
|
|
||||||
|
# Parse the JSON response
|
||||||
|
json_response = response.json()
|
||||||
|
|
||||||
|
# Extract citations from the response
|
||||||
|
citations = json_response.get("citations", [])
|
||||||
|
|
||||||
|
# Create search results from citations
|
||||||
|
results = []
|
||||||
|
for i, citation in enumerate(citations[:count]):
|
||||||
|
# Extract content from the response to use as snippet
|
||||||
|
content = ""
|
||||||
|
if "choices" in json_response and json_response["choices"]:
|
||||||
|
if i == 0:
|
||||||
|
content = json_response["choices"][0]["message"]["content"]
|
||||||
|
|
||||||
|
result = {"link": citation, "title": f"Source {i+1}", "snippet": content}
|
||||||
|
results.append(result)
|
||||||
|
|
||||||
|
if filter_list:
|
||||||
|
|
||||||
|
results = get_filtered_results(results, filter_list)
|
||||||
|
|
||||||
|
return [
|
||||||
|
SearchResult(
|
||||||
|
link=result["link"], title=result["title"], snippet=result["snippet"]
|
||||||
|
)
|
||||||
|
for result in results[:count]
|
||||||
|
]
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
log.error(f"Error searching with Perplexity API: {e}")
|
||||||
|
return []
|
||||||
@@ -54,7 +54,7 @@ MAX_FILE_SIZE = MAX_FILE_SIZE_MB * 1024 * 1024 # Convert MB to bytes
|
|||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
log.setLevel(SRC_LOG_LEVELS["AUDIO"])
|
log.setLevel(SRC_LOG_LEVELS["AUDIO"])
|
||||||
|
|
||||||
SPEECH_CACHE_DIR = Path(CACHE_DIR).joinpath("./audio/speech/")
|
SPEECH_CACHE_DIR = CACHE_DIR / "audio" / "speech"
|
||||||
SPEECH_CACHE_DIR.mkdir(parents=True, exist_ok=True)
|
SPEECH_CACHE_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
|
||||||
@@ -71,7 +71,7 @@ from pydub.utils import mediainfo
|
|||||||
def is_mp4_audio(file_path):
|
def is_mp4_audio(file_path):
|
||||||
"""Check if the given file is an MP4 audio file."""
|
"""Check if the given file is an MP4 audio file."""
|
||||||
if not os.path.isfile(file_path):
|
if not os.path.isfile(file_path):
|
||||||
print(f"File not found: {file_path}")
|
log.error(f"File not found: {file_path}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
info = mediainfo(file_path)
|
info = mediainfo(file_path)
|
||||||
@@ -88,7 +88,7 @@ def convert_mp4_to_wav(file_path, output_path):
|
|||||||
"""Convert MP4 audio file to WAV format."""
|
"""Convert MP4 audio file to WAV format."""
|
||||||
audio = AudioSegment.from_file(file_path, format="mp4")
|
audio = AudioSegment.from_file(file_path, format="mp4")
|
||||||
audio.export(output_path, format="wav")
|
audio.export(output_path, format="wav")
|
||||||
print(f"Converted {file_path} to {output_path}")
|
log.info(f"Converted {file_path} to {output_path}")
|
||||||
|
|
||||||
|
|
||||||
def set_faster_whisper_model(model: str, auto_update: bool = False):
|
def set_faster_whisper_model(model: str, auto_update: bool = False):
|
||||||
@@ -266,7 +266,6 @@ async def speech(request: Request, user=Depends(get_verified_user)):
|
|||||||
payload["model"] = request.app.state.config.TTS_MODEL
|
payload["model"] = request.app.state.config.TTS_MODEL
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# print(payload)
|
|
||||||
timeout = aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT)
|
timeout = aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT)
|
||||||
async with aiohttp.ClientSession(
|
async with aiohttp.ClientSession(
|
||||||
timeout=timeout, trust_env=True
|
timeout=timeout, trust_env=True
|
||||||
@@ -468,7 +467,7 @@ async def speech(request: Request, user=Depends(get_verified_user)):
|
|||||||
|
|
||||||
|
|
||||||
def transcribe(request: Request, file_path):
|
def transcribe(request: Request, file_path):
|
||||||
print("transcribe", file_path)
|
log.info(f"transcribe: {file_path}")
|
||||||
filename = os.path.basename(file_path)
|
filename = os.path.basename(file_path)
|
||||||
file_dir = os.path.dirname(file_path)
|
file_dir = os.path.dirname(file_path)
|
||||||
id = filename.split(".")[0]
|
id = filename.split(".")[0]
|
||||||
@@ -680,7 +679,22 @@ def transcription(
|
|||||||
def get_available_models(request: Request) -> list[dict]:
|
def get_available_models(request: Request) -> list[dict]:
|
||||||
available_models = []
|
available_models = []
|
||||||
if request.app.state.config.TTS_ENGINE == "openai":
|
if request.app.state.config.TTS_ENGINE == "openai":
|
||||||
available_models = [{"id": "tts-1"}, {"id": "tts-1-hd"}]
|
# Use custom endpoint if not using the official OpenAI API URL
|
||||||
|
if not request.app.state.config.TTS_OPENAI_API_BASE_URL.startswith(
|
||||||
|
"https://api.openai.com"
|
||||||
|
):
|
||||||
|
try:
|
||||||
|
response = requests.get(
|
||||||
|
f"{request.app.state.config.TTS_OPENAI_API_BASE_URL}/audio/models"
|
||||||
|
)
|
||||||
|
response.raise_for_status()
|
||||||
|
data = response.json()
|
||||||
|
available_models = data.get("models", [])
|
||||||
|
except Exception as e:
|
||||||
|
log.error(f"Error fetching models from custom endpoint: {str(e)}")
|
||||||
|
available_models = [{"id": "tts-1"}, {"id": "tts-1-hd"}]
|
||||||
|
else:
|
||||||
|
available_models = [{"id": "tts-1"}, {"id": "tts-1-hd"}]
|
||||||
elif request.app.state.config.TTS_ENGINE == "elevenlabs":
|
elif request.app.state.config.TTS_ENGINE == "elevenlabs":
|
||||||
try:
|
try:
|
||||||
response = requests.get(
|
response = requests.get(
|
||||||
@@ -711,14 +725,37 @@ def get_available_voices(request) -> dict:
|
|||||||
"""Returns {voice_id: voice_name} dict"""
|
"""Returns {voice_id: voice_name} dict"""
|
||||||
available_voices = {}
|
available_voices = {}
|
||||||
if request.app.state.config.TTS_ENGINE == "openai":
|
if request.app.state.config.TTS_ENGINE == "openai":
|
||||||
available_voices = {
|
# Use custom endpoint if not using the official OpenAI API URL
|
||||||
"alloy": "alloy",
|
if not request.app.state.config.TTS_OPENAI_API_BASE_URL.startswith(
|
||||||
"echo": "echo",
|
"https://api.openai.com"
|
||||||
"fable": "fable",
|
):
|
||||||
"onyx": "onyx",
|
try:
|
||||||
"nova": "nova",
|
response = requests.get(
|
||||||
"shimmer": "shimmer",
|
f"{request.app.state.config.TTS_OPENAI_API_BASE_URL}/audio/voices"
|
||||||
}
|
)
|
||||||
|
response.raise_for_status()
|
||||||
|
data = response.json()
|
||||||
|
voices_list = data.get("voices", [])
|
||||||
|
available_voices = {voice["id"]: voice["name"] for voice in voices_list}
|
||||||
|
except Exception as e:
|
||||||
|
log.error(f"Error fetching voices from custom endpoint: {str(e)}")
|
||||||
|
available_voices = {
|
||||||
|
"alloy": "alloy",
|
||||||
|
"echo": "echo",
|
||||||
|
"fable": "fable",
|
||||||
|
"onyx": "onyx",
|
||||||
|
"nova": "nova",
|
||||||
|
"shimmer": "shimmer",
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
available_voices = {
|
||||||
|
"alloy": "alloy",
|
||||||
|
"echo": "echo",
|
||||||
|
"fable": "fable",
|
||||||
|
"onyx": "onyx",
|
||||||
|
"nova": "nova",
|
||||||
|
"shimmer": "shimmer",
|
||||||
|
}
|
||||||
elif request.app.state.config.TTS_ENGINE == "elevenlabs":
|
elif request.app.state.config.TTS_ENGINE == "elevenlabs":
|
||||||
try:
|
try:
|
||||||
available_voices = get_elevenlabs_voices(
|
available_voices = get_elevenlabs_voices(
|
||||||
|
|||||||
@@ -31,10 +31,7 @@ from open_webui.env import (
|
|||||||
)
|
)
|
||||||
from fastapi import APIRouter, Depends, HTTPException, Request, status
|
from fastapi import APIRouter, Depends, HTTPException, Request, status
|
||||||
from fastapi.responses import RedirectResponse, Response
|
from fastapi.responses import RedirectResponse, Response
|
||||||
from open_webui.config import (
|
from open_webui.config import OPENID_PROVIDER_URL, ENABLE_OAUTH_SIGNUP, ENABLE_LDAP
|
||||||
OPENID_PROVIDER_URL,
|
|
||||||
ENABLE_OAUTH_SIGNUP,
|
|
||||||
)
|
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
from open_webui.utils.misc import parse_duration, validate_email_format
|
from open_webui.utils.misc import parse_duration, validate_email_format
|
||||||
from open_webui.utils.auth import (
|
from open_webui.utils.auth import (
|
||||||
@@ -51,8 +48,10 @@ from open_webui.utils.access_control import get_permissions
|
|||||||
from typing import Optional, List
|
from typing import Optional, List
|
||||||
|
|
||||||
from ssl import CERT_REQUIRED, PROTOCOL_TLS
|
from ssl import CERT_REQUIRED, PROTOCOL_TLS
|
||||||
from ldap3 import Server, Connection, NONE, Tls
|
|
||||||
from ldap3.utils.conv import escape_filter_chars
|
if ENABLE_LDAP.value:
|
||||||
|
from ldap3 import Server, Connection, NONE, Tls
|
||||||
|
from ldap3.utils.conv import escape_filter_chars
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
@@ -231,9 +230,12 @@ async def ldap_auth(request: Request, response: Response, form_data: LdapForm):
|
|||||||
|
|
||||||
entry = connection_app.entries[0]
|
entry = connection_app.entries[0]
|
||||||
username = str(entry[f"{LDAP_ATTRIBUTE_FOR_USERNAME}"]).lower()
|
username = str(entry[f"{LDAP_ATTRIBUTE_FOR_USERNAME}"]).lower()
|
||||||
mail = str(entry[f"{LDAP_ATTRIBUTE_FOR_MAIL}"])
|
email = str(entry[f"{LDAP_ATTRIBUTE_FOR_MAIL}"])
|
||||||
if not mail or mail == "" or mail == "[]":
|
if not email or email == "" or email == "[]":
|
||||||
raise HTTPException(400, f"User {form_data.user} does not have mail.")
|
raise HTTPException(400, f"User {form_data.user} does not have email.")
|
||||||
|
else:
|
||||||
|
email = email.lower()
|
||||||
|
|
||||||
cn = str(entry["cn"])
|
cn = str(entry["cn"])
|
||||||
user_dn = entry.entry_dn
|
user_dn = entry.entry_dn
|
||||||
|
|
||||||
@@ -248,18 +250,10 @@ async def ldap_auth(request: Request, response: Response, form_data: LdapForm):
|
|||||||
if not connection_user.bind():
|
if not connection_user.bind():
|
||||||
raise HTTPException(400, f"Authentication failed for {form_data.user}")
|
raise HTTPException(400, f"Authentication failed for {form_data.user}")
|
||||||
|
|
||||||
user = Users.get_user_by_email(mail)
|
user = Users.get_user_by_email(email)
|
||||||
if not user:
|
if not user:
|
||||||
try:
|
try:
|
||||||
user_count = Users.get_num_users()
|
user_count = Users.get_num_users()
|
||||||
if (
|
|
||||||
request.app.state.USER_COUNT
|
|
||||||
and user_count >= request.app.state.USER_COUNT
|
|
||||||
):
|
|
||||||
raise HTTPException(
|
|
||||||
status.HTTP_403_FORBIDDEN,
|
|
||||||
detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
|
|
||||||
)
|
|
||||||
|
|
||||||
role = (
|
role = (
|
||||||
"admin"
|
"admin"
|
||||||
@@ -268,7 +262,10 @@ async def ldap_auth(request: Request, response: Response, form_data: LdapForm):
|
|||||||
)
|
)
|
||||||
|
|
||||||
user = Auths.insert_new_auth(
|
user = Auths.insert_new_auth(
|
||||||
email=mail, password=str(uuid.uuid4()), name=cn, role=role
|
email=email,
|
||||||
|
password=str(uuid.uuid4()),
|
||||||
|
name=cn,
|
||||||
|
role=role,
|
||||||
)
|
)
|
||||||
|
|
||||||
if not user:
|
if not user:
|
||||||
@@ -281,7 +278,7 @@ async def ldap_auth(request: Request, response: Response, form_data: LdapForm):
|
|||||||
except Exception as err:
|
except Exception as err:
|
||||||
raise HTTPException(500, detail=ERROR_MESSAGES.DEFAULT(err))
|
raise HTTPException(500, detail=ERROR_MESSAGES.DEFAULT(err))
|
||||||
|
|
||||||
user = Auths.authenticate_user_by_trusted_header(mail)
|
user = Auths.authenticate_user_by_trusted_header(email)
|
||||||
|
|
||||||
if user:
|
if user:
|
||||||
token = create_token(
|
token = create_token(
|
||||||
@@ -439,11 +436,6 @@ async def signup(request: Request, response: Response, form_data: SignupForm):
|
|||||||
)
|
)
|
||||||
|
|
||||||
user_count = Users.get_num_users()
|
user_count = Users.get_num_users()
|
||||||
if request.app.state.USER_COUNT and user_count >= request.app.state.USER_COUNT:
|
|
||||||
raise HTTPException(
|
|
||||||
status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.ACCESS_PROHIBITED
|
|
||||||
)
|
|
||||||
|
|
||||||
if not validate_email_format(form_data.email.lower()):
|
if not validate_email_format(form_data.email.lower()):
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.INVALID_EMAIL_FORMAT
|
status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.INVALID_EMAIL_FORMAT
|
||||||
@@ -613,7 +605,7 @@ async def get_admin_details(request: Request, user=Depends(get_current_user)):
|
|||||||
admin_email = request.app.state.config.ADMIN_EMAIL
|
admin_email = request.app.state.config.ADMIN_EMAIL
|
||||||
admin_name = None
|
admin_name = None
|
||||||
|
|
||||||
print(admin_email, admin_name)
|
log.info(f"Admin details - Email: {admin_email}, Name: {admin_name}")
|
||||||
|
|
||||||
if admin_email:
|
if admin_email:
|
||||||
admin = Users.get_user_by_email(admin_email)
|
admin = Users.get_user_by_email(admin_email)
|
||||||
|
|||||||
@@ -70,6 +70,7 @@ async def set_direct_connections_config(
|
|||||||
# CodeInterpreterConfig
|
# CodeInterpreterConfig
|
||||||
############################
|
############################
|
||||||
class CodeInterpreterConfigForm(BaseModel):
|
class CodeInterpreterConfigForm(BaseModel):
|
||||||
|
ENABLE_CODE_EXECUTION: bool
|
||||||
CODE_EXECUTION_ENGINE: str
|
CODE_EXECUTION_ENGINE: str
|
||||||
CODE_EXECUTION_JUPYTER_URL: Optional[str]
|
CODE_EXECUTION_JUPYTER_URL: Optional[str]
|
||||||
CODE_EXECUTION_JUPYTER_AUTH: Optional[str]
|
CODE_EXECUTION_JUPYTER_AUTH: Optional[str]
|
||||||
@@ -89,6 +90,7 @@ class CodeInterpreterConfigForm(BaseModel):
|
|||||||
@router.get("/code_execution", response_model=CodeInterpreterConfigForm)
|
@router.get("/code_execution", response_model=CodeInterpreterConfigForm)
|
||||||
async def get_code_execution_config(request: Request, user=Depends(get_admin_user)):
|
async def get_code_execution_config(request: Request, user=Depends(get_admin_user)):
|
||||||
return {
|
return {
|
||||||
|
"ENABLE_CODE_EXECUTION": request.app.state.config.ENABLE_CODE_EXECUTION,
|
||||||
"CODE_EXECUTION_ENGINE": request.app.state.config.CODE_EXECUTION_ENGINE,
|
"CODE_EXECUTION_ENGINE": request.app.state.config.CODE_EXECUTION_ENGINE,
|
||||||
"CODE_EXECUTION_JUPYTER_URL": request.app.state.config.CODE_EXECUTION_JUPYTER_URL,
|
"CODE_EXECUTION_JUPYTER_URL": request.app.state.config.CODE_EXECUTION_JUPYTER_URL,
|
||||||
"CODE_EXECUTION_JUPYTER_AUTH": request.app.state.config.CODE_EXECUTION_JUPYTER_AUTH,
|
"CODE_EXECUTION_JUPYTER_AUTH": request.app.state.config.CODE_EXECUTION_JUPYTER_AUTH,
|
||||||
@@ -111,6 +113,8 @@ async def set_code_execution_config(
|
|||||||
request: Request, form_data: CodeInterpreterConfigForm, user=Depends(get_admin_user)
|
request: Request, form_data: CodeInterpreterConfigForm, user=Depends(get_admin_user)
|
||||||
):
|
):
|
||||||
|
|
||||||
|
request.app.state.config.ENABLE_CODE_EXECUTION = form_data.ENABLE_CODE_EXECUTION
|
||||||
|
|
||||||
request.app.state.config.CODE_EXECUTION_ENGINE = form_data.CODE_EXECUTION_ENGINE
|
request.app.state.config.CODE_EXECUTION_ENGINE = form_data.CODE_EXECUTION_ENGINE
|
||||||
request.app.state.config.CODE_EXECUTION_JUPYTER_URL = (
|
request.app.state.config.CODE_EXECUTION_JUPYTER_URL = (
|
||||||
form_data.CODE_EXECUTION_JUPYTER_URL
|
form_data.CODE_EXECUTION_JUPYTER_URL
|
||||||
@@ -153,6 +157,7 @@ async def set_code_execution_config(
|
|||||||
)
|
)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
"ENABLE_CODE_EXECUTION": request.app.state.config.ENABLE_CODE_EXECUTION,
|
||||||
"CODE_EXECUTION_ENGINE": request.app.state.config.CODE_EXECUTION_ENGINE,
|
"CODE_EXECUTION_ENGINE": request.app.state.config.CODE_EXECUTION_ENGINE,
|
||||||
"CODE_EXECUTION_JUPYTER_URL": request.app.state.config.CODE_EXECUTION_JUPYTER_URL,
|
"CODE_EXECUTION_JUPYTER_URL": request.app.state.config.CODE_EXECUTION_JUPYTER_URL,
|
||||||
"CODE_EXECUTION_JUPYTER_AUTH": request.app.state.config.CODE_EXECUTION_JUPYTER_AUTH,
|
"CODE_EXECUTION_JUPYTER_AUTH": request.app.state.config.CODE_EXECUTION_JUPYTER_AUTH,
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ from open_webui.models.files import (
|
|||||||
Files,
|
Files,
|
||||||
)
|
)
|
||||||
from open_webui.routers.retrieval import ProcessFileForm, process_file
|
from open_webui.routers.retrieval import ProcessFileForm, process_file
|
||||||
|
from open_webui.routers.audio import transcribe
|
||||||
from open_webui.storage.provider import Storage
|
from open_webui.storage.provider import Storage
|
||||||
from open_webui.utils.auth import get_admin_user, get_verified_user
|
from open_webui.utils.auth import get_admin_user, get_verified_user
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
@@ -67,7 +68,22 @@ def upload_file(
|
|||||||
)
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
process_file(request, ProcessFileForm(file_id=id), user=user)
|
if file.content_type in [
|
||||||
|
"audio/mpeg",
|
||||||
|
"audio/wav",
|
||||||
|
"audio/ogg",
|
||||||
|
"audio/x-m4a",
|
||||||
|
]:
|
||||||
|
file_path = Storage.get_file(file_path)
|
||||||
|
result = transcribe(request, file_path)
|
||||||
|
process_file(
|
||||||
|
request,
|
||||||
|
ProcessFileForm(file_id=id, content=result.get("text", "")),
|
||||||
|
user=user,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
process_file(request, ProcessFileForm(file_id=id), user=user)
|
||||||
|
|
||||||
file_item = Files.get_file_by_id(id=id)
|
file_item = Files.get_file_by_id(id=id)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.exception(e)
|
log.exception(e)
|
||||||
@@ -273,7 +289,7 @@ async def get_html_file_content_by_id(id: str, user=Depends(get_verified_user)):
|
|||||||
|
|
||||||
# Check if the file already exists in the cache
|
# Check if the file already exists in the cache
|
||||||
if file_path.is_file():
|
if file_path.is_file():
|
||||||
print(f"file_path: {file_path}")
|
log.info(f"file_path: {file_path}")
|
||||||
return FileResponse(file_path)
|
return FileResponse(file_path)
|
||||||
else:
|
else:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import os
|
import os
|
||||||
|
import logging
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
@@ -13,6 +14,11 @@ from open_webui.config import CACHE_DIR
|
|||||||
from open_webui.constants import ERROR_MESSAGES
|
from open_webui.constants import ERROR_MESSAGES
|
||||||
from fastapi import APIRouter, Depends, HTTPException, Request, status
|
from fastapi import APIRouter, Depends, HTTPException, Request, status
|
||||||
from open_webui.utils.auth import get_admin_user, get_verified_user
|
from open_webui.utils.auth import get_admin_user, get_verified_user
|
||||||
|
from open_webui.env import SRC_LOG_LEVELS
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
log.setLevel(SRC_LOG_LEVELS["MAIN"])
|
||||||
|
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
@@ -68,7 +74,7 @@ async def create_new_function(
|
|||||||
|
|
||||||
function = Functions.insert_new_function(user.id, function_type, form_data)
|
function = Functions.insert_new_function(user.id, function_type, form_data)
|
||||||
|
|
||||||
function_cache_dir = Path(CACHE_DIR) / "functions" / form_data.id
|
function_cache_dir = CACHE_DIR / "functions" / form_data.id
|
||||||
function_cache_dir.mkdir(parents=True, exist_ok=True)
|
function_cache_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
if function:
|
if function:
|
||||||
@@ -79,7 +85,7 @@ async def create_new_function(
|
|||||||
detail=ERROR_MESSAGES.DEFAULT("Error creating function"),
|
detail=ERROR_MESSAGES.DEFAULT("Error creating function"),
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(e)
|
log.exception(f"Failed to create a new function: {e}")
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_400_BAD_REQUEST,
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
detail=ERROR_MESSAGES.DEFAULT(e),
|
detail=ERROR_MESSAGES.DEFAULT(e),
|
||||||
@@ -183,7 +189,7 @@ async def update_function_by_id(
|
|||||||
FUNCTIONS[id] = function_module
|
FUNCTIONS[id] = function_module
|
||||||
|
|
||||||
updated = {**form_data.model_dump(exclude={"id"}), "type": function_type}
|
updated = {**form_data.model_dump(exclude={"id"}), "type": function_type}
|
||||||
print(updated)
|
log.debug(updated)
|
||||||
|
|
||||||
function = Functions.update_function_by_id(id, updated)
|
function = Functions.update_function_by_id(id, updated)
|
||||||
|
|
||||||
@@ -299,7 +305,7 @@ async def update_function_valves_by_id(
|
|||||||
Functions.update_function_valves_by_id(id, valves.model_dump())
|
Functions.update_function_valves_by_id(id, valves.model_dump())
|
||||||
return valves.model_dump()
|
return valves.model_dump()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(e)
|
log.exception(f"Error updating function values by id {id}: {e}")
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_400_BAD_REQUEST,
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
detail=ERROR_MESSAGES.DEFAULT(e),
|
detail=ERROR_MESSAGES.DEFAULT(e),
|
||||||
@@ -388,7 +394,7 @@ async def update_function_user_valves_by_id(
|
|||||||
)
|
)
|
||||||
return user_valves.model_dump()
|
return user_valves.model_dump()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(e)
|
log.exception(f"Error updating function user valves by id {id}: {e}")
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_400_BAD_REQUEST,
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
detail=ERROR_MESSAGES.DEFAULT(e),
|
detail=ERROR_MESSAGES.DEFAULT(e),
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import os
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
import logging
|
||||||
|
|
||||||
from open_webui.models.users import Users
|
from open_webui.models.users import Users
|
||||||
from open_webui.models.groups import (
|
from open_webui.models.groups import (
|
||||||
@@ -14,7 +14,13 @@ from open_webui.models.groups import (
|
|||||||
from open_webui.config import CACHE_DIR
|
from open_webui.config import CACHE_DIR
|
||||||
from open_webui.constants import ERROR_MESSAGES
|
from open_webui.constants import ERROR_MESSAGES
|
||||||
from fastapi import APIRouter, Depends, HTTPException, Request, status
|
from fastapi import APIRouter, Depends, HTTPException, Request, status
|
||||||
|
|
||||||
from open_webui.utils.auth import get_admin_user, get_verified_user
|
from open_webui.utils.auth import get_admin_user, get_verified_user
|
||||||
|
from open_webui.env import SRC_LOG_LEVELS
|
||||||
|
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
log.setLevel(SRC_LOG_LEVELS["MAIN"])
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
@@ -37,7 +43,7 @@ async def get_groups(user=Depends(get_verified_user)):
|
|||||||
|
|
||||||
|
|
||||||
@router.post("/create", response_model=Optional[GroupResponse])
|
@router.post("/create", response_model=Optional[GroupResponse])
|
||||||
async def create_new_function(form_data: GroupForm, user=Depends(get_admin_user)):
|
async def create_new_group(form_data: GroupForm, user=Depends(get_admin_user)):
|
||||||
try:
|
try:
|
||||||
group = Groups.insert_new_group(user.id, form_data)
|
group = Groups.insert_new_group(user.id, form_data)
|
||||||
if group:
|
if group:
|
||||||
@@ -48,7 +54,7 @@ async def create_new_function(form_data: GroupForm, user=Depends(get_admin_user)
|
|||||||
detail=ERROR_MESSAGES.DEFAULT("Error creating group"),
|
detail=ERROR_MESSAGES.DEFAULT("Error creating group"),
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(e)
|
log.exception(f"Error creating a new group: {e}")
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_400_BAD_REQUEST,
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
detail=ERROR_MESSAGES.DEFAULT(e),
|
detail=ERROR_MESSAGES.DEFAULT(e),
|
||||||
@@ -94,7 +100,7 @@ async def update_group_by_id(
|
|||||||
detail=ERROR_MESSAGES.DEFAULT("Error updating group"),
|
detail=ERROR_MESSAGES.DEFAULT("Error updating group"),
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(e)
|
log.exception(f"Error updating group {id}: {e}")
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_400_BAD_REQUEST,
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
detail=ERROR_MESSAGES.DEFAULT(e),
|
detail=ERROR_MESSAGES.DEFAULT(e),
|
||||||
@@ -118,7 +124,7 @@ async def delete_group_by_id(id: str, user=Depends(get_admin_user)):
|
|||||||
detail=ERROR_MESSAGES.DEFAULT("Error deleting group"),
|
detail=ERROR_MESSAGES.DEFAULT("Error deleting group"),
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(e)
|
log.exception(f"Error deleting group {id}: {e}")
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_400_BAD_REQUEST,
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
detail=ERROR_MESSAGES.DEFAULT(e),
|
detail=ERROR_MESSAGES.DEFAULT(e),
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ from pydantic import BaseModel
|
|||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
log.setLevel(SRC_LOG_LEVELS["IMAGES"])
|
log.setLevel(SRC_LOG_LEVELS["IMAGES"])
|
||||||
|
|
||||||
IMAGE_CACHE_DIR = Path(CACHE_DIR).joinpath("./image/generations/")
|
IMAGE_CACHE_DIR = CACHE_DIR / "image" / "generations"
|
||||||
IMAGE_CACHE_DIR.mkdir(parents=True, exist_ok=True)
|
IMAGE_CACHE_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
|
||||||
@@ -144,6 +144,8 @@ async def update_config(
|
|||||||
request.app.state.config.COMFYUI_BASE_URL = (
|
request.app.state.config.COMFYUI_BASE_URL = (
|
||||||
form_data.comfyui.COMFYUI_BASE_URL.strip("/")
|
form_data.comfyui.COMFYUI_BASE_URL.strip("/")
|
||||||
)
|
)
|
||||||
|
request.app.state.config.COMFYUI_API_KEY = form_data.comfyui.COMFYUI_API_KEY
|
||||||
|
|
||||||
request.app.state.config.COMFYUI_WORKFLOW = form_data.comfyui.COMFYUI_WORKFLOW
|
request.app.state.config.COMFYUI_WORKFLOW = form_data.comfyui.COMFYUI_WORKFLOW
|
||||||
request.app.state.config.COMFYUI_WORKFLOW_NODES = (
|
request.app.state.config.COMFYUI_WORKFLOW_NODES = (
|
||||||
form_data.comfyui.COMFYUI_WORKFLOW_NODES
|
form_data.comfyui.COMFYUI_WORKFLOW_NODES
|
||||||
@@ -203,9 +205,17 @@ async def verify_url(request: Request, user=Depends(get_admin_user)):
|
|||||||
request.app.state.config.ENABLE_IMAGE_GENERATION = False
|
request.app.state.config.ENABLE_IMAGE_GENERATION = False
|
||||||
raise HTTPException(status_code=400, detail=ERROR_MESSAGES.INVALID_URL)
|
raise HTTPException(status_code=400, detail=ERROR_MESSAGES.INVALID_URL)
|
||||||
elif request.app.state.config.IMAGE_GENERATION_ENGINE == "comfyui":
|
elif request.app.state.config.IMAGE_GENERATION_ENGINE == "comfyui":
|
||||||
|
|
||||||
|
headers = None
|
||||||
|
if request.app.state.config.COMFYUI_API_KEY:
|
||||||
|
headers = {
|
||||||
|
"Authorization": f"Bearer {request.app.state.config.COMFYUI_API_KEY}"
|
||||||
|
}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
r = requests.get(
|
r = requests.get(
|
||||||
url=f"{request.app.state.config.COMFYUI_BASE_URL}/object_info"
|
url=f"{request.app.state.config.COMFYUI_BASE_URL}/object_info",
|
||||||
|
headers=headers,
|
||||||
)
|
)
|
||||||
r.raise_for_status()
|
r.raise_for_status()
|
||||||
return True
|
return True
|
||||||
@@ -351,7 +361,7 @@ def get_models(request: Request, user=Depends(get_verified_user)):
|
|||||||
if model_node_id:
|
if model_node_id:
|
||||||
model_list_key = None
|
model_list_key = None
|
||||||
|
|
||||||
print(workflow[model_node_id]["class_type"])
|
log.info(workflow[model_node_id]["class_type"])
|
||||||
for key in info[workflow[model_node_id]["class_type"]]["input"][
|
for key in info[workflow[model_node_id]["class_type"]]["input"][
|
||||||
"required"
|
"required"
|
||||||
]:
|
]:
|
||||||
@@ -507,7 +517,13 @@ async def image_generations(
|
|||||||
images = []
|
images = []
|
||||||
|
|
||||||
for image in res["data"]:
|
for image in res["data"]:
|
||||||
image_data, content_type = load_b64_image_data(image["b64_json"])
|
if "url" in image:
|
||||||
|
image_data, content_type = load_url_image_data(
|
||||||
|
image["url"], headers
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
image_data, content_type = load_b64_image_data(image["b64_json"])
|
||||||
|
|
||||||
url = upload_image(request, data, image_data, content_type, user)
|
url = upload_image(request, data, image_data, content_type, user)
|
||||||
images.append({"url": url})
|
images.append({"url": url})
|
||||||
return images
|
return images
|
||||||
|
|||||||
@@ -614,7 +614,7 @@ def add_files_to_knowledge_batch(
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Get files content
|
# Get files content
|
||||||
print(f"files/batch/add - {len(form_data)} files")
|
log.info(f"files/batch/add - {len(form_data)} files")
|
||||||
files: List[FileModel] = []
|
files: List[FileModel] = []
|
||||||
for form in form_data:
|
for form in form_data:
|
||||||
file = Files.get_file_by_id(form.file_id)
|
file = Files.get_file_by_id(form.file_id)
|
||||||
|
|||||||
@@ -14,6 +14,11 @@ from urllib.parse import urlparse
|
|||||||
import aiohttp
|
import aiohttp
|
||||||
from aiocache import cached
|
from aiocache import cached
|
||||||
import requests
|
import requests
|
||||||
|
from open_webui.models.users import UserModel
|
||||||
|
|
||||||
|
from open_webui.env import (
|
||||||
|
ENABLE_FORWARD_USER_INFO_HEADERS,
|
||||||
|
)
|
||||||
|
|
||||||
from fastapi import (
|
from fastapi import (
|
||||||
Depends,
|
Depends,
|
||||||
@@ -50,7 +55,7 @@ from open_webui.env import (
|
|||||||
ENV,
|
ENV,
|
||||||
SRC_LOG_LEVELS,
|
SRC_LOG_LEVELS,
|
||||||
AIOHTTP_CLIENT_TIMEOUT,
|
AIOHTTP_CLIENT_TIMEOUT,
|
||||||
AIOHTTP_CLIENT_TIMEOUT_OPENAI_MODEL_LIST,
|
AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST,
|
||||||
BYPASS_MODEL_ACCESS_CONTROL,
|
BYPASS_MODEL_ACCESS_CONTROL,
|
||||||
)
|
)
|
||||||
from open_webui.constants import ERROR_MESSAGES
|
from open_webui.constants import ERROR_MESSAGES
|
||||||
@@ -66,12 +71,26 @@ log.setLevel(SRC_LOG_LEVELS["OLLAMA"])
|
|||||||
##########################################
|
##########################################
|
||||||
|
|
||||||
|
|
||||||
async def send_get_request(url, key=None):
|
async def send_get_request(url, key=None, user: UserModel = None):
|
||||||
timeout = aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT_OPENAI_MODEL_LIST)
|
timeout = aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST)
|
||||||
try:
|
try:
|
||||||
async with aiohttp.ClientSession(timeout=timeout, trust_env=True) as session:
|
async with aiohttp.ClientSession(timeout=timeout, trust_env=True) as session:
|
||||||
async with session.get(
|
async with session.get(
|
||||||
url, headers={**({"Authorization": f"Bearer {key}"} if key else {})}
|
url,
|
||||||
|
headers={
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
**({"Authorization": f"Bearer {key}"} if key else {}),
|
||||||
|
**(
|
||||||
|
{
|
||||||
|
"X-OpenWebUI-User-Name": user.name,
|
||||||
|
"X-OpenWebUI-User-Id": user.id,
|
||||||
|
"X-OpenWebUI-User-Email": user.email,
|
||||||
|
"X-OpenWebUI-User-Role": user.role,
|
||||||
|
}
|
||||||
|
if ENABLE_FORWARD_USER_INFO_HEADERS and user
|
||||||
|
else {}
|
||||||
|
),
|
||||||
|
},
|
||||||
) as response:
|
) as response:
|
||||||
return await response.json()
|
return await response.json()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -96,6 +115,7 @@ async def send_post_request(
|
|||||||
stream: bool = True,
|
stream: bool = True,
|
||||||
key: Optional[str] = None,
|
key: Optional[str] = None,
|
||||||
content_type: Optional[str] = None,
|
content_type: Optional[str] = None,
|
||||||
|
user: UserModel = None,
|
||||||
):
|
):
|
||||||
|
|
||||||
r = None
|
r = None
|
||||||
@@ -110,6 +130,16 @@ async def send_post_request(
|
|||||||
headers={
|
headers={
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
**({"Authorization": f"Bearer {key}"} if key else {}),
|
**({"Authorization": f"Bearer {key}"} if key else {}),
|
||||||
|
**(
|
||||||
|
{
|
||||||
|
"X-OpenWebUI-User-Name": user.name,
|
||||||
|
"X-OpenWebUI-User-Id": user.id,
|
||||||
|
"X-OpenWebUI-User-Email": user.email,
|
||||||
|
"X-OpenWebUI-User-Role": user.role,
|
||||||
|
}
|
||||||
|
if ENABLE_FORWARD_USER_INFO_HEADERS and user
|
||||||
|
else {}
|
||||||
|
),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
r.raise_for_status()
|
r.raise_for_status()
|
||||||
@@ -186,12 +216,24 @@ async def verify_connection(
|
|||||||
key = form_data.key
|
key = form_data.key
|
||||||
|
|
||||||
async with aiohttp.ClientSession(
|
async with aiohttp.ClientSession(
|
||||||
timeout=aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT_OPENAI_MODEL_LIST)
|
timeout=aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST)
|
||||||
) as session:
|
) as session:
|
||||||
try:
|
try:
|
||||||
async with session.get(
|
async with session.get(
|
||||||
f"{url}/api/version",
|
f"{url}/api/version",
|
||||||
headers={**({"Authorization": f"Bearer {key}"} if key else {})},
|
headers={
|
||||||
|
**({"Authorization": f"Bearer {key}"} if key else {}),
|
||||||
|
**(
|
||||||
|
{
|
||||||
|
"X-OpenWebUI-User-Name": user.name,
|
||||||
|
"X-OpenWebUI-User-Id": user.id,
|
||||||
|
"X-OpenWebUI-User-Email": user.email,
|
||||||
|
"X-OpenWebUI-User-Role": user.role,
|
||||||
|
}
|
||||||
|
if ENABLE_FORWARD_USER_INFO_HEADERS and user
|
||||||
|
else {}
|
||||||
|
),
|
||||||
|
},
|
||||||
) as r:
|
) as r:
|
||||||
if r.status != 200:
|
if r.status != 200:
|
||||||
detail = f"HTTP Error: {r.status}"
|
detail = f"HTTP Error: {r.status}"
|
||||||
@@ -254,7 +296,7 @@ async def update_config(
|
|||||||
|
|
||||||
|
|
||||||
@cached(ttl=3)
|
@cached(ttl=3)
|
||||||
async def get_all_models(request: Request):
|
async def get_all_models(request: Request, user: UserModel = None):
|
||||||
log.info("get_all_models()")
|
log.info("get_all_models()")
|
||||||
if request.app.state.config.ENABLE_OLLAMA_API:
|
if request.app.state.config.ENABLE_OLLAMA_API:
|
||||||
request_tasks = []
|
request_tasks = []
|
||||||
@@ -262,7 +304,7 @@ async def get_all_models(request: Request):
|
|||||||
if (str(idx) not in request.app.state.config.OLLAMA_API_CONFIGS) and (
|
if (str(idx) not in request.app.state.config.OLLAMA_API_CONFIGS) and (
|
||||||
url not in request.app.state.config.OLLAMA_API_CONFIGS # Legacy support
|
url not in request.app.state.config.OLLAMA_API_CONFIGS # Legacy support
|
||||||
):
|
):
|
||||||
request_tasks.append(send_get_request(f"{url}/api/tags"))
|
request_tasks.append(send_get_request(f"{url}/api/tags", user=user))
|
||||||
else:
|
else:
|
||||||
api_config = request.app.state.config.OLLAMA_API_CONFIGS.get(
|
api_config = request.app.state.config.OLLAMA_API_CONFIGS.get(
|
||||||
str(idx),
|
str(idx),
|
||||||
@@ -275,7 +317,9 @@ async def get_all_models(request: Request):
|
|||||||
key = api_config.get("key", None)
|
key = api_config.get("key", None)
|
||||||
|
|
||||||
if enable:
|
if enable:
|
||||||
request_tasks.append(send_get_request(f"{url}/api/tags", key))
|
request_tasks.append(
|
||||||
|
send_get_request(f"{url}/api/tags", key, user=user)
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
request_tasks.append(asyncio.ensure_future(asyncio.sleep(0, None)))
|
request_tasks.append(asyncio.ensure_future(asyncio.sleep(0, None)))
|
||||||
|
|
||||||
@@ -360,7 +404,7 @@ async def get_ollama_tags(
|
|||||||
models = []
|
models = []
|
||||||
|
|
||||||
if url_idx is None:
|
if url_idx is None:
|
||||||
models = await get_all_models(request)
|
models = await get_all_models(request, user=user)
|
||||||
else:
|
else:
|
||||||
url = request.app.state.config.OLLAMA_BASE_URLS[url_idx]
|
url = request.app.state.config.OLLAMA_BASE_URLS[url_idx]
|
||||||
key = get_api_key(url_idx, url, request.app.state.config.OLLAMA_API_CONFIGS)
|
key = get_api_key(url_idx, url, request.app.state.config.OLLAMA_API_CONFIGS)
|
||||||
@@ -370,7 +414,19 @@ async def get_ollama_tags(
|
|||||||
r = requests.request(
|
r = requests.request(
|
||||||
method="GET",
|
method="GET",
|
||||||
url=f"{url}/api/tags",
|
url=f"{url}/api/tags",
|
||||||
headers={**({"Authorization": f"Bearer {key}"} if key else {})},
|
headers={
|
||||||
|
**({"Authorization": f"Bearer {key}"} if key else {}),
|
||||||
|
**(
|
||||||
|
{
|
||||||
|
"X-OpenWebUI-User-Name": user.name,
|
||||||
|
"X-OpenWebUI-User-Id": user.id,
|
||||||
|
"X-OpenWebUI-User-Email": user.email,
|
||||||
|
"X-OpenWebUI-User-Role": user.role,
|
||||||
|
}
|
||||||
|
if ENABLE_FORWARD_USER_INFO_HEADERS and user
|
||||||
|
else {}
|
||||||
|
),
|
||||||
|
},
|
||||||
)
|
)
|
||||||
r.raise_for_status()
|
r.raise_for_status()
|
||||||
|
|
||||||
@@ -477,6 +533,7 @@ async def get_ollama_loaded_models(request: Request, user=Depends(get_verified_u
|
|||||||
url, {}
|
url, {}
|
||||||
), # Legacy support
|
), # Legacy support
|
||||||
).get("key", None),
|
).get("key", None),
|
||||||
|
user=user,
|
||||||
)
|
)
|
||||||
for idx, url in enumerate(request.app.state.config.OLLAMA_BASE_URLS)
|
for idx, url in enumerate(request.app.state.config.OLLAMA_BASE_URLS)
|
||||||
]
|
]
|
||||||
@@ -509,6 +566,7 @@ async def pull_model(
|
|||||||
url=f"{url}/api/pull",
|
url=f"{url}/api/pull",
|
||||||
payload=json.dumps(payload),
|
payload=json.dumps(payload),
|
||||||
key=get_api_key(url_idx, url, request.app.state.config.OLLAMA_API_CONFIGS),
|
key=get_api_key(url_idx, url, request.app.state.config.OLLAMA_API_CONFIGS),
|
||||||
|
user=user,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -527,7 +585,7 @@ async def push_model(
|
|||||||
user=Depends(get_admin_user),
|
user=Depends(get_admin_user),
|
||||||
):
|
):
|
||||||
if url_idx is None:
|
if url_idx is None:
|
||||||
await get_all_models(request)
|
await get_all_models(request, user=user)
|
||||||
models = request.app.state.OLLAMA_MODELS
|
models = request.app.state.OLLAMA_MODELS
|
||||||
|
|
||||||
if form_data.name in models:
|
if form_data.name in models:
|
||||||
@@ -545,6 +603,7 @@ async def push_model(
|
|||||||
url=f"{url}/api/push",
|
url=f"{url}/api/push",
|
||||||
payload=form_data.model_dump_json(exclude_none=True).encode(),
|
payload=form_data.model_dump_json(exclude_none=True).encode(),
|
||||||
key=get_api_key(url_idx, url, request.app.state.config.OLLAMA_API_CONFIGS),
|
key=get_api_key(url_idx, url, request.app.state.config.OLLAMA_API_CONFIGS),
|
||||||
|
user=user,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -571,6 +630,7 @@ async def create_model(
|
|||||||
url=f"{url}/api/create",
|
url=f"{url}/api/create",
|
||||||
payload=form_data.model_dump_json(exclude_none=True).encode(),
|
payload=form_data.model_dump_json(exclude_none=True).encode(),
|
||||||
key=get_api_key(url_idx, url, request.app.state.config.OLLAMA_API_CONFIGS),
|
key=get_api_key(url_idx, url, request.app.state.config.OLLAMA_API_CONFIGS),
|
||||||
|
user=user,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -588,7 +648,7 @@ async def copy_model(
|
|||||||
user=Depends(get_admin_user),
|
user=Depends(get_admin_user),
|
||||||
):
|
):
|
||||||
if url_idx is None:
|
if url_idx is None:
|
||||||
await get_all_models(request)
|
await get_all_models(request, user=user)
|
||||||
models = request.app.state.OLLAMA_MODELS
|
models = request.app.state.OLLAMA_MODELS
|
||||||
|
|
||||||
if form_data.source in models:
|
if form_data.source in models:
|
||||||
@@ -609,6 +669,16 @@ async def copy_model(
|
|||||||
headers={
|
headers={
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
**({"Authorization": f"Bearer {key}"} if key else {}),
|
**({"Authorization": f"Bearer {key}"} if key else {}),
|
||||||
|
**(
|
||||||
|
{
|
||||||
|
"X-OpenWebUI-User-Name": user.name,
|
||||||
|
"X-OpenWebUI-User-Id": user.id,
|
||||||
|
"X-OpenWebUI-User-Email": user.email,
|
||||||
|
"X-OpenWebUI-User-Role": user.role,
|
||||||
|
}
|
||||||
|
if ENABLE_FORWARD_USER_INFO_HEADERS and user
|
||||||
|
else {}
|
||||||
|
),
|
||||||
},
|
},
|
||||||
data=form_data.model_dump_json(exclude_none=True).encode(),
|
data=form_data.model_dump_json(exclude_none=True).encode(),
|
||||||
)
|
)
|
||||||
@@ -643,7 +713,7 @@ async def delete_model(
|
|||||||
user=Depends(get_admin_user),
|
user=Depends(get_admin_user),
|
||||||
):
|
):
|
||||||
if url_idx is None:
|
if url_idx is None:
|
||||||
await get_all_models(request)
|
await get_all_models(request, user=user)
|
||||||
models = request.app.state.OLLAMA_MODELS
|
models = request.app.state.OLLAMA_MODELS
|
||||||
|
|
||||||
if form_data.name in models:
|
if form_data.name in models:
|
||||||
@@ -665,6 +735,16 @@ async def delete_model(
|
|||||||
headers={
|
headers={
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
**({"Authorization": f"Bearer {key}"} if key else {}),
|
**({"Authorization": f"Bearer {key}"} if key else {}),
|
||||||
|
**(
|
||||||
|
{
|
||||||
|
"X-OpenWebUI-User-Name": user.name,
|
||||||
|
"X-OpenWebUI-User-Id": user.id,
|
||||||
|
"X-OpenWebUI-User-Email": user.email,
|
||||||
|
"X-OpenWebUI-User-Role": user.role,
|
||||||
|
}
|
||||||
|
if ENABLE_FORWARD_USER_INFO_HEADERS and user
|
||||||
|
else {}
|
||||||
|
),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
r.raise_for_status()
|
r.raise_for_status()
|
||||||
@@ -693,7 +773,7 @@ async def delete_model(
|
|||||||
async def show_model_info(
|
async def show_model_info(
|
||||||
request: Request, form_data: ModelNameForm, user=Depends(get_verified_user)
|
request: Request, form_data: ModelNameForm, user=Depends(get_verified_user)
|
||||||
):
|
):
|
||||||
await get_all_models(request)
|
await get_all_models(request, user=user)
|
||||||
models = request.app.state.OLLAMA_MODELS
|
models = request.app.state.OLLAMA_MODELS
|
||||||
|
|
||||||
if form_data.name not in models:
|
if form_data.name not in models:
|
||||||
@@ -714,6 +794,16 @@ async def show_model_info(
|
|||||||
headers={
|
headers={
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
**({"Authorization": f"Bearer {key}"} if key else {}),
|
**({"Authorization": f"Bearer {key}"} if key else {}),
|
||||||
|
**(
|
||||||
|
{
|
||||||
|
"X-OpenWebUI-User-Name": user.name,
|
||||||
|
"X-OpenWebUI-User-Id": user.id,
|
||||||
|
"X-OpenWebUI-User-Email": user.email,
|
||||||
|
"X-OpenWebUI-User-Role": user.role,
|
||||||
|
}
|
||||||
|
if ENABLE_FORWARD_USER_INFO_HEADERS and user
|
||||||
|
else {}
|
||||||
|
),
|
||||||
},
|
},
|
||||||
data=form_data.model_dump_json(exclude_none=True).encode(),
|
data=form_data.model_dump_json(exclude_none=True).encode(),
|
||||||
)
|
)
|
||||||
@@ -757,7 +847,7 @@ async def embed(
|
|||||||
log.info(f"generate_ollama_batch_embeddings {form_data}")
|
log.info(f"generate_ollama_batch_embeddings {form_data}")
|
||||||
|
|
||||||
if url_idx is None:
|
if url_idx is None:
|
||||||
await get_all_models(request)
|
await get_all_models(request, user=user)
|
||||||
models = request.app.state.OLLAMA_MODELS
|
models = request.app.state.OLLAMA_MODELS
|
||||||
|
|
||||||
model = form_data.model
|
model = form_data.model
|
||||||
@@ -783,6 +873,16 @@ async def embed(
|
|||||||
headers={
|
headers={
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
**({"Authorization": f"Bearer {key}"} if key else {}),
|
**({"Authorization": f"Bearer {key}"} if key else {}),
|
||||||
|
**(
|
||||||
|
{
|
||||||
|
"X-OpenWebUI-User-Name": user.name,
|
||||||
|
"X-OpenWebUI-User-Id": user.id,
|
||||||
|
"X-OpenWebUI-User-Email": user.email,
|
||||||
|
"X-OpenWebUI-User-Role": user.role,
|
||||||
|
}
|
||||||
|
if ENABLE_FORWARD_USER_INFO_HEADERS and user
|
||||||
|
else {}
|
||||||
|
),
|
||||||
},
|
},
|
||||||
data=form_data.model_dump_json(exclude_none=True).encode(),
|
data=form_data.model_dump_json(exclude_none=True).encode(),
|
||||||
)
|
)
|
||||||
@@ -826,7 +926,7 @@ async def embeddings(
|
|||||||
log.info(f"generate_ollama_embeddings {form_data}")
|
log.info(f"generate_ollama_embeddings {form_data}")
|
||||||
|
|
||||||
if url_idx is None:
|
if url_idx is None:
|
||||||
await get_all_models(request)
|
await get_all_models(request, user=user)
|
||||||
models = request.app.state.OLLAMA_MODELS
|
models = request.app.state.OLLAMA_MODELS
|
||||||
|
|
||||||
model = form_data.model
|
model = form_data.model
|
||||||
@@ -852,6 +952,16 @@ async def embeddings(
|
|||||||
headers={
|
headers={
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
**({"Authorization": f"Bearer {key}"} if key else {}),
|
**({"Authorization": f"Bearer {key}"} if key else {}),
|
||||||
|
**(
|
||||||
|
{
|
||||||
|
"X-OpenWebUI-User-Name": user.name,
|
||||||
|
"X-OpenWebUI-User-Id": user.id,
|
||||||
|
"X-OpenWebUI-User-Email": user.email,
|
||||||
|
"X-OpenWebUI-User-Role": user.role,
|
||||||
|
}
|
||||||
|
if ENABLE_FORWARD_USER_INFO_HEADERS and user
|
||||||
|
else {}
|
||||||
|
),
|
||||||
},
|
},
|
||||||
data=form_data.model_dump_json(exclude_none=True).encode(),
|
data=form_data.model_dump_json(exclude_none=True).encode(),
|
||||||
)
|
)
|
||||||
@@ -901,7 +1011,7 @@ async def generate_completion(
|
|||||||
user=Depends(get_verified_user),
|
user=Depends(get_verified_user),
|
||||||
):
|
):
|
||||||
if url_idx is None:
|
if url_idx is None:
|
||||||
await get_all_models(request)
|
await get_all_models(request, user=user)
|
||||||
models = request.app.state.OLLAMA_MODELS
|
models = request.app.state.OLLAMA_MODELS
|
||||||
|
|
||||||
model = form_data.model
|
model = form_data.model
|
||||||
@@ -931,6 +1041,7 @@ async def generate_completion(
|
|||||||
url=f"{url}/api/generate",
|
url=f"{url}/api/generate",
|
||||||
payload=form_data.model_dump_json(exclude_none=True).encode(),
|
payload=form_data.model_dump_json(exclude_none=True).encode(),
|
||||||
key=get_api_key(url_idx, url, request.app.state.config.OLLAMA_API_CONFIGS),
|
key=get_api_key(url_idx, url, request.app.state.config.OLLAMA_API_CONFIGS),
|
||||||
|
user=user,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -1060,6 +1171,7 @@ async def generate_chat_completion(
|
|||||||
stream=form_data.stream,
|
stream=form_data.stream,
|
||||||
key=get_api_key(url_idx, url, request.app.state.config.OLLAMA_API_CONFIGS),
|
key=get_api_key(url_idx, url, request.app.state.config.OLLAMA_API_CONFIGS),
|
||||||
content_type="application/x-ndjson",
|
content_type="application/x-ndjson",
|
||||||
|
user=user,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -1162,6 +1274,7 @@ async def generate_openai_completion(
|
|||||||
payload=json.dumps(payload),
|
payload=json.dumps(payload),
|
||||||
stream=payload.get("stream", False),
|
stream=payload.get("stream", False),
|
||||||
key=get_api_key(url_idx, url, request.app.state.config.OLLAMA_API_CONFIGS),
|
key=get_api_key(url_idx, url, request.app.state.config.OLLAMA_API_CONFIGS),
|
||||||
|
user=user,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -1240,6 +1353,7 @@ async def generate_openai_chat_completion(
|
|||||||
payload=json.dumps(payload),
|
payload=json.dumps(payload),
|
||||||
stream=payload.get("stream", False),
|
stream=payload.get("stream", False),
|
||||||
key=get_api_key(url_idx, url, request.app.state.config.OLLAMA_API_CONFIGS),
|
key=get_api_key(url_idx, url, request.app.state.config.OLLAMA_API_CONFIGS),
|
||||||
|
user=user,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -1253,7 +1367,7 @@ async def get_openai_models(
|
|||||||
|
|
||||||
models = []
|
models = []
|
||||||
if url_idx is None:
|
if url_idx is None:
|
||||||
model_list = await get_all_models(request)
|
model_list = await get_all_models(request, user=user)
|
||||||
models = [
|
models = [
|
||||||
{
|
{
|
||||||
"id": model["model"],
|
"id": model["model"],
|
||||||
|
|||||||
@@ -22,10 +22,11 @@ from open_webui.config import (
|
|||||||
)
|
)
|
||||||
from open_webui.env import (
|
from open_webui.env import (
|
||||||
AIOHTTP_CLIENT_TIMEOUT,
|
AIOHTTP_CLIENT_TIMEOUT,
|
||||||
AIOHTTP_CLIENT_TIMEOUT_OPENAI_MODEL_LIST,
|
AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST,
|
||||||
ENABLE_FORWARD_USER_INFO_HEADERS,
|
ENABLE_FORWARD_USER_INFO_HEADERS,
|
||||||
BYPASS_MODEL_ACCESS_CONTROL,
|
BYPASS_MODEL_ACCESS_CONTROL,
|
||||||
)
|
)
|
||||||
|
from open_webui.models.users import UserModel
|
||||||
|
|
||||||
from open_webui.constants import ERROR_MESSAGES
|
from open_webui.constants import ERROR_MESSAGES
|
||||||
from open_webui.env import ENV, SRC_LOG_LEVELS
|
from open_webui.env import ENV, SRC_LOG_LEVELS
|
||||||
@@ -51,12 +52,25 @@ log.setLevel(SRC_LOG_LEVELS["OPENAI"])
|
|||||||
##########################################
|
##########################################
|
||||||
|
|
||||||
|
|
||||||
async def send_get_request(url, key=None):
|
async def send_get_request(url, key=None, user: UserModel = None):
|
||||||
timeout = aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT_OPENAI_MODEL_LIST)
|
timeout = aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST)
|
||||||
try:
|
try:
|
||||||
async with aiohttp.ClientSession(timeout=timeout, trust_env=True) as session:
|
async with aiohttp.ClientSession(timeout=timeout, trust_env=True) as session:
|
||||||
async with session.get(
|
async with session.get(
|
||||||
url, headers={**({"Authorization": f"Bearer {key}"} if key else {})}
|
url,
|
||||||
|
headers={
|
||||||
|
**({"Authorization": f"Bearer {key}"} if key else {}),
|
||||||
|
**(
|
||||||
|
{
|
||||||
|
"X-OpenWebUI-User-Name": user.name,
|
||||||
|
"X-OpenWebUI-User-Id": user.id,
|
||||||
|
"X-OpenWebUI-User-Email": user.email,
|
||||||
|
"X-OpenWebUI-User-Role": user.role,
|
||||||
|
}
|
||||||
|
if ENABLE_FORWARD_USER_INFO_HEADERS and user
|
||||||
|
else {}
|
||||||
|
),
|
||||||
|
},
|
||||||
) as response:
|
) as response:
|
||||||
return await response.json()
|
return await response.json()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -84,9 +98,15 @@ def openai_o1_o3_handler(payload):
|
|||||||
payload["max_completion_tokens"] = payload["max_tokens"]
|
payload["max_completion_tokens"] = payload["max_tokens"]
|
||||||
del payload["max_tokens"]
|
del payload["max_tokens"]
|
||||||
|
|
||||||
# Fix: O1 does not support the "system" parameter, Modify "system" to "user"
|
# Fix: o1 and o3 do not support the "system" role directly.
|
||||||
|
# For older models like "o1-mini" or "o1-preview", use role "user".
|
||||||
|
# For newer o1/o3 models, replace "system" with "developer".
|
||||||
if payload["messages"][0]["role"] == "system":
|
if payload["messages"][0]["role"] == "system":
|
||||||
payload["messages"][0]["role"] = "user"
|
model_lower = payload["model"].lower()
|
||||||
|
if model_lower.startswith("o1-mini") or model_lower.startswith("o1-preview"):
|
||||||
|
payload["messages"][0]["role"] = "user"
|
||||||
|
else:
|
||||||
|
payload["messages"][0]["role"] = "developer"
|
||||||
|
|
||||||
return payload
|
return payload
|
||||||
|
|
||||||
@@ -172,7 +192,7 @@ async def speech(request: Request, user=Depends(get_verified_user)):
|
|||||||
body = await request.body()
|
body = await request.body()
|
||||||
name = hashlib.sha256(body).hexdigest()
|
name = hashlib.sha256(body).hexdigest()
|
||||||
|
|
||||||
SPEECH_CACHE_DIR = Path(CACHE_DIR).joinpath("./audio/speech/")
|
SPEECH_CACHE_DIR = CACHE_DIR / "audio" / "speech"
|
||||||
SPEECH_CACHE_DIR.mkdir(parents=True, exist_ok=True)
|
SPEECH_CACHE_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
file_path = SPEECH_CACHE_DIR.joinpath(f"{name}.mp3")
|
file_path = SPEECH_CACHE_DIR.joinpath(f"{name}.mp3")
|
||||||
file_body_path = SPEECH_CACHE_DIR.joinpath(f"{name}.json")
|
file_body_path = SPEECH_CACHE_DIR.joinpath(f"{name}.json")
|
||||||
@@ -247,7 +267,7 @@ async def speech(request: Request, user=Depends(get_verified_user)):
|
|||||||
raise HTTPException(status_code=401, detail=ERROR_MESSAGES.OPENAI_NOT_FOUND)
|
raise HTTPException(status_code=401, detail=ERROR_MESSAGES.OPENAI_NOT_FOUND)
|
||||||
|
|
||||||
|
|
||||||
async def get_all_models_responses(request: Request) -> list:
|
async def get_all_models_responses(request: Request, user: UserModel) -> list:
|
||||||
if not request.app.state.config.ENABLE_OPENAI_API:
|
if not request.app.state.config.ENABLE_OPENAI_API:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
@@ -271,7 +291,9 @@ async def get_all_models_responses(request: Request) -> list:
|
|||||||
):
|
):
|
||||||
request_tasks.append(
|
request_tasks.append(
|
||||||
send_get_request(
|
send_get_request(
|
||||||
f"{url}/models", request.app.state.config.OPENAI_API_KEYS[idx]
|
f"{url}/models",
|
||||||
|
request.app.state.config.OPENAI_API_KEYS[idx],
|
||||||
|
user=user,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
@@ -291,6 +313,7 @@ async def get_all_models_responses(request: Request) -> list:
|
|||||||
send_get_request(
|
send_get_request(
|
||||||
f"{url}/models",
|
f"{url}/models",
|
||||||
request.app.state.config.OPENAI_API_KEYS[idx],
|
request.app.state.config.OPENAI_API_KEYS[idx],
|
||||||
|
user=user,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
@@ -352,13 +375,13 @@ async def get_filtered_models(models, user):
|
|||||||
|
|
||||||
|
|
||||||
@cached(ttl=3)
|
@cached(ttl=3)
|
||||||
async def get_all_models(request: Request) -> dict[str, list]:
|
async def get_all_models(request: Request, user: UserModel) -> dict[str, list]:
|
||||||
log.info("get_all_models()")
|
log.info("get_all_models()")
|
||||||
|
|
||||||
if not request.app.state.config.ENABLE_OPENAI_API:
|
if not request.app.state.config.ENABLE_OPENAI_API:
|
||||||
return {"data": []}
|
return {"data": []}
|
||||||
|
|
||||||
responses = await get_all_models_responses(request)
|
responses = await get_all_models_responses(request, user=user)
|
||||||
|
|
||||||
def extract_data(response):
|
def extract_data(response):
|
||||||
if response and "data" in response:
|
if response and "data" in response:
|
||||||
@@ -418,16 +441,14 @@ async def get_models(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if url_idx is None:
|
if url_idx is None:
|
||||||
models = await get_all_models(request)
|
models = await get_all_models(request, user=user)
|
||||||
else:
|
else:
|
||||||
url = request.app.state.config.OPENAI_API_BASE_URLS[url_idx]
|
url = request.app.state.config.OPENAI_API_BASE_URLS[url_idx]
|
||||||
key = request.app.state.config.OPENAI_API_KEYS[url_idx]
|
key = request.app.state.config.OPENAI_API_KEYS[url_idx]
|
||||||
|
|
||||||
r = None
|
r = None
|
||||||
async with aiohttp.ClientSession(
|
async with aiohttp.ClientSession(
|
||||||
timeout=aiohttp.ClientTimeout(
|
timeout=aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST)
|
||||||
total=AIOHTTP_CLIENT_TIMEOUT_OPENAI_MODEL_LIST
|
|
||||||
)
|
|
||||||
) as session:
|
) as session:
|
||||||
try:
|
try:
|
||||||
async with session.get(
|
async with session.get(
|
||||||
@@ -507,7 +528,7 @@ async def verify_connection(
|
|||||||
key = form_data.key
|
key = form_data.key
|
||||||
|
|
||||||
async with aiohttp.ClientSession(
|
async with aiohttp.ClientSession(
|
||||||
timeout=aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT_OPENAI_MODEL_LIST)
|
timeout=aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST)
|
||||||
) as session:
|
) as session:
|
||||||
try:
|
try:
|
||||||
async with session.get(
|
async with session.get(
|
||||||
@@ -515,6 +536,16 @@ async def verify_connection(
|
|||||||
headers={
|
headers={
|
||||||
"Authorization": f"Bearer {key}",
|
"Authorization": f"Bearer {key}",
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
|
**(
|
||||||
|
{
|
||||||
|
"X-OpenWebUI-User-Name": user.name,
|
||||||
|
"X-OpenWebUI-User-Id": user.id,
|
||||||
|
"X-OpenWebUI-User-Email": user.email,
|
||||||
|
"X-OpenWebUI-User-Role": user.role,
|
||||||
|
}
|
||||||
|
if ENABLE_FORWARD_USER_INFO_HEADERS
|
||||||
|
else {}
|
||||||
|
),
|
||||||
},
|
},
|
||||||
) as r:
|
) as r:
|
||||||
if r.status != 200:
|
if r.status != 200:
|
||||||
@@ -587,7 +618,7 @@ async def generate_chat_completion(
|
|||||||
detail="Model not found",
|
detail="Model not found",
|
||||||
)
|
)
|
||||||
|
|
||||||
await get_all_models(request)
|
await get_all_models(request, user=user)
|
||||||
model = request.app.state.OPENAI_MODELS.get(model_id)
|
model = request.app.state.OPENAI_MODELS.get(model_id)
|
||||||
if model:
|
if model:
|
||||||
idx = model["urlIdx"]
|
idx = model["urlIdx"]
|
||||||
@@ -777,7 +808,7 @@ async def proxy(path: str, request: Request, user=Depends(get_verified_user)):
|
|||||||
if r is not None:
|
if r is not None:
|
||||||
try:
|
try:
|
||||||
res = await r.json()
|
res = await r.json()
|
||||||
print(res)
|
log.error(res)
|
||||||
if "error" in res:
|
if "error" in res:
|
||||||
detail = f"External: {res['error']['message'] if 'message' in res['error'] else res['error']}"
|
detail = f"External: {res['error']['message'] if 'message' in res['error'] else res['error']}"
|
||||||
except Exception:
|
except Exception:
|
||||||
|
|||||||
@@ -101,7 +101,7 @@ async def process_pipeline_inlet_filter(request, payload, user, models):
|
|||||||
if "detail" in res:
|
if "detail" in res:
|
||||||
raise Exception(response.status, res["detail"])
|
raise Exception(response.status, res["detail"])
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Connection error: {e}")
|
log.exception(f"Connection error: {e}")
|
||||||
|
|
||||||
return payload
|
return payload
|
||||||
|
|
||||||
@@ -153,7 +153,7 @@ async def process_pipeline_outlet_filter(request, payload, user, models):
|
|||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Connection error: {e}")
|
log.exception(f"Connection error: {e}")
|
||||||
|
|
||||||
return payload
|
return payload
|
||||||
|
|
||||||
@@ -169,7 +169,7 @@ router = APIRouter()
|
|||||||
|
|
||||||
@router.get("/list")
|
@router.get("/list")
|
||||||
async def get_pipelines_list(request: Request, user=Depends(get_admin_user)):
|
async def get_pipelines_list(request: Request, user=Depends(get_admin_user)):
|
||||||
responses = await get_all_models_responses(request)
|
responses = await get_all_models_responses(request, user)
|
||||||
log.debug(f"get_pipelines_list: get_openai_models_responses returned {responses}")
|
log.debug(f"get_pipelines_list: get_openai_models_responses returned {responses}")
|
||||||
|
|
||||||
urlIdxs = [
|
urlIdxs = [
|
||||||
@@ -196,7 +196,7 @@ async def upload_pipeline(
|
|||||||
file: UploadFile = File(...),
|
file: UploadFile = File(...),
|
||||||
user=Depends(get_admin_user),
|
user=Depends(get_admin_user),
|
||||||
):
|
):
|
||||||
print("upload_pipeline", urlIdx, file.filename)
|
log.info(f"upload_pipeline: urlIdx={urlIdx}, filename={file.filename}")
|
||||||
# Check if the uploaded file is a python file
|
# Check if the uploaded file is a python file
|
||||||
if not (file.filename and file.filename.endswith(".py")):
|
if not (file.filename and file.filename.endswith(".py")):
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
@@ -231,7 +231,7 @@ async def upload_pipeline(
|
|||||||
return {**data}
|
return {**data}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
# Handle connection error here
|
# Handle connection error here
|
||||||
print(f"Connection error: {e}")
|
log.exception(f"Connection error: {e}")
|
||||||
|
|
||||||
detail = None
|
detail = None
|
||||||
status_code = status.HTTP_404_NOT_FOUND
|
status_code = status.HTTP_404_NOT_FOUND
|
||||||
@@ -282,7 +282,7 @@ async def add_pipeline(
|
|||||||
return {**data}
|
return {**data}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
# Handle connection error here
|
# Handle connection error here
|
||||||
print(f"Connection error: {e}")
|
log.exception(f"Connection error: {e}")
|
||||||
|
|
||||||
detail = None
|
detail = None
|
||||||
if r is not None:
|
if r is not None:
|
||||||
@@ -327,7 +327,7 @@ async def delete_pipeline(
|
|||||||
return {**data}
|
return {**data}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
# Handle connection error here
|
# Handle connection error here
|
||||||
print(f"Connection error: {e}")
|
log.exception(f"Connection error: {e}")
|
||||||
|
|
||||||
detail = None
|
detail = None
|
||||||
if r is not None:
|
if r is not None:
|
||||||
@@ -361,7 +361,7 @@ async def get_pipelines(
|
|||||||
return {**data}
|
return {**data}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
# Handle connection error here
|
# Handle connection error here
|
||||||
print(f"Connection error: {e}")
|
log.exception(f"Connection error: {e}")
|
||||||
|
|
||||||
detail = None
|
detail = None
|
||||||
if r is not None:
|
if r is not None:
|
||||||
@@ -400,7 +400,7 @@ async def get_pipeline_valves(
|
|||||||
return {**data}
|
return {**data}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
# Handle connection error here
|
# Handle connection error here
|
||||||
print(f"Connection error: {e}")
|
log.exception(f"Connection error: {e}")
|
||||||
|
|
||||||
detail = None
|
detail = None
|
||||||
if r is not None:
|
if r is not None:
|
||||||
@@ -440,7 +440,7 @@ async def get_pipeline_valves_spec(
|
|||||||
return {**data}
|
return {**data}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
# Handle connection error here
|
# Handle connection error here
|
||||||
print(f"Connection error: {e}")
|
log.exception(f"Connection error: {e}")
|
||||||
|
|
||||||
detail = None
|
detail = None
|
||||||
if r is not None:
|
if r is not None:
|
||||||
@@ -482,7 +482,7 @@ async def update_pipeline_valves(
|
|||||||
return {**data}
|
return {**data}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
# Handle connection error here
|
# Handle connection error here
|
||||||
print(f"Connection error: {e}")
|
log.exception(f"Connection error: {e}")
|
||||||
|
|
||||||
detail = None
|
detail = None
|
||||||
|
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ from open_webui.retrieval.web.serpstack import search_serpstack
|
|||||||
from open_webui.retrieval.web.tavily import search_tavily
|
from open_webui.retrieval.web.tavily import search_tavily
|
||||||
from open_webui.retrieval.web.bing import search_bing
|
from open_webui.retrieval.web.bing import search_bing
|
||||||
from open_webui.retrieval.web.exa import search_exa
|
from open_webui.retrieval.web.exa import search_exa
|
||||||
|
from open_webui.retrieval.web.perplexity import search_perplexity
|
||||||
|
|
||||||
from open_webui.retrieval.utils import (
|
from open_webui.retrieval.utils import (
|
||||||
get_embedding_function,
|
get_embedding_function,
|
||||||
@@ -352,10 +352,16 @@ async def get_rag_config(request: Request, user=Depends(get_admin_user)):
|
|||||||
"status": True,
|
"status": True,
|
||||||
"pdf_extract_images": request.app.state.config.PDF_EXTRACT_IMAGES,
|
"pdf_extract_images": request.app.state.config.PDF_EXTRACT_IMAGES,
|
||||||
"RAG_FULL_CONTEXT": request.app.state.config.RAG_FULL_CONTEXT,
|
"RAG_FULL_CONTEXT": request.app.state.config.RAG_FULL_CONTEXT,
|
||||||
|
"BYPASS_EMBEDDING_AND_RETRIEVAL": request.app.state.config.BYPASS_EMBEDDING_AND_RETRIEVAL,
|
||||||
"enable_google_drive_integration": request.app.state.config.ENABLE_GOOGLE_DRIVE_INTEGRATION,
|
"enable_google_drive_integration": request.app.state.config.ENABLE_GOOGLE_DRIVE_INTEGRATION,
|
||||||
|
"enable_onedrive_integration": request.app.state.config.ENABLE_ONEDRIVE_INTEGRATION,
|
||||||
"content_extraction": {
|
"content_extraction": {
|
||||||
"engine": request.app.state.config.CONTENT_EXTRACTION_ENGINE,
|
"engine": request.app.state.config.CONTENT_EXTRACTION_ENGINE,
|
||||||
"tika_server_url": request.app.state.config.TIKA_SERVER_URL,
|
"tika_server_url": request.app.state.config.TIKA_SERVER_URL,
|
||||||
|
"document_intelligence_config": {
|
||||||
|
"endpoint": request.app.state.config.DOCUMENT_INTELLIGENCE_ENDPOINT,
|
||||||
|
"key": request.app.state.config.DOCUMENT_INTELLIGENCE_KEY,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
"chunk": {
|
"chunk": {
|
||||||
"text_splitter": request.app.state.config.TEXT_SPLITTER,
|
"text_splitter": request.app.state.config.TEXT_SPLITTER,
|
||||||
@@ -373,10 +379,11 @@ async def get_rag_config(request: Request, user=Depends(get_admin_user)):
|
|||||||
},
|
},
|
||||||
"web": {
|
"web": {
|
||||||
"ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION": request.app.state.config.ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION,
|
"ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION": request.app.state.config.ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION,
|
||||||
"RAG_WEB_SEARCH_FULL_CONTEXT": request.app.state.config.RAG_WEB_SEARCH_FULL_CONTEXT,
|
"BYPASS_WEB_SEARCH_EMBEDDING_AND_RETRIEVAL": request.app.state.config.BYPASS_WEB_SEARCH_EMBEDDING_AND_RETRIEVAL,
|
||||||
"search": {
|
"search": {
|
||||||
"enabled": request.app.state.config.ENABLE_RAG_WEB_SEARCH,
|
"enabled": request.app.state.config.ENABLE_RAG_WEB_SEARCH,
|
||||||
"drive": request.app.state.config.ENABLE_GOOGLE_DRIVE_INTEGRATION,
|
"drive": request.app.state.config.ENABLE_GOOGLE_DRIVE_INTEGRATION,
|
||||||
|
"onedrive": request.app.state.config.ENABLE_ONEDRIVE_INTEGRATION,
|
||||||
"engine": request.app.state.config.RAG_WEB_SEARCH_ENGINE,
|
"engine": request.app.state.config.RAG_WEB_SEARCH_ENGINE,
|
||||||
"searxng_query_url": request.app.state.config.SEARXNG_QUERY_URL,
|
"searxng_query_url": request.app.state.config.SEARXNG_QUERY_URL,
|
||||||
"google_pse_api_key": request.app.state.config.GOOGLE_PSE_API_KEY,
|
"google_pse_api_key": request.app.state.config.GOOGLE_PSE_API_KEY,
|
||||||
@@ -398,7 +405,9 @@ async def get_rag_config(request: Request, user=Depends(get_admin_user)):
|
|||||||
"bing_search_v7_endpoint": request.app.state.config.BING_SEARCH_V7_ENDPOINT,
|
"bing_search_v7_endpoint": request.app.state.config.BING_SEARCH_V7_ENDPOINT,
|
||||||
"bing_search_v7_subscription_key": request.app.state.config.BING_SEARCH_V7_SUBSCRIPTION_KEY,
|
"bing_search_v7_subscription_key": request.app.state.config.BING_SEARCH_V7_SUBSCRIPTION_KEY,
|
||||||
"exa_api_key": request.app.state.config.EXA_API_KEY,
|
"exa_api_key": request.app.state.config.EXA_API_KEY,
|
||||||
|
"perplexity_api_key": request.app.state.config.PERPLEXITY_API_KEY,
|
||||||
"result_count": request.app.state.config.RAG_WEB_SEARCH_RESULT_COUNT,
|
"result_count": request.app.state.config.RAG_WEB_SEARCH_RESULT_COUNT,
|
||||||
|
"trust_env": request.app.state.config.RAG_WEB_SEARCH_TRUST_ENV,
|
||||||
"concurrent_requests": request.app.state.config.RAG_WEB_SEARCH_CONCURRENT_REQUESTS,
|
"concurrent_requests": request.app.state.config.RAG_WEB_SEARCH_CONCURRENT_REQUESTS,
|
||||||
"domain_filter_list": request.app.state.config.RAG_WEB_SEARCH_DOMAIN_FILTER_LIST,
|
"domain_filter_list": request.app.state.config.RAG_WEB_SEARCH_DOMAIN_FILTER_LIST,
|
||||||
},
|
},
|
||||||
@@ -411,9 +420,15 @@ class FileConfig(BaseModel):
|
|||||||
max_count: Optional[int] = None
|
max_count: Optional[int] = None
|
||||||
|
|
||||||
|
|
||||||
|
class DocumentIntelligenceConfigForm(BaseModel):
|
||||||
|
endpoint: str
|
||||||
|
key: str
|
||||||
|
|
||||||
|
|
||||||
class ContentExtractionConfig(BaseModel):
|
class ContentExtractionConfig(BaseModel):
|
||||||
engine: str = ""
|
engine: str = ""
|
||||||
tika_server_url: Optional[str] = None
|
tika_server_url: Optional[str] = None
|
||||||
|
document_intelligence_config: Optional[DocumentIntelligenceConfigForm] = None
|
||||||
|
|
||||||
|
|
||||||
class ChunkParamUpdateForm(BaseModel):
|
class ChunkParamUpdateForm(BaseModel):
|
||||||
@@ -451,6 +466,7 @@ class WebSearchConfig(BaseModel):
|
|||||||
bing_search_v7_endpoint: Optional[str] = None
|
bing_search_v7_endpoint: Optional[str] = None
|
||||||
bing_search_v7_subscription_key: Optional[str] = None
|
bing_search_v7_subscription_key: Optional[str] = None
|
||||||
exa_api_key: Optional[str] = None
|
exa_api_key: Optional[str] = None
|
||||||
|
perplexity_api_key: Optional[str] = None
|
||||||
result_count: Optional[int] = None
|
result_count: Optional[int] = None
|
||||||
concurrent_requests: Optional[int] = None
|
concurrent_requests: Optional[int] = None
|
||||||
trust_env: Optional[bool] = None
|
trust_env: Optional[bool] = None
|
||||||
@@ -460,13 +476,15 @@ class WebSearchConfig(BaseModel):
|
|||||||
class WebConfig(BaseModel):
|
class WebConfig(BaseModel):
|
||||||
search: WebSearchConfig
|
search: WebSearchConfig
|
||||||
ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION: Optional[bool] = None
|
ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION: Optional[bool] = None
|
||||||
RAG_WEB_SEARCH_FULL_CONTEXT: Optional[bool] = None
|
BYPASS_WEB_SEARCH_EMBEDDING_AND_RETRIEVAL: Optional[bool] = None
|
||||||
|
|
||||||
|
|
||||||
class ConfigUpdateForm(BaseModel):
|
class ConfigUpdateForm(BaseModel):
|
||||||
RAG_FULL_CONTEXT: Optional[bool] = None
|
RAG_FULL_CONTEXT: Optional[bool] = None
|
||||||
|
BYPASS_EMBEDDING_AND_RETRIEVAL: Optional[bool] = None
|
||||||
pdf_extract_images: Optional[bool] = None
|
pdf_extract_images: Optional[bool] = None
|
||||||
enable_google_drive_integration: Optional[bool] = None
|
enable_google_drive_integration: Optional[bool] = None
|
||||||
|
enable_onedrive_integration: Optional[bool] = None
|
||||||
file: Optional[FileConfig] = None
|
file: Optional[FileConfig] = None
|
||||||
content_extraction: Optional[ContentExtractionConfig] = None
|
content_extraction: Optional[ContentExtractionConfig] = None
|
||||||
chunk: Optional[ChunkParamUpdateForm] = None
|
chunk: Optional[ChunkParamUpdateForm] = None
|
||||||
@@ -490,24 +508,45 @@ async def update_rag_config(
|
|||||||
else request.app.state.config.RAG_FULL_CONTEXT
|
else request.app.state.config.RAG_FULL_CONTEXT
|
||||||
)
|
)
|
||||||
|
|
||||||
|
request.app.state.config.BYPASS_EMBEDDING_AND_RETRIEVAL = (
|
||||||
|
form_data.BYPASS_EMBEDDING_AND_RETRIEVAL
|
||||||
|
if form_data.BYPASS_EMBEDDING_AND_RETRIEVAL is not None
|
||||||
|
else request.app.state.config.BYPASS_EMBEDDING_AND_RETRIEVAL
|
||||||
|
)
|
||||||
|
|
||||||
request.app.state.config.ENABLE_GOOGLE_DRIVE_INTEGRATION = (
|
request.app.state.config.ENABLE_GOOGLE_DRIVE_INTEGRATION = (
|
||||||
form_data.enable_google_drive_integration
|
form_data.enable_google_drive_integration
|
||||||
if form_data.enable_google_drive_integration is not None
|
if form_data.enable_google_drive_integration is not None
|
||||||
else request.app.state.config.ENABLE_GOOGLE_DRIVE_INTEGRATION
|
else request.app.state.config.ENABLE_GOOGLE_DRIVE_INTEGRATION
|
||||||
)
|
)
|
||||||
|
|
||||||
|
request.app.state.config.ENABLE_ONEDRIVE_INTEGRATION = (
|
||||||
|
form_data.enable_onedrive_integration
|
||||||
|
if form_data.enable_onedrive_integration is not None
|
||||||
|
else request.app.state.config.ENABLE_ONEDRIVE_INTEGRATION
|
||||||
|
)
|
||||||
|
|
||||||
if form_data.file is not None:
|
if form_data.file is not None:
|
||||||
request.app.state.config.FILE_MAX_SIZE = form_data.file.max_size
|
request.app.state.config.FILE_MAX_SIZE = form_data.file.max_size
|
||||||
request.app.state.config.FILE_MAX_COUNT = form_data.file.max_count
|
request.app.state.config.FILE_MAX_COUNT = form_data.file.max_count
|
||||||
|
|
||||||
if form_data.content_extraction is not None:
|
if form_data.content_extraction is not None:
|
||||||
log.info(f"Updating text settings: {form_data.content_extraction}")
|
log.info(
|
||||||
|
f"Updating content extraction: {request.app.state.config.CONTENT_EXTRACTION_ENGINE} to {form_data.content_extraction.engine}"
|
||||||
|
)
|
||||||
request.app.state.config.CONTENT_EXTRACTION_ENGINE = (
|
request.app.state.config.CONTENT_EXTRACTION_ENGINE = (
|
||||||
form_data.content_extraction.engine
|
form_data.content_extraction.engine
|
||||||
)
|
)
|
||||||
request.app.state.config.TIKA_SERVER_URL = (
|
request.app.state.config.TIKA_SERVER_URL = (
|
||||||
form_data.content_extraction.tika_server_url
|
form_data.content_extraction.tika_server_url
|
||||||
)
|
)
|
||||||
|
if form_data.content_extraction.document_intelligence_config is not None:
|
||||||
|
request.app.state.config.DOCUMENT_INTELLIGENCE_ENDPOINT = (
|
||||||
|
form_data.content_extraction.document_intelligence_config.endpoint
|
||||||
|
)
|
||||||
|
request.app.state.config.DOCUMENT_INTELLIGENCE_KEY = (
|
||||||
|
form_data.content_extraction.document_intelligence_config.key
|
||||||
|
)
|
||||||
|
|
||||||
if form_data.chunk is not None:
|
if form_data.chunk is not None:
|
||||||
request.app.state.config.TEXT_SPLITTER = form_data.chunk.text_splitter
|
request.app.state.config.TEXT_SPLITTER = form_data.chunk.text_splitter
|
||||||
@@ -528,8 +567,8 @@ async def update_rag_config(
|
|||||||
request.app.state.config.ENABLE_RAG_WEB_SEARCH = form_data.web.search.enabled
|
request.app.state.config.ENABLE_RAG_WEB_SEARCH = form_data.web.search.enabled
|
||||||
request.app.state.config.RAG_WEB_SEARCH_ENGINE = form_data.web.search.engine
|
request.app.state.config.RAG_WEB_SEARCH_ENGINE = form_data.web.search.engine
|
||||||
|
|
||||||
request.app.state.config.RAG_WEB_SEARCH_FULL_CONTEXT = (
|
request.app.state.config.BYPASS_WEB_SEARCH_EMBEDDING_AND_RETRIEVAL = (
|
||||||
form_data.web.RAG_WEB_SEARCH_FULL_CONTEXT
|
form_data.web.BYPASS_WEB_SEARCH_EMBEDDING_AND_RETRIEVAL
|
||||||
)
|
)
|
||||||
|
|
||||||
request.app.state.config.SEARXNG_QUERY_URL = (
|
request.app.state.config.SEARXNG_QUERY_URL = (
|
||||||
@@ -580,6 +619,10 @@ async def update_rag_config(
|
|||||||
|
|
||||||
request.app.state.config.EXA_API_KEY = form_data.web.search.exa_api_key
|
request.app.state.config.EXA_API_KEY = form_data.web.search.exa_api_key
|
||||||
|
|
||||||
|
request.app.state.config.PERPLEXITY_API_KEY = (
|
||||||
|
form_data.web.search.perplexity_api_key
|
||||||
|
)
|
||||||
|
|
||||||
request.app.state.config.RAG_WEB_SEARCH_RESULT_COUNT = (
|
request.app.state.config.RAG_WEB_SEARCH_RESULT_COUNT = (
|
||||||
form_data.web.search.result_count
|
form_data.web.search.result_count
|
||||||
)
|
)
|
||||||
@@ -597,6 +640,7 @@ async def update_rag_config(
|
|||||||
"status": True,
|
"status": True,
|
||||||
"pdf_extract_images": request.app.state.config.PDF_EXTRACT_IMAGES,
|
"pdf_extract_images": request.app.state.config.PDF_EXTRACT_IMAGES,
|
||||||
"RAG_FULL_CONTEXT": request.app.state.config.RAG_FULL_CONTEXT,
|
"RAG_FULL_CONTEXT": request.app.state.config.RAG_FULL_CONTEXT,
|
||||||
|
"BYPASS_EMBEDDING_AND_RETRIEVAL": request.app.state.config.BYPASS_EMBEDDING_AND_RETRIEVAL,
|
||||||
"file": {
|
"file": {
|
||||||
"max_size": request.app.state.config.FILE_MAX_SIZE,
|
"max_size": request.app.state.config.FILE_MAX_SIZE,
|
||||||
"max_count": request.app.state.config.FILE_MAX_COUNT,
|
"max_count": request.app.state.config.FILE_MAX_COUNT,
|
||||||
@@ -604,6 +648,10 @@ async def update_rag_config(
|
|||||||
"content_extraction": {
|
"content_extraction": {
|
||||||
"engine": request.app.state.config.CONTENT_EXTRACTION_ENGINE,
|
"engine": request.app.state.config.CONTENT_EXTRACTION_ENGINE,
|
||||||
"tika_server_url": request.app.state.config.TIKA_SERVER_URL,
|
"tika_server_url": request.app.state.config.TIKA_SERVER_URL,
|
||||||
|
"document_intelligence_config": {
|
||||||
|
"endpoint": request.app.state.config.DOCUMENT_INTELLIGENCE_ENDPOINT,
|
||||||
|
"key": request.app.state.config.DOCUMENT_INTELLIGENCE_KEY,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
"chunk": {
|
"chunk": {
|
||||||
"text_splitter": request.app.state.config.TEXT_SPLITTER,
|
"text_splitter": request.app.state.config.TEXT_SPLITTER,
|
||||||
@@ -617,7 +665,7 @@ async def update_rag_config(
|
|||||||
},
|
},
|
||||||
"web": {
|
"web": {
|
||||||
"ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION": request.app.state.config.ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION,
|
"ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION": request.app.state.config.ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION,
|
||||||
"RAG_WEB_SEARCH_FULL_CONTEXT": request.app.state.config.RAG_WEB_SEARCH_FULL_CONTEXT,
|
"BYPASS_WEB_SEARCH_EMBEDDING_AND_RETRIEVAL": request.app.state.config.BYPASS_WEB_SEARCH_EMBEDDING_AND_RETRIEVAL,
|
||||||
"search": {
|
"search": {
|
||||||
"enabled": request.app.state.config.ENABLE_RAG_WEB_SEARCH,
|
"enabled": request.app.state.config.ENABLE_RAG_WEB_SEARCH,
|
||||||
"engine": request.app.state.config.RAG_WEB_SEARCH_ENGINE,
|
"engine": request.app.state.config.RAG_WEB_SEARCH_ENGINE,
|
||||||
@@ -641,6 +689,7 @@ async def update_rag_config(
|
|||||||
"bing_search_v7_endpoint": request.app.state.config.BING_SEARCH_V7_ENDPOINT,
|
"bing_search_v7_endpoint": request.app.state.config.BING_SEARCH_V7_ENDPOINT,
|
||||||
"bing_search_v7_subscription_key": request.app.state.config.BING_SEARCH_V7_SUBSCRIPTION_KEY,
|
"bing_search_v7_subscription_key": request.app.state.config.BING_SEARCH_V7_SUBSCRIPTION_KEY,
|
||||||
"exa_api_key": request.app.state.config.EXA_API_KEY,
|
"exa_api_key": request.app.state.config.EXA_API_KEY,
|
||||||
|
"perplexity_api_key": request.app.state.config.PERPLEXITY_API_KEY,
|
||||||
"result_count": request.app.state.config.RAG_WEB_SEARCH_RESULT_COUNT,
|
"result_count": request.app.state.config.RAG_WEB_SEARCH_RESULT_COUNT,
|
||||||
"concurrent_requests": request.app.state.config.RAG_WEB_SEARCH_CONCURRENT_REQUESTS,
|
"concurrent_requests": request.app.state.config.RAG_WEB_SEARCH_CONCURRENT_REQUESTS,
|
||||||
"trust_env": request.app.state.config.RAG_WEB_SEARCH_TRUST_ENV,
|
"trust_env": request.app.state.config.RAG_WEB_SEARCH_TRUST_ENV,
|
||||||
@@ -880,7 +929,12 @@ def process_file(
|
|||||||
# Update the content in the file
|
# Update the content in the file
|
||||||
# Usage: /files/{file_id}/data/content/update
|
# Usage: /files/{file_id}/data/content/update
|
||||||
|
|
||||||
VECTOR_DB_CLIENT.delete_collection(collection_name=f"file-{file.id}")
|
try:
|
||||||
|
# /files/{file_id}/data/content/update
|
||||||
|
VECTOR_DB_CLIENT.delete_collection(collection_name=f"file-{file.id}")
|
||||||
|
except:
|
||||||
|
# Audio file upload pipeline
|
||||||
|
pass
|
||||||
|
|
||||||
docs = [
|
docs = [
|
||||||
Document(
|
Document(
|
||||||
@@ -937,6 +991,8 @@ def process_file(
|
|||||||
engine=request.app.state.config.CONTENT_EXTRACTION_ENGINE,
|
engine=request.app.state.config.CONTENT_EXTRACTION_ENGINE,
|
||||||
TIKA_SERVER_URL=request.app.state.config.TIKA_SERVER_URL,
|
TIKA_SERVER_URL=request.app.state.config.TIKA_SERVER_URL,
|
||||||
PDF_EXTRACT_IMAGES=request.app.state.config.PDF_EXTRACT_IMAGES,
|
PDF_EXTRACT_IMAGES=request.app.state.config.PDF_EXTRACT_IMAGES,
|
||||||
|
DOCUMENT_INTELLIGENCE_ENDPOINT=request.app.state.config.DOCUMENT_INTELLIGENCE_ENDPOINT,
|
||||||
|
DOCUMENT_INTELLIGENCE_KEY=request.app.state.config.DOCUMENT_INTELLIGENCE_KEY,
|
||||||
)
|
)
|
||||||
docs = loader.load(
|
docs = loader.load(
|
||||||
file.filename, file.meta.get("content_type"), file_path
|
file.filename, file.meta.get("content_type"), file_path
|
||||||
@@ -979,36 +1035,45 @@ def process_file(
|
|||||||
hash = calculate_sha256_string(text_content)
|
hash = calculate_sha256_string(text_content)
|
||||||
Files.update_file_hash_by_id(file.id, hash)
|
Files.update_file_hash_by_id(file.id, hash)
|
||||||
|
|
||||||
try:
|
if not request.app.state.config.BYPASS_EMBEDDING_AND_RETRIEVAL:
|
||||||
result = save_docs_to_vector_db(
|
try:
|
||||||
request,
|
result = save_docs_to_vector_db(
|
||||||
docs=docs,
|
request,
|
||||||
collection_name=collection_name,
|
docs=docs,
|
||||||
metadata={
|
collection_name=collection_name,
|
||||||
"file_id": file.id,
|
metadata={
|
||||||
"name": file.filename,
|
"file_id": file.id,
|
||||||
"hash": hash,
|
"name": file.filename,
|
||||||
},
|
"hash": hash,
|
||||||
add=(True if form_data.collection_name else False),
|
|
||||||
user=user,
|
|
||||||
)
|
|
||||||
|
|
||||||
if result:
|
|
||||||
Files.update_file_metadata_by_id(
|
|
||||||
file.id,
|
|
||||||
{
|
|
||||||
"collection_name": collection_name,
|
|
||||||
},
|
},
|
||||||
|
add=(True if form_data.collection_name else False),
|
||||||
|
user=user,
|
||||||
)
|
)
|
||||||
|
|
||||||
return {
|
if result:
|
||||||
"status": True,
|
Files.update_file_metadata_by_id(
|
||||||
"collection_name": collection_name,
|
file.id,
|
||||||
"filename": file.filename,
|
{
|
||||||
"content": text_content,
|
"collection_name": collection_name,
|
||||||
}
|
},
|
||||||
except Exception as e:
|
)
|
||||||
raise e
|
|
||||||
|
return {
|
||||||
|
"status": True,
|
||||||
|
"collection_name": collection_name,
|
||||||
|
"filename": file.filename,
|
||||||
|
"content": text_content,
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
|
raise e
|
||||||
|
else:
|
||||||
|
return {
|
||||||
|
"status": True,
|
||||||
|
"collection_name": None,
|
||||||
|
"filename": file.filename,
|
||||||
|
"content": text_content,
|
||||||
|
}
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.exception(e)
|
log.exception(e)
|
||||||
if "No pandoc was found" in str(e):
|
if "No pandoc was found" in str(e):
|
||||||
@@ -1124,9 +1189,13 @@ def process_web(
|
|||||||
content = " ".join([doc.page_content for doc in docs])
|
content = " ".join([doc.page_content for doc in docs])
|
||||||
|
|
||||||
log.debug(f"text_content: {content}")
|
log.debug(f"text_content: {content}")
|
||||||
save_docs_to_vector_db(
|
|
||||||
request, docs, collection_name, overwrite=True, user=user
|
if not request.app.state.config.BYPASS_WEB_SEARCH_EMBEDDING_AND_RETRIEVAL:
|
||||||
)
|
save_docs_to_vector_db(
|
||||||
|
request, docs, collection_name, overwrite=True, user=user
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
collection_name = None
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"status": True,
|
"status": True,
|
||||||
@@ -1138,6 +1207,7 @@ def process_web(
|
|||||||
},
|
},
|
||||||
"meta": {
|
"meta": {
|
||||||
"name": form_data.url,
|
"name": form_data.url,
|
||||||
|
"source": form_data.url,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -1163,6 +1233,7 @@ def search_web(request: Request, engine: str, query: str) -> list[SearchResult]:
|
|||||||
- SERPLY_API_KEY
|
- SERPLY_API_KEY
|
||||||
- TAVILY_API_KEY
|
- TAVILY_API_KEY
|
||||||
- EXA_API_KEY
|
- EXA_API_KEY
|
||||||
|
- PERPLEXITY_API_KEY
|
||||||
- SEARCHAPI_API_KEY + SEARCHAPI_ENGINE (by default `google`)
|
- SEARCHAPI_API_KEY + SEARCHAPI_ENGINE (by default `google`)
|
||||||
- SERPAPI_API_KEY + SERPAPI_ENGINE (by default `google`)
|
- SERPAPI_API_KEY + SERPAPI_ENGINE (by default `google`)
|
||||||
Args:
|
Args:
|
||||||
@@ -1327,6 +1398,13 @@ def search_web(request: Request, engine: str, query: str) -> list[SearchResult]:
|
|||||||
request.app.state.config.RAG_WEB_SEARCH_RESULT_COUNT,
|
request.app.state.config.RAG_WEB_SEARCH_RESULT_COUNT,
|
||||||
request.app.state.config.RAG_WEB_SEARCH_DOMAIN_FILTER_LIST,
|
request.app.state.config.RAG_WEB_SEARCH_DOMAIN_FILTER_LIST,
|
||||||
)
|
)
|
||||||
|
elif engine == "perplexity":
|
||||||
|
return search_perplexity(
|
||||||
|
request.app.state.config.PERPLEXITY_API_KEY,
|
||||||
|
query,
|
||||||
|
request.app.state.config.RAG_WEB_SEARCH_RESULT_COUNT,
|
||||||
|
request.app.state.config.RAG_WEB_SEARCH_DOMAIN_FILTER_LIST,
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
raise Exception("No search engine API key found in environment variables")
|
raise Exception("No search engine API key found in environment variables")
|
||||||
|
|
||||||
@@ -1368,9 +1446,11 @@ async def process_web_search(
|
|||||||
)
|
)
|
||||||
docs = await loader.aload()
|
docs = await loader.aload()
|
||||||
|
|
||||||
if request.app.state.config.RAG_WEB_SEARCH_FULL_CONTEXT:
|
if request.app.state.config.BYPASS_WEB_SEARCH_EMBEDDING_AND_RETRIEVAL:
|
||||||
return {
|
return {
|
||||||
"status": True,
|
"status": True,
|
||||||
|
"collection_name": None,
|
||||||
|
"filenames": urls,
|
||||||
"docs": [
|
"docs": [
|
||||||
{
|
{
|
||||||
"content": doc.page_content,
|
"content": doc.page_content,
|
||||||
@@ -1378,7 +1458,6 @@ async def process_web_search(
|
|||||||
}
|
}
|
||||||
for doc in docs
|
for doc in docs
|
||||||
],
|
],
|
||||||
"filenames": urls,
|
|
||||||
"loaded_count": len(docs),
|
"loaded_count": len(docs),
|
||||||
}
|
}
|
||||||
else:
|
else:
|
||||||
@@ -1553,11 +1632,11 @@ def reset_upload_dir(user=Depends(get_admin_user)) -> bool:
|
|||||||
elif os.path.isdir(file_path):
|
elif os.path.isdir(file_path):
|
||||||
shutil.rmtree(file_path) # Remove the directory
|
shutil.rmtree(file_path) # Remove the directory
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Failed to delete {file_path}. Reason: {e}")
|
log.exception(f"Failed to delete {file_path}. Reason: {e}")
|
||||||
else:
|
else:
|
||||||
print(f"The directory {folder} does not exist")
|
log.warning(f"The directory {folder} does not exist")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Failed to process the directory {folder}. Reason: {e}")
|
log.exception(f"Failed to process the directory {folder}. Reason: {e}")
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -20,6 +20,10 @@ from open_webui.utils.auth import get_admin_user, get_verified_user
|
|||||||
from open_webui.constants import TASKS
|
from open_webui.constants import TASKS
|
||||||
|
|
||||||
from open_webui.routers.pipelines import process_pipeline_inlet_filter
|
from open_webui.routers.pipelines import process_pipeline_inlet_filter
|
||||||
|
from open_webui.utils.filter import (
|
||||||
|
get_sorted_filter_ids,
|
||||||
|
process_filter_functions,
|
||||||
|
)
|
||||||
from open_webui.utils.task import get_task_model_id
|
from open_webui.utils.task import get_task_model_id
|
||||||
|
|
||||||
from open_webui.config import (
|
from open_webui.config import (
|
||||||
@@ -221,6 +225,12 @@ async def generate_title(
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Process the payload through the pipeline
|
||||||
|
try:
|
||||||
|
payload = await process_pipeline_inlet_filter(request, payload, user, models)
|
||||||
|
except Exception as e:
|
||||||
|
raise e
|
||||||
|
|
||||||
try:
|
try:
|
||||||
return await generate_chat_completion(request, form_data=payload, user=user)
|
return await generate_chat_completion(request, form_data=payload, user=user)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -290,6 +300,12 @@ async def generate_chat_tags(
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Process the payload through the pipeline
|
||||||
|
try:
|
||||||
|
payload = await process_pipeline_inlet_filter(request, payload, user, models)
|
||||||
|
except Exception as e:
|
||||||
|
raise e
|
||||||
|
|
||||||
try:
|
try:
|
||||||
return await generate_chat_completion(request, form_data=payload, user=user)
|
return await generate_chat_completion(request, form_data=payload, user=user)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -356,6 +372,12 @@ async def generate_image_prompt(
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Process the payload through the pipeline
|
||||||
|
try:
|
||||||
|
payload = await process_pipeline_inlet_filter(request, payload, user, models)
|
||||||
|
except Exception as e:
|
||||||
|
raise e
|
||||||
|
|
||||||
try:
|
try:
|
||||||
return await generate_chat_completion(request, form_data=payload, user=user)
|
return await generate_chat_completion(request, form_data=payload, user=user)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -433,6 +455,12 @@ async def generate_queries(
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Process the payload through the pipeline
|
||||||
|
try:
|
||||||
|
payload = await process_pipeline_inlet_filter(request, payload, user, models)
|
||||||
|
except Exception as e:
|
||||||
|
raise e
|
||||||
|
|
||||||
try:
|
try:
|
||||||
return await generate_chat_completion(request, form_data=payload, user=user)
|
return await generate_chat_completion(request, form_data=payload, user=user)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -514,6 +542,12 @@ async def generate_autocompletion(
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Process the payload through the pipeline
|
||||||
|
try:
|
||||||
|
payload = await process_pipeline_inlet_filter(request, payload, user, models)
|
||||||
|
except Exception as e:
|
||||||
|
raise e
|
||||||
|
|
||||||
try:
|
try:
|
||||||
return await generate_chat_completion(request, form_data=payload, user=user)
|
return await generate_chat_completion(request, form_data=payload, user=user)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -584,6 +618,12 @@ async def generate_emoji(
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Process the payload through the pipeline
|
||||||
|
try:
|
||||||
|
payload = await process_pipeline_inlet_filter(request, payload, user, models)
|
||||||
|
except Exception as e:
|
||||||
|
raise e
|
||||||
|
|
||||||
try:
|
try:
|
||||||
return await generate_chat_completion(request, form_data=payload, user=user)
|
return await generate_chat_completion(request, form_data=payload, user=user)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -644,6 +684,12 @@ async def generate_moa_response(
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Process the payload through the pipeline
|
||||||
|
try:
|
||||||
|
payload = await process_pipeline_inlet_filter(request, payload, user, models)
|
||||||
|
except Exception as e:
|
||||||
|
raise e
|
||||||
|
|
||||||
try:
|
try:
|
||||||
return await generate_chat_completion(request, form_data=payload, user=user)
|
return await generate_chat_completion(request, form_data=payload, user=user)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import logging
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
@@ -15,6 +16,10 @@ from fastapi import APIRouter, Depends, HTTPException, Request, status
|
|||||||
from open_webui.utils.tools import get_tools_specs
|
from open_webui.utils.tools import get_tools_specs
|
||||||
from open_webui.utils.auth import get_admin_user, get_verified_user
|
from open_webui.utils.auth import get_admin_user, get_verified_user
|
||||||
from open_webui.utils.access_control import has_access, has_permission
|
from open_webui.utils.access_control import has_access, has_permission
|
||||||
|
from open_webui.env import SRC_LOG_LEVELS
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
log.setLevel(SRC_LOG_LEVELS["MAIN"])
|
||||||
|
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
@@ -100,7 +105,7 @@ async def create_new_tools(
|
|||||||
specs = get_tools_specs(TOOLS[form_data.id])
|
specs = get_tools_specs(TOOLS[form_data.id])
|
||||||
tools = Tools.insert_new_tool(user.id, form_data, specs)
|
tools = Tools.insert_new_tool(user.id, form_data, specs)
|
||||||
|
|
||||||
tool_cache_dir = Path(CACHE_DIR) / "tools" / form_data.id
|
tool_cache_dir = CACHE_DIR / "tools" / form_data.id
|
||||||
tool_cache_dir.mkdir(parents=True, exist_ok=True)
|
tool_cache_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
if tools:
|
if tools:
|
||||||
@@ -111,7 +116,7 @@ async def create_new_tools(
|
|||||||
detail=ERROR_MESSAGES.DEFAULT("Error creating tools"),
|
detail=ERROR_MESSAGES.DEFAULT("Error creating tools"),
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(e)
|
log.exception(f"Failed to load the tool by id {form_data.id}: {e}")
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_400_BAD_REQUEST,
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
detail=ERROR_MESSAGES.DEFAULT(str(e)),
|
detail=ERROR_MESSAGES.DEFAULT(str(e)),
|
||||||
@@ -193,7 +198,7 @@ async def update_tools_by_id(
|
|||||||
"specs": specs,
|
"specs": specs,
|
||||||
}
|
}
|
||||||
|
|
||||||
print(updated)
|
log.debug(updated)
|
||||||
tools = Tools.update_tool_by_id(id, updated)
|
tools = Tools.update_tool_by_id(id, updated)
|
||||||
|
|
||||||
if tools:
|
if tools:
|
||||||
@@ -343,7 +348,7 @@ async def update_tools_valves_by_id(
|
|||||||
Tools.update_tool_valves_by_id(id, valves.model_dump())
|
Tools.update_tool_valves_by_id(id, valves.model_dump())
|
||||||
return valves.model_dump()
|
return valves.model_dump()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(e)
|
log.exception(f"Failed to update tool valves by id {id}: {e}")
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_400_BAD_REQUEST,
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
detail=ERROR_MESSAGES.DEFAULT(str(e)),
|
detail=ERROR_MESSAGES.DEFAULT(str(e)),
|
||||||
@@ -421,7 +426,7 @@ async def update_tools_user_valves_by_id(
|
|||||||
)
|
)
|
||||||
return user_valves.model_dump()
|
return user_valves.model_dump()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(e)
|
log.exception(f"Failed to update user valves by id {id}: {e}")
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_400_BAD_REQUEST,
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
detail=ERROR_MESSAGES.DEFAULT(str(e)),
|
detail=ERROR_MESSAGES.DEFAULT(str(e)),
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import black
|
import black
|
||||||
|
import logging
|
||||||
import markdown
|
import markdown
|
||||||
|
|
||||||
from open_webui.models.chats import ChatTitleMessagesForm
|
from open_webui.models.chats import ChatTitleMessagesForm
|
||||||
@@ -13,8 +14,12 @@ from open_webui.utils.misc import get_gravatar_url
|
|||||||
from open_webui.utils.pdf_generator import PDFGenerator
|
from open_webui.utils.pdf_generator import PDFGenerator
|
||||||
from open_webui.utils.auth import get_admin_user, get_verified_user
|
from open_webui.utils.auth import get_admin_user, get_verified_user
|
||||||
from open_webui.utils.code_interpreter import execute_code_jupyter
|
from open_webui.utils.code_interpreter import execute_code_jupyter
|
||||||
|
from open_webui.env import SRC_LOG_LEVELS
|
||||||
|
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
log.setLevel(SRC_LOG_LEVELS["MAIN"])
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
@@ -96,7 +101,7 @@ async def download_chat_as_pdf(
|
|||||||
headers={"Content-Disposition": "attachment;filename=chat.pdf"},
|
headers={"Content-Disposition": "attachment;filename=chat.pdf"},
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(e)
|
log.exception(f"Error generating PDF: {e}")
|
||||||
raise HTTPException(status_code=400, detail=str(e))
|
raise HTTPException(status_code=400, detail=str(e))
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ from open_webui.env import (
|
|||||||
ENABLE_WEBSOCKET_SUPPORT,
|
ENABLE_WEBSOCKET_SUPPORT,
|
||||||
WEBSOCKET_MANAGER,
|
WEBSOCKET_MANAGER,
|
||||||
WEBSOCKET_REDIS_URL,
|
WEBSOCKET_REDIS_URL,
|
||||||
|
WEBSOCKET_REDIS_LOCK_TIMEOUT,
|
||||||
)
|
)
|
||||||
from open_webui.utils.auth import decode_token
|
from open_webui.utils.auth import decode_token
|
||||||
from open_webui.socket.utils import RedisDict, RedisLock
|
from open_webui.socket.utils import RedisDict, RedisLock
|
||||||
@@ -61,7 +62,7 @@ if WEBSOCKET_MANAGER == "redis":
|
|||||||
clean_up_lock = RedisLock(
|
clean_up_lock = RedisLock(
|
||||||
redis_url=WEBSOCKET_REDIS_URL,
|
redis_url=WEBSOCKET_REDIS_URL,
|
||||||
lock_name="usage_cleanup_lock",
|
lock_name="usage_cleanup_lock",
|
||||||
timeout_secs=TIMEOUT_DURATION * 2,
|
timeout_secs=WEBSOCKET_REDIS_LOCK_TIMEOUT,
|
||||||
)
|
)
|
||||||
aquire_func = clean_up_lock.aquire_lock
|
aquire_func = clean_up_lock.aquire_lock
|
||||||
renew_func = clean_up_lock.renew_lock
|
renew_func = clean_up_lock.renew_lock
|
||||||
|
|||||||
|
After Width: | Height: | Size: 7.3 KiB |
|
After Width: | Height: | Size: 3.7 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 14 KiB |
@@ -3,13 +3,13 @@
|
|||||||
"short_name": "WebUI",
|
"short_name": "WebUI",
|
||||||
"icons": [
|
"icons": [
|
||||||
{
|
{
|
||||||
"src": "/favicon/web-app-manifest-192x192.png",
|
"src": "/static/web-app-manifest-192x192.png",
|
||||||
"sizes": "192x192",
|
"sizes": "192x192",
|
||||||
"type": "image/png",
|
"type": "image/png",
|
||||||
"purpose": "maskable"
|
"purpose": "maskable"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"src": "/favicon/web-app-manifest-512x512.png",
|
"src": "/static/web-app-manifest-512x512.png",
|
||||||
"sizes": "512x512",
|
"sizes": "512x512",
|
||||||
"type": "image/png",
|
"type": "image/png",
|
||||||
"purpose": "maskable"
|
"purpose": "maskable"
|
||||||
|
After Width: | Height: | Size: 5.3 KiB |
|
After Width: | Height: | Size: 8.2 KiB |
|
After Width: | Height: | Size: 29 KiB |
@@ -1,10 +1,12 @@
|
|||||||
import os
|
import os
|
||||||
import shutil
|
import shutil
|
||||||
import json
|
import json
|
||||||
|
import logging
|
||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
from typing import BinaryIO, Tuple
|
from typing import BinaryIO, Tuple
|
||||||
|
|
||||||
import boto3
|
import boto3
|
||||||
|
from botocore.config import Config
|
||||||
from botocore.exceptions import ClientError
|
from botocore.exceptions import ClientError
|
||||||
from open_webui.config import (
|
from open_webui.config import (
|
||||||
S3_ACCESS_KEY_ID,
|
S3_ACCESS_KEY_ID,
|
||||||
@@ -13,6 +15,8 @@ from open_webui.config import (
|
|||||||
S3_KEY_PREFIX,
|
S3_KEY_PREFIX,
|
||||||
S3_REGION_NAME,
|
S3_REGION_NAME,
|
||||||
S3_SECRET_ACCESS_KEY,
|
S3_SECRET_ACCESS_KEY,
|
||||||
|
S3_USE_ACCELERATE_ENDPOINT,
|
||||||
|
S3_ADDRESSING_STYLE,
|
||||||
GCS_BUCKET_NAME,
|
GCS_BUCKET_NAME,
|
||||||
GOOGLE_APPLICATION_CREDENTIALS_JSON,
|
GOOGLE_APPLICATION_CREDENTIALS_JSON,
|
||||||
AZURE_STORAGE_ENDPOINT,
|
AZURE_STORAGE_ENDPOINT,
|
||||||
@@ -27,6 +31,11 @@ from open_webui.constants import ERROR_MESSAGES
|
|||||||
from azure.identity import DefaultAzureCredential
|
from azure.identity import DefaultAzureCredential
|
||||||
from azure.storage.blob import BlobServiceClient
|
from azure.storage.blob import BlobServiceClient
|
||||||
from azure.core.exceptions import ResourceNotFoundError
|
from azure.core.exceptions import ResourceNotFoundError
|
||||||
|
from open_webui.env import SRC_LOG_LEVELS
|
||||||
|
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
log.setLevel(SRC_LOG_LEVELS["MAIN"])
|
||||||
|
|
||||||
|
|
||||||
class StorageProvider(ABC):
|
class StorageProvider(ABC):
|
||||||
@@ -71,7 +80,7 @@ class LocalStorageProvider(StorageProvider):
|
|||||||
if os.path.isfile(file_path):
|
if os.path.isfile(file_path):
|
||||||
os.remove(file_path)
|
os.remove(file_path)
|
||||||
else:
|
else:
|
||||||
print(f"File {file_path} not found in local storage.")
|
log.warning(f"File {file_path} not found in local storage.")
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def delete_all_files() -> None:
|
def delete_all_files() -> None:
|
||||||
@@ -85,20 +94,40 @@ class LocalStorageProvider(StorageProvider):
|
|||||||
elif os.path.isdir(file_path):
|
elif os.path.isdir(file_path):
|
||||||
shutil.rmtree(file_path) # Remove the directory
|
shutil.rmtree(file_path) # Remove the directory
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Failed to delete {file_path}. Reason: {e}")
|
log.exception(f"Failed to delete {file_path}. Reason: {e}")
|
||||||
else:
|
else:
|
||||||
print(f"Directory {UPLOAD_DIR} not found in local storage.")
|
log.warning(f"Directory {UPLOAD_DIR} not found in local storage.")
|
||||||
|
|
||||||
|
|
||||||
class S3StorageProvider(StorageProvider):
|
class S3StorageProvider(StorageProvider):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.s3_client = boto3.client(
|
config = Config(
|
||||||
"s3",
|
s3={
|
||||||
region_name=S3_REGION_NAME,
|
"use_accelerate_endpoint": S3_USE_ACCELERATE_ENDPOINT,
|
||||||
endpoint_url=S3_ENDPOINT_URL,
|
"addressing_style": S3_ADDRESSING_STYLE,
|
||||||
aws_access_key_id=S3_ACCESS_KEY_ID,
|
},
|
||||||
aws_secret_access_key=S3_SECRET_ACCESS_KEY,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# If access key and secret are provided, use them for authentication
|
||||||
|
if S3_ACCESS_KEY_ID and S3_SECRET_ACCESS_KEY:
|
||||||
|
self.s3_client = boto3.client(
|
||||||
|
"s3",
|
||||||
|
region_name=S3_REGION_NAME,
|
||||||
|
endpoint_url=S3_ENDPOINT_URL,
|
||||||
|
aws_access_key_id=S3_ACCESS_KEY_ID,
|
||||||
|
aws_secret_access_key=S3_SECRET_ACCESS_KEY,
|
||||||
|
config=config,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# If no explicit credentials are provided, fall back to default AWS credentials
|
||||||
|
# This supports workload identity (IAM roles for EC2, EKS, etc.)
|
||||||
|
self.s3_client = boto3.client(
|
||||||
|
"s3",
|
||||||
|
region_name=S3_REGION_NAME,
|
||||||
|
endpoint_url=S3_ENDPOINT_URL,
|
||||||
|
config=config,
|
||||||
|
)
|
||||||
|
|
||||||
self.bucket_name = S3_BUCKET_NAME
|
self.bucket_name = S3_BUCKET_NAME
|
||||||
self.key_prefix = S3_KEY_PREFIX if S3_KEY_PREFIX else ""
|
self.key_prefix = S3_KEY_PREFIX if S3_KEY_PREFIX else ""
|
||||||
|
|
||||||
|
|||||||
@@ -187,6 +187,17 @@ class TestS3StorageProvider:
|
|||||||
assert not (upload_dir / self.filename).exists()
|
assert not (upload_dir / self.filename).exists()
|
||||||
assert not (upload_dir / self.filename_extra).exists()
|
assert not (upload_dir / self.filename_extra).exists()
|
||||||
|
|
||||||
|
def test_init_without_credentials(self, monkeypatch):
|
||||||
|
"""Test that S3StorageProvider can initialize without explicit credentials."""
|
||||||
|
# Temporarily unset the environment variables
|
||||||
|
monkeypatch.setattr(provider, "S3_ACCESS_KEY_ID", None)
|
||||||
|
monkeypatch.setattr(provider, "S3_SECRET_ACCESS_KEY", None)
|
||||||
|
|
||||||
|
# Should not raise an exception
|
||||||
|
storage = provider.S3StorageProvider()
|
||||||
|
assert storage.s3_client is not None
|
||||||
|
assert storage.bucket_name == provider.S3_BUCKET_NAME
|
||||||
|
|
||||||
|
|
||||||
class TestGCSStorageProvider:
|
class TestGCSStorageProvider:
|
||||||
Storage = provider.GCSStorageProvider()
|
Storage = provider.GCSStorageProvider()
|
||||||
|
|||||||
@@ -0,0 +1,249 @@
|
|||||||
|
from contextlib import asynccontextmanager
|
||||||
|
from dataclasses import asdict, dataclass
|
||||||
|
from enum import Enum
|
||||||
|
import re
|
||||||
|
from typing import (
|
||||||
|
TYPE_CHECKING,
|
||||||
|
Any,
|
||||||
|
AsyncGenerator,
|
||||||
|
Dict,
|
||||||
|
MutableMapping,
|
||||||
|
Optional,
|
||||||
|
cast,
|
||||||
|
)
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
from asgiref.typing import (
|
||||||
|
ASGI3Application,
|
||||||
|
ASGIReceiveCallable,
|
||||||
|
ASGIReceiveEvent,
|
||||||
|
ASGISendCallable,
|
||||||
|
ASGISendEvent,
|
||||||
|
Scope as ASGIScope,
|
||||||
|
)
|
||||||
|
from loguru import logger
|
||||||
|
from starlette.requests import Request
|
||||||
|
|
||||||
|
from open_webui.env import AUDIT_LOG_LEVEL, MAX_BODY_LOG_SIZE
|
||||||
|
from open_webui.utils.auth import get_current_user, get_http_authorization_cred
|
||||||
|
from open_webui.models.users import UserModel
|
||||||
|
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from loguru import Logger
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class AuditLogEntry:
|
||||||
|
# `Metadata` audit level properties
|
||||||
|
id: str
|
||||||
|
user: dict[str, Any]
|
||||||
|
audit_level: str
|
||||||
|
verb: str
|
||||||
|
request_uri: str
|
||||||
|
user_agent: Optional[str] = None
|
||||||
|
source_ip: Optional[str] = None
|
||||||
|
# `Request` audit level properties
|
||||||
|
request_object: Any = None
|
||||||
|
# `Request Response` level
|
||||||
|
response_object: Any = None
|
||||||
|
response_status_code: Optional[int] = None
|
||||||
|
|
||||||
|
|
||||||
|
class AuditLevel(str, Enum):
|
||||||
|
NONE = "NONE"
|
||||||
|
METADATA = "METADATA"
|
||||||
|
REQUEST = "REQUEST"
|
||||||
|
REQUEST_RESPONSE = "REQUEST_RESPONSE"
|
||||||
|
|
||||||
|
|
||||||
|
class AuditLogger:
|
||||||
|
"""
|
||||||
|
A helper class that encapsulates audit logging functionality. It uses Loguru’s logger with an auditable binding to ensure that audit log entries are filtered correctly.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
logger (Logger): An instance of Loguru’s logger.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, logger: "Logger"):
|
||||||
|
self.logger = logger.bind(auditable=True)
|
||||||
|
|
||||||
|
def write(
|
||||||
|
self,
|
||||||
|
audit_entry: AuditLogEntry,
|
||||||
|
*,
|
||||||
|
log_level: str = "INFO",
|
||||||
|
extra: Optional[dict] = None,
|
||||||
|
):
|
||||||
|
|
||||||
|
entry = asdict(audit_entry)
|
||||||
|
|
||||||
|
if extra:
|
||||||
|
entry["extra"] = extra
|
||||||
|
|
||||||
|
self.logger.log(
|
||||||
|
log_level,
|
||||||
|
"",
|
||||||
|
**entry,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class AuditContext:
|
||||||
|
"""
|
||||||
|
Captures and aggregates the HTTP request and response bodies during the processing of a request. It ensures that only a configurable maximum amount of data is stored to prevent excessive memory usage.
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
request_body (bytearray): Accumulated request payload.
|
||||||
|
response_body (bytearray): Accumulated response payload.
|
||||||
|
max_body_size (int): Maximum number of bytes to capture.
|
||||||
|
metadata (Dict[str, Any]): A dictionary to store additional audit metadata (user, http verb, user agent, etc.).
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, max_body_size: int = MAX_BODY_LOG_SIZE):
|
||||||
|
self.request_body = bytearray()
|
||||||
|
self.response_body = bytearray()
|
||||||
|
self.max_body_size = max_body_size
|
||||||
|
self.metadata: Dict[str, Any] = {}
|
||||||
|
|
||||||
|
def add_request_chunk(self, chunk: bytes):
|
||||||
|
if len(self.request_body) < self.max_body_size:
|
||||||
|
self.request_body.extend(
|
||||||
|
chunk[: self.max_body_size - len(self.request_body)]
|
||||||
|
)
|
||||||
|
|
||||||
|
def add_response_chunk(self, chunk: bytes):
|
||||||
|
if len(self.response_body) < self.max_body_size:
|
||||||
|
self.response_body.extend(
|
||||||
|
chunk[: self.max_body_size - len(self.response_body)]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class AuditLoggingMiddleware:
|
||||||
|
"""
|
||||||
|
ASGI middleware that intercepts HTTP requests and responses to perform audit logging. It captures request/response bodies (depending on audit level), headers, HTTP methods, and user information, then logs a structured audit entry at the end of the request cycle.
|
||||||
|
"""
|
||||||
|
|
||||||
|
AUDITED_METHODS = {"PUT", "PATCH", "DELETE", "POST"}
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
app: ASGI3Application,
|
||||||
|
*,
|
||||||
|
excluded_paths: Optional[list[str]] = None,
|
||||||
|
max_body_size: int = MAX_BODY_LOG_SIZE,
|
||||||
|
audit_level: AuditLevel = AuditLevel.NONE,
|
||||||
|
) -> None:
|
||||||
|
self.app = app
|
||||||
|
self.audit_logger = AuditLogger(logger)
|
||||||
|
self.excluded_paths = excluded_paths or []
|
||||||
|
self.max_body_size = max_body_size
|
||||||
|
self.audit_level = audit_level
|
||||||
|
|
||||||
|
async def __call__(
|
||||||
|
self,
|
||||||
|
scope: ASGIScope,
|
||||||
|
receive: ASGIReceiveCallable,
|
||||||
|
send: ASGISendCallable,
|
||||||
|
) -> None:
|
||||||
|
if scope["type"] != "http":
|
||||||
|
return await self.app(scope, receive, send)
|
||||||
|
|
||||||
|
request = Request(scope=cast(MutableMapping, scope))
|
||||||
|
|
||||||
|
if self._should_skip_auditing(request):
|
||||||
|
return await self.app(scope, receive, send)
|
||||||
|
|
||||||
|
async with self._audit_context(request) as context:
|
||||||
|
|
||||||
|
async def send_wrapper(message: ASGISendEvent) -> None:
|
||||||
|
if self.audit_level == AuditLevel.REQUEST_RESPONSE:
|
||||||
|
await self._capture_response(message, context)
|
||||||
|
|
||||||
|
await send(message)
|
||||||
|
|
||||||
|
original_receive = receive
|
||||||
|
|
||||||
|
async def receive_wrapper() -> ASGIReceiveEvent:
|
||||||
|
nonlocal original_receive
|
||||||
|
message = await original_receive()
|
||||||
|
|
||||||
|
if self.audit_level in (
|
||||||
|
AuditLevel.REQUEST,
|
||||||
|
AuditLevel.REQUEST_RESPONSE,
|
||||||
|
):
|
||||||
|
await self._capture_request(message, context)
|
||||||
|
|
||||||
|
return message
|
||||||
|
|
||||||
|
await self.app(scope, receive_wrapper, send_wrapper)
|
||||||
|
|
||||||
|
@asynccontextmanager
|
||||||
|
async def _audit_context(
|
||||||
|
self, request: Request
|
||||||
|
) -> AsyncGenerator[AuditContext, None]:
|
||||||
|
"""
|
||||||
|
async context manager that ensures that an audit log entry is recorded after the request is processed.
|
||||||
|
"""
|
||||||
|
context = AuditContext()
|
||||||
|
try:
|
||||||
|
yield context
|
||||||
|
finally:
|
||||||
|
await self._log_audit_entry(request, context)
|
||||||
|
|
||||||
|
async def _get_authenticated_user(self, request: Request) -> UserModel:
|
||||||
|
|
||||||
|
auth_header = request.headers.get("Authorization")
|
||||||
|
assert auth_header
|
||||||
|
user = get_current_user(request, None, get_http_authorization_cred(auth_header))
|
||||||
|
|
||||||
|
return user
|
||||||
|
|
||||||
|
def _should_skip_auditing(self, request: Request) -> bool:
|
||||||
|
if (
|
||||||
|
request.method not in {"POST", "PUT", "PATCH", "DELETE"}
|
||||||
|
or AUDIT_LOG_LEVEL == "NONE"
|
||||||
|
or not request.headers.get("authorization")
|
||||||
|
):
|
||||||
|
return True
|
||||||
|
# match either /api/<resource>/...(for the endpoint /api/chat case) or /api/v1/<resource>/...
|
||||||
|
pattern = re.compile(
|
||||||
|
r"^/api(?:/v1)?/(" + "|".join(self.excluded_paths) + r")\b"
|
||||||
|
)
|
||||||
|
if pattern.match(request.url.path):
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
async def _capture_request(self, message: ASGIReceiveEvent, context: AuditContext):
|
||||||
|
if message["type"] == "http.request":
|
||||||
|
body = message.get("body", b"")
|
||||||
|
context.add_request_chunk(body)
|
||||||
|
|
||||||
|
async def _capture_response(self, message: ASGISendEvent, context: AuditContext):
|
||||||
|
if message["type"] == "http.response.start":
|
||||||
|
context.metadata["response_status_code"] = message["status"]
|
||||||
|
|
||||||
|
elif message["type"] == "http.response.body":
|
||||||
|
body = message.get("body", b"")
|
||||||
|
context.add_response_chunk(body)
|
||||||
|
|
||||||
|
async def _log_audit_entry(self, request: Request, context: AuditContext):
|
||||||
|
try:
|
||||||
|
user = await self._get_authenticated_user(request)
|
||||||
|
|
||||||
|
entry = AuditLogEntry(
|
||||||
|
id=str(uuid.uuid4()),
|
||||||
|
user=user.model_dump(include={"id", "name", "email", "role"}),
|
||||||
|
audit_level=self.audit_level.value,
|
||||||
|
verb=request.method,
|
||||||
|
request_uri=str(request.url),
|
||||||
|
response_status_code=context.metadata.get("response_status_code", None),
|
||||||
|
source_ip=request.client.host if request.client else None,
|
||||||
|
user_agent=request.headers.get("user-agent"),
|
||||||
|
request_object=context.request_body.decode("utf-8", errors="replace"),
|
||||||
|
response_object=context.response_body.decode("utf-8", errors="replace"),
|
||||||
|
)
|
||||||
|
|
||||||
|
self.audit_logger.write(entry)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to log audit entry: {str(e)}")
|
||||||
@@ -14,14 +14,22 @@ from typing import Optional, Union, List, Dict
|
|||||||
from open_webui.models.users import Users
|
from open_webui.models.users import Users
|
||||||
|
|
||||||
from open_webui.constants import ERROR_MESSAGES
|
from open_webui.constants import ERROR_MESSAGES
|
||||||
from open_webui.env import WEBUI_SECRET_KEY, TRUSTED_SIGNATURE_KEY, STATIC_DIR
|
from open_webui.env import (
|
||||||
|
WEBUI_SECRET_KEY,
|
||||||
|
TRUSTED_SIGNATURE_KEY,
|
||||||
|
STATIC_DIR,
|
||||||
|
SRC_LOG_LEVELS,
|
||||||
|
)
|
||||||
|
|
||||||
from fastapi import Depends, HTTPException, Request, Response, status
|
from fastapi import BackgroundTasks, Depends, HTTPException, Request, Response, status
|
||||||
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
|
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
|
||||||
from passlib.context import CryptContext
|
from passlib.context import CryptContext
|
||||||
|
|
||||||
|
|
||||||
logging.getLogger("passlib").setLevel(logging.ERROR)
|
logging.getLogger("passlib").setLevel(logging.ERROR)
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
log.setLevel(SRC_LOG_LEVELS["OAUTH"])
|
||||||
|
|
||||||
SESSION_SECRET = WEBUI_SECRET_KEY
|
SESSION_SECRET = WEBUI_SECRET_KEY
|
||||||
ALGORITHM = "HS256"
|
ALGORITHM = "HS256"
|
||||||
@@ -50,7 +58,7 @@ def verify_signature(payload: str, signature: str) -> bool:
|
|||||||
def override_static(path: str, content: str):
|
def override_static(path: str, content: str):
|
||||||
# Ensure path is safe
|
# Ensure path is safe
|
||||||
if "/" in path or ".." in path:
|
if "/" in path or ".." in path:
|
||||||
print(f"Invalid path: {path}")
|
log.error(f"Invalid path: {path}")
|
||||||
return
|
return
|
||||||
|
|
||||||
file_path = os.path.join(STATIC_DIR, path)
|
file_path = os.path.join(STATIC_DIR, path)
|
||||||
@@ -64,7 +72,7 @@ def get_license_data(app, key):
|
|||||||
if key:
|
if key:
|
||||||
try:
|
try:
|
||||||
res = requests.post(
|
res = requests.post(
|
||||||
"https://api.openwebui.com/api/v1/license",
|
"https://api.openwebui.com/api/v1/license/",
|
||||||
json={"key": key, "version": "1"},
|
json={"key": key, "version": "1"},
|
||||||
timeout=5,
|
timeout=5,
|
||||||
)
|
)
|
||||||
@@ -75,18 +83,19 @@ def get_license_data(app, key):
|
|||||||
if k == "resources":
|
if k == "resources":
|
||||||
for p, c in v.items():
|
for p, c in v.items():
|
||||||
globals().get("override_static", lambda a, b: None)(p, c)
|
globals().get("override_static", lambda a, b: None)(p, c)
|
||||||
elif k == "user_count":
|
elif k == "count":
|
||||||
setattr(app.state, "USER_COUNT", v)
|
setattr(app.state, "USER_COUNT", v)
|
||||||
elif k == "webui_name":
|
elif k == "name":
|
||||||
setattr(app.state, "WEBUI_NAME", v)
|
setattr(app.state, "WEBUI_NAME", v)
|
||||||
|
elif k == "metadata":
|
||||||
|
setattr(app.state, "LICENSE_METADATA", v)
|
||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
print(
|
log.error(
|
||||||
f"License: retrieval issue: {getattr(res, 'text', 'unknown error')}"
|
f"License: retrieval issue: {getattr(res, 'text', 'unknown error')}"
|
||||||
)
|
)
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
print(f"License: Uncaught Exception: {ex}")
|
log.exception(f"License: Uncaught Exception: {ex}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
@@ -142,6 +151,7 @@ def get_http_authorization_cred(auth_header: str):
|
|||||||
|
|
||||||
def get_current_user(
|
def get_current_user(
|
||||||
request: Request,
|
request: Request,
|
||||||
|
background_tasks: BackgroundTasks,
|
||||||
auth_token: HTTPAuthorizationCredentials = Depends(bearer_security),
|
auth_token: HTTPAuthorizationCredentials = Depends(bearer_security),
|
||||||
):
|
):
|
||||||
token = None
|
token = None
|
||||||
@@ -194,7 +204,10 @@ def get_current_user(
|
|||||||
detail=ERROR_MESSAGES.INVALID_TOKEN,
|
detail=ERROR_MESSAGES.INVALID_TOKEN,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
Users.update_user_last_active_by_id(user.id)
|
# Refresh the user's last active timestamp asynchronously
|
||||||
|
# to prevent blocking the request
|
||||||
|
if background_tasks:
|
||||||
|
background_tasks.add_task(Users.update_user_last_active_by_id, user.id)
|
||||||
return user
|
return user
|
||||||
else:
|
else:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ async def generate_direct_chat_completion(
|
|||||||
user: Any,
|
user: Any,
|
||||||
models: dict,
|
models: dict,
|
||||||
):
|
):
|
||||||
print("generate_direct_chat_completion")
|
log.info("generate_direct_chat_completion")
|
||||||
|
|
||||||
metadata = form_data.pop("metadata", {})
|
metadata = form_data.pop("metadata", {})
|
||||||
|
|
||||||
@@ -103,7 +103,7 @@ async def generate_direct_chat_completion(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
print("res", res)
|
log.info(f"res: {res}")
|
||||||
|
|
||||||
if res.get("status", False):
|
if res.get("status", False):
|
||||||
# Define a generator to stream responses
|
# Define a generator to stream responses
|
||||||
@@ -149,7 +149,7 @@ async def generate_direct_chat_completion(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
if "error" in res:
|
if "error" in res and res["error"]:
|
||||||
raise Exception(res["error"])
|
raise Exception(res["error"])
|
||||||
|
|
||||||
return res
|
return res
|
||||||
@@ -285,7 +285,7 @@ chat_completion = generate_chat_completion
|
|||||||
|
|
||||||
async def chat_completed(request: Request, form_data: dict, user: Any):
|
async def chat_completed(request: Request, form_data: dict, user: Any):
|
||||||
if not request.app.state.MODELS:
|
if not request.app.state.MODELS:
|
||||||
await get_all_models(request)
|
await get_all_models(request, user=user)
|
||||||
|
|
||||||
if getattr(request.state, "direct", False) and hasattr(request.state, "model"):
|
if getattr(request.state, "direct", False) and hasattr(request.state, "model"):
|
||||||
models = {
|
models = {
|
||||||
@@ -328,9 +328,14 @@ async def chat_completed(request: Request, form_data: dict, user: Any):
|
|||||||
}
|
}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
filter_functions = [
|
||||||
|
Functions.get_function_by_id(filter_id)
|
||||||
|
for filter_id in get_sorted_filter_ids(model)
|
||||||
|
]
|
||||||
|
|
||||||
result, _ = await process_filter_functions(
|
result, _ = await process_filter_functions(
|
||||||
request=request,
|
request=request,
|
||||||
filter_ids=get_sorted_filter_ids(model),
|
filter_functions=filter_functions,
|
||||||
filter_type="outlet",
|
filter_type="outlet",
|
||||||
form_data=data,
|
form_data=data,
|
||||||
extra_params=extra_params,
|
extra_params=extra_params,
|
||||||
@@ -351,7 +356,7 @@ async def chat_action(request: Request, action_id: str, form_data: dict, user: A
|
|||||||
raise Exception(f"Action not found: {action_id}")
|
raise Exception(f"Action not found: {action_id}")
|
||||||
|
|
||||||
if not request.app.state.MODELS:
|
if not request.app.state.MODELS:
|
||||||
await get_all_models(request)
|
await get_all_models(request, user=user)
|
||||||
|
|
||||||
if getattr(request.state, "direct", False) and hasattr(request.state, "model"):
|
if getattr(request.state, "direct", False) and hasattr(request.state, "model"):
|
||||||
models = {
|
models = {
|
||||||
@@ -432,7 +437,7 @@ async def chat_action(request: Request, action_id: str, form_data: dict, user: A
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(e)
|
log.exception(f"Failed to get user values: {e}")
|
||||||
|
|
||||||
params = {**params, "__user__": __user__}
|
params = {**params, "__user__": __user__}
|
||||||
|
|
||||||
|
|||||||
@@ -1,148 +1,210 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
import json
|
import json
|
||||||
|
import logging
|
||||||
import uuid
|
import uuid
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
import aiohttp
|
||||||
import websockets
|
import websockets
|
||||||
import requests
|
from pydantic import BaseModel
|
||||||
from urllib.parse import urljoin
|
|
||||||
|
from open_webui.env import SRC_LOG_LEVELS
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
logger.setLevel(SRC_LOG_LEVELS["MAIN"])
|
||||||
|
|
||||||
|
|
||||||
async def execute_code_jupyter(
|
class ResultModel(BaseModel):
|
||||||
jupyter_url, code, token=None, password=None, timeout=10
|
|
||||||
):
|
|
||||||
"""
|
"""
|
||||||
Executes Python code in a Jupyter kernel.
|
Execute Code Result Model
|
||||||
Supports authentication with a token or password.
|
|
||||||
:param jupyter_url: Jupyter server URL (e.g., "http://localhost:8888")
|
|
||||||
:param code: Code to execute
|
|
||||||
:param token: Jupyter authentication token (optional)
|
|
||||||
:param password: Jupyter password (optional)
|
|
||||||
:param timeout: WebSocket timeout in seconds (default: 10s)
|
|
||||||
:return: Dictionary with stdout, stderr, and result
|
|
||||||
- Images are prefixed with "base64:image/png," and separated by newlines if multiple.
|
|
||||||
"""
|
"""
|
||||||
session = requests.Session() # Maintain cookies
|
|
||||||
headers = {} # Headers for requests
|
|
||||||
|
|
||||||
# Authenticate using password
|
stdout: Optional[str] = ""
|
||||||
if password and not token:
|
stderr: Optional[str] = ""
|
||||||
|
result: Optional[str] = ""
|
||||||
|
|
||||||
|
|
||||||
|
class JupyterCodeExecuter:
|
||||||
|
"""
|
||||||
|
Execute code in jupyter notebook
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
base_url: str,
|
||||||
|
code: str,
|
||||||
|
token: str = "",
|
||||||
|
password: str = "",
|
||||||
|
timeout: int = 60,
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
:param base_url: Jupyter server URL (e.g., "http://localhost:8888")
|
||||||
|
:param code: Code to execute
|
||||||
|
:param token: Jupyter authentication token (optional)
|
||||||
|
:param password: Jupyter password (optional)
|
||||||
|
:param timeout: WebSocket timeout in seconds (default: 60s)
|
||||||
|
"""
|
||||||
|
self.base_url = base_url.rstrip("/")
|
||||||
|
self.code = code
|
||||||
|
self.token = token
|
||||||
|
self.password = password
|
||||||
|
self.timeout = timeout
|
||||||
|
self.kernel_id = ""
|
||||||
|
self.session = aiohttp.ClientSession(base_url=self.base_url)
|
||||||
|
self.params = {}
|
||||||
|
self.result = ResultModel()
|
||||||
|
|
||||||
|
async def __aenter__(self):
|
||||||
|
return self
|
||||||
|
|
||||||
|
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
||||||
|
if self.kernel_id:
|
||||||
|
try:
|
||||||
|
async with self.session.delete(
|
||||||
|
f"/api/kernels/{self.kernel_id}", params=self.params
|
||||||
|
) as response:
|
||||||
|
response.raise_for_status()
|
||||||
|
except Exception as err:
|
||||||
|
logger.exception("close kernel failed, %s", err)
|
||||||
|
await self.session.close()
|
||||||
|
|
||||||
|
async def run(self) -> ResultModel:
|
||||||
try:
|
try:
|
||||||
login_url = urljoin(jupyter_url, "/login")
|
await self.sign_in()
|
||||||
response = session.get(login_url)
|
await self.init_kernel()
|
||||||
|
await self.execute_code()
|
||||||
|
except Exception as err:
|
||||||
|
logger.exception("execute code failed, %s", err)
|
||||||
|
self.result.stderr = f"Error: {err}"
|
||||||
|
return self.result
|
||||||
|
|
||||||
|
async def sign_in(self) -> None:
|
||||||
|
# password authentication
|
||||||
|
if self.password and not self.token:
|
||||||
|
async with self.session.get("/login") as response:
|
||||||
|
response.raise_for_status()
|
||||||
|
xsrf_token = response.cookies["_xsrf"].value
|
||||||
|
if not xsrf_token:
|
||||||
|
raise ValueError("_xsrf token not found")
|
||||||
|
self.session.cookie_jar.update_cookies(response.cookies)
|
||||||
|
self.session.headers.update({"X-XSRFToken": xsrf_token})
|
||||||
|
async with self.session.post(
|
||||||
|
"/login",
|
||||||
|
data={"_xsrf": xsrf_token, "password": self.password},
|
||||||
|
allow_redirects=False,
|
||||||
|
) as response:
|
||||||
|
response.raise_for_status()
|
||||||
|
self.session.cookie_jar.update_cookies(response.cookies)
|
||||||
|
|
||||||
|
# token authentication
|
||||||
|
if self.token:
|
||||||
|
self.params.update({"token": self.token})
|
||||||
|
|
||||||
|
async def init_kernel(self) -> None:
|
||||||
|
async with self.session.post(
|
||||||
|
url="/api/kernels", params=self.params
|
||||||
|
) as response:
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
xsrf_token = session.cookies.get("_xsrf")
|
kernel_data = await response.json()
|
||||||
if not xsrf_token:
|
self.kernel_id = kernel_data["id"]
|
||||||
raise ValueError("Failed to fetch _xsrf token")
|
|
||||||
|
|
||||||
login_data = {"_xsrf": xsrf_token, "password": password}
|
|
||||||
login_response = session.post(
|
|
||||||
login_url, data=login_data, cookies=session.cookies
|
|
||||||
)
|
|
||||||
login_response.raise_for_status()
|
|
||||||
headers["X-XSRFToken"] = xsrf_token
|
|
||||||
except Exception as e:
|
|
||||||
return {
|
|
||||||
"stdout": "",
|
|
||||||
"stderr": f"Authentication Error: {str(e)}",
|
|
||||||
"result": "",
|
|
||||||
}
|
|
||||||
|
|
||||||
# Construct API URLs with authentication token if provided
|
|
||||||
params = f"?token={token}" if token else ""
|
|
||||||
kernel_url = urljoin(jupyter_url, f"/api/kernels{params}")
|
|
||||||
|
|
||||||
try:
|
|
||||||
response = session.post(kernel_url, headers=headers, cookies=session.cookies)
|
|
||||||
response.raise_for_status()
|
|
||||||
kernel_id = response.json()["id"]
|
|
||||||
|
|
||||||
websocket_url = urljoin(
|
|
||||||
jupyter_url.replace("http", "ws"),
|
|
||||||
f"/api/kernels/{kernel_id}/channels{params}",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
def init_ws(self) -> (str, dict):
|
||||||
|
ws_base = self.base_url.replace("http", "ws")
|
||||||
|
ws_params = "?" + "&".join([f"{key}={val}" for key, val in self.params.items()])
|
||||||
|
websocket_url = f"{ws_base}/api/kernels/{self.kernel_id}/channels{ws_params if len(ws_params) > 1 else ''}"
|
||||||
ws_headers = {}
|
ws_headers = {}
|
||||||
if password and not token:
|
if self.password and not self.token:
|
||||||
ws_headers["X-XSRFToken"] = session.cookies.get("_xsrf")
|
ws_headers = {
|
||||||
cookies = {name: value for name, value in session.cookies.items()}
|
"Cookie": "; ".join(
|
||||||
ws_headers["Cookie"] = "; ".join(
|
[
|
||||||
[f"{name}={value}" for name, value in cookies.items()]
|
f"{cookie.key}={cookie.value}"
|
||||||
)
|
for cookie in self.session.cookie_jar
|
||||||
|
]
|
||||||
|
),
|
||||||
|
**self.session.headers,
|
||||||
|
}
|
||||||
|
return websocket_url, ws_headers
|
||||||
|
|
||||||
|
async def execute_code(self) -> None:
|
||||||
|
# initialize ws
|
||||||
|
websocket_url, ws_headers = self.init_ws()
|
||||||
|
# execute
|
||||||
async with websockets.connect(
|
async with websockets.connect(
|
||||||
websocket_url, additional_headers=ws_headers
|
websocket_url, additional_headers=ws_headers
|
||||||
) as ws:
|
) as ws:
|
||||||
msg_id = str(uuid.uuid4())
|
await self.execute_in_jupyter(ws)
|
||||||
execute_request = {
|
|
||||||
"header": {
|
|
||||||
"msg_id": msg_id,
|
|
||||||
"msg_type": "execute_request",
|
|
||||||
"username": "user",
|
|
||||||
"session": str(uuid.uuid4()),
|
|
||||||
"date": "",
|
|
||||||
"version": "5.3",
|
|
||||||
},
|
|
||||||
"parent_header": {},
|
|
||||||
"metadata": {},
|
|
||||||
"content": {
|
|
||||||
"code": code,
|
|
||||||
"silent": False,
|
|
||||||
"store_history": True,
|
|
||||||
"user_expressions": {},
|
|
||||||
"allow_stdin": False,
|
|
||||||
"stop_on_error": True,
|
|
||||||
},
|
|
||||||
"channel": "shell",
|
|
||||||
}
|
|
||||||
await ws.send(json.dumps(execute_request))
|
|
||||||
|
|
||||||
stdout, stderr, result = "", "", []
|
async def execute_in_jupyter(self, ws) -> None:
|
||||||
|
# send message
|
||||||
while True:
|
msg_id = uuid.uuid4().hex
|
||||||
try:
|
await ws.send(
|
||||||
message = await asyncio.wait_for(ws.recv(), timeout)
|
json.dumps(
|
||||||
message_data = json.loads(message)
|
{
|
||||||
if message_data.get("parent_header", {}).get("msg_id") == msg_id:
|
"header": {
|
||||||
msg_type = message_data.get("msg_type")
|
"msg_id": msg_id,
|
||||||
|
"msg_type": "execute_request",
|
||||||
if msg_type == "stream":
|
"username": "user",
|
||||||
if message_data["content"]["name"] == "stdout":
|
"session": uuid.uuid4().hex,
|
||||||
stdout += message_data["content"]["text"]
|
"date": "",
|
||||||
elif message_data["content"]["name"] == "stderr":
|
"version": "5.3",
|
||||||
stderr += message_data["content"]["text"]
|
},
|
||||||
|
"parent_header": {},
|
||||||
elif msg_type in ("execute_result", "display_data"):
|
"metadata": {},
|
||||||
data = message_data["content"]["data"]
|
"content": {
|
||||||
if "image/png" in data:
|
"code": self.code,
|
||||||
result.append(
|
"silent": False,
|
||||||
f"data:image/png;base64,{data['image/png']}"
|
"store_history": True,
|
||||||
)
|
"user_expressions": {},
|
||||||
elif "text/plain" in data:
|
"allow_stdin": False,
|
||||||
result.append(data["text/plain"])
|
"stop_on_error": True,
|
||||||
|
},
|
||||||
elif msg_type == "error":
|
"channel": "shell",
|
||||||
stderr += "\n".join(message_data["content"]["traceback"])
|
}
|
||||||
|
)
|
||||||
elif (
|
)
|
||||||
msg_type == "status"
|
# parse message
|
||||||
and message_data["content"]["execution_state"] == "idle"
|
stdout, stderr, result = "", "", []
|
||||||
):
|
while True:
|
||||||
|
try:
|
||||||
|
# wait for message
|
||||||
|
message = await asyncio.wait_for(ws.recv(), self.timeout)
|
||||||
|
message_data = json.loads(message)
|
||||||
|
# msg id not match, skip
|
||||||
|
if message_data.get("parent_header", {}).get("msg_id") != msg_id:
|
||||||
|
continue
|
||||||
|
# check message type
|
||||||
|
msg_type = message_data.get("msg_type")
|
||||||
|
match msg_type:
|
||||||
|
case "stream":
|
||||||
|
if message_data["content"]["name"] == "stdout":
|
||||||
|
stdout += message_data["content"]["text"]
|
||||||
|
elif message_data["content"]["name"] == "stderr":
|
||||||
|
stderr += message_data["content"]["text"]
|
||||||
|
case "execute_result" | "display_data":
|
||||||
|
data = message_data["content"]["data"]
|
||||||
|
if "image/png" in data:
|
||||||
|
result.append(f"data:image/png;base64,{data['image/png']}")
|
||||||
|
elif "text/plain" in data:
|
||||||
|
result.append(data["text/plain"])
|
||||||
|
case "error":
|
||||||
|
stderr += "\n".join(message_data["content"]["traceback"])
|
||||||
|
case "status":
|
||||||
|
if message_data["content"]["execution_state"] == "idle":
|
||||||
break
|
break
|
||||||
|
|
||||||
except asyncio.TimeoutError:
|
except asyncio.TimeoutError:
|
||||||
stderr += "\nExecution timed out."
|
stderr += "\nExecution timed out."
|
||||||
break
|
break
|
||||||
|
self.result.stdout = stdout.strip()
|
||||||
|
self.result.stderr = stderr.strip()
|
||||||
|
self.result.result = "\n".join(result).strip() if result else ""
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
return {"stdout": "", "stderr": f"Error: {str(e)}", "result": ""}
|
|
||||||
|
|
||||||
finally:
|
async def execute_code_jupyter(
|
||||||
if kernel_id:
|
base_url: str, code: str, token: str = "", password: str = "", timeout: int = 60
|
||||||
requests.delete(
|
) -> dict:
|
||||||
f"{kernel_url}/{kernel_id}", headers=headers, cookies=session.cookies
|
async with JupyterCodeExecuter(
|
||||||
)
|
base_url, code, token, password, timeout
|
||||||
|
) as executor:
|
||||||
return {
|
result = await executor.run()
|
||||||
"stdout": stdout.strip(),
|
return result.model_dump()
|
||||||
"stderr": stderr.strip(),
|
|
||||||
"result": "\n".join(result).strip() if result else "",
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,9 +1,15 @@
|
|||||||
import inspect
|
import inspect
|
||||||
|
import logging
|
||||||
|
|
||||||
from open_webui.utils.plugin import load_function_module_by_id
|
from open_webui.utils.plugin import load_function_module_by_id
|
||||||
from open_webui.models.functions import Functions
|
from open_webui.models.functions import Functions
|
||||||
|
from open_webui.env import SRC_LOG_LEVELS
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
log.setLevel(SRC_LOG_LEVELS["MAIN"])
|
||||||
|
|
||||||
|
|
||||||
def get_sorted_filter_ids(model):
|
def get_sorted_filter_ids(model: dict):
|
||||||
def get_priority(function_id):
|
def get_priority(function_id):
|
||||||
function = Functions.get_function_by_id(function_id)
|
function = Functions.get_function_by_id(function_id)
|
||||||
if function is not None and hasattr(function, "valves"):
|
if function is not None and hasattr(function, "valves"):
|
||||||
@@ -27,12 +33,13 @@ def get_sorted_filter_ids(model):
|
|||||||
|
|
||||||
|
|
||||||
async def process_filter_functions(
|
async def process_filter_functions(
|
||||||
request, filter_ids, filter_type, form_data, extra_params
|
request, filter_functions, filter_type, form_data, extra_params
|
||||||
):
|
):
|
||||||
skip_files = None
|
skip_files = None
|
||||||
|
|
||||||
for filter_id in filter_ids:
|
for function in filter_functions:
|
||||||
filter = Functions.get_function_by_id(filter_id)
|
filter = function
|
||||||
|
filter_id = function.id
|
||||||
if not filter:
|
if not filter:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
@@ -42,6 +49,11 @@ async def process_filter_functions(
|
|||||||
function_module, _, _ = load_function_module_by_id(filter_id)
|
function_module, _, _ = load_function_module_by_id(filter_id)
|
||||||
request.app.state.FUNCTIONS[filter_id] = function_module
|
request.app.state.FUNCTIONS[filter_id] = function_module
|
||||||
|
|
||||||
|
# Prepare handler function
|
||||||
|
handler = getattr(function_module, filter_type, None)
|
||||||
|
if not handler:
|
||||||
|
continue
|
||||||
|
|
||||||
# Check if the function has a file_handler variable
|
# Check if the function has a file_handler variable
|
||||||
if filter_type == "inlet" and hasattr(function_module, "file_handler"):
|
if filter_type == "inlet" and hasattr(function_module, "file_handler"):
|
||||||
skip_files = function_module.file_handler
|
skip_files = function_module.file_handler
|
||||||
@@ -53,15 +65,15 @@ async def process_filter_functions(
|
|||||||
**(valves if valves else {})
|
**(valves if valves else {})
|
||||||
)
|
)
|
||||||
|
|
||||||
# Prepare handler function
|
|
||||||
handler = getattr(function_module, filter_type, None)
|
|
||||||
if not handler:
|
|
||||||
continue
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Prepare parameters
|
# Prepare parameters
|
||||||
sig = inspect.signature(handler)
|
sig = inspect.signature(handler)
|
||||||
params = {"body": form_data} | {
|
|
||||||
|
params = {"body": form_data}
|
||||||
|
if filter_type == "stream":
|
||||||
|
params = {"event": form_data}
|
||||||
|
|
||||||
|
params = params | {
|
||||||
k: v
|
k: v
|
||||||
for k, v in {
|
for k, v in {
|
||||||
**extra_params,
|
**extra_params,
|
||||||
@@ -80,7 +92,7 @@ async def process_filter_functions(
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(e)
|
log.exception(f"Failed to get user values: {e}")
|
||||||
|
|
||||||
# Execute handler
|
# Execute handler
|
||||||
if inspect.iscoroutinefunction(handler):
|
if inspect.iscoroutinefunction(handler):
|
||||||
@@ -89,7 +101,7 @@ async def process_filter_functions(
|
|||||||
form_data = handler(**params)
|
form_data = handler(**params)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error in {filter_type} handler {filter_id}: {e}")
|
log.exception(f"Error in {filter_type} handler {filter_id}: {e}")
|
||||||
raise e
|
raise e
|
||||||
|
|
||||||
# Handle file cleanup for inlet
|
# Handle file cleanup for inlet
|
||||||
|
|||||||
@@ -0,0 +1,140 @@
|
|||||||
|
import json
|
||||||
|
import logging
|
||||||
|
import sys
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
from loguru import logger
|
||||||
|
|
||||||
|
from open_webui.env import (
|
||||||
|
AUDIT_LOG_FILE_ROTATION_SIZE,
|
||||||
|
AUDIT_LOG_LEVEL,
|
||||||
|
AUDIT_LOGS_FILE_PATH,
|
||||||
|
GLOBAL_LOG_LEVEL,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from loguru import Record
|
||||||
|
|
||||||
|
|
||||||
|
def stdout_format(record: "Record") -> str:
|
||||||
|
"""
|
||||||
|
Generates a formatted string for log records that are output to the console. This format includes a timestamp, log level, source location (module, function, and line), the log message, and any extra data (serialized as JSON).
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
record (Record): A Loguru record that contains logging details including time, level, name, function, line, message, and any extra context.
|
||||||
|
Returns:
|
||||||
|
str: A formatted log string intended for stdout.
|
||||||
|
"""
|
||||||
|
record["extra"]["extra_json"] = json.dumps(record["extra"])
|
||||||
|
return (
|
||||||
|
"<green>{time:YYYY-MM-DD HH:mm:ss.SSS}</green> | "
|
||||||
|
"<level>{level: <8}</level> | "
|
||||||
|
"<cyan>{name}</cyan>:<cyan>{function}</cyan>:<cyan>{line}</cyan> - "
|
||||||
|
"<level>{message}</level> - {extra[extra_json]}"
|
||||||
|
"\n{exception}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class InterceptHandler(logging.Handler):
|
||||||
|
"""
|
||||||
|
Intercepts log records from Python's standard logging module
|
||||||
|
and redirects them to Loguru's logger.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def emit(self, record):
|
||||||
|
"""
|
||||||
|
Called by the standard logging module for each log event.
|
||||||
|
It transforms the standard `LogRecord` into a format compatible with Loguru
|
||||||
|
and passes it to Loguru's logger.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
level = logger.level(record.levelname).name
|
||||||
|
except ValueError:
|
||||||
|
level = record.levelno
|
||||||
|
|
||||||
|
frame, depth = sys._getframe(6), 6
|
||||||
|
while frame and frame.f_code.co_filename == logging.__file__:
|
||||||
|
frame = frame.f_back
|
||||||
|
depth += 1
|
||||||
|
|
||||||
|
logger.opt(depth=depth, exception=record.exc_info).log(
|
||||||
|
level, record.getMessage()
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def file_format(record: "Record"):
|
||||||
|
"""
|
||||||
|
Formats audit log records into a structured JSON string for file output.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
record (Record): A Loguru record containing extra audit data.
|
||||||
|
Returns:
|
||||||
|
str: A JSON-formatted string representing the audit data.
|
||||||
|
"""
|
||||||
|
|
||||||
|
audit_data = {
|
||||||
|
"id": record["extra"].get("id", ""),
|
||||||
|
"timestamp": int(record["time"].timestamp()),
|
||||||
|
"user": record["extra"].get("user", dict()),
|
||||||
|
"audit_level": record["extra"].get("audit_level", ""),
|
||||||
|
"verb": record["extra"].get("verb", ""),
|
||||||
|
"request_uri": record["extra"].get("request_uri", ""),
|
||||||
|
"response_status_code": record["extra"].get("response_status_code", 0),
|
||||||
|
"source_ip": record["extra"].get("source_ip", ""),
|
||||||
|
"user_agent": record["extra"].get("user_agent", ""),
|
||||||
|
"request_object": record["extra"].get("request_object", b""),
|
||||||
|
"response_object": record["extra"].get("response_object", b""),
|
||||||
|
"extra": record["extra"].get("extra", {}),
|
||||||
|
}
|
||||||
|
|
||||||
|
record["extra"]["file_extra"] = json.dumps(audit_data, default=str)
|
||||||
|
return "{extra[file_extra]}\n"
|
||||||
|
|
||||||
|
|
||||||
|
def start_logger():
|
||||||
|
"""
|
||||||
|
Initializes and configures Loguru's logger with distinct handlers:
|
||||||
|
|
||||||
|
A console (stdout) handler for general log messages (excluding those marked as auditable).
|
||||||
|
An optional file handler for audit logs if audit logging is enabled.
|
||||||
|
Additionally, this function reconfigures Python’s standard logging to route through Loguru and adjusts logging levels for Uvicorn.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
enable_audit_logging (bool): Determines whether audit-specific log entries should be recorded to file.
|
||||||
|
"""
|
||||||
|
logger.remove()
|
||||||
|
|
||||||
|
logger.add(
|
||||||
|
sys.stdout,
|
||||||
|
level=GLOBAL_LOG_LEVEL,
|
||||||
|
format=stdout_format,
|
||||||
|
filter=lambda record: "auditable" not in record["extra"],
|
||||||
|
)
|
||||||
|
|
||||||
|
if AUDIT_LOG_LEVEL != "NONE":
|
||||||
|
try:
|
||||||
|
logger.add(
|
||||||
|
AUDIT_LOGS_FILE_PATH,
|
||||||
|
level="INFO",
|
||||||
|
rotation=AUDIT_LOG_FILE_ROTATION_SIZE,
|
||||||
|
compression="zip",
|
||||||
|
format=file_format,
|
||||||
|
filter=lambda record: record["extra"].get("auditable") is True,
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to initialize audit log file handler: {str(e)}")
|
||||||
|
|
||||||
|
logging.basicConfig(
|
||||||
|
handlers=[InterceptHandler()], level=GLOBAL_LOG_LEVEL, force=True
|
||||||
|
)
|
||||||
|
for uvicorn_logger_name in ["uvicorn", "uvicorn.error"]:
|
||||||
|
uvicorn_logger = logging.getLogger(uvicorn_logger_name)
|
||||||
|
uvicorn_logger.setLevel(GLOBAL_LOG_LEVEL)
|
||||||
|
uvicorn_logger.handlers = []
|
||||||
|
for uvicorn_logger_name in ["uvicorn.access"]:
|
||||||
|
uvicorn_logger = logging.getLogger(uvicorn_logger_name)
|
||||||
|
uvicorn_logger.setLevel(GLOBAL_LOG_LEVEL)
|
||||||
|
uvicorn_logger.handlers = [InterceptHandler()]
|
||||||
|
|
||||||
|
logger.info(f"GLOBAL_LOG_LEVEL: {GLOBAL_LOG_LEVEL}")
|
||||||
@@ -68,6 +68,7 @@ from open_webui.utils.misc import (
|
|||||||
get_last_user_message,
|
get_last_user_message,
|
||||||
get_last_assistant_message,
|
get_last_assistant_message,
|
||||||
prepend_to_first_user_message_content,
|
prepend_to_first_user_message_content,
|
||||||
|
convert_logit_bias_input_to_json,
|
||||||
)
|
)
|
||||||
from open_webui.utils.tools import get_tools
|
from open_webui.utils.tools import get_tools
|
||||||
from open_webui.utils.plugin import load_function_module_by_id
|
from open_webui.utils.plugin import load_function_module_by_id
|
||||||
@@ -351,24 +352,25 @@ async def chat_web_search_handler(
|
|||||||
all_results.append(results)
|
all_results.append(results)
|
||||||
files = form_data.get("files", [])
|
files = form_data.get("files", [])
|
||||||
|
|
||||||
if request.app.state.config.RAG_WEB_SEARCH_FULL_CONTEXT:
|
if results.get("collection_name"):
|
||||||
files.append(
|
|
||||||
{
|
|
||||||
"docs": results.get("docs", []),
|
|
||||||
"name": searchQuery,
|
|
||||||
"type": "web_search_docs",
|
|
||||||
"urls": results["filenames"],
|
|
||||||
}
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
files.append(
|
files.append(
|
||||||
{
|
{
|
||||||
"collection_name": results["collection_name"],
|
"collection_name": results["collection_name"],
|
||||||
"name": searchQuery,
|
"name": searchQuery,
|
||||||
"type": "web_search_results",
|
"type": "web_search",
|
||||||
"urls": results["filenames"],
|
"urls": results["filenames"],
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
elif results.get("docs"):
|
||||||
|
files.append(
|
||||||
|
{
|
||||||
|
"docs": results.get("docs", []),
|
||||||
|
"name": searchQuery,
|
||||||
|
"type": "web_search",
|
||||||
|
"urls": results["filenames"],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
form_data["files"] = files
|
form_data["files"] = files
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.exception(e)
|
log.exception(e)
|
||||||
@@ -518,6 +520,7 @@ async def chat_completion_files_handler(
|
|||||||
sources = []
|
sources = []
|
||||||
|
|
||||||
if files := body.get("metadata", {}).get("files", None):
|
if files := body.get("metadata", {}).get("files", None):
|
||||||
|
queries = []
|
||||||
try:
|
try:
|
||||||
queries_response = await generate_queries(
|
queries_response = await generate_queries(
|
||||||
request,
|
request,
|
||||||
@@ -543,8 +546,8 @@ async def chat_completion_files_handler(
|
|||||||
queries_response = {"queries": [queries_response]}
|
queries_response = {"queries": [queries_response]}
|
||||||
|
|
||||||
queries = queries_response.get("queries", [])
|
queries = queries_response.get("queries", [])
|
||||||
except Exception as e:
|
except:
|
||||||
queries = []
|
pass
|
||||||
|
|
||||||
if len(queries) == 0:
|
if len(queries) == 0:
|
||||||
queries = [get_last_user_message(body["messages"])]
|
queries = [get_last_user_message(body["messages"])]
|
||||||
@@ -556,6 +559,7 @@ async def chat_completion_files_handler(
|
|||||||
sources = await loop.run_in_executor(
|
sources = await loop.run_in_executor(
|
||||||
executor,
|
executor,
|
||||||
lambda: get_sources_from_files(
|
lambda: get_sources_from_files(
|
||||||
|
request=request,
|
||||||
files=files,
|
files=files,
|
||||||
queries=queries,
|
queries=queries,
|
||||||
embedding_function=lambda query: request.app.state.EMBEDDING_FUNCTION(
|
embedding_function=lambda query: request.app.state.EMBEDDING_FUNCTION(
|
||||||
@@ -607,11 +611,18 @@ def apply_params_to_form_data(form_data, model):
|
|||||||
|
|
||||||
if "reasoning_effort" in params:
|
if "reasoning_effort" in params:
|
||||||
form_data["reasoning_effort"] = params["reasoning_effort"]
|
form_data["reasoning_effort"] = params["reasoning_effort"]
|
||||||
|
if "logit_bias" in params:
|
||||||
|
try:
|
||||||
|
form_data["logit_bias"] = json.loads(
|
||||||
|
convert_logit_bias_input_to_json(params["logit_bias"])
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error parsing logit_bias: {e}")
|
||||||
|
|
||||||
return form_data
|
return form_data
|
||||||
|
|
||||||
|
|
||||||
async def process_chat_payload(request, form_data, metadata, user, model):
|
async def process_chat_payload(request, form_data, user, metadata, model):
|
||||||
|
|
||||||
form_data = apply_params_to_form_data(form_data, model)
|
form_data = apply_params_to_form_data(form_data, model)
|
||||||
log.debug(f"form_data: {form_data}")
|
log.debug(f"form_data: {form_data}")
|
||||||
@@ -704,9 +715,14 @@ async def process_chat_payload(request, form_data, metadata, user, model):
|
|||||||
raise e
|
raise e
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
filter_functions = [
|
||||||
|
Functions.get_function_by_id(filter_id)
|
||||||
|
for filter_id in get_sorted_filter_ids(model)
|
||||||
|
]
|
||||||
|
|
||||||
form_data, flags = await process_filter_functions(
|
form_data, flags = await process_filter_functions(
|
||||||
request=request,
|
request=request,
|
||||||
filter_ids=get_sorted_filter_ids(model),
|
filter_functions=filter_functions,
|
||||||
filter_type="inlet",
|
filter_type="inlet",
|
||||||
form_data=form_data,
|
form_data=form_data,
|
||||||
extra_params=extra_params,
|
extra_params=extra_params,
|
||||||
@@ -738,6 +754,7 @@ async def process_chat_payload(request, form_data, metadata, user, model):
|
|||||||
|
|
||||||
tool_ids = form_data.pop("tool_ids", None)
|
tool_ids = form_data.pop("tool_ids", None)
|
||||||
files = form_data.pop("files", None)
|
files = form_data.pop("files", None)
|
||||||
|
|
||||||
# Remove files duplicates
|
# Remove files duplicates
|
||||||
if files:
|
if files:
|
||||||
files = list({json.dumps(f, sort_keys=True): f for f in files}.values())
|
files = list({json.dumps(f, sort_keys=True): f for f in files}.values())
|
||||||
@@ -795,8 +812,6 @@ async def process_chat_payload(request, form_data, metadata, user, model):
|
|||||||
if len(sources) > 0:
|
if len(sources) > 0:
|
||||||
context_string = ""
|
context_string = ""
|
||||||
for source_idx, source in enumerate(sources):
|
for source_idx, source in enumerate(sources):
|
||||||
source_id = source.get("source", {}).get("name", "")
|
|
||||||
|
|
||||||
if "document" in source:
|
if "document" in source:
|
||||||
for doc_idx, doc_context in enumerate(source["document"]):
|
for doc_idx, doc_context in enumerate(source["document"]):
|
||||||
context_string += f"<source><source_id>{source_idx}</source_id><source_context>{doc_context}</source_context></source>\n"
|
context_string += f"<source><source_id>{source_idx}</source_id><source_context>{doc_context}</source_context></source>\n"
|
||||||
@@ -854,7 +869,7 @@ async def process_chat_payload(request, form_data, metadata, user, model):
|
|||||||
|
|
||||||
|
|
||||||
async def process_chat_response(
|
async def process_chat_response(
|
||||||
request, response, form_data, user, events, metadata, tasks
|
request, response, form_data, user, metadata, model, events, tasks
|
||||||
):
|
):
|
||||||
async def background_tasks_handler():
|
async def background_tasks_handler():
|
||||||
message_map = Chats.get_messages_by_chat_id(metadata["chat_id"])
|
message_map = Chats.get_messages_by_chat_id(metadata["chat_id"])
|
||||||
@@ -1048,6 +1063,26 @@ async def process_chat_response(
|
|||||||
):
|
):
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
extra_params = {
|
||||||
|
"__event_emitter__": event_emitter,
|
||||||
|
"__event_call__": event_caller,
|
||||||
|
"__user__": {
|
||||||
|
"id": user.id,
|
||||||
|
"email": user.email,
|
||||||
|
"name": user.name,
|
||||||
|
"role": user.role,
|
||||||
|
},
|
||||||
|
"__metadata__": metadata,
|
||||||
|
"__request__": request,
|
||||||
|
"__model__": model,
|
||||||
|
}
|
||||||
|
filter_functions = [
|
||||||
|
Functions.get_function_by_id(filter_id)
|
||||||
|
for filter_id in get_sorted_filter_ids(model)
|
||||||
|
]
|
||||||
|
|
||||||
|
print(f"{filter_functions=}")
|
||||||
|
|
||||||
# Streaming response
|
# Streaming response
|
||||||
if event_emitter and event_caller:
|
if event_emitter and event_caller:
|
||||||
task_id = str(uuid4()) # Create a unique task ID.
|
task_id = str(uuid4()) # Create a unique task ID.
|
||||||
@@ -1127,12 +1162,12 @@ async def process_chat_response(
|
|||||||
|
|
||||||
if reasoning_duration is not None:
|
if reasoning_duration is not None:
|
||||||
if raw:
|
if raw:
|
||||||
content = f'{content}\n<{block["tag"]}>{block["content"]}</{block["tag"]}>\n'
|
content = f'{content}\n<{block["start_tag"]}>{block["content"]}<{block["end_tag"]}>\n'
|
||||||
else:
|
else:
|
||||||
content = f'{content}\n<details type="reasoning" done="true" duration="{reasoning_duration}">\n<summary>Thought for {reasoning_duration} seconds</summary>\n{reasoning_display_content}\n</details>\n'
|
content = f'{content}\n<details type="reasoning" done="true" duration="{reasoning_duration}">\n<summary>Thought for {reasoning_duration} seconds</summary>\n{reasoning_display_content}\n</details>\n'
|
||||||
else:
|
else:
|
||||||
if raw:
|
if raw:
|
||||||
content = f'{content}\n<{block["tag"]}>{block["content"]}</{block["tag"]}>\n'
|
content = f'{content}\n<{block["start_tag"]}>{block["content"]}<{block["end_tag"]}>\n'
|
||||||
else:
|
else:
|
||||||
content = f'{content}\n<details type="reasoning" done="false">\n<summary>Thinking…</summary>\n{reasoning_display_content}\n</details>\n'
|
content = f'{content}\n<details type="reasoning" done="false">\n<summary>Thinking…</summary>\n{reasoning_display_content}\n</details>\n'
|
||||||
|
|
||||||
@@ -1228,9 +1263,9 @@ async def process_chat_response(
|
|||||||
return attributes
|
return attributes
|
||||||
|
|
||||||
if content_blocks[-1]["type"] == "text":
|
if content_blocks[-1]["type"] == "text":
|
||||||
for tag in tags:
|
for start_tag, end_tag in tags:
|
||||||
# Match start tag e.g., <tag> or <tag attr="value">
|
# Match start tag e.g., <tag> or <tag attr="value">
|
||||||
start_tag_pattern = rf"<{tag}(\s.*?)?>"
|
start_tag_pattern = rf"<{re.escape(start_tag)}(\s.*?)?>"
|
||||||
match = re.search(start_tag_pattern, content)
|
match = re.search(start_tag_pattern, content)
|
||||||
if match:
|
if match:
|
||||||
attr_content = (
|
attr_content = (
|
||||||
@@ -1263,7 +1298,8 @@ async def process_chat_response(
|
|||||||
content_blocks.append(
|
content_blocks.append(
|
||||||
{
|
{
|
||||||
"type": content_type,
|
"type": content_type,
|
||||||
"tag": tag,
|
"start_tag": start_tag,
|
||||||
|
"end_tag": end_tag,
|
||||||
"attributes": attributes,
|
"attributes": attributes,
|
||||||
"content": "",
|
"content": "",
|
||||||
"started_at": time.time(),
|
"started_at": time.time(),
|
||||||
@@ -1275,9 +1311,10 @@ async def process_chat_response(
|
|||||||
|
|
||||||
break
|
break
|
||||||
elif content_blocks[-1]["type"] == content_type:
|
elif content_blocks[-1]["type"] == content_type:
|
||||||
tag = content_blocks[-1]["tag"]
|
start_tag = content_blocks[-1]["start_tag"]
|
||||||
|
end_tag = content_blocks[-1]["end_tag"]
|
||||||
# Match end tag e.g., </tag>
|
# Match end tag e.g., </tag>
|
||||||
end_tag_pattern = rf"</{tag}>"
|
end_tag_pattern = rf"<{re.escape(end_tag)}>"
|
||||||
|
|
||||||
# Check if the content has the end tag
|
# Check if the content has the end tag
|
||||||
if re.search(end_tag_pattern, content):
|
if re.search(end_tag_pattern, content):
|
||||||
@@ -1285,7 +1322,7 @@ async def process_chat_response(
|
|||||||
|
|
||||||
block_content = content_blocks[-1]["content"]
|
block_content = content_blocks[-1]["content"]
|
||||||
# Strip start and end tags from the content
|
# Strip start and end tags from the content
|
||||||
start_tag_pattern = rf"<{tag}(.*?)>"
|
start_tag_pattern = rf"<{re.escape(start_tag)}(.*?)>"
|
||||||
block_content = re.sub(
|
block_content = re.sub(
|
||||||
start_tag_pattern, "", block_content
|
start_tag_pattern, "", block_content
|
||||||
).strip()
|
).strip()
|
||||||
@@ -1350,7 +1387,7 @@ async def process_chat_response(
|
|||||||
|
|
||||||
# Clean processed content
|
# Clean processed content
|
||||||
content = re.sub(
|
content = re.sub(
|
||||||
rf"<{tag}(.*?)>(.|\n)*?</{tag}>",
|
rf"<{re.escape(start_tag)}(.*?)>(.|\n)*?<{re.escape(end_tag)}>",
|
||||||
"",
|
"",
|
||||||
content,
|
content,
|
||||||
flags=re.DOTALL,
|
flags=re.DOTALL,
|
||||||
@@ -1388,19 +1425,24 @@ async def process_chat_response(
|
|||||||
|
|
||||||
# We might want to disable this by default
|
# We might want to disable this by default
|
||||||
DETECT_REASONING = True
|
DETECT_REASONING = True
|
||||||
|
DETECT_SOLUTION = True
|
||||||
DETECT_CODE_INTERPRETER = metadata.get("features", {}).get(
|
DETECT_CODE_INTERPRETER = metadata.get("features", {}).get(
|
||||||
"code_interpreter", False
|
"code_interpreter", False
|
||||||
)
|
)
|
||||||
|
|
||||||
reasoning_tags = [
|
reasoning_tags = [
|
||||||
"think",
|
("think", "/think"),
|
||||||
"thinking",
|
("thinking", "/thinking"),
|
||||||
"reason",
|
("reason", "/reason"),
|
||||||
"reasoning",
|
("reasoning", "/reasoning"),
|
||||||
"thought",
|
("thought", "/thought"),
|
||||||
"Thought",
|
("Thought", "/Thought"),
|
||||||
|
("|begin_of_thought|", "|end_of_thought|"),
|
||||||
]
|
]
|
||||||
code_interpreter_tags = ["code_interpreter"]
|
|
||||||
|
code_interpreter_tags = [("code_interpreter", "/code_interpreter")]
|
||||||
|
|
||||||
|
solution_tags = [("|begin_of_solution|", "|end_of_solution|")]
|
||||||
|
|
||||||
try:
|
try:
|
||||||
for event in events:
|
for event in events:
|
||||||
@@ -1444,119 +1486,204 @@ async def process_chat_response(
|
|||||||
try:
|
try:
|
||||||
data = json.loads(data)
|
data = json.loads(data)
|
||||||
|
|
||||||
if "selected_model_id" in data:
|
data, _ = await process_filter_functions(
|
||||||
model_id = data["selected_model_id"]
|
request=request,
|
||||||
Chats.upsert_message_to_chat_by_id_and_message_id(
|
filter_functions=filter_functions,
|
||||||
metadata["chat_id"],
|
filter_type="stream",
|
||||||
metadata["message_id"],
|
form_data=data,
|
||||||
{
|
extra_params=extra_params,
|
||||||
"selectedModelId": model_id,
|
)
|
||||||
},
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
choices = data.get("choices", [])
|
|
||||||
if not choices:
|
|
||||||
continue
|
|
||||||
|
|
||||||
delta = choices[0].get("delta", {})
|
if data:
|
||||||
delta_tool_calls = delta.get("tool_calls", None)
|
if "selected_model_id" in data:
|
||||||
|
model_id = data["selected_model_id"]
|
||||||
if delta_tool_calls:
|
Chats.upsert_message_to_chat_by_id_and_message_id(
|
||||||
for delta_tool_call in delta_tool_calls:
|
metadata["chat_id"],
|
||||||
tool_call_index = delta_tool_call.get("index")
|
metadata["message_id"],
|
||||||
|
{
|
||||||
if tool_call_index is not None:
|
"selectedModelId": model_id,
|
||||||
if (
|
},
|
||||||
len(response_tool_calls)
|
|
||||||
<= tool_call_index
|
|
||||||
):
|
|
||||||
response_tool_calls.append(
|
|
||||||
delta_tool_call
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
delta_name = delta_tool_call.get(
|
|
||||||
"function", {}
|
|
||||||
).get("name")
|
|
||||||
delta_arguments = delta_tool_call.get(
|
|
||||||
"function", {}
|
|
||||||
).get("arguments")
|
|
||||||
|
|
||||||
if delta_name:
|
|
||||||
response_tool_calls[
|
|
||||||
tool_call_index
|
|
||||||
]["function"]["name"] += delta_name
|
|
||||||
|
|
||||||
if delta_arguments:
|
|
||||||
response_tool_calls[
|
|
||||||
tool_call_index
|
|
||||||
]["function"][
|
|
||||||
"arguments"
|
|
||||||
] += delta_arguments
|
|
||||||
|
|
||||||
value = delta.get("content")
|
|
||||||
|
|
||||||
if value:
|
|
||||||
content = f"{content}{value}"
|
|
||||||
|
|
||||||
if not content_blocks:
|
|
||||||
content_blocks.append(
|
|
||||||
{
|
|
||||||
"type": "text",
|
|
||||||
"content": "",
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
content_blocks[-1]["content"] = (
|
|
||||||
content_blocks[-1]["content"] + value
|
|
||||||
)
|
)
|
||||||
|
else:
|
||||||
if DETECT_REASONING:
|
choices = data.get("choices", [])
|
||||||
content, content_blocks, _ = (
|
if not choices:
|
||||||
tag_content_handler(
|
usage = data.get("usage", {})
|
||||||
"reasoning",
|
if usage:
|
||||||
reasoning_tags,
|
await event_emitter(
|
||||||
content,
|
{
|
||||||
content_blocks,
|
"type": "chat:completion",
|
||||||
|
"data": {
|
||||||
|
"usage": usage,
|
||||||
|
},
|
||||||
|
}
|
||||||
)
|
)
|
||||||
)
|
continue
|
||||||
|
|
||||||
if DETECT_CODE_INTERPRETER:
|
delta = choices[0].get("delta", {})
|
||||||
content, content_blocks, end = (
|
delta_tool_calls = delta.get("tool_calls", None)
|
||||||
tag_content_handler(
|
|
||||||
"code_interpreter",
|
if delta_tool_calls:
|
||||||
code_interpreter_tags,
|
for delta_tool_call in delta_tool_calls:
|
||||||
content,
|
tool_call_index = delta_tool_call.get(
|
||||||
content_blocks,
|
"index"
|
||||||
)
|
)
|
||||||
)
|
|
||||||
|
|
||||||
if end:
|
if tool_call_index is not None:
|
||||||
break
|
if (
|
||||||
|
len(response_tool_calls)
|
||||||
|
<= tool_call_index
|
||||||
|
):
|
||||||
|
response_tool_calls.append(
|
||||||
|
delta_tool_call
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
delta_name = delta_tool_call.get(
|
||||||
|
"function", {}
|
||||||
|
).get("name")
|
||||||
|
delta_arguments = (
|
||||||
|
delta_tool_call.get(
|
||||||
|
"function", {}
|
||||||
|
).get("arguments")
|
||||||
|
)
|
||||||
|
|
||||||
|
if delta_name:
|
||||||
|
response_tool_calls[
|
||||||
|
tool_call_index
|
||||||
|
]["function"][
|
||||||
|
"name"
|
||||||
|
] += delta_name
|
||||||
|
|
||||||
|
if delta_arguments:
|
||||||
|
response_tool_calls[
|
||||||
|
tool_call_index
|
||||||
|
]["function"][
|
||||||
|
"arguments"
|
||||||
|
] += delta_arguments
|
||||||
|
|
||||||
|
value = delta.get("content")
|
||||||
|
|
||||||
|
reasoning_content = delta.get("reasoning_content")
|
||||||
|
if reasoning_content:
|
||||||
|
if (
|
||||||
|
not content_blocks
|
||||||
|
or content_blocks[-1]["type"] != "reasoning"
|
||||||
|
):
|
||||||
|
reasoning_block = {
|
||||||
|
"type": "reasoning",
|
||||||
|
"start_tag": "think",
|
||||||
|
"end_tag": "/think",
|
||||||
|
"attributes": {
|
||||||
|
"type": "reasoning_content"
|
||||||
|
},
|
||||||
|
"content": "",
|
||||||
|
"started_at": time.time(),
|
||||||
|
}
|
||||||
|
content_blocks.append(reasoning_block)
|
||||||
|
else:
|
||||||
|
reasoning_block = content_blocks[-1]
|
||||||
|
|
||||||
|
reasoning_block["content"] += reasoning_content
|
||||||
|
|
||||||
if ENABLE_REALTIME_CHAT_SAVE:
|
|
||||||
# Save message in the database
|
|
||||||
Chats.upsert_message_to_chat_by_id_and_message_id(
|
|
||||||
metadata["chat_id"],
|
|
||||||
metadata["message_id"],
|
|
||||||
{
|
|
||||||
"content": serialize_content_blocks(
|
|
||||||
content_blocks
|
|
||||||
),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
data = {
|
data = {
|
||||||
"content": serialize_content_blocks(
|
"content": serialize_content_blocks(
|
||||||
content_blocks
|
content_blocks
|
||||||
),
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
await event_emitter(
|
if value:
|
||||||
{
|
if (
|
||||||
"type": "chat:completion",
|
content_blocks
|
||||||
"data": data,
|
and content_blocks[-1]["type"]
|
||||||
}
|
== "reasoning"
|
||||||
)
|
and content_blocks[-1]
|
||||||
|
.get("attributes", {})
|
||||||
|
.get("type")
|
||||||
|
== "reasoning_content"
|
||||||
|
):
|
||||||
|
reasoning_block = content_blocks[-1]
|
||||||
|
reasoning_block["ended_at"] = time.time()
|
||||||
|
reasoning_block["duration"] = int(
|
||||||
|
reasoning_block["ended_at"]
|
||||||
|
- reasoning_block["started_at"]
|
||||||
|
)
|
||||||
|
|
||||||
|
content_blocks.append(
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"content": "",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
content = f"{content}{value}"
|
||||||
|
if not content_blocks:
|
||||||
|
content_blocks.append(
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"content": "",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
content_blocks[-1]["content"] = (
|
||||||
|
content_blocks[-1]["content"] + value
|
||||||
|
)
|
||||||
|
|
||||||
|
if DETECT_REASONING:
|
||||||
|
content, content_blocks, _ = (
|
||||||
|
tag_content_handler(
|
||||||
|
"reasoning",
|
||||||
|
reasoning_tags,
|
||||||
|
content,
|
||||||
|
content_blocks,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if DETECT_CODE_INTERPRETER:
|
||||||
|
content, content_blocks, end = (
|
||||||
|
tag_content_handler(
|
||||||
|
"code_interpreter",
|
||||||
|
code_interpreter_tags,
|
||||||
|
content,
|
||||||
|
content_blocks,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if end:
|
||||||
|
break
|
||||||
|
|
||||||
|
if DETECT_SOLUTION:
|
||||||
|
content, content_blocks, _ = (
|
||||||
|
tag_content_handler(
|
||||||
|
"solution",
|
||||||
|
solution_tags,
|
||||||
|
content,
|
||||||
|
content_blocks,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if ENABLE_REALTIME_CHAT_SAVE:
|
||||||
|
# Save message in the database
|
||||||
|
Chats.upsert_message_to_chat_by_id_and_message_id(
|
||||||
|
metadata["chat_id"],
|
||||||
|
metadata["message_id"],
|
||||||
|
{
|
||||||
|
"content": serialize_content_blocks(
|
||||||
|
content_blocks
|
||||||
|
),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
data = {
|
||||||
|
"content": serialize_content_blocks(
|
||||||
|
content_blocks
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
await event_emitter(
|
||||||
|
{
|
||||||
|
"type": "chat:completion",
|
||||||
|
"data": data,
|
||||||
|
}
|
||||||
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
done = "data: [DONE]" in line
|
done = "data: [DONE]" in line
|
||||||
if done:
|
if done:
|
||||||
@@ -1855,7 +1982,10 @@ async def process_chat_response(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
print(content_blocks, serialize_content_blocks(content_blocks))
|
log.info(f"content_blocks={content_blocks}")
|
||||||
|
log.info(
|
||||||
|
f"serialize_content_blocks={serialize_content_blocks(content_blocks)}"
|
||||||
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
res = await generate_chat_completion(
|
res = await generate_chat_completion(
|
||||||
@@ -1926,7 +2056,7 @@ async def process_chat_response(
|
|||||||
|
|
||||||
await background_tasks_handler()
|
await background_tasks_handler()
|
||||||
except asyncio.CancelledError:
|
except asyncio.CancelledError:
|
||||||
print("Task was cancelled!")
|
log.warning("Task was cancelled!")
|
||||||
await event_emitter({"type": "task-cancelled"})
|
await event_emitter({"type": "task-cancelled"})
|
||||||
|
|
||||||
if not ENABLE_REALTIME_CHAT_SAVE:
|
if not ENABLE_REALTIME_CHAT_SAVE:
|
||||||
@@ -1947,17 +2077,34 @@ async def process_chat_response(
|
|||||||
return {"status": True, "task_id": task_id}
|
return {"status": True, "task_id": task_id}
|
||||||
|
|
||||||
else:
|
else:
|
||||||
|
|
||||||
# Fallback to the original response
|
# Fallback to the original response
|
||||||
async def stream_wrapper(original_generator, events):
|
async def stream_wrapper(original_generator, events):
|
||||||
def wrap_item(item):
|
def wrap_item(item):
|
||||||
return f"data: {item}\n\n"
|
return f"data: {item}\n\n"
|
||||||
|
|
||||||
for event in events:
|
for event in events:
|
||||||
yield wrap_item(json.dumps(event))
|
event, _ = await process_filter_functions(
|
||||||
|
request=request,
|
||||||
|
filter_functions=filter_functions,
|
||||||
|
filter_type="stream",
|
||||||
|
form_data=event,
|
||||||
|
extra_params=extra_params,
|
||||||
|
)
|
||||||
|
|
||||||
|
if event:
|
||||||
|
yield wrap_item(json.dumps(event))
|
||||||
|
|
||||||
async for data in original_generator:
|
async for data in original_generator:
|
||||||
yield data
|
data, _ = await process_filter_functions(
|
||||||
|
request=request,
|
||||||
|
filter_functions=filter_functions,
|
||||||
|
filter_type="stream",
|
||||||
|
form_data=data,
|
||||||
|
extra_params=extra_params,
|
||||||
|
)
|
||||||
|
|
||||||
|
if data:
|
||||||
|
yield data
|
||||||
|
|
||||||
return StreamingResponse(
|
return StreamingResponse(
|
||||||
stream_wrapper(response.body_iterator, events),
|
stream_wrapper(response.body_iterator, events),
|
||||||
|
|||||||
@@ -2,12 +2,18 @@ import hashlib
|
|||||||
import re
|
import re
|
||||||
import time
|
import time
|
||||||
import uuid
|
import uuid
|
||||||
|
import logging
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Callable, Optional
|
from typing import Callable, Optional
|
||||||
|
import json
|
||||||
|
|
||||||
|
|
||||||
import collections.abc
|
import collections.abc
|
||||||
|
from open_webui.env import SRC_LOG_LEVELS
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
log.setLevel(SRC_LOG_LEVELS["MAIN"])
|
||||||
|
|
||||||
|
|
||||||
def deep_update(d, u):
|
def deep_update(d, u):
|
||||||
@@ -412,7 +418,7 @@ def parse_ollama_modelfile(model_text):
|
|||||||
elif param_type is bool:
|
elif param_type is bool:
|
||||||
value = value.lower() == "true"
|
value = value.lower() == "true"
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(e)
|
log.exception(f"Failed to parse parameter {param}: {e}")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
data["params"][param] = value
|
data["params"][param] = value
|
||||||
@@ -445,3 +451,15 @@ def parse_ollama_modelfile(model_text):
|
|||||||
data["params"]["messages"] = messages
|
data["params"]["messages"] = messages
|
||||||
|
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
def convert_logit_bias_input_to_json(user_input):
|
||||||
|
logit_bias_pairs = user_input.split(",")
|
||||||
|
logit_bias_json = {}
|
||||||
|
for pair in logit_bias_pairs:
|
||||||
|
token, bias = pair.split(":")
|
||||||
|
token = str(token.strip())
|
||||||
|
bias = int(bias.strip())
|
||||||
|
bias = 100 if bias > 100 else -100 if bias < -100 else bias
|
||||||
|
logit_bias_json[token] = bias
|
||||||
|
return json.dumps(logit_bias_json)
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ from open_webui.config import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
from open_webui.env import SRC_LOG_LEVELS, GLOBAL_LOG_LEVEL
|
from open_webui.env import SRC_LOG_LEVELS, GLOBAL_LOG_LEVEL
|
||||||
|
from open_webui.models.users import UserModel
|
||||||
|
|
||||||
|
|
||||||
logging.basicConfig(stream=sys.stdout, level=GLOBAL_LOG_LEVEL)
|
logging.basicConfig(stream=sys.stdout, level=GLOBAL_LOG_LEVEL)
|
||||||
@@ -29,17 +30,17 @@ log = logging.getLogger(__name__)
|
|||||||
log.setLevel(SRC_LOG_LEVELS["MAIN"])
|
log.setLevel(SRC_LOG_LEVELS["MAIN"])
|
||||||
|
|
||||||
|
|
||||||
async def get_all_base_models(request: Request):
|
async def get_all_base_models(request: Request, user: UserModel = None):
|
||||||
function_models = []
|
function_models = []
|
||||||
openai_models = []
|
openai_models = []
|
||||||
ollama_models = []
|
ollama_models = []
|
||||||
|
|
||||||
if request.app.state.config.ENABLE_OPENAI_API:
|
if request.app.state.config.ENABLE_OPENAI_API:
|
||||||
openai_models = await openai.get_all_models(request)
|
openai_models = await openai.get_all_models(request, user=user)
|
||||||
openai_models = openai_models["data"]
|
openai_models = openai_models["data"]
|
||||||
|
|
||||||
if request.app.state.config.ENABLE_OLLAMA_API:
|
if request.app.state.config.ENABLE_OLLAMA_API:
|
||||||
ollama_models = await ollama.get_all_models(request)
|
ollama_models = await ollama.get_all_models(request, user=user)
|
||||||
ollama_models = [
|
ollama_models = [
|
||||||
{
|
{
|
||||||
"id": model["model"],
|
"id": model["model"],
|
||||||
@@ -58,8 +59,8 @@ async def get_all_base_models(request: Request):
|
|||||||
return models
|
return models
|
||||||
|
|
||||||
|
|
||||||
async def get_all_models(request):
|
async def get_all_models(request, user: UserModel = None):
|
||||||
models = await get_all_base_models(request)
|
models = await get_all_base_models(request, user=user)
|
||||||
|
|
||||||
# If there are no models, return an empty list
|
# If there are no models, return an empty list
|
||||||
if len(models) == 0:
|
if len(models) == 0:
|
||||||
|
|||||||
@@ -146,7 +146,7 @@ class OAuthManager:
|
|||||||
nested_claims = oauth_claim.split(".")
|
nested_claims = oauth_claim.split(".")
|
||||||
for nested_claim in nested_claims:
|
for nested_claim in nested_claims:
|
||||||
claim_data = claim_data.get(nested_claim, {})
|
claim_data = claim_data.get(nested_claim, {})
|
||||||
user_oauth_groups = claim_data if isinstance(claim_data, list) else None
|
user_oauth_groups = claim_data if isinstance(claim_data, list) else []
|
||||||
|
|
||||||
user_current_groups: list[GroupModel] = Groups.get_groups_by_member_id(user.id)
|
user_current_groups: list[GroupModel] = Groups.get_groups_by_member_id(user.id)
|
||||||
all_available_groups: list[GroupModel] = Groups.get_groups()
|
all_available_groups: list[GroupModel] = Groups.get_groups()
|
||||||
@@ -234,7 +234,7 @@ class OAuthManager:
|
|||||||
log.warning(f"OAuth callback error: {e}")
|
log.warning(f"OAuth callback error: {e}")
|
||||||
raise HTTPException(400, detail=ERROR_MESSAGES.INVALID_CRED)
|
raise HTTPException(400, detail=ERROR_MESSAGES.INVALID_CRED)
|
||||||
user_data: UserInfo = token.get("userinfo")
|
user_data: UserInfo = token.get("userinfo")
|
||||||
if not user_data or "email" not in user_data:
|
if not user_data or auth_manager_config.OAUTH_EMAIL_CLAIM not in user_data:
|
||||||
user_data: UserInfo = await client.userinfo(token=token)
|
user_data: UserInfo = await client.userinfo(token=token)
|
||||||
if not user_data:
|
if not user_data:
|
||||||
log.warning(f"OAuth callback failed, user data is missing: {token}")
|
log.warning(f"OAuth callback failed, user data is missing: {token}")
|
||||||
@@ -315,15 +315,6 @@ class OAuthManager:
|
|||||||
if not user:
|
if not user:
|
||||||
user_count = Users.get_num_users()
|
user_count = Users.get_num_users()
|
||||||
|
|
||||||
if (
|
|
||||||
request.app.state.USER_COUNT
|
|
||||||
and user_count >= request.app.state.USER_COUNT
|
|
||||||
):
|
|
||||||
raise HTTPException(
|
|
||||||
403,
|
|
||||||
detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
|
|
||||||
)
|
|
||||||
|
|
||||||
# If the user does not exist, check if signups are enabled
|
# If the user does not exist, check if signups are enabled
|
||||||
if auth_manager_config.ENABLE_OAUTH_SIGNUP:
|
if auth_manager_config.ENABLE_OAUTH_SIGNUP:
|
||||||
# Check if an existing user with the same email already exists
|
# Check if an existing user with the same email already exists
|
||||||
|
|||||||
@@ -62,6 +62,7 @@ def apply_model_params_to_body_openai(params: dict, form_data: dict) -> dict:
|
|||||||
"reasoning_effort": str,
|
"reasoning_effort": str,
|
||||||
"seed": lambda x: x,
|
"seed": lambda x: x,
|
||||||
"stop": lambda x: [bytes(s, "utf-8").decode("unicode_escape") for s in x],
|
"stop": lambda x: [bytes(s, "utf-8").decode("unicode_escape") for s in x],
|
||||||
|
"logit_bias": lambda x: x,
|
||||||
}
|
}
|
||||||
return apply_model_params_to_body(params, form_data, mappings)
|
return apply_model_params_to_body(params, form_data, mappings)
|
||||||
|
|
||||||
@@ -124,7 +125,7 @@ def convert_messages_openai_to_ollama(messages: list[dict]) -> list[dict]:
|
|||||||
tool_call_id = message.get("tool_call_id", None)
|
tool_call_id = message.get("tool_call_id", None)
|
||||||
|
|
||||||
# Check if the content is a string (just a simple message)
|
# Check if the content is a string (just a simple message)
|
||||||
if isinstance(content, str):
|
if isinstance(content, str) and not tool_calls:
|
||||||
# If the content is a string, it's pure text
|
# If the content is a string, it's pure text
|
||||||
new_message["content"] = content
|
new_message["content"] = content
|
||||||
|
|
||||||
@@ -230,6 +231,12 @@ def convert_payload_openai_to_ollama(openai_payload: dict) -> dict:
|
|||||||
"system"
|
"system"
|
||||||
] # To prevent Ollama warning of invalid option provided
|
] # To prevent Ollama warning of invalid option provided
|
||||||
|
|
||||||
|
# If there is the "stop" parameter in the openai_payload, remap it to the ollama_payload.options
|
||||||
|
if "stop" in openai_payload:
|
||||||
|
ollama_options = ollama_payload.get("options", {})
|
||||||
|
ollama_options["stop"] = openai_payload.get("stop")
|
||||||
|
ollama_payload["options"] = ollama_options
|
||||||
|
|
||||||
if "metadata" in openai_payload:
|
if "metadata" in openai_payload:
|
||||||
ollama_payload["metadata"] = openai_payload["metadata"]
|
ollama_payload["metadata"] = openai_payload["metadata"]
|
||||||
|
|
||||||
|
|||||||
@@ -110,7 +110,7 @@ class PDFGenerator:
|
|||||||
# When running using `pip install -e .` the static directory is in the site packages.
|
# 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.
|
# This path only works if `open-webui serve` is run from the root of this project.
|
||||||
if not FONTS_DIR.exists():
|
if not FONTS_DIR.exists():
|
||||||
FONTS_DIR = Path("./backend/static/fonts")
|
FONTS_DIR = Path(".") / "backend" / "static" / "fonts"
|
||||||
|
|
||||||
pdf.add_font("NotoSans", "", f"{FONTS_DIR}/NotoSans-Regular.ttf")
|
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", "b", f"{FONTS_DIR}/NotoSans-Bold.ttf")
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ def extract_frontmatter(content):
|
|||||||
frontmatter[key.strip()] = value.strip()
|
frontmatter[key.strip()] = value.strip()
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"An error occurred: {e}")
|
log.exception(f"Failed to extract frontmatter: {e}")
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
return frontmatter
|
return frontmatter
|
||||||
|
|||||||
@@ -104,7 +104,7 @@ async def convert_streaming_response_ollama_to_openai(ollama_streaming_response)
|
|||||||
data = json.loads(data)
|
data = json.loads(data)
|
||||||
|
|
||||||
model = data.get("model", "ollama")
|
model = data.get("model", "ollama")
|
||||||
message_content = data.get("message", {}).get("content", "")
|
message_content = data.get("message", {}).get("content", None)
|
||||||
tool_calls = data.get("message", {}).get("tool_calls", None)
|
tool_calls = data.get("message", {}).get("tool_calls", None)
|
||||||
openai_tool_calls = None
|
openai_tool_calls = None
|
||||||
|
|
||||||
@@ -118,7 +118,7 @@ async def convert_streaming_response_ollama_to_openai(ollama_streaming_response)
|
|||||||
usage = convert_ollama_usage_to_openai(data)
|
usage = convert_ollama_usage_to_openai(data)
|
||||||
|
|
||||||
data = openai_chat_chunk_message_template(
|
data = openai_chat_chunk_message_template(
|
||||||
model, message_content if not done else None, openai_tool_calls, usage
|
model, message_content, openai_tool_calls, usage
|
||||||
)
|
)
|
||||||
|
|
||||||
line = f"data: {json.dumps(data)}\n\n"
|
line = f"data: {json.dumps(data)}\n\n"
|
||||||
|
|||||||
@@ -104,7 +104,7 @@ def replace_prompt_variable(template: str, prompt: str) -> str:
|
|||||||
|
|
||||||
|
|
||||||
def replace_messages_variable(
|
def replace_messages_variable(
|
||||||
template: str, messages: Optional[list[str]] = None
|
template: str, messages: Optional[list[dict]] = None
|
||||||
) -> str:
|
) -> str:
|
||||||
def replacement_function(match):
|
def replacement_function(match):
|
||||||
full_match = match.group(0)
|
full_match = match.group(0)
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
fastapi==0.115.7
|
fastapi==0.115.7
|
||||||
uvicorn[standard]==0.30.6
|
uvicorn[standard]==0.34.0
|
||||||
pydantic==2.10.6
|
pydantic==2.10.6
|
||||||
python-multipart==0.0.18
|
python-multipart==0.0.18
|
||||||
|
|
||||||
@@ -13,14 +13,14 @@ async-timeout
|
|||||||
aiocache
|
aiocache
|
||||||
aiofiles
|
aiofiles
|
||||||
|
|
||||||
sqlalchemy==2.0.32
|
sqlalchemy==2.0.38
|
||||||
alembic==1.14.0
|
alembic==1.14.0
|
||||||
peewee==3.17.8
|
peewee==3.17.9
|
||||||
peewee-migrate==1.12.2
|
peewee-migrate==1.12.2
|
||||||
psycopg2-binary==2.9.9
|
psycopg2-binary==2.9.9
|
||||||
pgvector==0.3.5
|
pgvector==0.3.5
|
||||||
PyMySQL==1.1.1
|
PyMySQL==1.1.1
|
||||||
bcrypt==4.2.0
|
bcrypt==4.3.0
|
||||||
|
|
||||||
pymongo
|
pymongo
|
||||||
redis
|
redis
|
||||||
@@ -31,14 +31,17 @@ APScheduler==3.10.4
|
|||||||
|
|
||||||
RestrictedPython==8.0
|
RestrictedPython==8.0
|
||||||
|
|
||||||
|
loguru==0.7.2
|
||||||
|
asgiref==3.8.1
|
||||||
|
|
||||||
# AI libraries
|
# AI libraries
|
||||||
openai
|
openai
|
||||||
anthropic
|
anthropic
|
||||||
google-generativeai==0.7.2
|
google-generativeai==0.7.2
|
||||||
tiktoken
|
tiktoken
|
||||||
|
|
||||||
langchain==0.3.7
|
langchain==0.3.19
|
||||||
langchain-community==0.3.7
|
langchain-community==0.3.18
|
||||||
|
|
||||||
fake-useragent==1.5.1
|
fake-useragent==1.5.1
|
||||||
chromadb==0.6.2
|
chromadb==0.6.2
|
||||||
@@ -46,6 +49,8 @@ pymilvus==2.5.0
|
|||||||
qdrant-client~=1.12.0
|
qdrant-client~=1.12.0
|
||||||
opensearch-py==2.8.0
|
opensearch-py==2.8.0
|
||||||
playwright==1.49.1 # Caution: version must match docker-compose.playwright.yaml
|
playwright==1.49.1 # Caution: version must match docker-compose.playwright.yaml
|
||||||
|
elasticsearch==8.17.1
|
||||||
|
|
||||||
|
|
||||||
transformers
|
transformers
|
||||||
sentence-transformers==3.3.1
|
sentence-transformers==3.3.1
|
||||||
@@ -71,6 +76,7 @@ validators==0.34.0
|
|||||||
psutil
|
psutil
|
||||||
sentencepiece
|
sentencepiece
|
||||||
soundfile==0.13.1
|
soundfile==0.13.1
|
||||||
|
azure-ai-documentintelligence==1.0.0
|
||||||
|
|
||||||
opencv-python-headless==4.11.0.86
|
opencv-python-headless==4.11.0.86
|
||||||
rapidocr-onnxruntime==1.3.24
|
rapidocr-onnxruntime==1.3.24
|
||||||
@@ -81,7 +87,7 @@ faster-whisper==1.1.1
|
|||||||
PyJWT[crypto]==2.10.1
|
PyJWT[crypto]==2.10.1
|
||||||
authlib==1.4.1
|
authlib==1.4.1
|
||||||
|
|
||||||
black==24.8.0
|
black==25.1.0
|
||||||
langfuse==2.44.0
|
langfuse==2.44.0
|
||||||
youtube-transcript-api==0.6.3
|
youtube-transcript-api==0.6.3
|
||||||
pytube==15.0.0
|
pytube==15.0.0
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
{
|
{
|
||||||
"name": "open-webui",
|
"name": "open-webui",
|
||||||
"version": "0.5.16",
|
"version": "0.5.20",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "open-webui",
|
"name": "open-webui",
|
||||||
"version": "0.5.16",
|
"version": "0.5.20",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@azure/msal-browser": "^4.5.0",
|
||||||
"@codemirror/lang-javascript": "^6.2.2",
|
"@codemirror/lang-javascript": "^6.2.2",
|
||||||
"@codemirror/lang-python": "^6.1.6",
|
"@codemirror/lang-python": "^6.1.6",
|
||||||
"@codemirror/language-data": "^6.5.1",
|
"@codemirror/language-data": "^6.5.1",
|
||||||
@@ -41,6 +42,7 @@
|
|||||||
"i18next-resources-to-backend": "^1.2.0",
|
"i18next-resources-to-backend": "^1.2.0",
|
||||||
"idb": "^7.1.1",
|
"idb": "^7.1.1",
|
||||||
"js-sha256": "^0.10.1",
|
"js-sha256": "^0.10.1",
|
||||||
|
"jspdf": "^3.0.0",
|
||||||
"katex": "^0.16.21",
|
"katex": "^0.16.21",
|
||||||
"kokoro-js": "^1.1.1",
|
"kokoro-js": "^1.1.1",
|
||||||
"marked": "^9.1.0",
|
"marked": "^9.1.0",
|
||||||
@@ -134,10 +136,32 @@
|
|||||||
"node": ">=6.0.0"
|
"node": ">=6.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@azure/msal-browser": {
|
||||||
|
"version": "4.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@azure/msal-browser/-/msal-browser-4.5.0.tgz",
|
||||||
|
"integrity": "sha512-H7mWmu8yI0n0XxhJobrgncXI6IU5h8DKMiWDHL5y+Dc58cdg26GbmaMUehbUkdKAQV2OTiFa4FUa6Fdu/wIxBg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@azure/msal-common": "15.2.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@azure/msal-common": {
|
||||||
|
"version": "15.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-15.2.0.tgz",
|
||||||
|
"integrity": "sha512-HiYfGAKthisUYqHG1nImCf/uzcyS31wng3o+CycWLIM9chnYJ9Lk6jZ30Y6YiYYpTQ9+z/FGUpiKKekd3Arc0A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@babel/runtime": {
|
"node_modules/@babel/runtime": {
|
||||||
"version": "7.24.1",
|
"version": "7.26.9",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.1.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.9.tgz",
|
||||||
"integrity": "sha512-+BIznRzyqBf+2wCTxcKE3wDjfGeCoVE61KSHGpkzqrLi8qxqFwBeUFyId2cxkTmm55fzDGnm0+yCxaxygrLUnQ==",
|
"integrity": "sha512-aA63XwOkcl4xxQa3HjPMqOP6LiK0ZDv3mUPYEFXkpHbaFjtGggE1A61FjFzJnB+p7/oy2gA8E+rcBNl/zC1tMg==",
|
||||||
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"regenerator-runtime": "^0.14.0"
|
"regenerator-runtime": "^0.14.0"
|
||||||
},
|
},
|
||||||
@@ -3170,6 +3194,13 @@
|
|||||||
"integrity": "sha512-Sk/uYFOBAB7mb74XcpizmH0KOR2Pv3D2Hmrh1Dmy5BmK3MpdSa5kqZcg6EKBdklU0bFXX9gCfzvpnyUehrPIuA==",
|
"integrity": "sha512-Sk/uYFOBAB7mb74XcpizmH0KOR2Pv3D2Hmrh1Dmy5BmK3MpdSa5kqZcg6EKBdklU0bFXX9gCfzvpnyUehrPIuA==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/raf": {
|
||||||
|
"version": "3.4.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/raf/-/raf-3.4.3.tgz",
|
||||||
|
"integrity": "sha512-c4YAvMedbPZ5tEyxzQdMoOhhJ4RD3rngZIdwC2/qDN3d7JpEhB6fiBRKVY1lg5B7Wk+uPBjn5f39j1/2MY1oOw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
"node_modules/@types/resolve": {
|
"node_modules/@types/resolve": {
|
||||||
"version": "1.20.2",
|
"version": "1.20.2",
|
||||||
"resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz",
|
"resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz",
|
||||||
@@ -3199,6 +3230,13 @@
|
|||||||
"integrity": "sha512-MQ1AnmTLOncwEf9IVU+B2e4Hchrku5N67NkgcAHW0p3sdzPe0FNMANxEm6OJUzPniEQGkeT3OROLlCwZJLWFZA==",
|
"integrity": "sha512-MQ1AnmTLOncwEf9IVU+B2e4Hchrku5N67NkgcAHW0p3sdzPe0FNMANxEm6OJUzPniEQGkeT3OROLlCwZJLWFZA==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/trusted-types": {
|
||||||
|
"version": "2.0.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
|
||||||
|
"integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
"node_modules/@types/unist": {
|
"node_modules/@types/unist": {
|
||||||
"version": "2.0.10",
|
"version": "2.0.10",
|
||||||
"resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.10.tgz",
|
"resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.10.tgz",
|
||||||
@@ -3794,6 +3832,18 @@
|
|||||||
"node": ">= 4.0.0"
|
"node": ">= 4.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/atob": {
|
||||||
|
"version": "2.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz",
|
||||||
|
"integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==",
|
||||||
|
"license": "(MIT OR Apache-2.0)",
|
||||||
|
"bin": {
|
||||||
|
"atob": "bin/atob.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 4.5.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/aws-sign2": {
|
"node_modules/aws-sign2": {
|
||||||
"version": "0.7.0",
|
"version": "0.7.0",
|
||||||
"resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz",
|
"resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz",
|
||||||
@@ -3829,6 +3879,16 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
|
"node_modules/base64-arraybuffer": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/base64-js": {
|
"node_modules/base64-js": {
|
||||||
"version": "1.5.1",
|
"version": "1.5.1",
|
||||||
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
|
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
|
||||||
@@ -4033,6 +4093,18 @@
|
|||||||
"node": "10.* || >= 12.*"
|
"node": "10.* || >= 12.*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/btoa": {
|
||||||
|
"version": "1.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/btoa/-/btoa-1.2.1.tgz",
|
||||||
|
"integrity": "sha512-SB4/MIGlsiVkMcHmT+pSmIPoNDoHg+7cMzmt3Uxt628MTz2487DKSqK/fuhFBrkuqrYv5UCEnACpF4dTFNKc/g==",
|
||||||
|
"license": "(MIT OR Apache-2.0)",
|
||||||
|
"bin": {
|
||||||
|
"btoa": "bin/btoa.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/buffer": {
|
"node_modules/buffer": {
|
||||||
"version": "6.0.3",
|
"version": "6.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
|
||||||
@@ -4131,6 +4203,33 @@
|
|||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/canvg": {
|
||||||
|
"version": "3.0.10",
|
||||||
|
"resolved": "https://registry.npmjs.org/canvg/-/canvg-3.0.10.tgz",
|
||||||
|
"integrity": "sha512-qwR2FRNO9NlzTeKIPIKpnTY6fqwuYSequ8Ru8c0YkYU7U0oW+hLUvWadLvAu1Rl72OMNiFhoLu4f8eUjQ7l/+Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/runtime": "^7.12.5",
|
||||||
|
"@types/raf": "^3.4.0",
|
||||||
|
"core-js": "^3.8.3",
|
||||||
|
"raf": "^3.4.1",
|
||||||
|
"regenerator-runtime": "^0.13.7",
|
||||||
|
"rgbcolor": "^1.0.1",
|
||||||
|
"stackblur-canvas": "^2.0.0",
|
||||||
|
"svg-pathdata": "^6.0.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/canvg/node_modules/regenerator-runtime": {
|
||||||
|
"version": "0.13.11",
|
||||||
|
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz",
|
||||||
|
"integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
"node_modules/caseless": {
|
"node_modules/caseless": {
|
||||||
"version": "0.12.0",
|
"version": "0.12.0",
|
||||||
"resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz",
|
"resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz",
|
||||||
@@ -4599,6 +4698,18 @@
|
|||||||
"node": ">= 0.6"
|
"node": ">= 0.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/core-js": {
|
||||||
|
"version": "3.40.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.40.0.tgz",
|
||||||
|
"integrity": "sha512-7vsMc/Lty6AGnn7uFpYT56QesI5D2Y/UkgKounk87OP9Z2H9Z8kj6jzcSGAxFmUtDOS0ntK6lbQz+Nsa0Jj6mQ==",
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/core-js"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/core-util-is": {
|
"node_modules/core-util-is": {
|
||||||
"version": "1.0.3",
|
"version": "1.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
|
||||||
@@ -4643,6 +4754,16 @@
|
|||||||
"node": ">= 8"
|
"node": ">= 8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/css-line-break": {
|
||||||
|
"version": "2.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-2.1.0.tgz",
|
||||||
|
"integrity": "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"utrie": "^1.0.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/css-select": {
|
"node_modules/css-select": {
|
||||||
"version": "5.1.0",
|
"version": "5.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz",
|
||||||
@@ -6106,6 +6227,12 @@
|
|||||||
"pend": "~1.2.0"
|
"pend": "~1.2.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/fflate": {
|
||||||
|
"version": "0.8.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz",
|
||||||
|
"integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/figures": {
|
"node_modules/figures": {
|
||||||
"version": "3.2.0",
|
"version": "3.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz",
|
||||||
@@ -6701,6 +6828,20 @@
|
|||||||
"resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-3.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-3.0.3.tgz",
|
||||||
"integrity": "sha512-RuMffC89BOWQoY0WKGpIhn5gX3iI54O6nRA0yC124NYVtzjmFWBIiFd8M0x+ZdX0P9R4lADg1mgP8C7PxGOWuQ=="
|
"integrity": "sha512-RuMffC89BOWQoY0WKGpIhn5gX3iI54O6nRA0yC124NYVtzjmFWBIiFd8M0x+ZdX0P9R4lADg1mgP8C7PxGOWuQ=="
|
||||||
},
|
},
|
||||||
|
"node_modules/html2canvas": {
|
||||||
|
"version": "1.4.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz",
|
||||||
|
"integrity": "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"css-line-break": "^2.1.0",
|
||||||
|
"text-segmentation": "^1.0.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/htmlparser2": {
|
"node_modules/htmlparser2": {
|
||||||
"version": "8.0.2",
|
"version": "8.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz",
|
||||||
@@ -7213,6 +7354,34 @@
|
|||||||
"graceful-fs": "^4.1.6"
|
"graceful-fs": "^4.1.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/jspdf": {
|
||||||
|
"version": "3.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/jspdf/-/jspdf-3.0.0.tgz",
|
||||||
|
"integrity": "sha512-QvuQZvOI8CjfjVgtajdL0ihrDYif1cN5gXiF9lb9Pd9JOpmocvnNyFO9sdiJ/8RA5Bu8zyGOUjJLj5kiku16ug==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/runtime": "^7.26.0",
|
||||||
|
"atob": "^2.1.2",
|
||||||
|
"btoa": "^1.2.1",
|
||||||
|
"fflate": "^0.8.1"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"canvg": "^3.0.6",
|
||||||
|
"core-js": "^3.6.0",
|
||||||
|
"dompurify": "^3.2.4",
|
||||||
|
"html2canvas": "^1.0.0-rc.5"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/jspdf/node_modules/dompurify": {
|
||||||
|
"version": "3.2.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.4.tgz",
|
||||||
|
"integrity": "sha512-ysFSFEDVduQpyhzAob/kkuJjf5zWkZD8/A9ywSp1byueyuCfHamrCBa14/Oc2iiB0e51B+NpxSl5gmzn+Ms/mg==",
|
||||||
|
"license": "(MPL-2.0 OR Apache-2.0)",
|
||||||
|
"optional": true,
|
||||||
|
"optionalDependencies": {
|
||||||
|
"@types/trusted-types": "^2.0.7"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/jsprim": {
|
"node_modules/jsprim": {
|
||||||
"version": "2.0.2",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/jsprim/-/jsprim-2.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/jsprim/-/jsprim-2.0.2.tgz",
|
||||||
@@ -9029,7 +9198,7 @@
|
|||||||
"version": "2.1.0",
|
"version": "2.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz",
|
||||||
"integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==",
|
"integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==",
|
||||||
"dev": true
|
"devOptional": true
|
||||||
},
|
},
|
||||||
"node_modules/periscopic": {
|
"node_modules/periscopic": {
|
||||||
"version": "3.1.0",
|
"version": "3.1.0",
|
||||||
@@ -9755,6 +9924,16 @@
|
|||||||
"rimraf": "bin.js"
|
"rimraf": "bin.js"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/raf": {
|
||||||
|
"version": "3.4.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/raf/-/raf-3.4.1.tgz",
|
||||||
|
"integrity": "sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"performance-now": "^2.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/react-is": {
|
"node_modules/react-is": {
|
||||||
"version": "18.3.1",
|
"version": "18.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz",
|
||||||
@@ -9894,6 +10073,16 @@
|
|||||||
"integrity": "sha512-r5a3l5HzYlIC68TpmYKlxWjmOP6wiPJ1vWv2HeLhNsRZMrCkxeqxiHlQ21oXmQ4F3SiryXBHhAD7JZqvOJjFmg==",
|
"integrity": "sha512-r5a3l5HzYlIC68TpmYKlxWjmOP6wiPJ1vWv2HeLhNsRZMrCkxeqxiHlQ21oXmQ4F3SiryXBHhAD7JZqvOJjFmg==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/rgbcolor": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/rgbcolor/-/rgbcolor-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-9aZLIrhRaD97sgVhtJOW6ckOEh6/GnvQtdVNfdZ6s67+3/XwLS9lBcQYzEEhYVeUowN7pRzMLsyGhK2i/xvWbw==",
|
||||||
|
"license": "MIT OR SEE LICENSE IN FEEL-FREE.md",
|
||||||
|
"optional": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8.15"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/rimraf": {
|
"node_modules/rimraf": {
|
||||||
"version": "3.0.2",
|
"version": "3.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
|
||||||
@@ -10785,6 +10974,16 @@
|
|||||||
"integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==",
|
"integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/stackblur-canvas": {
|
||||||
|
"version": "2.7.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/stackblur-canvas/-/stackblur-canvas-2.7.0.tgz",
|
||||||
|
"integrity": "sha512-yf7OENo23AGJhBriGx0QivY5JP6Y1HbrrDI6WLt6C5auYZXlQrheoY8hD4ibekFKz1HOfE48Ww8kMWMnJD/zcQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.1.14"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/std-env": {
|
"node_modules/std-env": {
|
||||||
"version": "3.7.0",
|
"version": "3.7.0",
|
||||||
"resolved": "https://registry.npmjs.org/std-env/-/std-env-3.7.0.tgz",
|
"resolved": "https://registry.npmjs.org/std-env/-/std-env-3.7.0.tgz",
|
||||||
@@ -11166,6 +11365,16 @@
|
|||||||
"@types/estree": "*"
|
"@types/estree": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/svg-pathdata": {
|
||||||
|
"version": "6.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/svg-pathdata/-/svg-pathdata-6.0.3.tgz",
|
||||||
|
"integrity": "sha512-qsjeeq5YjBZ5eMdFuUa4ZosMLxgr5RZ+F+Y1OrDhuOCEInRMA3x74XdBtggJcj9kOeInz0WE+LgCPDkZFlBYJw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/symlink-or-copy": {
|
"node_modules/symlink-or-copy": {
|
||||||
"version": "1.3.1",
|
"version": "1.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/symlink-or-copy/-/symlink-or-copy-1.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/symlink-or-copy/-/symlink-or-copy-1.3.1.tgz",
|
||||||
@@ -11258,6 +11467,16 @@
|
|||||||
"streamx": "^2.12.5"
|
"streamx": "^2.12.5"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/text-segmentation": {
|
||||||
|
"version": "1.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/text-segmentation/-/text-segmentation-1.0.3.tgz",
|
||||||
|
"integrity": "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"utrie": "^1.0.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/text-table": {
|
"node_modules/text-table": {
|
||||||
"version": "0.2.0",
|
"version": "0.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
|
||||||
@@ -11597,6 +11816,16 @@
|
|||||||
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
|
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/utrie": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/utrie/-/utrie-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"base64-arraybuffer": "^1.0.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/uuid": {
|
"node_modules/uuid": {
|
||||||
"version": "9.0.1",
|
"version": "9.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "open-webui",
|
"name": "open-webui",
|
||||||
"version": "0.5.16",
|
"version": "0.5.20",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "npm run pyodide:fetch && vite dev --host",
|
"dev": "npm run pyodide:fetch && vite dev --host",
|
||||||
@@ -51,6 +51,7 @@
|
|||||||
},
|
},
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@azure/msal-browser": "^4.5.0",
|
||||||
"@codemirror/lang-javascript": "^6.2.2",
|
"@codemirror/lang-javascript": "^6.2.2",
|
||||||
"@codemirror/lang-python": "^6.1.6",
|
"@codemirror/lang-python": "^6.1.6",
|
||||||
"@codemirror/language-data": "^6.5.1",
|
"@codemirror/language-data": "^6.5.1",
|
||||||
@@ -84,6 +85,7 @@
|
|||||||
"i18next-resources-to-backend": "^1.2.0",
|
"i18next-resources-to-backend": "^1.2.0",
|
||||||
"idb": "^7.1.1",
|
"idb": "^7.1.1",
|
||||||
"js-sha256": "^0.10.1",
|
"js-sha256": "^0.10.1",
|
||||||
|
"jspdf": "^3.0.0",
|
||||||
"katex": "^0.16.21",
|
"katex": "^0.16.21",
|
||||||
"kokoro-js": "^1.1.1",
|
"kokoro-js": "^1.1.1",
|
||||||
"marked": "^9.1.0",
|
"marked": "^9.1.0",
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ authors = [
|
|||||||
license = { file = "LICENSE" }
|
license = { file = "LICENSE" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"fastapi==0.115.7",
|
"fastapi==0.115.7",
|
||||||
"uvicorn[standard]==0.30.6",
|
"uvicorn[standard]==0.34.0",
|
||||||
"pydantic==2.10.6",
|
"pydantic==2.10.6",
|
||||||
"python-multipart==0.0.18",
|
"python-multipart==0.0.18",
|
||||||
|
|
||||||
@@ -21,14 +21,14 @@ dependencies = [
|
|||||||
"aiocache",
|
"aiocache",
|
||||||
"aiofiles",
|
"aiofiles",
|
||||||
|
|
||||||
"sqlalchemy==2.0.32",
|
"sqlalchemy==2.0.38",
|
||||||
"alembic==1.14.0",
|
"alembic==1.14.0",
|
||||||
"peewee==3.17.8",
|
"peewee==3.17.9",
|
||||||
"peewee-migrate==1.12.2",
|
"peewee-migrate==1.12.2",
|
||||||
"psycopg2-binary==2.9.9",
|
"psycopg2-binary==2.9.9",
|
||||||
"pgvector==0.3.5",
|
"pgvector==0.3.5",
|
||||||
"PyMySQL==1.1.1",
|
"PyMySQL==1.1.1",
|
||||||
"bcrypt==4.2.0",
|
"bcrypt==4.3.0",
|
||||||
|
|
||||||
"pymongo",
|
"pymongo",
|
||||||
"redis",
|
"redis",
|
||||||
@@ -40,13 +40,16 @@ dependencies = [
|
|||||||
|
|
||||||
"RestrictedPython==8.0",
|
"RestrictedPython==8.0",
|
||||||
|
|
||||||
|
"loguru==0.7.2",
|
||||||
|
"asgiref==3.8.1",
|
||||||
|
|
||||||
"openai",
|
"openai",
|
||||||
"anthropic",
|
"anthropic",
|
||||||
"google-generativeai==0.7.2",
|
"google-generativeai==0.7.2",
|
||||||
"tiktoken",
|
"tiktoken",
|
||||||
|
|
||||||
"langchain==0.3.7",
|
"langchain==0.3.19",
|
||||||
"langchain-community==0.3.7",
|
"langchain-community==0.3.18",
|
||||||
|
|
||||||
"fake-useragent==1.5.1",
|
"fake-useragent==1.5.1",
|
||||||
"chromadb==0.6.2",
|
"chromadb==0.6.2",
|
||||||
@@ -54,6 +57,7 @@ dependencies = [
|
|||||||
"qdrant-client~=1.12.0",
|
"qdrant-client~=1.12.0",
|
||||||
"opensearch-py==2.8.0",
|
"opensearch-py==2.8.0",
|
||||||
"playwright==1.49.1",
|
"playwright==1.49.1",
|
||||||
|
"elasticsearch==8.17.1",
|
||||||
|
|
||||||
"transformers",
|
"transformers",
|
||||||
"sentence-transformers==3.3.1",
|
"sentence-transformers==3.3.1",
|
||||||
@@ -78,6 +82,7 @@ dependencies = [
|
|||||||
"psutil",
|
"psutil",
|
||||||
"sentencepiece",
|
"sentencepiece",
|
||||||
"soundfile==0.13.1",
|
"soundfile==0.13.1",
|
||||||
|
"azure-ai-documentintelligence==1.0.0",
|
||||||
|
|
||||||
"opencv-python-headless==4.11.0.86",
|
"opencv-python-headless==4.11.0.86",
|
||||||
"rapidocr-onnxruntime==1.3.24",
|
"rapidocr-onnxruntime==1.3.24",
|
||||||
@@ -88,7 +93,7 @@ dependencies = [
|
|||||||
"PyJWT[crypto]==2.10.1",
|
"PyJWT[crypto]==2.10.1",
|
||||||
"authlib==1.4.1",
|
"authlib==1.4.1",
|
||||||
|
|
||||||
"black==24.8.0",
|
"black==25.1.0",
|
||||||
"langfuse==2.44.0",
|
"langfuse==2.44.0",
|
||||||
"youtube-transcript-api==0.6.3",
|
"youtube-transcript-api==0.6.3",
|
||||||
"pytube==15.0.0",
|
"pytube==15.0.0",
|
||||||
|
|||||||
@@ -2,12 +2,14 @@
|
|||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<link rel="icon" type="image/png" href="/favicon/favicon-96x96.png" sizes="96x96" />
|
<link rel="icon" type="image/png" href="/static/favicon.png" />
|
||||||
<link rel="icon" type="image/svg+xml" href="/favicon/favicon.svg" />
|
<link rel="icon" type="image/png" href="/static/favicon-96x96.png" sizes="96x96" />
|
||||||
<link rel="shortcut icon" href="/favicon/favicon.ico" />
|
<link rel="icon" type="image/svg+xml" href="/static/favicon.svg" />
|
||||||
<link rel="apple-touch-icon" sizes="180x180" href="/favicon/apple-touch-icon.png" />
|
<link rel="shortcut icon" href="/static/favicon.ico" />
|
||||||
|
<link rel="apple-touch-icon" sizes="180x180" href="/static/apple-touch-icon.png" />
|
||||||
<meta name="apple-mobile-web-app-title" content="Open WebUI" />
|
<meta name="apple-mobile-web-app-title" content="Open WebUI" />
|
||||||
<link rel="manifest" href="/favicon/site.webmanifest" />
|
|
||||||
|
<link rel="manifest" href="/manifest.json" />
|
||||||
<meta
|
<meta
|
||||||
name="viewport"
|
name="viewport"
|
||||||
content="width=device-width, initial-scale=1, maximum-scale=1, viewport-fit=cover"
|
content="width=device-width, initial-scale=1, maximum-scale=1, viewport-fit=cover"
|
||||||
@@ -74,6 +76,28 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function setSplashImage() {
|
||||||
|
const logo = document.getElementById('logo');
|
||||||
|
const isDarkMode = document.documentElement.classList.contains('dark');
|
||||||
|
|
||||||
|
if (isDarkMode) {
|
||||||
|
const darkImage = new Image();
|
||||||
|
darkImage.src = '/static/splash-dark.png';
|
||||||
|
|
||||||
|
darkImage.onload = () => {
|
||||||
|
logo.src = '/static/splash-dark.png';
|
||||||
|
logo.style.filter = ''; // Ensure no inversion is applied if splash-dark.png exists
|
||||||
|
};
|
||||||
|
|
||||||
|
darkImage.onerror = () => {
|
||||||
|
logo.style.filter = 'invert(1)'; // Invert image if splash-dark.png is missing
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Runs after classes are assigned
|
||||||
|
window.onload = setSplashImage;
|
||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -176,10 +200,6 @@
|
|||||||
background: #000;
|
background: #000;
|
||||||
}
|
}
|
||||||
|
|
||||||
html.dark #splash-screen img {
|
|
||||||
filter: invert(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
html.her #splash-screen {
|
html.her #splash-screen {
|
||||||
background: #983724;
|
background: #983724;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,9 +32,15 @@ type ChunkConfigForm = {
|
|||||||
chunk_overlap: number;
|
chunk_overlap: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type DocumentIntelligenceConfigForm = {
|
||||||
|
key: string;
|
||||||
|
endpoint: string;
|
||||||
|
};
|
||||||
|
|
||||||
type ContentExtractConfigForm = {
|
type ContentExtractConfigForm = {
|
||||||
engine: string;
|
engine: string;
|
||||||
tika_server_url: string | null;
|
tika_server_url: string | null;
|
||||||
|
document_intelligence_config: DocumentIntelligenceConfigForm | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
type YoutubeConfigForm = {
|
type YoutubeConfigForm = {
|
||||||
@@ -46,6 +52,7 @@ type YoutubeConfigForm = {
|
|||||||
type RAGConfigForm = {
|
type RAGConfigForm = {
|
||||||
pdf_extract_images?: boolean;
|
pdf_extract_images?: boolean;
|
||||||
enable_google_drive_integration?: boolean;
|
enable_google_drive_integration?: boolean;
|
||||||
|
enable_onedrive_integration?: boolean;
|
||||||
chunk?: ChunkConfigForm;
|
chunk?: ChunkConfigForm;
|
||||||
content_extraction?: ContentExtractConfigForm;
|
content_extraction?: ContentExtractConfigForm;
|
||||||
web_loader_ssl_verification?: boolean;
|
web_loader_ssl_verification?: boolean;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script>
|
<script>
|
||||||
import { getContext } from 'svelte';
|
import { getContext, onMount } from 'svelte';
|
||||||
const i18n = getContext('i18n');
|
const i18n = getContext('i18n');
|
||||||
|
|
||||||
import { WEBUI_BASE_URL } from '$lib/constants';
|
import { WEBUI_BASE_URL } from '$lib/constants';
|
||||||
@@ -10,6 +10,32 @@
|
|||||||
|
|
||||||
export let show = true;
|
export let show = true;
|
||||||
export let getStartedHandler = () => {};
|
export let getStartedHandler = () => {};
|
||||||
|
|
||||||
|
function setLogoImage() {
|
||||||
|
const logo = document.getElementById('logo');
|
||||||
|
|
||||||
|
if (logo) {
|
||||||
|
const isDarkMode = document.documentElement.classList.contains('dark');
|
||||||
|
|
||||||
|
if (isDarkMode) {
|
||||||
|
const darkImage = new Image();
|
||||||
|
darkImage.src = '/static/favicon-dark.png';
|
||||||
|
|
||||||
|
darkImage.onload = () => {
|
||||||
|
logo.src = '/static/favicon-dark.png';
|
||||||
|
logo.style.filter = ''; // Ensure no inversion is applied if splash-dark.png exists
|
||||||
|
};
|
||||||
|
|
||||||
|
darkImage.onerror = () => {
|
||||||
|
logo.style.filter = 'invert(1)'; // Invert image if splash-dark.png is missing
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$: if (show) {
|
||||||
|
setLogoImage();
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if show}
|
{#if show}
|
||||||
@@ -18,6 +44,7 @@
|
|||||||
<div class="flex space-x-2">
|
<div class="flex space-x-2">
|
||||||
<div class=" self-center">
|
<div class=" self-center">
|
||||||
<img
|
<img
|
||||||
|
id="logo"
|
||||||
crossorigin="anonymous"
|
crossorigin="anonymous"
|
||||||
src="{WEBUI_BASE_URL}/static/favicon.png"
|
src="{WEBUI_BASE_URL}/static/favicon.png"
|
||||||
class=" w-6 rounded-full"
|
class=" w-6 rounded-full"
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
<script>
|
<script>
|
||||||
import { getContext, createEventDispatcher, onMount, tick } from 'svelte';
|
import { getContext, onMount, tick } from 'svelte';
|
||||||
import { goto } from '$app/navigation';
|
import { goto } from '$app/navigation';
|
||||||
|
|
||||||
const dispatch = createEventDispatcher();
|
|
||||||
const i18n = getContext('i18n');
|
const i18n = getContext('i18n');
|
||||||
|
|
||||||
import CodeEditor from '$lib/components/common/CodeEditor.svelte';
|
import CodeEditor from '$lib/components/common/CodeEditor.svelte';
|
||||||
@@ -15,6 +14,8 @@
|
|||||||
let loading = false;
|
let loading = false;
|
||||||
let showConfirm = false;
|
let showConfirm = false;
|
||||||
|
|
||||||
|
export let onSave = () => {};
|
||||||
|
|
||||||
export let edit = false;
|
export let edit = false;
|
||||||
export let clone = false;
|
export let clone = false;
|
||||||
|
|
||||||
@@ -256,7 +257,7 @@ class Pipe:
|
|||||||
|
|
||||||
const saveHandler = async () => {
|
const saveHandler = async () => {
|
||||||
loading = true;
|
loading = true;
|
||||||
dispatch('save', {
|
onSave({
|
||||||
id,
|
id,
|
||||||
name,
|
name,
|
||||||
meta,
|
meta,
|
||||||
@@ -371,10 +372,10 @@ class Pipe:
|
|||||||
value={content}
|
value={content}
|
||||||
lang="python"
|
lang="python"
|
||||||
{boilerplate}
|
{boilerplate}
|
||||||
on:change={(e) => {
|
onChange={(e) => {
|
||||||
_content = e.detail.value;
|
_content = e;
|
||||||
}}
|
}}
|
||||||
on:save={async () => {
|
onSave={async () => {
|
||||||
if (formElement) {
|
if (formElement) {
|
||||||
formElement.requestSubmit();
|
formElement.requestSubmit();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,6 +45,16 @@
|
|||||||
|
|
||||||
<hr class=" border-gray-100 dark:border-gray-850 my-2" />
|
<hr class=" border-gray-100 dark:border-gray-850 my-2" />
|
||||||
|
|
||||||
|
<div class="mb-2.5">
|
||||||
|
<div class=" flex w-full justify-between">
|
||||||
|
<div class=" self-center text-xs font-medium">
|
||||||
|
{$i18n.t('Enable Code Execution')}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Switch bind:state={config.ENABLE_CODE_EXECUTION} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="mb-2.5">
|
<div class="mb-2.5">
|
||||||
<div class="flex w-full justify-between">
|
<div class="flex w-full justify-between">
|
||||||
<div class=" self-center text-xs font-medium">{$i18n.t('Code Execution Engine')}</div>
|
<div class=" self-center text-xs font-medium">{$i18n.t('Code Execution Engine')}</div>
|
||||||
|
|||||||
@@ -274,6 +274,7 @@
|
|||||||
newConfig[newIdx] = OPENAI_API_CONFIGS[newIdx < idx ? newIdx : newIdx + 1];
|
newConfig[newIdx] = OPENAI_API_CONFIGS[newIdx < idx ? newIdx : newIdx + 1];
|
||||||
});
|
});
|
||||||
OPENAI_API_CONFIGS = newConfig;
|
OPENAI_API_CONFIGS = newConfig;
|
||||||
|
updateOpenAIHandler();
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{/each}
|
{/each}
|
||||||
|
|||||||
@@ -103,10 +103,12 @@
|
|||||||
<div class="overflow-y-scroll scrollbar-hidden h-full">
|
<div class="overflow-y-scroll scrollbar-hidden h-full">
|
||||||
{#if evaluationConfig !== null}
|
{#if evaluationConfig !== null}
|
||||||
<div class="">
|
<div class="">
|
||||||
<div class="text-sm font-medium mb-2">{$i18n.t('General Settings')}</div>
|
<div class="mb-3">
|
||||||
|
<div class=" mb-2.5 text-base font-medium">{$i18n.t('General')}</div>
|
||||||
|
|
||||||
<div class=" mb-2">
|
<hr class=" border-gray-100 dark:border-gray-850 my-2" />
|
||||||
<div class="flex justify-between items-center text-xs">
|
|
||||||
|
<div class="mb-2.5 flex w-full justify-between">
|
||||||
<div class=" text-xs font-medium">{$i18n.t('Arena Models')}</div>
|
<div class=" text-xs font-medium">{$i18n.t('Arena Models')}</div>
|
||||||
|
|
||||||
<Tooltip content={$i18n.t(`Message rating should be enabled to use this feature`)}>
|
<Tooltip content={$i18n.t(`Message rating should be enabled to use this feature`)}>
|
||||||
@@ -116,46 +118,50 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if evaluationConfig.ENABLE_EVALUATION_ARENA_MODELS}
|
{#if evaluationConfig.ENABLE_EVALUATION_ARENA_MODELS}
|
||||||
<hr class=" border-gray-50 dark:border-gray-700/10 my-2" />
|
<div class="mb-3">
|
||||||
|
<div class=" mb-2.5 text-base font-medium flex justify-between items-center">
|
||||||
<div class="flex justify-between items-center mb-2">
|
<div>
|
||||||
<div class="text-sm font-medium">{$i18n.t('Manage Arena Models')}</div>
|
{$i18n.t('Manage')}
|
||||||
|
|
||||||
<div>
|
|
||||||
<Tooltip content={$i18n.t('Add Arena Model')}>
|
|
||||||
<button
|
|
||||||
class="p-1"
|
|
||||||
type="button"
|
|
||||||
on:click={() => {
|
|
||||||
showAddModel = true;
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Plus />
|
|
||||||
</button>
|
|
||||||
</Tooltip>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex flex-col gap-2">
|
|
||||||
{#if (evaluationConfig?.EVALUATION_ARENA_MODELS ?? []).length > 0}
|
|
||||||
{#each evaluationConfig.EVALUATION_ARENA_MODELS as model, index}
|
|
||||||
<Model
|
|
||||||
{model}
|
|
||||||
on:edit={(e) => {
|
|
||||||
editModelHandler(e.detail, index);
|
|
||||||
}}
|
|
||||||
on:delete={(e) => {
|
|
||||||
deleteModelHandler(index);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
{/each}
|
|
||||||
{:else}
|
|
||||||
<div class=" text-center text-xs text-gray-500">
|
|
||||||
{$i18n.t(
|
|
||||||
`Using the default arena model with all models. Click the plus button to add custom models.`
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
|
||||||
|
<div>
|
||||||
|
<Tooltip content={$i18n.t('Add Arena Model')}>
|
||||||
|
<button
|
||||||
|
class="p-1"
|
||||||
|
type="button"
|
||||||
|
on:click={() => {
|
||||||
|
showAddModel = true;
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Plus />
|
||||||
|
</button>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr class=" border-gray-100 dark:border-gray-850 my-2" />
|
||||||
|
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
{#if (evaluationConfig?.EVALUATION_ARENA_MODELS ?? []).length > 0}
|
||||||
|
{#each evaluationConfig.EVALUATION_ARENA_MODELS as model, index}
|
||||||
|
<Model
|
||||||
|
{model}
|
||||||
|
on:edit={(e) => {
|
||||||
|
editModelHandler(e.detail, index);
|
||||||
|
}}
|
||||||
|
on:delete={(e) => {
|
||||||
|
deleteModelHandler(index);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{/each}
|
||||||
|
{:else}
|
||||||
|
<div class=" text-center text-xs text-gray-500">
|
||||||
|
{$i18n.t(
|
||||||
|
`Using the default arena model with all models. Click the plus button to add custom models.`
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import DOMPurify from 'dompurify';
|
||||||
|
|
||||||
import { getBackendConfig, getVersionUpdates, getWebhookUrl, updateWebhookUrl } from '$lib/apis';
|
import { getBackendConfig, getVersionUpdates, getWebhookUrl, updateWebhookUrl } from '$lib/apis';
|
||||||
import {
|
import {
|
||||||
getAdminConfig,
|
getAdminConfig,
|
||||||
@@ -220,15 +222,44 @@
|
|||||||
<div class="">
|
<div class="">
|
||||||
{$i18n.t('License')}
|
{$i18n.t('License')}
|
||||||
</div>
|
</div>
|
||||||
<a
|
|
||||||
class=" text-xs text-gray-500 hover:underline"
|
{#if $config?.license_metadata}
|
||||||
href="https://docs.openwebui.com/enterprise"
|
<a
|
||||||
target="_blank"
|
href="https://docs.openwebui.com/enterprise"
|
||||||
>
|
target="_blank"
|
||||||
{$i18n.t(
|
class="text-gray-500 mt-0.5"
|
||||||
'Upgrade to a licensed plan for enhanced capabilities, including custom theming and branding, and dedicated support.'
|
>
|
||||||
)}
|
<span class=" capitalize text-black dark:text-white"
|
||||||
</a>
|
>{$config?.license_metadata?.type}
|
||||||
|
license</span
|
||||||
|
>
|
||||||
|
registered to
|
||||||
|
<span class=" capitalize text-black dark:text-white"
|
||||||
|
>{$config?.license_metadata?.organization_name}</span
|
||||||
|
>
|
||||||
|
for
|
||||||
|
<span class=" font-medium text-black dark:text-white"
|
||||||
|
>{$config?.license_metadata?.seats ?? 'Unlimited'} users.</span
|
||||||
|
>
|
||||||
|
</a>
|
||||||
|
{#if $config?.license_metadata?.html}
|
||||||
|
<div class="mt-0.5">
|
||||||
|
{@html DOMPurify.sanitize($config?.license_metadata?.html)}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{:else}
|
||||||
|
<a
|
||||||
|
class=" text-xs hover:underline"
|
||||||
|
href="https://docs.openwebui.com/enterprise"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
<span class="text-gray-500">
|
||||||
|
{$i18n.t(
|
||||||
|
'Upgrade to a licensed plan for enhanced capabilities, including custom theming and branding, and dedicated support.'
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- <button
|
<!-- <button
|
||||||
|
|||||||
@@ -16,6 +16,8 @@
|
|||||||
import Spinner from '$lib/components/common/Spinner.svelte';
|
import Spinner from '$lib/components/common/Spinner.svelte';
|
||||||
import Minus from '$lib/components/icons/Minus.svelte';
|
import Minus from '$lib/components/icons/Minus.svelte';
|
||||||
import Plus from '$lib/components/icons/Plus.svelte';
|
import Plus from '$lib/components/icons/Plus.svelte';
|
||||||
|
import ChevronUp from '$lib/components/icons/ChevronUp.svelte';
|
||||||
|
import ChevronDown from '$lib/components/icons/ChevronDown.svelte';
|
||||||
|
|
||||||
export let show = false;
|
export let show = false;
|
||||||
export let initHandler = () => {};
|
export let initHandler = () => {};
|
||||||
@@ -26,6 +28,9 @@
|
|||||||
let defaultModelIds = [];
|
let defaultModelIds = [];
|
||||||
let modelIds = [];
|
let modelIds = [];
|
||||||
|
|
||||||
|
let sortKey = '';
|
||||||
|
let sortOrder = '';
|
||||||
|
|
||||||
let loading = false;
|
let loading = false;
|
||||||
let showResetModal = false;
|
let showResetModal = false;
|
||||||
|
|
||||||
@@ -71,6 +76,9 @@
|
|||||||
// Add remaining IDs not in MODEL_ORDER_LIST, sorted alphabetically
|
// Add remaining IDs not in MODEL_ORDER_LIST, sorted alphabetically
|
||||||
...allModelIds.filter((id) => !orderedSet.has(id)).sort((a, b) => a.localeCompare(b))
|
...allModelIds.filter((id) => !orderedSet.has(id)).sort((a, b) => a.localeCompare(b))
|
||||||
];
|
];
|
||||||
|
|
||||||
|
sortKey = '';
|
||||||
|
sortOrder = '';
|
||||||
};
|
};
|
||||||
const submitHandler = async () => {
|
const submitHandler = async () => {
|
||||||
loading = true;
|
loading = true;
|
||||||
@@ -145,9 +153,45 @@
|
|||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
<div class="flex flex-col w-full">
|
<div class="flex flex-col w-full">
|
||||||
<div class="mb-1 flex justify-between">
|
<button
|
||||||
|
class="mb-1 flex gap-2"
|
||||||
|
type="button"
|
||||||
|
on:click={() => {
|
||||||
|
sortKey = 'model';
|
||||||
|
|
||||||
|
if (sortOrder === 'asc') {
|
||||||
|
sortOrder = 'desc';
|
||||||
|
} else {
|
||||||
|
sortOrder = 'asc';
|
||||||
|
}
|
||||||
|
|
||||||
|
modelIds = modelIds
|
||||||
|
.filter((id) => id !== '')
|
||||||
|
.sort((a, b) => {
|
||||||
|
const nameA = $models.find((model) => model.id === a)?.name || a;
|
||||||
|
const nameB = $models.find((model) => model.id === b)?.name || b;
|
||||||
|
return sortOrder === 'desc'
|
||||||
|
? nameA.localeCompare(nameB)
|
||||||
|
: nameB.localeCompare(nameA);
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
<div class="text-xs text-gray-500">{$i18n.t('Reorder Models')}</div>
|
<div class="text-xs text-gray-500">{$i18n.t('Reorder Models')}</div>
|
||||||
</div>
|
|
||||||
|
{#if sortKey === 'model'}
|
||||||
|
<span class="font-normal self-center">
|
||||||
|
{#if sortOrder === 'asc'}
|
||||||
|
<ChevronUp className="size-3" />
|
||||||
|
{:else}
|
||||||
|
<ChevronDown className="size-3" />
|
||||||
|
{/if}
|
||||||
|
</span>
|
||||||
|
{:else}
|
||||||
|
<span class="invisible">
|
||||||
|
<ChevronUp className="size-3" />
|
||||||
|
</span>
|
||||||
|
{/if}
|
||||||
|
</button>
|
||||||
|
|
||||||
<ModelList bind:modelIds />
|
<ModelList bind:modelIds />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -21,14 +21,24 @@
|
|||||||
modelIds = modelList;
|
modelIds = modelList;
|
||||||
};
|
};
|
||||||
|
|
||||||
onMount(() => {
|
$: if (modelIds) {
|
||||||
sortable = Sortable.create(modelListElement, {
|
init();
|
||||||
animation: 150,
|
}
|
||||||
onUpdate: async (event) => {
|
|
||||||
positionChangeHandler();
|
const init = () => {
|
||||||
}
|
if (sortable) {
|
||||||
});
|
sortable.destroy();
|
||||||
});
|
}
|
||||||
|
|
||||||
|
if (modelListElement) {
|
||||||
|
sortable = Sortable.create(modelListElement, {
|
||||||
|
animation: 150,
|
||||||
|
onUpdate: async (event) => {
|
||||||
|
positionChangeHandler();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if modelIds.length > 0}
|
{#if modelIds.length > 0}
|
||||||
|
|||||||
@@ -29,7 +29,8 @@
|
|||||||
'tavily',
|
'tavily',
|
||||||
'jina',
|
'jina',
|
||||||
'bing',
|
'bing',
|
||||||
'exa'
|
'exa',
|
||||||
|
'perplexity'
|
||||||
];
|
];
|
||||||
|
|
||||||
let youtubeLanguage = 'en';
|
let youtubeLanguage = 'en';
|
||||||
@@ -85,384 +86,427 @@
|
|||||||
>
|
>
|
||||||
<div class=" space-y-3 overflow-y-scroll scrollbar-hidden h-full">
|
<div class=" space-y-3 overflow-y-scroll scrollbar-hidden h-full">
|
||||||
{#if webConfig}
|
{#if webConfig}
|
||||||
<div>
|
<div class="">
|
||||||
<div class=" mb-1 text-sm font-medium">
|
<div class="mb-3">
|
||||||
{$i18n.t('Web Search')}
|
<div class=" mb-2.5 text-base font-medium">{$i18n.t('General')}</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
<hr class=" border-gray-100 dark:border-gray-850 my-2" />
|
||||||
<div class=" py-0.5 flex w-full justify-between">
|
|
||||||
|
<div class=" mb-2.5 flex w-full justify-between">
|
||||||
<div class=" self-center text-xs font-medium">
|
<div class=" self-center text-xs font-medium">
|
||||||
{$i18n.t('Enable Web Search')}
|
{$i18n.t('Web Search')}
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center relative">
|
||||||
|
<Switch bind:state={webConfig.search.enabled} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Switch bind:state={webConfig.search.enabled} />
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class=" py-0.5 flex w-full justify-between">
|
<div class=" mb-2.5 flex w-full justify-between">
|
||||||
<div class=" self-center text-xs font-medium">{$i18n.t('Web Search Engine')}</div>
|
<div class=" self-center text-xs font-medium">
|
||||||
<div class="flex items-center relative">
|
{$i18n.t('Web Search Engine')}
|
||||||
<select
|
</div>
|
||||||
class="dark:bg-gray-900 w-fit pr-8 rounded-sm px-2 p-1 text-xs bg-transparent outline-hidden text-right"
|
<div class="flex items-center relative">
|
||||||
bind:value={webConfig.search.engine}
|
<select
|
||||||
placeholder={$i18n.t('Select a engine')}
|
class="dark:bg-gray-900 w-fit pr-8 rounded-sm px-2 p-1 text-xs bg-transparent outline-hidden text-right"
|
||||||
required
|
bind:value={webConfig.search.engine}
|
||||||
>
|
placeholder={$i18n.t('Select a engine')}
|
||||||
<option disabled selected value="">{$i18n.t('Select a engine')}</option>
|
required
|
||||||
{#each webSearchEngines as engine}
|
>
|
||||||
<option value={engine}>{engine}</option>
|
<option disabled selected value="">{$i18n.t('Select a engine')}</option>
|
||||||
{/each}
|
{#each webSearchEngines as engine}
|
||||||
</select>
|
<option value={engine}>{engine}</option>
|
||||||
|
{/each}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class=" py-0.5 flex w-full justify-between">
|
{#if webConfig.search.engine !== ''}
|
||||||
<div class=" self-center text-xs font-medium">{$i18n.t('Full Context Mode')}</div>
|
|
||||||
<div class="flex items-center relative">
|
|
||||||
<Tooltip
|
|
||||||
content={webConfig.RAG_WEB_SEARCH_FULL_CONTEXT
|
|
||||||
? 'Inject the entire web results as context for comprehensive processing, this is recommended for complex queries.'
|
|
||||||
: 'Default to segmented retrieval for focused and relevant content extraction, this is recommended for most cases.'}
|
|
||||||
>
|
|
||||||
<Switch bind:state={webConfig.RAG_WEB_SEARCH_FULL_CONTEXT} />
|
|
||||||
</Tooltip>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{#if webConfig.search.engine !== ''}
|
|
||||||
<div class="mt-1.5">
|
|
||||||
{#if webConfig.search.engine === 'searxng'}
|
{#if webConfig.search.engine === 'searxng'}
|
||||||
<div>
|
<div class="mb-2.5 flex w-full flex-col">
|
||||||
<div class=" self-center text-xs font-medium mb-1">
|
<div>
|
||||||
{$i18n.t('Searxng Query URL')}
|
<div class=" self-center text-xs font-medium mb-1">
|
||||||
</div>
|
{$i18n.t('Searxng Query URL')}
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="flex w-full">
|
<div class="flex w-full">
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<input
|
<input
|
||||||
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden"
|
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden"
|
||||||
type="text"
|
type="text"
|
||||||
placeholder={$i18n.t('Enter Searxng Query URL')}
|
placeholder={$i18n.t('Enter Searxng Query URL')}
|
||||||
bind:value={webConfig.search.searxng_query_url}
|
bind:value={webConfig.search.searxng_query_url}
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{:else if webConfig.search.engine === 'google_pse'}
|
{:else if webConfig.search.engine === 'google_pse'}
|
||||||
<div>
|
<div class="mb-2.5 flex w-full flex-col">
|
||||||
<div class=" self-center text-xs font-medium mb-1">
|
<div>
|
||||||
{$i18n.t('Google PSE API Key')}
|
<div class=" self-center text-xs font-medium mb-1">
|
||||||
</div>
|
{$i18n.t('Google PSE API Key')}
|
||||||
|
</div>
|
||||||
|
|
||||||
<SensitiveInput
|
<SensitiveInput
|
||||||
placeholder={$i18n.t('Enter Google PSE API Key')}
|
placeholder={$i18n.t('Enter Google PSE API Key')}
|
||||||
bind:value={webConfig.search.google_pse_api_key}
|
bind:value={webConfig.search.google_pse_api_key}
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
<div class="mt-1.5">
|
|
||||||
<div class=" self-center text-xs font-medium mb-1">
|
|
||||||
{$i18n.t('Google PSE Engine Id')}
|
|
||||||
</div>
|
</div>
|
||||||
|
<div class="mt-1.5">
|
||||||
|
<div class=" self-center text-xs font-medium mb-1">
|
||||||
|
{$i18n.t('Google PSE Engine Id')}
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="flex w-full">
|
<div class="flex w-full">
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<input
|
<input
|
||||||
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden"
|
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden"
|
||||||
type="text"
|
type="text"
|
||||||
placeholder={$i18n.t('Enter Google PSE Engine Id')}
|
placeholder={$i18n.t('Enter Google PSE Engine Id')}
|
||||||
bind:value={webConfig.search.google_pse_engine_id}
|
bind:value={webConfig.search.google_pse_engine_id}
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{:else if webConfig.search.engine === 'brave'}
|
{:else if webConfig.search.engine === 'brave'}
|
||||||
<div>
|
<div class="mb-2.5 flex w-full flex-col">
|
||||||
<div class=" self-center text-xs font-medium mb-1">
|
<div>
|
||||||
{$i18n.t('Brave Search API Key')}
|
<div class=" self-center text-xs font-medium mb-1">
|
||||||
</div>
|
{$i18n.t('Brave Search API Key')}
|
||||||
|
</div>
|
||||||
|
|
||||||
<SensitiveInput
|
<SensitiveInput
|
||||||
placeholder={$i18n.t('Enter Brave Search API Key')}
|
placeholder={$i18n.t('Enter Brave Search API Key')}
|
||||||
bind:value={webConfig.search.brave_search_api_key}
|
bind:value={webConfig.search.brave_search_api_key}
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{:else if webConfig.search.engine === 'kagi'}
|
{:else if webConfig.search.engine === 'kagi'}
|
||||||
<div>
|
<div class="mb-2.5 flex w-full flex-col">
|
||||||
<div class=" self-center text-xs font-medium mb-1">
|
<div>
|
||||||
{$i18n.t('Kagi Search API Key')}
|
<div class=" self-center text-xs font-medium mb-1">
|
||||||
</div>
|
{$i18n.t('Kagi Search API Key')}
|
||||||
|
</div>
|
||||||
|
|
||||||
<SensitiveInput
|
<SensitiveInput
|
||||||
placeholder={$i18n.t('Enter Kagi Search API Key')}
|
placeholder={$i18n.t('Enter Kagi Search API Key')}
|
||||||
bind:value={webConfig.search.kagi_search_api_key}
|
bind:value={webConfig.search.kagi_search_api_key}
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
|
.
|
||||||
</div>
|
</div>
|
||||||
{:else if webConfig.search.engine === 'mojeek'}
|
{:else if webConfig.search.engine === 'mojeek'}
|
||||||
<div>
|
<div class="mb-2.5 flex w-full flex-col">
|
||||||
<div class=" self-center text-xs font-medium mb-1">
|
<div>
|
||||||
{$i18n.t('Mojeek Search API Key')}
|
<div class=" self-center text-xs font-medium mb-1">
|
||||||
</div>
|
{$i18n.t('Mojeek Search API Key')}
|
||||||
|
</div>
|
||||||
|
|
||||||
<SensitiveInput
|
<SensitiveInput
|
||||||
placeholder={$i18n.t('Enter Mojeek Search API Key')}
|
placeholder={$i18n.t('Enter Mojeek Search API Key')}
|
||||||
bind:value={webConfig.search.mojeek_search_api_key}
|
bind:value={webConfig.search.mojeek_search_api_key}
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{:else if webConfig.search.engine === 'bocha'}
|
{:else if webConfig.search.engine === 'bocha'}
|
||||||
<div>
|
<div class="mb-2.5 flex w-full flex-col">
|
||||||
<div class=" self-center text-xs font-medium mb-1">
|
<div>
|
||||||
{$i18n.t('Bocha Search API Key')}
|
<div class=" self-center text-xs font-medium mb-1">
|
||||||
</div>
|
{$i18n.t('Bocha Search API Key')}
|
||||||
|
</div>
|
||||||
|
|
||||||
<SensitiveInput
|
<SensitiveInput
|
||||||
placeholder={$i18n.t('Enter Bocha Search API Key')}
|
placeholder={$i18n.t('Enter Bocha Search API Key')}
|
||||||
bind:value={webConfig.search.bocha_search_api_key}
|
bind:value={webConfig.search.bocha_search_api_key}
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{:else if webConfig.search.engine === 'serpstack'}
|
{:else if webConfig.search.engine === 'serpstack'}
|
||||||
<div>
|
<div class="mb-2.5 flex w-full flex-col">
|
||||||
<div class=" self-center text-xs font-medium mb-1">
|
<div>
|
||||||
{$i18n.t('Serpstack API Key')}
|
<div class=" self-center text-xs font-medium mb-1">
|
||||||
</div>
|
{$i18n.t('Serpstack API Key')}
|
||||||
|
</div>
|
||||||
|
|
||||||
<SensitiveInput
|
<SensitiveInput
|
||||||
placeholder={$i18n.t('Enter Serpstack API Key')}
|
placeholder={$i18n.t('Enter Serpstack API Key')}
|
||||||
bind:value={webConfig.search.serpstack_api_key}
|
bind:value={webConfig.search.serpstack_api_key}
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{:else if webConfig.search.engine === 'serper'}
|
{:else if webConfig.search.engine === 'serper'}
|
||||||
<div>
|
<div class="mb-2.5 flex w-full flex-col">
|
||||||
<div class=" self-center text-xs font-medium mb-1">
|
<div>
|
||||||
{$i18n.t('Serper API Key')}
|
<div class=" self-center text-xs font-medium mb-1">
|
||||||
</div>
|
{$i18n.t('Serper API Key')}
|
||||||
|
</div>
|
||||||
|
|
||||||
<SensitiveInput
|
<SensitiveInput
|
||||||
placeholder={$i18n.t('Enter Serper API Key')}
|
placeholder={$i18n.t('Enter Serper API Key')}
|
||||||
bind:value={webConfig.search.serper_api_key}
|
bind:value={webConfig.search.serper_api_key}
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{:else if webConfig.search.engine === 'serply'}
|
{:else if webConfig.search.engine === 'serply'}
|
||||||
<div>
|
<div class="mb-2.5 flex w-full flex-col">
|
||||||
<div class=" self-center text-xs font-medium mb-1">
|
<div>
|
||||||
{$i18n.t('Serply API Key')}
|
<div class=" self-center text-xs font-medium mb-1">
|
||||||
</div>
|
{$i18n.t('Serply API Key')}
|
||||||
|
</div>
|
||||||
|
|
||||||
<SensitiveInput
|
<SensitiveInput
|
||||||
placeholder={$i18n.t('Enter Serply API Key')}
|
placeholder={$i18n.t('Enter Serply API Key')}
|
||||||
bind:value={webConfig.search.serply_api_key}
|
bind:value={webConfig.search.serply_api_key}
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{:else if webConfig.search.engine === 'searchapi'}
|
{:else if webConfig.search.engine === 'searchapi'}
|
||||||
<div>
|
<div class="mb-2.5 flex w-full flex-col">
|
||||||
<div class=" self-center text-xs font-medium mb-1">
|
<div>
|
||||||
{$i18n.t('SearchApi API Key')}
|
<div class=" self-center text-xs font-medium mb-1">
|
||||||
</div>
|
{$i18n.t('SearchApi API Key')}
|
||||||
|
</div>
|
||||||
|
|
||||||
<SensitiveInput
|
<SensitiveInput
|
||||||
placeholder={$i18n.t('Enter SearchApi API Key')}
|
placeholder={$i18n.t('Enter SearchApi API Key')}
|
||||||
bind:value={webConfig.search.searchapi_api_key}
|
bind:value={webConfig.search.searchapi_api_key}
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
<div class="mt-1.5">
|
|
||||||
<div class=" self-center text-xs font-medium mb-1">
|
|
||||||
{$i18n.t('SearchApi Engine')}
|
|
||||||
</div>
|
</div>
|
||||||
|
<div class="mt-1.5">
|
||||||
|
<div class=" self-center text-xs font-medium mb-1">
|
||||||
|
{$i18n.t('SearchApi Engine')}
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="flex w-full">
|
<div class="flex w-full">
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<input
|
<input
|
||||||
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden"
|
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden"
|
||||||
type="text"
|
type="text"
|
||||||
placeholder={$i18n.t('Enter SearchApi Engine')}
|
placeholder={$i18n.t('Enter SearchApi Engine')}
|
||||||
bind:value={webConfig.search.searchapi_engine}
|
bind:value={webConfig.search.searchapi_engine}
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{:else if webConfig.search.engine === 'serpapi'}
|
{:else if webConfig.search.engine === 'serpapi'}
|
||||||
<div>
|
<div class="mb-2.5 flex w-full flex-col">
|
||||||
<div class=" self-center text-xs font-medium mb-1">
|
<div>
|
||||||
{$i18n.t('SerpApi API Key')}
|
<div class=" self-center text-xs font-medium mb-1">
|
||||||
</div>
|
{$i18n.t('SerpApi API Key')}
|
||||||
|
</div>
|
||||||
|
|
||||||
<SensitiveInput
|
<SensitiveInput
|
||||||
placeholder={$i18n.t('Enter SerpApi API Key')}
|
placeholder={$i18n.t('Enter SerpApi API Key')}
|
||||||
bind:value={webConfig.search.serpapi_api_key}
|
bind:value={webConfig.search.serpapi_api_key}
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
<div class="mt-1.5">
|
|
||||||
<div class=" self-center text-xs font-medium mb-1">
|
|
||||||
{$i18n.t('SerpApi Engine')}
|
|
||||||
</div>
|
</div>
|
||||||
|
<div class="mt-1.5">
|
||||||
|
<div class=" self-center text-xs font-medium mb-1">
|
||||||
|
{$i18n.t('SerpApi Engine')}
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="flex w-full">
|
<div class="flex w-full">
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<input
|
<input
|
||||||
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden"
|
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden"
|
||||||
type="text"
|
type="text"
|
||||||
placeholder={$i18n.t('Enter SerpApi Engine')}
|
placeholder={$i18n.t('Enter SerpApi Engine')}
|
||||||
bind:value={webConfig.search.serpapi_engine}
|
bind:value={webConfig.search.serpapi_engine}
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{:else if webConfig.search.engine === 'tavily'}
|
{:else if webConfig.search.engine === 'tavily'}
|
||||||
<div>
|
<div class="mb-2.5 flex w-full flex-col">
|
||||||
<div class=" self-center text-xs font-medium mb-1">
|
<div>
|
||||||
{$i18n.t('Tavily API Key')}
|
<div class=" self-center text-xs font-medium mb-1">
|
||||||
</div>
|
{$i18n.t('Tavily API Key')}
|
||||||
|
</div>
|
||||||
|
|
||||||
<SensitiveInput
|
<SensitiveInput
|
||||||
placeholder={$i18n.t('Enter Tavily API Key')}
|
placeholder={$i18n.t('Enter Tavily API Key')}
|
||||||
bind:value={webConfig.search.tavily_api_key}
|
bind:value={webConfig.search.tavily_api_key}
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{:else if webConfig.search.engine === 'jina'}
|
{:else if webConfig.search.engine === 'jina'}
|
||||||
<div>
|
<div class="mb-2.5 flex w-full flex-col">
|
||||||
<div class=" self-center text-xs font-medium mb-1">
|
<div>
|
||||||
{$i18n.t('Jina API Key')}
|
<div class=" self-center text-xs font-medium mb-1">
|
||||||
</div>
|
{$i18n.t('Jina API Key')}
|
||||||
|
</div>
|
||||||
|
|
||||||
<SensitiveInput
|
<SensitiveInput
|
||||||
placeholder={$i18n.t('Enter Jina API Key')}
|
placeholder={$i18n.t('Enter Jina API Key')}
|
||||||
bind:value={webConfig.search.jina_api_key}
|
bind:value={webConfig.search.jina_api_key}
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{:else if webConfig.search.engine === 'exa'}
|
{:else if webConfig.search.engine === 'exa'}
|
||||||
|
<div class="mb-2.5 flex w-full flex-col">
|
||||||
|
<div>
|
||||||
|
<div class=" self-center text-xs font-medium mb-1">
|
||||||
|
{$i18n.t('Exa API Key')}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<SensitiveInput
|
||||||
|
placeholder={$i18n.t('Enter Exa API Key')}
|
||||||
|
bind:value={webConfig.search.exa_api_key}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{:else if webConfig.search.engine === 'perplexity'}
|
||||||
<div>
|
<div>
|
||||||
<div class=" self-center text-xs font-medium mb-1">
|
<div class=" self-center text-xs font-medium mb-1">
|
||||||
{$i18n.t('Exa API Key')}
|
{$i18n.t('Perplexity API Key')}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<SensitiveInput
|
<SensitiveInput
|
||||||
placeholder={$i18n.t('Enter Exa API Key')}
|
placeholder={$i18n.t('Enter Perplexity API Key')}
|
||||||
bind:value={webConfig.search.exa_api_key}
|
bind:value={webConfig.search.perplexity_api_key}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{:else if webConfig.search.engine === 'bing'}
|
{:else if webConfig.search.engine === 'bing'}
|
||||||
<div>
|
<div class="mb-2.5 flex w-full flex-col">
|
||||||
<div class=" self-center text-xs font-medium mb-1">
|
<div>
|
||||||
{$i18n.t('Bing Search V7 Endpoint')}
|
<div class=" self-center text-xs font-medium mb-1">
|
||||||
</div>
|
{$i18n.t('Bing Search V7 Endpoint')}
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="flex w-full">
|
<div class="flex w-full">
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<input
|
<input
|
||||||
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden"
|
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden"
|
||||||
type="text"
|
type="text"
|
||||||
placeholder={$i18n.t('Enter Bing Search V7 Endpoint')}
|
placeholder={$i18n.t('Enter Bing Search V7 Endpoint')}
|
||||||
bind:value={webConfig.search.bing_search_v7_endpoint}
|
bind:value={webConfig.search.bing_search_v7_endpoint}
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mt-2">
|
<div class="mt-2">
|
||||||
<div class=" self-center text-xs font-medium mb-1">
|
<div class=" self-center text-xs font-medium mb-1">
|
||||||
{$i18n.t('Bing Search V7 Subscription Key')}
|
{$i18n.t('Bing Search V7 Subscription Key')}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<SensitiveInput
|
||||||
|
placeholder={$i18n.t('Enter Bing Search V7 Subscription Key')}
|
||||||
|
bind:value={webConfig.search.bing_search_v7_subscription_key}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<SensitiveInput
|
|
||||||
placeholder={$i18n.t('Enter Bing Search V7 Subscription Key')}
|
|
||||||
bind:value={webConfig.search.bing_search_v7_subscription_key}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
{/if}
|
||||||
{/if}
|
|
||||||
|
|
||||||
{#if webConfig.search.enabled}
|
{#if webConfig.search.enabled}
|
||||||
<div class="mt-2 flex gap-2 mb-1">
|
<div class="mb-2.5 flex w-full flex-col">
|
||||||
<div class="w-full">
|
<div class="flex gap-2">
|
||||||
<div class=" self-center text-xs font-medium mb-1">
|
<div class="w-full">
|
||||||
{$i18n.t('Search Result Count')}
|
<div class=" self-center text-xs font-medium mb-1">
|
||||||
|
{$i18n.t('Search Result Count')}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<input
|
||||||
|
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden"
|
||||||
|
placeholder={$i18n.t('Search Result Count')}
|
||||||
|
bind:value={webConfig.search.result_count}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="w-full">
|
||||||
|
<div class=" self-center text-xs font-medium mb-1">
|
||||||
|
{$i18n.t('Concurrent Requests')}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<input
|
||||||
|
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden"
|
||||||
|
placeholder={$i18n.t('Concurrent Requests')}
|
||||||
|
bind:value={webConfig.search.concurrent_requests}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-2.5 flex w-full flex-col">
|
||||||
|
<div class=" text-xs font-medium mb-1">
|
||||||
|
{$i18n.t('Domain Filter List')}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<input
|
<input
|
||||||
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden"
|
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden"
|
||||||
placeholder={$i18n.t('Search Result Count')}
|
placeholder={$i18n.t(
|
||||||
bind:value={webConfig.search.result_count}
|
'Enter domains separated by commas (e.g., example.com,site.org)'
|
||||||
required
|
)}
|
||||||
|
bind:value={webConfig.search.domain_filter_list}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
<div class="w-full">
|
<div class=" mb-2.5 flex w-full justify-between">
|
||||||
<div class=" self-center text-xs font-medium mb-1">
|
<div class=" self-center text-xs font-medium">
|
||||||
{$i18n.t('Concurrent Requests')}
|
<Tooltip content={$i18n.t('Full Context Mode')} placement="top-start">
|
||||||
</div>
|
{$i18n.t('Bypass Embedding and Retrieval')}
|
||||||
|
</Tooltip>
|
||||||
<input
|
</div>
|
||||||
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden"
|
<div class="flex items-center relative">
|
||||||
placeholder={$i18n.t('Concurrent Requests')}
|
<Tooltip
|
||||||
bind:value={webConfig.search.concurrent_requests}
|
content={webConfig.BYPASS_WEB_SEARCH_EMBEDDING_AND_RETRIEVAL
|
||||||
required
|
? 'Inject the entire content as context for comprehensive processing, this is recommended for complex queries.'
|
||||||
/>
|
: 'Default to segmented retrieval for focused and relevant content extraction, this is recommended for most cases.'}
|
||||||
|
>
|
||||||
|
<Switch bind:state={webConfig.BYPASS_WEB_SEARCH_EMBEDDING_AND_RETRIEVAL} />
|
||||||
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mt-2">
|
<div class=" mb-2.5 flex w-full justify-between">
|
||||||
<div class=" self-center text-xs font-medium mb-1">
|
<div class=" self-center text-xs font-medium">
|
||||||
{$i18n.t('Domain Filter List')}
|
{$i18n.t('Trust Proxy Environment')}
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center relative">
|
||||||
|
<Tooltip
|
||||||
|
content={webConfig.search.trust_env
|
||||||
|
? 'Use proxy designated by http_proxy and https_proxy environment variables to fetch page contents'
|
||||||
|
: 'Use no proxy to fetch page contents.'}
|
||||||
|
>
|
||||||
|
<Switch bind:state={webConfig.search.trust_env} />
|
||||||
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<input
|
|
||||||
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden"
|
|
||||||
placeholder={$i18n.t(
|
|
||||||
'Enter domains separated by commas (e.g., example.com,site.org)'
|
|
||||||
)}
|
|
||||||
bind:value={webConfig.search.domain_filter_list}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<hr class="border-gray-100 dark:border-gray-850 my-2" />
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<div class=" mb-1 text-sm font-medium">
|
|
||||||
{$i18n.t('Web Loader Settings')}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div class="mb-3">
|
||||||
<div class=" py-0.5 flex w-full justify-between">
|
<div class=" mb-2.5 text-base font-medium">{$i18n.t('Loader')}</div>
|
||||||
|
|
||||||
|
<hr class=" border-gray-100 dark:border-gray-850 my-2" />
|
||||||
|
|
||||||
|
<div class=" mb-2.5 flex w-full justify-between">
|
||||||
<div class=" self-center text-xs font-medium">
|
<div class=" self-center text-xs font-medium">
|
||||||
{$i18n.t('Bypass SSL verification for Websites')}
|
{$i18n.t('Bypass SSL verification for Websites')}
|
||||||
</div>
|
</div>
|
||||||
|
<div class="flex items-center relative">
|
||||||
<button
|
<Switch bind:state={webConfig.ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION} />
|
||||||
class="p-1 px-3 text-xs flex rounded-sm transition"
|
</div>
|
||||||
on:click={() => {
|
|
||||||
webConfig.ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION =
|
|
||||||
!webConfig.ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION;
|
|
||||||
submitHandler();
|
|
||||||
}}
|
|
||||||
type="button"
|
|
||||||
>
|
|
||||||
{#if webConfig.ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION === false}
|
|
||||||
<span class="ml-2 self-center">{$i18n.t('On')}</span>
|
|
||||||
{:else}
|
|
||||||
<span class="ml-2 self-center">{$i18n.t('Off')}</span>
|
|
||||||
{/if}
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class=" mt-2 mb-1 text-sm font-medium">
|
<div class=" mb-2.5 flex w-full justify-between">
|
||||||
{$i18n.t('Youtube Loader Settings')}
|
<div class=" self-center text-xs font-medium">
|
||||||
</div>
|
{$i18n.t('Youtube Language')}
|
||||||
|
</div>
|
||||||
<div>
|
<div class="flex items-center relative">
|
||||||
<div class=" py-0.5 flex w-full justify-between">
|
|
||||||
<div class=" w-20 text-xs font-medium self-center">{$i18n.t('Language')}</div>
|
|
||||||
<div class=" flex-1 self-center">
|
|
||||||
<input
|
<input
|
||||||
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden"
|
class="flex-1 w-full rounded-lg text-sm bg-transparent outline-hidden"
|
||||||
type="text"
|
type="text"
|
||||||
placeholder={$i18n.t('Enter language codes')}
|
placeholder={$i18n.t('Enter language codes')}
|
||||||
bind:value={youtubeLanguage}
|
bind:value={youtubeLanguage}
|
||||||
@@ -470,14 +514,14 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
<div class=" mb-2.5 flex flex-col w-full justify-between">
|
||||||
<div class=" py-0.5 flex w-full justify-between">
|
<div class=" mb-1 text-xs font-medium">
|
||||||
<div class=" w-20 text-xs font-medium self-center">{$i18n.t('Proxy URL')}</div>
|
{$i18n.t('Youtube Proxy URL')}
|
||||||
<div class=" flex-1 self-center">
|
</div>
|
||||||
|
<div class="flex items-center relative">
|
||||||
<input
|
<input
|
||||||
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden"
|
class="flex-1 w-full rounded-lg text-sm bg-transparent outline-hidden"
|
||||||
type="text"
|
type="text"
|
||||||
placeholder={$i18n.t('Enter proxy URL (e.g. https://user:password@host:port)')}
|
placeholder={$i18n.t('Enter proxy URL (e.g. https://user:password@host:port)')}
|
||||||
bind:value={youtubeProxyUrl}
|
bind:value={youtubeProxyUrl}
|
||||||
|
|||||||
@@ -28,6 +28,7 @@
|
|||||||
import ChevronUp from '$lib/components/icons/ChevronUp.svelte';
|
import ChevronUp from '$lib/components/icons/ChevronUp.svelte';
|
||||||
import ChevronDown from '$lib/components/icons/ChevronDown.svelte';
|
import ChevronDown from '$lib/components/icons/ChevronDown.svelte';
|
||||||
import About from '$lib/components/chat/Settings/About.svelte';
|
import About from '$lib/components/chat/Settings/About.svelte';
|
||||||
|
import Banner from '$lib/components/common/Banner.svelte';
|
||||||
|
|
||||||
const i18n = getContext('i18n');
|
const i18n = getContext('i18n');
|
||||||
|
|
||||||
@@ -124,12 +125,43 @@
|
|||||||
/>
|
/>
|
||||||
<UserChatsModal bind:show={showUserChatsModal} user={selectedUser} />
|
<UserChatsModal bind:show={showUserChatsModal} user={selectedUser} />
|
||||||
|
|
||||||
|
{#if ($config?.license_metadata?.seats ?? null) !== null && users.length > $config?.license_metadata?.seats}
|
||||||
|
<div class=" mt-1 mb-2 text-xs text-red-500">
|
||||||
|
<Banner
|
||||||
|
className="mx-0"
|
||||||
|
banner={{
|
||||||
|
type: 'error',
|
||||||
|
title: 'License Error',
|
||||||
|
content:
|
||||||
|
'Exceeded the number of seats in your license. Please contact support to increase the number of seats.',
|
||||||
|
dismissable: true
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
<div class="mt-0.5 mb-2 gap-1 flex flex-col md:flex-row justify-between">
|
<div class="mt-0.5 mb-2 gap-1 flex flex-col md:flex-row justify-between">
|
||||||
<div class="flex md:self-center text-lg font-medium px-0.5">
|
<div class="flex md:self-center text-lg font-medium px-0.5">
|
||||||
{$i18n.t('Users')}
|
<div class="flex-shrink-0">
|
||||||
|
{$i18n.t('Users')}
|
||||||
|
</div>
|
||||||
<div class="flex self-center w-[1px] h-6 mx-2.5 bg-gray-50 dark:bg-gray-850" />
|
<div class="flex self-center w-[1px] h-6 mx-2.5 bg-gray-50 dark:bg-gray-850" />
|
||||||
|
|
||||||
<span class="text-lg font-medium text-gray-500 dark:text-gray-300">{users.length}</span>
|
{#if ($config?.license_metadata?.seats ?? null) !== null}
|
||||||
|
{#if users.length > $config?.license_metadata?.seats}
|
||||||
|
<span class="text-lg font-medium text-red-500"
|
||||||
|
>{users.length} of {$config?.license_metadata?.seats}
|
||||||
|
<span class="text-sm font-normal">available users</span></span
|
||||||
|
>
|
||||||
|
{:else}
|
||||||
|
<span class="text-lg font-medium text-gray-500 dark:text-gray-300"
|
||||||
|
>{users.length} of {$config?.license_metadata?.seats}
|
||||||
|
<span class="text-sm font-normal">available users</span></span
|
||||||
|
>
|
||||||
|
{/if}
|
||||||
|
{:else}
|
||||||
|
<span class="text-lg font-medium text-gray-500 dark:text-gray-300">{users.length}</span>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex gap-1">
|
<div class="flex gap-1">
|
||||||
|
|||||||
@@ -157,22 +157,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
files = [...files, fileItem];
|
files = [...files, fileItem];
|
||||||
// Check if the file is an audio file and transcribe/convert it to text file
|
|
||||||
if (['audio/mpeg', 'audio/wav', 'audio/ogg', 'audio/x-m4a'].includes(file['type'])) {
|
|
||||||
const res = await transcribeAudio(localStorage.token, file).catch((error) => {
|
|
||||||
toast.error(`${error}`);
|
|
||||||
return null;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (res) {
|
|
||||||
console.log(res);
|
|
||||||
const blob = new Blob([res.text], { type: 'text/plain' });
|
|
||||||
file = blobToFile(blob, `${file.name}.txt`);
|
|
||||||
|
|
||||||
fileItem.name = file.name;
|
|
||||||
fileItem.size = file.size;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// During the file upload, file content is automatically extracted.
|
// During the file upload, file content is automatically extracted.
|
||||||
|
|||||||
@@ -187,15 +187,20 @@
|
|||||||
setToolIds();
|
setToolIds();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$: if (atSelectedModel || selectedModels) {
|
||||||
|
setToolIds();
|
||||||
|
}
|
||||||
|
|
||||||
const setToolIds = async () => {
|
const setToolIds = async () => {
|
||||||
if (!$tools) {
|
if (!$tools) {
|
||||||
tools.set(await getTools(localStorage.token));
|
tools.set(await getTools(localStorage.token));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (selectedModels.length !== 1) {
|
if (selectedModels.length !== 1 && !atSelectedModel) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const model = $models.find((m) => m.id === selectedModels[0]);
|
|
||||||
|
const model = atSelectedModel ?? $models.find((m) => m.id === selectedModels[0]);
|
||||||
if (model) {
|
if (model) {
|
||||||
selectedToolIds = (model?.info?.meta?.toolIds ?? []).filter((id) =>
|
selectedToolIds = (model?.info?.meta?.toolIds ?? []).filter((id) =>
|
||||||
$tools.find((t) => t.id === id)
|
$tools.find((t) => t.id === id)
|
||||||
@@ -836,6 +841,7 @@
|
|||||||
content: m.content,
|
content: m.content,
|
||||||
info: m.info ? m.info : undefined,
|
info: m.info ? m.info : undefined,
|
||||||
timestamp: m.timestamp,
|
timestamp: m.timestamp,
|
||||||
|
...(m.usage ? { usage: m.usage } : {}),
|
||||||
...(m.sources ? { sources: m.sources } : {})
|
...(m.sources ? { sources: m.sources } : {})
|
||||||
})),
|
})),
|
||||||
model_item: $models.find((m) => m.id === modelId),
|
model_item: $models.find((m) => m.id === modelId),
|
||||||
@@ -1273,7 +1279,9 @@
|
|||||||
const chatInputElement = document.getElementById('chat-input');
|
const chatInputElement = document.getElementById('chat-input');
|
||||||
|
|
||||||
if (chatInputElement) {
|
if (chatInputElement) {
|
||||||
|
await tick();
|
||||||
chatInputElement.style.height = '';
|
chatInputElement.style.height = '';
|
||||||
|
chatInputElement.style.height = Math.min(chatInputElement.scrollHeight, 320) + 'px';
|
||||||
}
|
}
|
||||||
|
|
||||||
const _files = JSON.parse(JSON.stringify(files));
|
const _files = JSON.parse(JSON.stringify(files));
|
||||||
@@ -1488,7 +1496,10 @@
|
|||||||
params?.system ?? $settings?.system ?? '',
|
params?.system ?? $settings?.system ?? '',
|
||||||
$user.name,
|
$user.name,
|
||||||
$settings?.userLocation
|
$settings?.userLocation
|
||||||
? await getAndUpdateUserLocation(localStorage.token)
|
? await getAndUpdateUserLocation(localStorage.token).catch((err) => {
|
||||||
|
console.error(err);
|
||||||
|
return undefined;
|
||||||
|
})
|
||||||
: undefined
|
: undefined
|
||||||
)}${
|
)}${
|
||||||
(responseMessage?.userContext ?? null)
|
(responseMessage?.userContext ?? null)
|
||||||
@@ -1573,7 +1584,12 @@
|
|||||||
variables: {
|
variables: {
|
||||||
...getPromptVariables(
|
...getPromptVariables(
|
||||||
$user.name,
|
$user.name,
|
||||||
$settings?.userLocation ? await getAndUpdateUserLocation(localStorage.token) : undefined
|
$settings?.userLocation
|
||||||
|
? await getAndUpdateUserLocation(localStorage.token).catch((err) => {
|
||||||
|
console.error(err);
|
||||||
|
return undefined;
|
||||||
|
})
|
||||||
|
: undefined
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
model_item: $models.find((m) => m.id === model.id),
|
model_item: $models.find((m) => m.id === model.id),
|
||||||
@@ -1921,9 +1937,33 @@
|
|||||||
|
|
||||||
<PaneGroup direction="horizontal" class="w-full h-full">
|
<PaneGroup direction="horizontal" class="w-full h-full">
|
||||||
<Pane defaultSize={50} class="h-full flex w-full relative">
|
<Pane defaultSize={50} class="h-full flex w-full relative">
|
||||||
{#if $banners.length > 0 && !history.currentId && !$chatId && selectedModels.length <= 1}
|
{#if !history.currentId && !$chatId && selectedModels.length <= 1 && ($banners.length > 0 || ($config?.license_metadata?.type ?? null) === 'trial' || (($config?.license_metadata?.seats ?? null) !== null && $config?.user_count > $config?.license_metadata?.seats))}
|
||||||
<div class="absolute top-12 left-0 right-0 w-full z-30">
|
<div class="absolute top-12 left-0 right-0 w-full z-30">
|
||||||
<div class=" flex flex-col gap-1 w-full">
|
<div class=" flex flex-col gap-1 w-full">
|
||||||
|
{#if ($config?.license_metadata?.type ?? null) === 'trial'}
|
||||||
|
<Banner
|
||||||
|
banner={{
|
||||||
|
type: 'info',
|
||||||
|
title: 'Trial License',
|
||||||
|
content: $i18n.t(
|
||||||
|
'You are currently using a trial license. Please contact support to upgrade your license.'
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if ($config?.license_metadata?.seats ?? null) !== null && $config?.user_count > $config?.license_metadata?.seats}
|
||||||
|
<Banner
|
||||||
|
banner={{
|
||||||
|
type: 'error',
|
||||||
|
title: 'License Error',
|
||||||
|
content: $i18n.t(
|
||||||
|
'Exceeded the number of seats in your license. Please contact support to increase the number of seats.'
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
|
||||||
{#each $banners.filter( (b) => (b.dismissible ? !JSON.parse(localStorage.getItem('dismissedBannerIds') ?? '[]').includes(b.id) : true) ) as banner}
|
{#each $banners.filter( (b) => (b.dismissible ? !JSON.parse(localStorage.getItem('dismissedBannerIds') ?? '[]').includes(b.id) : true) ) as banner}
|
||||||
<Banner
|
<Banner
|
||||||
{banner}
|
{banner}
|
||||||
@@ -1965,6 +2005,7 @@
|
|||||||
bind:autoScroll
|
bind:autoScroll
|
||||||
bind:prompt
|
bind:prompt
|
||||||
{selectedModels}
|
{selectedModels}
|
||||||
|
{atSelectedModel}
|
||||||
{sendPrompt}
|
{sendPrompt}
|
||||||
{showMessage}
|
{showMessage}
|
||||||
{submitMessage}
|
{submitMessage}
|
||||||
|
|||||||
@@ -16,6 +16,7 @@
|
|||||||
|
|
||||||
export let modelIds = [];
|
export let modelIds = [];
|
||||||
export let models = [];
|
export let models = [];
|
||||||
|
export let atSelectedModel;
|
||||||
|
|
||||||
export let submitPrompt;
|
export let submitPrompt;
|
||||||
|
|
||||||
@@ -126,7 +127,8 @@
|
|||||||
<div class=" w-full font-primary" in:fade={{ duration: 200, delay: 300 }}>
|
<div class=" w-full font-primary" in:fade={{ duration: 200, delay: 300 }}>
|
||||||
<Suggestions
|
<Suggestions
|
||||||
className="grid grid-cols-2"
|
className="grid grid-cols-2"
|
||||||
suggestionPrompts={models[selectedModelIdx]?.info?.meta?.suggestion_prompts ??
|
suggestionPrompts={atSelectedModel?.info?.meta?.suggestion_prompts ??
|
||||||
|
models[selectedModelIdx]?.info?.meta?.suggestion_prompts ??
|
||||||
$config?.default_prompt_suggestions ??
|
$config?.default_prompt_suggestions ??
|
||||||
[]}
|
[]}
|
||||||
on:select={(e) => {
|
on:select={(e) => {
|
||||||
|
|||||||
@@ -121,7 +121,8 @@
|
|||||||
toast.error('Model not selected');
|
toast.error('Model not selected');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
prompt = `Explain this section to me in more detail\n\n\`\`\`\n${selectedText}\n\`\`\``;
|
const explainText = $i18n.t('Explain this section to me in more detail');
|
||||||
|
prompt = `${explainText}\n\n\`\`\`\n${selectedText}\n\`\`\``;
|
||||||
|
|
||||||
responseContent = '';
|
responseContent = '';
|
||||||
const [res, controller] = await chatCompletion(localStorage.token, {
|
const [res, controller] = await chatCompletion(localStorage.token, {
|
||||||
@@ -246,7 +247,7 @@
|
|||||||
>
|
>
|
||||||
<ChatBubble className="size-3 shrink-0" />
|
<ChatBubble className="size-3 shrink-0" />
|
||||||
|
|
||||||
<div class="shrink-0">Ask</div>
|
<div class="shrink-0">{$i18n.t('Ask')}</div>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
class="px-1 hover:bg-gray-50 dark:hover:bg-gray-800 rounded-sm flex items-center gap-1 min-w-fit"
|
class="px-1 hover:bg-gray-50 dark:hover:bg-gray-800 rounded-sm flex items-center gap-1 min-w-fit"
|
||||||
@@ -257,7 +258,7 @@
|
|||||||
>
|
>
|
||||||
<LightBlub className="size-3 shrink-0" />
|
<LightBlub className="size-3 shrink-0" />
|
||||||
|
|
||||||
<div class="shrink-0">Explain</div>
|
<div class="shrink-0">{$i18n.t('Explain')}</div>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
import { toast } from 'svelte-sonner';
|
import { toast } from 'svelte-sonner';
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
import { createPicker, getAuthToken } from '$lib/utils/google-drive-picker';
|
import { createPicker, getAuthToken } from '$lib/utils/google-drive-picker';
|
||||||
|
import { pickAndDownloadFile } from '$lib/utils/onedrive-file-picker';
|
||||||
|
|
||||||
import { onMount, tick, getContext, createEventDispatcher, onDestroy } from 'svelte';
|
import { onMount, tick, getContext, createEventDispatcher, onDestroy } from 'svelte';
|
||||||
const dispatch = createEventDispatcher();
|
const dispatch = createEventDispatcher();
|
||||||
@@ -84,6 +85,8 @@
|
|||||||
let loaded = false;
|
let loaded = false;
|
||||||
let recording = false;
|
let recording = false;
|
||||||
|
|
||||||
|
let isComposing = false;
|
||||||
|
|
||||||
let chatInputContainerElement;
|
let chatInputContainerElement;
|
||||||
let chatInputElement;
|
let chatInputElement;
|
||||||
|
|
||||||
@@ -173,22 +176,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
files = [...files, fileItem];
|
files = [...files, fileItem];
|
||||||
// Check if the file is an audio file and transcribe/convert it to text file
|
|
||||||
if (['audio/mpeg', 'audio/wav', 'audio/ogg', 'audio/x-m4a'].includes(file['type'])) {
|
|
||||||
const res = await transcribeAudio(localStorage.token, file).catch((error) => {
|
|
||||||
toast.error(`${error}`);
|
|
||||||
return null;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (res) {
|
|
||||||
console.log(res);
|
|
||||||
const blob = new Blob([res.text], { type: 'text/plain' });
|
|
||||||
file = blobToFile(blob, `${file.name}.txt`);
|
|
||||||
|
|
||||||
fileItem.name = file.name;
|
|
||||||
fileItem.size = file.size;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// During the file upload, file content is automatically extracted.
|
// During the file upload, file content is automatically extracted.
|
||||||
@@ -691,12 +678,13 @@
|
|||||||
bind:value={prompt}
|
bind:value={prompt}
|
||||||
id="chat-input"
|
id="chat-input"
|
||||||
messageInput={true}
|
messageInput={true}
|
||||||
shiftEnter={!$mobile ||
|
shiftEnter={!($settings?.ctrlEnterToSend ?? false) &&
|
||||||
!(
|
(!$mobile ||
|
||||||
'ontouchstart' in window ||
|
!(
|
||||||
navigator.maxTouchPoints > 0 ||
|
'ontouchstart' in window ||
|
||||||
navigator.msMaxTouchPoints > 0
|
navigator.maxTouchPoints > 0 ||
|
||||||
)}
|
navigator.msMaxTouchPoints > 0
|
||||||
|
))}
|
||||||
placeholder={placeholder ? placeholder : $i18n.t('Send a Message')}
|
placeholder={placeholder ? placeholder : $i18n.t('Send a Message')}
|
||||||
largeTextAsFile={$settings?.largeTextAsFile ?? false}
|
largeTextAsFile={$settings?.largeTextAsFile ?? false}
|
||||||
autocomplete={$config?.features.enable_autocomplete_generation}
|
autocomplete={$config?.features.enable_autocomplete_generation}
|
||||||
@@ -721,6 +709,8 @@
|
|||||||
console.log(res);
|
console.log(res);
|
||||||
return res;
|
return res;
|
||||||
}}
|
}}
|
||||||
|
oncompositionstart={() => (isComposing = true)}
|
||||||
|
oncompositionend={() => (isComposing = false)}
|
||||||
on:keydown={async (e) => {
|
on:keydown={async (e) => {
|
||||||
e = e.detail.event;
|
e = e.detail.event;
|
||||||
|
|
||||||
@@ -820,15 +810,24 @@
|
|||||||
navigator.msMaxTouchPoints > 0
|
navigator.msMaxTouchPoints > 0
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
// Prevent Enter key from creating a new line
|
if (isComposing) {
|
||||||
// Uses keyCode '13' for Enter key for chinese/japanese keyboards
|
return;
|
||||||
if (e.keyCode === 13 && !e.shiftKey) {
|
|
||||||
e.preventDefault();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Submit the prompt when Enter key is pressed
|
// Uses keyCode '13' for Enter key for chinese/japanese keyboards.
|
||||||
if (prompt !== '' && e.keyCode === 13 && !e.shiftKey) {
|
//
|
||||||
dispatch('submit', prompt);
|
// Depending on the user's settings, it will send the message
|
||||||
|
// either when Enter is pressed or when Ctrl+Enter is pressed.
|
||||||
|
const enterPressed =
|
||||||
|
($settings?.ctrlEnterToSend ?? false)
|
||||||
|
? (e.key === 'Enter' || e.keyCode === 13) && isCtrlPressed
|
||||||
|
: (e.key === 'Enter' || e.keyCode === 13) && !e.shiftKey;
|
||||||
|
|
||||||
|
if (enterPressed) {
|
||||||
|
e.preventDefault();
|
||||||
|
if (prompt !== '' || files.length > 0) {
|
||||||
|
dispatch('submit', prompt);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -891,34 +890,19 @@
|
|||||||
class="scrollbar-hidden bg-transparent dark:text-gray-100 outline-hidden w-full pt-3 px-1 resize-none"
|
class="scrollbar-hidden bg-transparent dark:text-gray-100 outline-hidden w-full pt-3 px-1 resize-none"
|
||||||
placeholder={placeholder ? placeholder : $i18n.t('Send a Message')}
|
placeholder={placeholder ? placeholder : $i18n.t('Send a Message')}
|
||||||
bind:value={prompt}
|
bind:value={prompt}
|
||||||
on:keypress={(e) => {
|
on:compositionstart={() => (isComposing = true)}
|
||||||
if (
|
on:compositionend={() => (isComposing = false)}
|
||||||
!$mobile ||
|
|
||||||
!(
|
|
||||||
'ontouchstart' in window ||
|
|
||||||
navigator.maxTouchPoints > 0 ||
|
|
||||||
navigator.msMaxTouchPoints > 0
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
// Prevent Enter key from creating a new line
|
|
||||||
if (e.key === 'Enter' && !e.shiftKey) {
|
|
||||||
e.preventDefault();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Submit the prompt when Enter key is pressed
|
|
||||||
if (prompt !== '' && e.key === 'Enter' && !e.shiftKey) {
|
|
||||||
dispatch('submit', prompt);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
on:keydown={async (e) => {
|
on:keydown={async (e) => {
|
||||||
const isCtrlPressed = e.ctrlKey || e.metaKey; // metaKey is for Cmd key on Mac
|
const isCtrlPressed = e.ctrlKey || e.metaKey; // metaKey is for Cmd key on Mac
|
||||||
|
|
||||||
|
console.log('keydown', e);
|
||||||
const commandsContainerElement =
|
const commandsContainerElement =
|
||||||
document.getElementById('commands-container');
|
document.getElementById('commands-container');
|
||||||
|
|
||||||
if (e.key === 'Escape') {
|
if (e.key === 'Escape') {
|
||||||
stopResponse();
|
stopResponse();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Command/Ctrl + Shift + Enter to submit a message pair
|
// Command/Ctrl + Shift + Enter to submit a message pair
|
||||||
if (isCtrlPressed && e.key === 'Enter' && e.shiftKey) {
|
if (isCtrlPressed && e.key === 'Enter' && e.shiftKey) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@@ -954,51 +938,87 @@
|
|||||||
editButton?.click();
|
editButton?.click();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (commandsContainerElement && e.key === 'ArrowUp') {
|
if (commandsContainerElement) {
|
||||||
e.preventDefault();
|
if (commandsContainerElement && e.key === 'ArrowUp') {
|
||||||
commandsElement.selectUp();
|
e.preventDefault();
|
||||||
|
commandsElement.selectUp();
|
||||||
|
|
||||||
const commandOptionButton = [
|
const commandOptionButton = [
|
||||||
...document.getElementsByClassName('selected-command-option-button')
|
...document.getElementsByClassName('selected-command-option-button')
|
||||||
]?.at(-1);
|
]?.at(-1);
|
||||||
commandOptionButton.scrollIntoView({ block: 'center' });
|
commandOptionButton.scrollIntoView({ block: 'center' });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (commandsContainerElement && e.key === 'ArrowDown') {
|
if (commandsContainerElement && e.key === 'ArrowDown') {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
commandsElement.selectDown();
|
commandsElement.selectDown();
|
||||||
|
|
||||||
const commandOptionButton = [
|
const commandOptionButton = [
|
||||||
...document.getElementsByClassName('selected-command-option-button')
|
...document.getElementsByClassName('selected-command-option-button')
|
||||||
]?.at(-1);
|
]?.at(-1);
|
||||||
commandOptionButton.scrollIntoView({ block: 'center' });
|
commandOptionButton.scrollIntoView({ block: 'center' });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (commandsContainerElement && e.key === 'Enter') {
|
if (commandsContainerElement && e.key === 'Enter') {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
const commandOptionButton = [
|
const commandOptionButton = [
|
||||||
...document.getElementsByClassName('selected-command-option-button')
|
...document.getElementsByClassName('selected-command-option-button')
|
||||||
]?.at(-1);
|
]?.at(-1);
|
||||||
|
|
||||||
|
if (e.shiftKey) {
|
||||||
|
prompt = `${prompt}\n`;
|
||||||
|
} else if (commandOptionButton) {
|
||||||
|
commandOptionButton?.click();
|
||||||
|
} else {
|
||||||
|
document.getElementById('send-message-button')?.click();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (commandsContainerElement && e.key === 'Tab') {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
const commandOptionButton = [
|
||||||
|
...document.getElementsByClassName('selected-command-option-button')
|
||||||
|
]?.at(-1);
|
||||||
|
|
||||||
if (e.shiftKey) {
|
|
||||||
prompt = `${prompt}\n`;
|
|
||||||
} else if (commandOptionButton) {
|
|
||||||
commandOptionButton?.click();
|
commandOptionButton?.click();
|
||||||
} else {
|
}
|
||||||
document.getElementById('send-message-button')?.click();
|
} else {
|
||||||
|
if (
|
||||||
|
!$mobile ||
|
||||||
|
!(
|
||||||
|
'ontouchstart' in window ||
|
||||||
|
navigator.maxTouchPoints > 0 ||
|
||||||
|
navigator.msMaxTouchPoints > 0
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
if (isComposing) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('keypress', e);
|
||||||
|
// Prevent Enter key from creating a new line
|
||||||
|
const isCtrlPressed = e.ctrlKey || e.metaKey;
|
||||||
|
const enterPressed =
|
||||||
|
($settings?.ctrlEnterToSend ?? false)
|
||||||
|
? (e.key === 'Enter' || e.keyCode === 13) && isCtrlPressed
|
||||||
|
: (e.key === 'Enter' || e.keyCode === 13) && !e.shiftKey;
|
||||||
|
|
||||||
|
console.log('Enter pressed:', enterPressed);
|
||||||
|
|
||||||
|
if (enterPressed) {
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Submit the prompt when Enter key is pressed
|
||||||
|
if ((prompt !== '' || files.length > 0) && enterPressed) {
|
||||||
|
dispatch('submit', prompt);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (commandsContainerElement && e.key === 'Tab') {
|
if (e.key === 'Tab') {
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
const commandOptionButton = [
|
|
||||||
...document.getElementsByClassName('selected-command-option-button')
|
|
||||||
]?.at(-1);
|
|
||||||
|
|
||||||
commandOptionButton?.click();
|
|
||||||
} else if (e.key === 'Tab') {
|
|
||||||
const words = findWordIndices(prompt);
|
const words = findWordIndices(prompt);
|
||||||
|
|
||||||
if (words.length > 0) {
|
if (words.length > 0) {
|
||||||
@@ -1108,6 +1128,21 @@
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
|
uploadOneDriveHandler={async () => {
|
||||||
|
try {
|
||||||
|
const fileData = await pickAndDownloadFile();
|
||||||
|
if (fileData) {
|
||||||
|
const file = new File([fileData.blob], fileData.name, {
|
||||||
|
type: fileData.blob.type || 'application/octet-stream'
|
||||||
|
});
|
||||||
|
await uploadFileHandler(file);
|
||||||
|
} else {
|
||||||
|
console.log('No file was selected from OneDrive');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('OneDrive Error:', error);
|
||||||
|
}
|
||||||
|
}}
|
||||||
onClose={async () => {
|
onClose={async () => {
|
||||||
await tick();
|
await tick();
|
||||||
|
|
||||||
@@ -1285,14 +1320,17 @@
|
|||||||
|
|
||||||
stream = null;
|
stream = null;
|
||||||
|
|
||||||
if (!$TTSWorker) {
|
if ($settings.audio?.tts?.engine === 'browser-kokoro') {
|
||||||
await TTSWorker.set(
|
// If the user has not initialized the TTS worker, initialize it
|
||||||
new KokoroWorker({
|
if (!$TTSWorker) {
|
||||||
dtype: $settings.audio?.tts?.engineConfig?.dtype ?? 'fp32'
|
await TTSWorker.set(
|
||||||
})
|
new KokoroWorker({
|
||||||
);
|
dtype: $settings.audio?.tts?.engineConfig?.dtype ?? 'fp32'
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
await $TTSWorker.init();
|
await $TTSWorker.init();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
showCallOverlay.set(true);
|
showCallOverlay.set(true);
|
||||||
|
|||||||
@@ -231,7 +231,6 @@
|
|||||||
mediaRecorder.onstart = () => {
|
mediaRecorder.onstart = () => {
|
||||||
console.log('Recording started');
|
console.log('Recording started');
|
||||||
audioChunks = [];
|
audioChunks = [];
|
||||||
analyseAudio(audioStream);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
mediaRecorder.ondataavailable = (event) => {
|
mediaRecorder.ondataavailable = (event) => {
|
||||||
@@ -245,7 +244,7 @@
|
|||||||
stopRecordingCallback();
|
stopRecordingCallback();
|
||||||
};
|
};
|
||||||
|
|
||||||
mediaRecorder.start();
|
analyseAudio(audioStream);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -321,6 +320,9 @@
|
|||||||
if (hasSound) {
|
if (hasSound) {
|
||||||
// BIG RED TEXT
|
// BIG RED TEXT
|
||||||
console.log('%c%s', 'color: red; font-size: 20px;', '🔊 Sound detected');
|
console.log('%c%s', 'color: red; font-size: 20px;', '🔊 Sound detected');
|
||||||
|
if (mediaRecorder && mediaRecorder.state !== 'recording') {
|
||||||
|
mediaRecorder.start();
|
||||||
|
}
|
||||||
|
|
||||||
if (!hasStartedSpeaking) {
|
if (!hasStartedSpeaking) {
|
||||||
hasStartedSpeaking = true;
|
hasStartedSpeaking = true;
|
||||||
|
|||||||
@@ -74,7 +74,13 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (command.content.includes('{{USER_LOCATION}}')) {
|
if (command.content.includes('{{USER_LOCATION}}')) {
|
||||||
const location = await getUserPosition();
|
let location;
|
||||||
|
try {
|
||||||
|
location = await getUserPosition();
|
||||||
|
} catch (error) {
|
||||||
|
toast.error($i18n.t('Location access not allowed'));
|
||||||
|
location = 'LOCATION_UNKNOWN';
|
||||||
|
}
|
||||||
text = text.replaceAll('{{USER_LOCATION}}', String(location));
|
text = text.replaceAll('{{USER_LOCATION}}', String(location));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
|
|
||||||
import { config, user, tools as _tools, mobile } from '$lib/stores';
|
import { config, user, tools as _tools, mobile } from '$lib/stores';
|
||||||
import { createPicker } from '$lib/utils/google-drive-picker';
|
import { createPicker } from '$lib/utils/google-drive-picker';
|
||||||
|
|
||||||
import { getTools } from '$lib/apis/tools';
|
import { getTools } from '$lib/apis/tools';
|
||||||
|
|
||||||
import Dropdown from '$lib/components/common/Dropdown.svelte';
|
import Dropdown from '$lib/components/common/Dropdown.svelte';
|
||||||
@@ -24,6 +25,7 @@
|
|||||||
export let inputFilesHandler: Function;
|
export let inputFilesHandler: Function;
|
||||||
|
|
||||||
export let uploadGoogleDriveHandler: Function;
|
export let uploadGoogleDriveHandler: Function;
|
||||||
|
export let uploadOneDriveHandler: Function;
|
||||||
|
|
||||||
export let selectedToolIds: string[] = [];
|
export let selectedToolIds: string[] = [];
|
||||||
|
|
||||||
@@ -225,6 +227,97 @@
|
|||||||
<div class="line-clamp-1">{$i18n.t('Google Drive')}</div>
|
<div class="line-clamp-1">{$i18n.t('Google Drive')}</div>
|
||||||
</DropdownMenu.Item>
|
</DropdownMenu.Item>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
{#if $config?.features?.enable_onedrive_integration}
|
||||||
|
<DropdownMenu.Item
|
||||||
|
class="flex gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-xl"
|
||||||
|
on:click={() => {
|
||||||
|
uploadOneDriveHandler();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" class="w-5 h-5" fill="none">
|
||||||
|
<mask
|
||||||
|
id="mask0_87_7796"
|
||||||
|
style="mask-type:alpha"
|
||||||
|
maskUnits="userSpaceOnUse"
|
||||||
|
x="0"
|
||||||
|
y="6"
|
||||||
|
width="32"
|
||||||
|
height="20"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M7.82979 26C3.50549 26 0 22.5675 0 18.3333C0 14.1921 3.35322 10.8179 7.54613 10.6716C9.27535 7.87166 12.4144 6 16 6C20.6308 6 24.5169 9.12183 25.5829 13.3335C29.1316 13.3603 32 16.1855 32 19.6667C32 23.0527 29 26 25.8723 25.9914L7.82979 26Z"
|
||||||
|
fill="#C4C4C4"
|
||||||
|
/>
|
||||||
|
</mask>
|
||||||
|
<g mask="url(#mask0_87_7796)">
|
||||||
|
<path
|
||||||
|
d="M7.83017 26.0001C5.37824 26.0001 3.18957 24.8966 1.75391 23.1691L18.0429 16.3335L30.7089 23.4647C29.5926 24.9211 27.9066 26.0001 26.0004 25.9915C23.1254 26.0001 12.0629 26.0001 7.83017 26.0001Z"
|
||||||
|
fill="url(#paint0_linear_87_7796)"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M25.5785 13.3149L18.043 16.3334L30.709 23.4647C31.5199 22.4065 32.0004 21.0916 32.0004 19.6669C32.0004 16.1857 29.1321 13.3605 25.5833 13.3337C25.5817 13.3274 25.5801 13.3212 25.5785 13.3149Z"
|
||||||
|
fill="url(#paint1_linear_87_7796)"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M7.06445 10.7028L18.0423 16.3333L25.5779 13.3148C24.5051 9.11261 20.6237 6 15.9997 6C12.4141 6 9.27508 7.87166 7.54586 10.6716C7.3841 10.6773 7.22358 10.6877 7.06445 10.7028Z"
|
||||||
|
fill="url(#paint2_linear_87_7796)"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M1.7535 23.1687L18.0425 16.3331L7.06471 10.7026C3.09947 11.0792 0 14.3517 0 18.3331C0 20.1665 0.657197 21.8495 1.7535 23.1687Z"
|
||||||
|
fill="url(#paint3_linear_87_7796)"
|
||||||
|
/>
|
||||||
|
</g>
|
||||||
|
<defs>
|
||||||
|
<linearGradient
|
||||||
|
id="paint0_linear_87_7796"
|
||||||
|
x1="4.42591"
|
||||||
|
y1="24.6668"
|
||||||
|
x2="27.2309"
|
||||||
|
y2="23.2764"
|
||||||
|
gradientUnits="userSpaceOnUse"
|
||||||
|
>
|
||||||
|
<stop stop-color="#2086B8" />
|
||||||
|
<stop offset="1" stop-color="#46D3F6" />
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient
|
||||||
|
id="paint1_linear_87_7796"
|
||||||
|
x1="23.8302"
|
||||||
|
y1="19.6668"
|
||||||
|
x2="30.2108"
|
||||||
|
y2="15.2082"
|
||||||
|
gradientUnits="userSpaceOnUse"
|
||||||
|
>
|
||||||
|
<stop stop-color="#1694DB" />
|
||||||
|
<stop offset="1" stop-color="#62C3FE" />
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient
|
||||||
|
id="paint2_linear_87_7796"
|
||||||
|
x1="8.51037"
|
||||||
|
y1="7.33333"
|
||||||
|
x2="23.3335"
|
||||||
|
y2="15.9348"
|
||||||
|
gradientUnits="userSpaceOnUse"
|
||||||
|
>
|
||||||
|
<stop stop-color="#0D3D78" />
|
||||||
|
<stop offset="1" stop-color="#063B83" />
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient
|
||||||
|
id="paint3_linear_87_7796"
|
||||||
|
x1="-0.340429"
|
||||||
|
y1="19.9998"
|
||||||
|
x2="14.5634"
|
||||||
|
y2="14.4649"
|
||||||
|
gradientUnits="userSpaceOnUse"
|
||||||
|
>
|
||||||
|
<stop stop-color="#16589B" />
|
||||||
|
<stop offset="1" stop-color="#1464B7" />
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
<div class="line-clamp-1">{$i18n.t('OneDrive')}</div>
|
||||||
|
</DropdownMenu.Item>
|
||||||
|
{/if}
|
||||||
</DropdownMenu.Content>
|
</DropdownMenu.Content>
|
||||||
</div>
|
</div>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
|
|||||||
@@ -32,6 +32,7 @@
|
|||||||
export let prompt;
|
export let prompt;
|
||||||
export let history = {};
|
export let history = {};
|
||||||
export let selectedModels;
|
export let selectedModels;
|
||||||
|
export let atSelectedModel;
|
||||||
|
|
||||||
let messages = [];
|
let messages = [];
|
||||||
|
|
||||||
@@ -227,7 +228,8 @@
|
|||||||
role: 'user',
|
role: 'user',
|
||||||
content: userPrompt,
|
content: userPrompt,
|
||||||
...(history.messages[messageId].files && { files: history.messages[messageId].files }),
|
...(history.messages[messageId].files && { files: history.messages[messageId].files }),
|
||||||
models: selectedModels
|
models: selectedModels,
|
||||||
|
timestamp: Math.floor(Date.now() / 1000) // Unix epoch
|
||||||
};
|
};
|
||||||
|
|
||||||
let messageParentId = history.messages[messageId].parentId;
|
let messageParentId = history.messages[messageId].parentId;
|
||||||
@@ -349,6 +351,7 @@
|
|||||||
{#if Object.keys(history?.messages ?? {}).length == 0}
|
{#if Object.keys(history?.messages ?? {}).length == 0}
|
||||||
<ChatPlaceholder
|
<ChatPlaceholder
|
||||||
modelIds={selectedModels}
|
modelIds={selectedModels}
|
||||||
|
{atSelectedModel}
|
||||||
submitPrompt={async (p) => {
|
submitPrompt={async (p) => {
|
||||||
let text = p;
|
let text = p;
|
||||||
|
|
||||||
|
|||||||
@@ -43,6 +43,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
$: {
|
$: {
|
||||||
|
console.log('sources', sources);
|
||||||
citations = sources.reduce((acc, source) => {
|
citations = sources.reduce((acc, source) => {
|
||||||
if (Object.keys(source).length === 0) {
|
if (Object.keys(source).length === 0) {
|
||||||
return acc;
|
return acc;
|
||||||
@@ -53,7 +54,7 @@
|
|||||||
const distance = source.distances?.[index];
|
const distance = source.distances?.[index];
|
||||||
|
|
||||||
// Within the same citation there could be multiple documents
|
// Within the same citation there could be multiple documents
|
||||||
const id = metadata?.source ?? 'N/A';
|
const id = metadata?.source ?? source?.source?.id ?? 'N/A';
|
||||||
let _source = source?.source;
|
let _source = source?.source;
|
||||||
|
|
||||||
if (metadata?.name) {
|
if (metadata?.name) {
|
||||||
|
|||||||
@@ -1,18 +1,9 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import hljs from 'highlight.js';
|
|
||||||
import { loadPyodide } from 'pyodide';
|
|
||||||
import mermaid from 'mermaid';
|
import mermaid from 'mermaid';
|
||||||
|
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
|
|
||||||
import {
|
import { getContext, onMount, tick, onDestroy } from 'svelte';
|
||||||
getContext,
|
|
||||||
getAllContexts,
|
|
||||||
onMount,
|
|
||||||
tick,
|
|
||||||
createEventDispatcher,
|
|
||||||
onDestroy
|
|
||||||
} from 'svelte';
|
|
||||||
import { copyToClipboard } from '$lib/utils';
|
import { copyToClipboard } from '$lib/utils';
|
||||||
|
|
||||||
import 'highlight.js/styles/github-dark.min.css';
|
import 'highlight.js/styles/github-dark.min.css';
|
||||||
@@ -23,12 +14,17 @@
|
|||||||
import { config } from '$lib/stores';
|
import { config } from '$lib/stores';
|
||||||
import { executeCode } from '$lib/apis/utils';
|
import { executeCode } from '$lib/apis/utils';
|
||||||
import { toast } from 'svelte-sonner';
|
import { toast } from 'svelte-sonner';
|
||||||
|
import ChevronUp from '$lib/components/icons/ChevronUp.svelte';
|
||||||
|
import ChevronUpDown from '$lib/components/icons/ChevronUpDown.svelte';
|
||||||
|
import CommandLine from '$lib/components/icons/CommandLine.svelte';
|
||||||
|
|
||||||
const i18n = getContext('i18n');
|
const i18n = getContext('i18n');
|
||||||
const dispatch = createEventDispatcher();
|
|
||||||
|
|
||||||
export let id = '';
|
export let id = '';
|
||||||
|
|
||||||
|
export let onSave = (e) => {};
|
||||||
|
export let onCode = (e) => {};
|
||||||
|
|
||||||
export let save = false;
|
export let save = false;
|
||||||
export let run = true;
|
export let run = true;
|
||||||
|
|
||||||
@@ -64,14 +60,19 @@
|
|||||||
let result = null;
|
let result = null;
|
||||||
let files = null;
|
let files = null;
|
||||||
|
|
||||||
|
let collapsed = false;
|
||||||
let copied = false;
|
let copied = false;
|
||||||
let saved = false;
|
let saved = false;
|
||||||
|
|
||||||
|
const collapseCodeBlock = () => {
|
||||||
|
collapsed = !collapsed;
|
||||||
|
};
|
||||||
|
|
||||||
const saveCode = () => {
|
const saveCode = () => {
|
||||||
saved = true;
|
saved = true;
|
||||||
|
|
||||||
code = _code;
|
code = _code;
|
||||||
dispatch('save', code);
|
onSave(code);
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
saved = false;
|
saved = false;
|
||||||
@@ -344,7 +345,7 @@
|
|||||||
render();
|
render();
|
||||||
}
|
}
|
||||||
|
|
||||||
$: dispatch('code', { lang, code });
|
$: onCode({ lang, code });
|
||||||
|
|
||||||
$: if (attributes) {
|
$: if (attributes) {
|
||||||
onAttributesUpdate();
|
onAttributesUpdate();
|
||||||
@@ -380,7 +381,7 @@
|
|||||||
console.log('codeblock', lang, code);
|
console.log('codeblock', lang, code);
|
||||||
|
|
||||||
if (lang) {
|
if (lang) {
|
||||||
dispatch('code', { lang, code });
|
onCode({ lang, code });
|
||||||
}
|
}
|
||||||
if (document.documentElement.classList.contains('dark')) {
|
if (document.documentElement.classList.contains('dark')) {
|
||||||
mermaid.initialize({
|
mermaid.initialize({
|
||||||
@@ -425,18 +426,39 @@
|
|||||||
class="sticky {stickyButtonsClassName} mb-1 py-1 pr-2.5 flex items-center justify-end z-10 text-xs text-black dark:text-white"
|
class="sticky {stickyButtonsClassName} mb-1 py-1 pr-2.5 flex items-center justify-end z-10 text-xs text-black dark:text-white"
|
||||||
>
|
>
|
||||||
<div class="flex items-center gap-0.5 translate-y-[1px]">
|
<div class="flex items-center gap-0.5 translate-y-[1px]">
|
||||||
{#if lang.toLowerCase() === 'python' || lang.toLowerCase() === 'py' || (lang === '' && checkPythonCode(code))}
|
<button
|
||||||
|
class="flex gap-1 items-center bg-none border-none bg-gray-50 hover:bg-gray-100 dark:bg-gray-850 dark:hover:bg-gray-800 transition rounded-md px-1.5 py-0.5"
|
||||||
|
on:click={collapseCodeBlock}
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<ChevronUpDown className="size-3" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
{collapsed ? $i18n.t('Expand') : $i18n.t('Collapse')}
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{#if ($config?.features?.enable_code_execution ?? true) && (lang.toLowerCase() === 'python' || lang.toLowerCase() === 'py' || (lang === '' && checkPythonCode(code)))}
|
||||||
{#if executing}
|
{#if executing}
|
||||||
<div class="run-code-button bg-none border-none p-1 cursor-not-allowed">Running</div>
|
<div class="run-code-button bg-none border-none p-1 cursor-not-allowed">Running</div>
|
||||||
{:else if run}
|
{:else if run}
|
||||||
<button
|
<button
|
||||||
class="run-code-button bg-none border-none bg-gray-50 hover:bg-gray-100 dark:bg-gray-850 dark:hover:bg-gray-800 transition rounded-md px-1.5 py-0.5"
|
class="flex gap-1 items-center run-code-button bg-none border-none bg-gray-50 hover:bg-gray-100 dark:bg-gray-850 dark:hover:bg-gray-800 transition rounded-md px-1.5 py-0.5"
|
||||||
on:click={async () => {
|
on:click={async () => {
|
||||||
code = _code;
|
code = _code;
|
||||||
await tick();
|
await tick();
|
||||||
executePython(code);
|
executePython(code);
|
||||||
}}>{$i18n.t('Run')}</button
|
}}
|
||||||
>
|
>
|
||||||
|
<div>
|
||||||
|
<CommandLine className="size-3" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
{$i18n.t('Run')}
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
{/if}
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
@@ -464,65 +486,80 @@
|
|||||||
: 'rounded-b-lg'} overflow-hidden"
|
: 'rounded-b-lg'} overflow-hidden"
|
||||||
>
|
>
|
||||||
<div class=" pt-7 bg-gray-50 dark:bg-gray-850"></div>
|
<div class=" pt-7 bg-gray-50 dark:bg-gray-850"></div>
|
||||||
<CodeEditor
|
|
||||||
value={code}
|
{#if !collapsed}
|
||||||
{id}
|
<CodeEditor
|
||||||
{lang}
|
value={code}
|
||||||
on:save={() => {
|
{id}
|
||||||
saveCode();
|
{lang}
|
||||||
}}
|
onSave={() => {
|
||||||
on:change={(e) => {
|
saveCode();
|
||||||
_code = e.detail.value;
|
}}
|
||||||
}}
|
onChange={(value) => {
|
||||||
/>
|
_code = value;
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{:else}
|
||||||
|
<div
|
||||||
|
class="bg-gray-50 dark:bg-black dark:text-white rounded-b-lg! pt-2 pb-2 px-4 flex flex-col gap-2 text-xs"
|
||||||
|
>
|
||||||
|
<span class="text-gray-500 italic">
|
||||||
|
{$i18n.t('{{COUNT}} hidden lines', {
|
||||||
|
COUNT: code.split('\n').length
|
||||||
|
})}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
{#if !collapsed}
|
||||||
id="plt-canvas-{id}"
|
|
||||||
class="bg-gray-50 dark:bg-[#202123] dark:text-white max-w-full overflow-x-auto scrollbar-hidden"
|
|
||||||
/>
|
|
||||||
|
|
||||||
{#if executing || stdout || stderr || result || files}
|
|
||||||
<div
|
<div
|
||||||
class="bg-gray-50 dark:bg-[#202123] dark:text-white rounded-b-lg! py-4 px-4 flex flex-col gap-2"
|
id="plt-canvas-{id}"
|
||||||
>
|
class="bg-gray-50 dark:bg-[#202123] dark:text-white max-w-full overflow-x-auto scrollbar-hidden"
|
||||||
{#if executing}
|
/>
|
||||||
<div class=" ">
|
|
||||||
<div class=" text-gray-500 text-xs mb-1">STDOUT/STDERR</div>
|
{#if executing || stdout || stderr || result || files}
|
||||||
<div class="text-sm">Running...</div>
|
<div
|
||||||
</div>
|
class="bg-gray-50 dark:bg-[#202123] dark:text-white rounded-b-lg! py-4 px-4 flex flex-col gap-2"
|
||||||
{:else}
|
>
|
||||||
{#if stdout || stderr}
|
{#if executing}
|
||||||
<div class=" ">
|
<div class=" ">
|
||||||
<div class=" text-gray-500 text-xs mb-1">STDOUT/STDERR</div>
|
<div class=" text-gray-500 text-xs mb-1">STDOUT/STDERR</div>
|
||||||
<div
|
<div class="text-sm">Running...</div>
|
||||||
class="text-sm {stdout?.split('\n')?.length > 100
|
|
||||||
? `max-h-96`
|
|
||||||
: ''} overflow-y-auto"
|
|
||||||
>
|
|
||||||
{stdout || stderr}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{:else}
|
||||||
{#if result || files}
|
{#if stdout || stderr}
|
||||||
<div class=" ">
|
<div class=" ">
|
||||||
<div class=" text-gray-500 text-xs mb-1">RESULT</div>
|
<div class=" text-gray-500 text-xs mb-1">STDOUT/STDERR</div>
|
||||||
{#if result}
|
<div
|
||||||
<div class="text-sm">{`${JSON.stringify(result)}`}</div>
|
class="text-sm {stdout?.split('\n')?.length > 100
|
||||||
{/if}
|
? `max-h-96`
|
||||||
{#if files}
|
: ''} overflow-y-auto"
|
||||||
<div class="flex flex-col gap-2">
|
>
|
||||||
{#each files as file}
|
{stdout || stderr}
|
||||||
{#if file.type.startsWith('image')}
|
|
||||||
<img src={file.data} alt="Output" />
|
|
||||||
{/if}
|
|
||||||
{/each}
|
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
</div>
|
||||||
</div>
|
{/if}
|
||||||
|
{#if result || files}
|
||||||
|
<div class=" ">
|
||||||
|
<div class=" text-gray-500 text-xs mb-1">RESULT</div>
|
||||||
|
{#if result}
|
||||||
|
<div class="text-sm">{`${JSON.stringify(result)}`}</div>
|
||||||
|
{/if}
|
||||||
|
{#if files}
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
{#each files as file}
|
||||||
|
{#if file.type.startsWith('image')}
|
||||||
|
<img src={file.data} alt="Output" class=" w-full max-w-[36rem]" />
|
||||||
|
{/if}
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
{/if}
|
</div>
|
||||||
</div>
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -17,6 +17,7 @@
|
|||||||
import Collapsible from '$lib/components/common/Collapsible.svelte';
|
import Collapsible from '$lib/components/common/Collapsible.svelte';
|
||||||
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
||||||
import ArrowDownTray from '$lib/components/icons/ArrowDownTray.svelte';
|
import ArrowDownTray from '$lib/components/icons/ArrowDownTray.svelte';
|
||||||
|
import Source from './Source.svelte';
|
||||||
|
|
||||||
const dispatch = createEventDispatcher();
|
const dispatch = createEventDispatcher();
|
||||||
|
|
||||||
@@ -88,14 +89,14 @@
|
|||||||
code={token?.text ?? ''}
|
code={token?.text ?? ''}
|
||||||
{attributes}
|
{attributes}
|
||||||
{save}
|
{save}
|
||||||
on:code={(e) => {
|
onCode={(value) => {
|
||||||
dispatch('code', e.detail);
|
dispatch('code', value);
|
||||||
}}
|
}}
|
||||||
on:save={(e) => {
|
onSave={(value) => {
|
||||||
dispatch('update', {
|
dispatch('update', {
|
||||||
raw: token.raw,
|
raw: token.raw,
|
||||||
oldContent: token.text,
|
oldContent: token.text,
|
||||||
newContent: e.detail
|
newContent: value
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@@ -261,6 +262,8 @@
|
|||||||
{@html html}
|
{@html html}
|
||||||
{:else if token.text.includes(`<iframe src="${WEBUI_BASE_URL}/api/v1/files/`)}
|
{:else if token.text.includes(`<iframe src="${WEBUI_BASE_URL}/api/v1/files/`)}
|
||||||
{@html `${token.text}`}
|
{@html `${token.text}`}
|
||||||
|
{:else if token.text.includes(`<source_id`)}
|
||||||
|
<Source {id} {token} onClick={onSourceClick} />
|
||||||
{:else}
|
{:else}
|
||||||
{token.text}
|
{token.text}
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||