Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8a620cab44 | ||
|
|
fcffb0adf2 | ||
|
|
efcb56f0dc | ||
|
|
86ee19178e | ||
|
|
1fc306acd0 | ||
|
|
da1d5ad917 | ||
|
|
b45db22a17 | ||
|
|
19455ab04e | ||
|
|
71b1661f00 | ||
|
|
99764bfd29 | ||
|
|
063e006446 | ||
|
|
436f009dab | ||
|
|
71a66ed4cf | ||
|
|
29bef261be | ||
|
|
e2291f7148 | ||
|
|
8b5aed7a2b | ||
|
|
95057d2368 | ||
|
|
20dadf9b5a | ||
|
|
3d6ac3a7db | ||
|
|
bd47bbbce9 | ||
|
|
ddebfc7413 | ||
|
|
c5310e84db | ||
|
|
0c8301e79c | ||
|
|
73faa8dc80 | ||
|
|
ee526b4b07 | ||
|
|
454f59d59a | ||
|
|
2e3146263c | ||
|
|
bcbcd5fde9 | ||
|
|
83a596612a | ||
|
|
2b896989b8 | ||
|
|
27109d22e4 | ||
|
|
22d8f8f1ef | ||
|
|
fd26e5635d | ||
|
|
b350b0023f | ||
|
|
330eb0fbb1 | ||
|
|
d79d3f1352 | ||
|
|
44966db505 | ||
|
|
1e8abea753 | ||
|
|
21d8ff61bb | ||
|
|
9652c8f8af | ||
|
|
de8f5b9c13 | ||
|
|
c89df923c5 | ||
|
|
556bc8669a | ||
|
|
89ba98e927 | ||
|
|
6c8a15fae2 | ||
|
|
68d8fd69c0 | ||
|
|
b31de299e4 | ||
|
|
9d7037b730 | ||
|
|
5edc211392 | ||
|
|
1329eea5e5 | ||
|
|
28022b056b | ||
|
|
cbadf39d7d | ||
|
|
ec99ac7121 | ||
|
|
dfa5041b6f | ||
|
|
0fa85c5c64 | ||
|
|
5abe1076ed | ||
|
|
7feda56e7b | ||
|
|
a54d3ad512 | ||
|
|
cdf9f5a4ec | ||
|
|
a472bfe6dc | ||
|
|
f88d994c35 | ||
|
|
ad92aded84 | ||
|
|
b4de0a52f8 | ||
|
|
bd23dac92e | ||
|
|
528df12bf1 | ||
|
|
a933319adb | ||
|
|
13c03bfd7d | ||
|
|
18965dcdac | ||
|
|
a4a7d678f9 | ||
|
|
fd422d2e3c | ||
|
|
ce7a1a73ac | ||
|
|
32874a816d | ||
|
|
3164354c0b | ||
|
|
f96aaf1177 | ||
|
|
589c79f3c2 | ||
|
|
a0f6864216 | ||
|
|
a4cc2c5c48 | ||
|
|
0550d12106 | ||
|
|
845628c100 | ||
|
|
7bcdc10539 | ||
|
|
d2f10d50bf | ||
|
|
446b2a334a | ||
|
|
b0fd90ada9 | ||
|
|
6bfd48e412 | ||
|
|
07ffd6e231 | ||
|
|
c675beeda4 | ||
|
|
fe0cf9506c | ||
|
|
7c81509804 | ||
|
|
65923006a8 | ||
|
|
b02f6db475 | ||
|
|
7f394f3f00 | ||
|
|
fc8524bbfd | ||
|
|
c9505531fd | ||
|
|
1b809fe42e | ||
|
|
a1b4df1b85 | ||
|
|
079f37a2d6 | ||
|
|
0980066363 | ||
|
|
a3b6654cbb | ||
|
|
f4328325be | ||
|
|
7badff49d8 | ||
|
|
176c689f8d | ||
|
|
fa20b1dc09 | ||
|
|
2ca9989d20 | ||
|
|
3420818c52 | ||
|
|
0ae6ca608c | ||
|
|
536b40890a | ||
|
|
d5337917db | ||
|
|
15f3ebba93 | ||
|
|
e71f55e58f | ||
|
|
fe747382c1 | ||
|
|
c4946d42e0 | ||
|
|
c1823b4b73 | ||
|
|
862a30842c | ||
|
|
cbb0940ff8 | ||
|
|
bd8df3583d | ||
|
|
5f36807dbe | ||
|
|
442f50303a | ||
|
|
4560f3b1ae | ||
|
|
59d2c670ba | ||
|
|
02577f6a45 | ||
|
|
094fdd8943 | ||
|
|
17169dff1f | ||
|
|
28e3e6e8cb | ||
|
|
4f47053e93 | ||
|
|
9025e9dbb1 | ||
|
|
eee1dad217 | ||
|
|
769df698be | ||
|
|
4ef042e966 | ||
|
|
92062ff722 | ||
|
|
623aa08b9e | ||
|
|
a529343b2b | ||
|
|
2161903163 | ||
|
|
3de9a1a130 | ||
|
|
587c1a3ca2 | ||
|
|
d224566957 | ||
|
|
8ea1a10525 | ||
|
|
b6d6094018 | ||
|
|
e5e1bac242 | ||
|
|
8c2ba7f7ea | ||
|
|
dc6ca61548 | ||
|
|
723caf2a09 | ||
|
|
439cb66672 | ||
|
|
dbd5b4c9f1 | ||
|
|
4dd404ac3b | ||
|
|
ba370438b2 | ||
|
|
f73a60d96c | ||
|
|
afe1f13c5b | ||
|
|
0554cc6128 | ||
|
|
5a6ece9513 | ||
|
|
4042219b3e | ||
|
|
fdc89cbcee | ||
|
|
6df6170c44 | ||
|
|
d598d4bb93 | ||
|
|
790bdcf9fc | ||
|
|
2efcda837c | ||
|
|
e86688284a | ||
|
|
ff9d899f9c | ||
|
|
a68b918cbb | ||
|
|
9fb70969d7 | ||
|
|
0c9119d619 | ||
|
|
556141cdd8 | ||
|
|
60003c976a | ||
|
|
23f1bee7bd | ||
|
|
589efcdc5f | ||
|
|
3befadb29f | ||
|
|
13b0e7d64a | ||
|
|
c8badfe21f | ||
|
|
2a1b9cae91 | ||
|
|
5ba7bbdd98 | ||
|
|
9435d2044a | ||
|
|
609a42c29c | ||
|
|
6a1e7ab038 | ||
|
|
53daa15b9a | ||
|
|
55097410f6 | ||
|
|
04e2b6e2bd | ||
|
|
9f0c9d973c | ||
|
|
1597e33a74 | ||
|
|
0b218bbb72 | ||
|
|
7b21b718fe | ||
|
|
6e5b557a1f | ||
|
|
6aefc79807 | ||
|
|
ec9e0dadea | ||
|
|
0470146d7b | ||
|
|
a518d50477 | ||
|
|
29f904db45 | ||
|
|
038fc48ac0 | ||
|
|
6f72def1ac | ||
|
|
0ec1f9e331 | ||
|
|
9682806476 | ||
|
|
fc6fa7887b | ||
|
|
e1e69cfbcb | ||
|
|
7a8f8960c5 | ||
|
|
30c44d431b | ||
|
|
b177976f29 | ||
|
|
af6b92a6fa | ||
|
|
a1888b3757 | ||
|
|
c75e77cdde | ||
|
|
6447b484c1 | ||
|
|
0c5236ac27 | ||
|
|
7ef5aa520c | ||
|
|
d620bcf516 | ||
|
|
b0c2e607b8 | ||
|
|
56e2d579f2 | ||
|
|
70f580ec45 | ||
|
|
eae35dddc2 | ||
|
|
bb979c9a78 | ||
|
|
4f959c31de | ||
|
|
3bbe065db1 | ||
|
|
41460e1335 | ||
|
|
ffdb44f887 | ||
|
|
549d3b4d10 | ||
|
|
879f85802a | ||
|
|
d652a1bbb1 | ||
|
|
9c2429ff97 | ||
|
|
d0645d3c4f | ||
|
|
748be3e637 | ||
|
|
153ba168a0 | ||
|
|
c70b18b2ef | ||
|
|
af6420e06d | ||
|
|
547611b703 | ||
|
|
1b2ae7bb77 | ||
|
|
5cd8011d53 | ||
|
|
208833d9f2 | ||
|
|
6863028540 | ||
|
|
5e97a52cbe | ||
|
|
af865ab035 | ||
|
|
01f622866f | ||
|
|
e9d14601a1 | ||
|
|
92e77d7b33 | ||
|
|
3b370bbcb3 | ||
|
|
20b1753a91 | ||
|
|
b2379c7104 | ||
|
|
e491e96f88 | ||
|
|
8afc8c5714 | ||
|
|
d388c20373 | ||
|
|
13403cd7dc | ||
|
|
52fd701f2e | ||
|
|
204a4fbe7a | ||
|
|
549817627f | ||
|
|
fa4d1d42a5 | ||
|
|
309cd645f1 | ||
|
|
a725801e55 | ||
|
|
8cdf9814bd | ||
|
|
e6bbce439d | ||
|
|
8d257ed596 | ||
|
|
a7063a598d | ||
|
|
367fa039a0 | ||
|
|
71d88fe35d | ||
|
|
958fe9639a | ||
|
|
670f28d694 | ||
|
|
3715994c25 | ||
|
|
d72d5d0e8e | ||
|
|
dbe463a53d | ||
|
|
678dd780ee | ||
|
|
44c870447f | ||
|
|
70a8f6e707 | ||
|
|
2fb4d3356c | ||
|
|
590fd129c8 | ||
|
|
99d10d1189 | ||
|
|
ad9a7cb1e2 | ||
|
|
8187922ef1 | ||
|
|
c869652ef4 | ||
|
|
240a30147d | ||
|
|
91fdb86fcc | ||
|
|
0e5a56e2cf | ||
|
|
40ecc2563a | ||
|
|
7e473f194d | ||
|
|
f62281a0c7 | ||
|
|
0c231fd387 | ||
|
|
7f3ebcaa91 | ||
|
|
1f8d08eaa2 | ||
|
|
5904ef86f2 | ||
|
|
d692649bac | ||
|
|
0d019a00c9 | ||
|
|
e6b3de310b | ||
|
|
d19b96d0c2 | ||
|
|
7cbc94592e | ||
|
|
9747f1e841 | ||
|
|
42bc24a646 | ||
|
|
35115957d8 | ||
|
|
b193eb1d82 | ||
|
|
4105c9735e | ||
|
|
ed205d82e8 | ||
|
|
fc31267a54 | ||
|
|
44c781f414 | ||
|
|
ff90b125ee | ||
|
|
831fe9f509 | ||
|
|
a140d319fe | ||
|
|
9581bf5dde | ||
|
|
76dcabdce6 | ||
|
|
dc262a5f79 | ||
|
|
db5830dc28 | ||
|
|
ecf3d567cf | ||
|
|
e8375e9acd | ||
|
|
8c84c74197 | ||
|
|
f7d1225c23 | ||
|
|
3dc90b854c | ||
|
|
ac3c657315 | ||
|
|
37b117a84c | ||
|
|
ab6346ea1c | ||
|
|
b289eef523 | ||
|
|
6e6f9862e6 | ||
|
|
7d42a79177 | ||
|
|
5315fb201d | ||
|
|
6a012d290e | ||
|
|
05bbca5b07 | ||
|
|
5afe0be5b4 | ||
|
|
d8aa2cc05f | ||
|
|
7b071d403f | ||
|
|
d5b1e5e0c0 | ||
|
|
cec5fdd144 | ||
|
|
d8498aa2b0 | ||
|
|
e593d3aee3 | ||
|
|
323cfaf2d7 | ||
|
|
637f0c6fb2 | ||
|
|
2756403102 | ||
|
|
92861d095a | ||
|
|
bf6c6627e8 | ||
|
|
b217cb76f0 | ||
|
|
b414fde6ca | ||
|
|
d8c39569be | ||
|
|
5ce15bb0df | ||
|
|
88086935af | ||
|
|
ace3552e15 | ||
|
|
003ceff7e4 | ||
|
|
209ccdf668 | ||
|
|
e5dd7e65d4 | ||
|
|
b2999ad590 | ||
|
|
1871a7a2fa | ||
|
|
49677e9c9d | ||
|
|
73954f4a63 | ||
|
|
a084938d9c | ||
|
|
1bf042ac84 | ||
|
|
a2f9f7c975 | ||
|
|
565f40c642 | ||
|
|
4441338574 | ||
|
|
389d650ee3 | ||
|
|
080d45239a | ||
|
|
91851114e4 | ||
|
|
5b11099a0b | ||
|
|
2996d1e096 | ||
|
|
be82175f86 | ||
|
|
05977bd1c7 | ||
|
|
8074289a8f | ||
|
|
f8ba0334e8 | ||
|
|
7c9bed83d2 | ||
|
|
482a7723b2 | ||
|
|
948d29b676 | ||
|
|
3fa6c41303 | ||
|
|
220a4bb535 | ||
|
|
287559e834 | ||
|
|
cdac0cd1df | ||
|
|
3653126179 | ||
|
|
2eb662df85 | ||
|
|
cdcf214455 | ||
|
|
a75a9c953a | ||
|
|
800ba206f7 | ||
|
|
8d5aa98297 | ||
|
|
283234d51d | ||
|
|
f9e1a933a9 | ||
|
|
ebd8ef1a9e | ||
|
|
ed691e26a6 | ||
|
|
067d76fece | ||
|
|
534c18c94c | ||
|
|
af9e0cc33a | ||
|
|
6847c2fc8c | ||
|
|
028eb7b351 | ||
|
|
12c21fac22 | ||
|
|
774defd184 | ||
|
|
6657f28694 | ||
|
|
9d7507236f | ||
|
|
8f49429228 | ||
|
|
f21e9dbd9a | ||
|
|
d3146d20ad | ||
|
|
24a177a149 | ||
|
|
06a64219bc | ||
|
|
2c4bc7a2b2 | ||
|
|
62dc486c85 | ||
|
|
d11961626c | ||
|
|
519375b4c0 | ||
|
|
49199819db |
@@ -8,36 +8,43 @@ assignees: ''
|
||||
|
||||
# Bug Report
|
||||
|
||||
## Description
|
||||
## Installation Method
|
||||
|
||||
**Bug Summary:**
|
||||
[Provide a brief but clear summary of the bug]
|
||||
|
||||
**Steps to Reproduce:**
|
||||
[Outline the steps to reproduce the bug. Be as detailed as possible.]
|
||||
|
||||
**Expected Behavior:**
|
||||
[Describe what you expected to happen.]
|
||||
|
||||
**Actual Behavior:**
|
||||
[Describe what actually happened.]
|
||||
[Describe the method you used to install the project, e.g., git clone, Docker, pip, etc.]
|
||||
|
||||
## Environment
|
||||
|
||||
- **Open WebUI Version:** [e.g., 0.1.120]
|
||||
- **Ollama (if applicable):** [e.g., 0.1.30, 0.1.32-rc1]
|
||||
- **Open WebUI Version:** [e.g., v0.3.11]
|
||||
- **Ollama (if applicable):** [e.g., v0.2.0, v0.1.32-rc1]
|
||||
|
||||
- **Operating System:** [e.g., Windows 10, macOS Big Sur, Ubuntu 20.04]
|
||||
- **Browser (if applicable):** [e.g., Chrome 100.0, Firefox 98.0]
|
||||
|
||||
## Reproduction Details
|
||||
|
||||
**Confirmation:**
|
||||
|
||||
- [ ] I have read and followed all the instructions provided in the README.md.
|
||||
- [ ] I am on the latest version of both Open WebUI and Ollama.
|
||||
- [ ] I have included the browser console logs.
|
||||
- [ ] I have included the Docker container logs.
|
||||
- [ ] I have provided the exact steps to reproduce the bug in the "Steps to Reproduce" section below.
|
||||
|
||||
## Expected Behavior:
|
||||
|
||||
[Describe what you expected to happen.]
|
||||
|
||||
## Actual Behavior:
|
||||
|
||||
[Describe what actually happened.]
|
||||
|
||||
## Description
|
||||
|
||||
**Bug Summary:**
|
||||
[Provide a brief but clear summary of the bug]
|
||||
|
||||
## Reproduction Details
|
||||
|
||||
**Steps to Reproduce:**
|
||||
[Outline the steps to reproduce the bug. Be as detailed as possible.]
|
||||
|
||||
## Logs and Screenshots
|
||||
|
||||
@@ -47,13 +54,9 @@ assignees: ''
|
||||
**Docker Container Logs:**
|
||||
[Include relevant Docker container logs, if applicable]
|
||||
|
||||
**Screenshots (if applicable):**
|
||||
**Screenshots/Screen Recordings (if applicable):**
|
||||
[Attach any relevant screenshots to help illustrate the issue]
|
||||
|
||||
## Installation Method
|
||||
|
||||
[Describe the method you used to install the project, e.g., manual installation, Docker, package manager, etc.]
|
||||
|
||||
## Additional Information
|
||||
|
||||
[Include any additional details that may help in understanding and reproducing the issue. This could include specific configurations, error messages, or anything else relevant to the bug.]
|
||||
|
||||
@@ -3,10 +3,10 @@ updates:
|
||||
- package-ecosystem: pip
|
||||
directory: '/backend'
|
||||
schedule:
|
||||
interval: weekly
|
||||
interval: monthly
|
||||
target-branch: 'dev'
|
||||
- package-ecosystem: 'github-actions'
|
||||
directory: '/'
|
||||
schedule:
|
||||
# Check for updates to GitHub Actions every week
|
||||
interval: 'weekly'
|
||||
interval: monthly
|
||||
|
||||
@@ -10,61 +10,63 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Check for changes in package.json
|
||||
run: |
|
||||
git diff --cached --diff-filter=d package.json || {
|
||||
echo "No changes to package.json"
|
||||
exit 1
|
||||
}
|
||||
|
||||
- name: Get version number from package.json
|
||||
id: get_version
|
||||
run: |
|
||||
VERSION=$(jq -r '.version' package.json)
|
||||
echo "::set-output name=version::$VERSION"
|
||||
- name: Check for changes in package.json
|
||||
run: |
|
||||
git diff --cached --diff-filter=d package.json || {
|
||||
echo "No changes to package.json"
|
||||
exit 1
|
||||
}
|
||||
|
||||
- name: Extract latest CHANGELOG entry
|
||||
id: changelog
|
||||
run: |
|
||||
CHANGELOG_CONTENT=$(awk 'BEGIN {print_section=0;} /^## \[/ {if (print_section == 0) {print_section=1;} else {exit;}} print_section {print;}' CHANGELOG.md)
|
||||
CHANGELOG_ESCAPED=$(echo "$CHANGELOG_CONTENT" | sed ':a;N;$!ba;s/\n/%0A/g')
|
||||
echo "Extracted latest release notes from CHANGELOG.md:"
|
||||
echo -e "$CHANGELOG_CONTENT"
|
||||
echo "::set-output name=content::$CHANGELOG_ESCAPED"
|
||||
- name: Get version number from package.json
|
||||
id: get_version
|
||||
run: |
|
||||
VERSION=$(jq -r '.version' package.json)
|
||||
echo "::set-output name=version::$VERSION"
|
||||
|
||||
- name: Create GitHub release
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
script: |
|
||||
const changelog = `${{ steps.changelog.outputs.content }}`;
|
||||
const release = await github.rest.repos.createRelease({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
tag_name: `v${{ steps.get_version.outputs.version }}`,
|
||||
name: `v${{ steps.get_version.outputs.version }}`,
|
||||
body: changelog,
|
||||
})
|
||||
console.log(`Created release ${release.data.html_url}`)
|
||||
- name: Extract latest CHANGELOG entry
|
||||
id: changelog
|
||||
run: |
|
||||
CHANGELOG_CONTENT=$(awk 'BEGIN {print_section=0;} /^## \[/ {if (print_section == 0) {print_section=1;} else {exit;}} print_section {print;}' CHANGELOG.md)
|
||||
CHANGELOG_ESCAPED=$(echo "$CHANGELOG_CONTENT" | sed ':a;N;$!ba;s/\n/%0A/g')
|
||||
echo "Extracted latest release notes from CHANGELOG.md:"
|
||||
echo -e "$CHANGELOG_CONTENT"
|
||||
echo "::set-output name=content::$CHANGELOG_ESCAPED"
|
||||
|
||||
- name: Upload package to GitHub release
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: package
|
||||
path: .
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Create GitHub release
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
script: |
|
||||
const changelog = `${{ steps.changelog.outputs.content }}`;
|
||||
const release = await github.rest.repos.createRelease({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
tag_name: `v${{ steps.get_version.outputs.version }}`,
|
||||
name: `v${{ steps.get_version.outputs.version }}`,
|
||||
body: changelog,
|
||||
})
|
||||
console.log(`Created release ${release.data.html_url}`)
|
||||
|
||||
- name: Trigger Docker build workflow
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
github.rest.actions.createWorkflowDispatch({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
workflow_id: 'docker-build.yaml',
|
||||
ref: 'v${{ steps.get_version.outputs.version }}',
|
||||
})
|
||||
- name: Upload package to GitHub release
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: package
|
||||
path: |
|
||||
.
|
||||
!.git
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Trigger Docker build workflow
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
github.rest.actions.createWorkflowDispatch({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
workflow_id: 'docker-build.yaml',
|
||||
ref: 'v${{ steps.get_version.outputs.version }}',
|
||||
})
|
||||
|
||||
@@ -15,6 +15,13 @@ jobs:
|
||||
name: Run Cypress Integration Tests
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Maximize build space
|
||||
uses: AdityaGarg8/remove-unwanted-software@v4.1
|
||||
with:
|
||||
remove-android: 'true'
|
||||
remove-haskell: 'true'
|
||||
remove-codeql: 'true'
|
||||
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
@@ -26,6 +33,10 @@ jobs:
|
||||
--file docker-compose.a1111-test.yaml \
|
||||
up --detach --build
|
||||
|
||||
- name: Delete Docker build cache
|
||||
run: |
|
||||
docker builder prune --all --force
|
||||
|
||||
- name: Wait for Ollama to be up
|
||||
timeout-minutes: 5
|
||||
run: |
|
||||
@@ -35,10 +46,6 @@ jobs:
|
||||
done
|
||||
echo "Service is up!"
|
||||
|
||||
- name: Delete Docker build cache
|
||||
run: |
|
||||
docker builder prune --all --force
|
||||
|
||||
- name: Preload Ollama model
|
||||
run: |
|
||||
docker exec ollama ollama pull qwen:0.5b-chat-v1.5-q2_K
|
||||
|
||||
@@ -5,6 +5,86 @@ All notable changes to this project will be documented in this file.
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [0.3.14] - 2024-08-21
|
||||
|
||||
### Added
|
||||
|
||||
- **🛠️ Custom ComfyUI Workflow**: Deprecating several older environment variables, this enhancement introduces a new, customizable workflow for a more tailored user experience.
|
||||
- **🔀 Merge Responses in Many Model Chat**: Enhances the dialogue by merging responses from multiple models into a single, coherent reply, improving the interaction quality in many model chats.
|
||||
- **✅ Multiple Instances of Same Model in Chats**: Enhanced many model chat to support adding multiple instances of the same model.
|
||||
- **🔧 Quick Actions in Model Workspace**: Enhanced Shift key quick actions for hiding/unhiding and deleting models, facilitating a smoother workflow.
|
||||
- **🗨️ Markdown Rendering in User Messages**: User messages are now rendered in Markdown, enhancing readability and interaction.
|
||||
- **💬 Temporary Chat Feature**: Introduced a temporary chat feature, deprecating the old chat history setting to enhance user interaction flexibility.
|
||||
- **🖋️ User Message Editing**: Enhanced the user chat editing feature to allow saving changes without sending, providing more flexibility in message management.
|
||||
- **🛡️ Security Enhancements**: Various security improvements implemented across the platform to ensure safer user experiences.
|
||||
- **🌍 Updated Translations**: Enhanced translations for Chinese, Ukrainian, and Bahasa Malaysia, improving localization and user comprehension.
|
||||
|
||||
### Fixed
|
||||
|
||||
- **📑 Mermaid Rendering Issue**: Addressed issues with Mermaid chart rendering to ensure clean and clear visual data representation.
|
||||
- **🎭 PWA Icon Maskability**: Fixed the Progressive Web App icon to be maskable, ensuring proper display on various device home screens.
|
||||
- **🔀 Cloned Model Chat Freezing Issue**: Fixed a bug where cloning many model chats would cause freezing, enhancing stability and responsiveness.
|
||||
- **🔍 Generic Error Handling and Refinements**: Various minor fixes and refinements to address previously untracked issues, ensuring smoother operations.
|
||||
|
||||
### Changed
|
||||
|
||||
- **🖼️ Image Generation Refactor**: Overhauled image generation processes for improved efficiency and quality.
|
||||
- **🔨 Refactor Tool and Function Calling**: Refactored tool and function calling mechanisms for improved clarity and maintainability.
|
||||
- **🌐 Backend Library Updates**: Updated critical backend libraries including SQLAlchemy, uvicorn[standard], faster-whisper, bcrypt, and boto3 for enhanced performance and security.
|
||||
|
||||
### Removed
|
||||
|
||||
- **🚫 Deprecated ComfyUI Environment Variables**: Removed several outdated environment variables related to ComfyUI settings, simplifying configuration management.
|
||||
|
||||
## [0.3.13] - 2024-08-14
|
||||
|
||||
### Added
|
||||
|
||||
- **🎨 Enhanced Markdown Rendering**: Significant improvements in rendering markdown, ensuring smooth and reliable display of LaTeX and Mermaid charts, enhancing user experience with more robust visual content.
|
||||
- **🔄 Auto-Install Tools & Functions Python Dependencies**: For 'Tools' and 'Functions', Open WebUI now automatically install extra python requirements specified in the frontmatter, streamlining setup processes and customization.
|
||||
- **🌀 OAuth Email Claim Customization**: Introduced an 'OAUTH_EMAIL_CLAIM' variable to allow customization of the default "email" claim within OAuth configurations, providing greater flexibility in authentication processes.
|
||||
- **📶 Websocket Reconnection**: Enhanced reliability with the capability to automatically reconnect when a websocket is closed, ensuring consistent and stable communication.
|
||||
- **🤳 Haptic Feedback on Support Devices**: Android devices now support haptic feedback for an immersive tactile experience during certain interactions.
|
||||
|
||||
### Fixed
|
||||
|
||||
- **🛠️ ComfyUI Performance Improvement**: Addressed an issue causing FastAPI to stall when ComfyUI image generation was active; now runs in a separate thread to prevent UI unresponsiveness.
|
||||
- **🔀 Session Handling**: Fixed an issue mandating session_id on client-side to ensure smoother session management and transitions.
|
||||
- **🖋️ Minor Bug Fixes and Format Corrections**: Various minor fixes including typo corrections, backend formatting improvements, and test amendments enhancing overall system stability and performance.
|
||||
|
||||
### Changed
|
||||
|
||||
- **🚀 Migration to SvelteKit 2**: Upgraded the underlying framework to SvelteKit version 2, offering enhanced speed, better code structure, and improved deployment capabilities.
|
||||
- **🧹 General Cleanup and Refactoring**: Performed broad cleanup and refactoring across the platform, improving code efficiency and maintaining high standards of code health.
|
||||
- **🚧 Integration Testing Improvements**: Modified how Cypress integration tests detect chat messages and updated sharing tests for better reliability and accuracy.
|
||||
- **📁 Standardized '.safetensors' File Extension**: Renamed the '.sft' file extension to '.safetensors' for ComfyUI workflows, standardizing file formats across the platform.
|
||||
|
||||
### Removed
|
||||
|
||||
- **🗑️ Deprecated Frontend Functions**: Removed frontend functions that were migrated to backend to declutter the codebase and reduce redundancy.
|
||||
|
||||
## [0.3.12] - 2024-08-07
|
||||
|
||||
### Added
|
||||
|
||||
- **🔄 Sidebar Infinite Scroll**: Added an infinite scroll feature in the sidebar for more efficient chat navigation, reducing load times and enhancing user experience.
|
||||
- **🚀 Enhanced Markdown Rendering**: Support for rendering all code blocks and making images clickable for preview; codespan styling is also enhanced to improve readability and user interaction.
|
||||
- **🔒 Admin Shared Chat Visibility**: Admins no longer have default visibility over shared chats when ENABLE_ADMIN_CHAT_ACCESS is set to false, tightening security and privacy settings for users.
|
||||
- **🌍 Language Updates**: Added Malay (Bahasa Malaysia) translation and updated Catalan and Traditional Chinese translations to improve accessibility for more users.
|
||||
|
||||
### Fixed
|
||||
|
||||
- **📊 Markdown Rendering Issues**: Resolved issues with markdown rendering to ensure consistent and correct display across components.
|
||||
- **🛠️ Styling Issues**: Multiple fixes applied to styling throughout the application, improving the overall visual experience and interface consistency.
|
||||
- **🗃️ Modal Handling**: Fixed an issue where modals were not closing correctly in various model chat scenarios, enhancing usability and interface reliability.
|
||||
- **📄 Missing OpenAI Usage Information**: Resolved issues where usage statistics for OpenAI services were not being correctly displayed, ensuring users have access to crucial data for managing and monitoring their API consumption.
|
||||
- **🔧 Non-Streaming Support for Functions Plugin**: Fixed a functionality issue with the Functions plugin where non-streaming operations were not functioning as intended, restoring full capabilities for async and sync integration within the platform.
|
||||
- **🔄 Environment Variable Type Correction (COMFYUI_FLUX_FP8_CLIP)**: Corrected the data type of the 'COMFYUI_FLUX_FP8_CLIP' environment variable from string to boolean, ensuring environment settings apply correctly and enhance configuration management.
|
||||
|
||||
### Changed
|
||||
|
||||
- **🔧 Backend Dependency Updates**: Updated several backend dependencies such as boto3, pypdf, python-pptx, validators, and black, ensuring up-to-date security and performance optimizations.
|
||||
|
||||
## [0.3.11] - 2024-08-02
|
||||
|
||||
### Added
|
||||
|
||||
+75
-55
@@ -1,5 +1,12 @@
|
||||
import os
|
||||
import hashlib
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import uuid
|
||||
from functools import lru_cache
|
||||
from pathlib import Path
|
||||
|
||||
import requests
|
||||
from fastapi import (
|
||||
FastAPI,
|
||||
Request,
|
||||
@@ -8,34 +15,14 @@ from fastapi import (
|
||||
status,
|
||||
UploadFile,
|
||||
File,
|
||||
Form,
|
||||
)
|
||||
from fastapi.responses import StreamingResponse, JSONResponse, FileResponse
|
||||
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.responses import FileResponse
|
||||
from pydantic import BaseModel
|
||||
|
||||
from typing import List
|
||||
import uuid
|
||||
import requests
|
||||
import hashlib
|
||||
from pathlib import Path
|
||||
import json
|
||||
|
||||
from constants import ERROR_MESSAGES
|
||||
from utils.utils import (
|
||||
decode_token,
|
||||
get_current_user,
|
||||
get_verified_user,
|
||||
get_admin_user,
|
||||
)
|
||||
from utils.misc import calculate_sha256
|
||||
|
||||
|
||||
from config import (
|
||||
SRC_LOG_LEVELS,
|
||||
CACHE_DIR,
|
||||
UPLOAD_DIR,
|
||||
WHISPER_MODEL,
|
||||
WHISPER_MODEL_DIR,
|
||||
WHISPER_MODEL_AUTO_UPDATE,
|
||||
@@ -51,6 +38,13 @@ from config import (
|
||||
AUDIO_TTS_MODEL,
|
||||
AUDIO_TTS_VOICE,
|
||||
AppConfig,
|
||||
CORS_ALLOW_ORIGIN,
|
||||
)
|
||||
from constants import ERROR_MESSAGES
|
||||
from utils.utils import (
|
||||
get_current_user,
|
||||
get_verified_user,
|
||||
get_admin_user,
|
||||
)
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
@@ -59,7 +53,7 @@ log.setLevel(SRC_LOG_LEVELS["AUDIO"])
|
||||
app = FastAPI()
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["*"],
|
||||
allow_origins=CORS_ALLOW_ORIGIN,
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
@@ -244,7 +238,7 @@ async def speech(request: Request, user=Depends(get_verified_user)):
|
||||
res = r.json()
|
||||
if "error" in res:
|
||||
error_detail = f"External: {res['error']['message']}"
|
||||
except:
|
||||
except Exception:
|
||||
error_detail = f"External: {e}"
|
||||
|
||||
raise HTTPException(
|
||||
@@ -261,6 +255,13 @@ async def speech(request: Request, user=Depends(get_verified_user)):
|
||||
raise HTTPException(status_code=400, detail="Invalid JSON payload")
|
||||
|
||||
voice_id = payload.get("voice", "")
|
||||
|
||||
if voice_id not in get_available_voices():
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="Invalid voice id",
|
||||
)
|
||||
|
||||
url = f"https://api.elevenlabs.io/v1/text-to-speech/{voice_id}"
|
||||
|
||||
headers = {
|
||||
@@ -299,7 +300,7 @@ async def speech(request: Request, user=Depends(get_verified_user)):
|
||||
res = r.json()
|
||||
if "error" in res:
|
||||
error_detail = f"External: {res['error']['message']}"
|
||||
except:
|
||||
except Exception:
|
||||
error_detail = f"External: {e}"
|
||||
|
||||
raise HTTPException(
|
||||
@@ -353,7 +354,7 @@ def transcribe(
|
||||
|
||||
try:
|
||||
model = WhisperModel(**whisper_kwargs)
|
||||
except:
|
||||
except Exception:
|
||||
log.warning(
|
||||
"WhisperModel initialization failed, attempting download with local_files_only=False"
|
||||
)
|
||||
@@ -421,7 +422,7 @@ def transcribe(
|
||||
res = r.json()
|
||||
if "error" in res:
|
||||
error_detail = f"External: {res['error']['message']}"
|
||||
except:
|
||||
except Exception:
|
||||
error_detail = f"External: {e}"
|
||||
|
||||
raise HTTPException(
|
||||
@@ -438,7 +439,7 @@ def transcribe(
|
||||
)
|
||||
|
||||
|
||||
def get_available_models() -> List[dict]:
|
||||
def get_available_models() -> list[dict]:
|
||||
if app.state.config.TTS_ENGINE == "openai":
|
||||
return [{"id": "tts-1"}, {"id": "tts-1-hd"}]
|
||||
elif app.state.config.TTS_ENGINE == "elevenlabs":
|
||||
@@ -466,39 +467,58 @@ async def get_models(user=Depends(get_verified_user)):
|
||||
return {"models": get_available_models()}
|
||||
|
||||
|
||||
def get_available_voices() -> List[dict]:
|
||||
def get_available_voices() -> dict:
|
||||
"""Returns {voice_id: voice_name} dict"""
|
||||
ret = {}
|
||||
if app.state.config.TTS_ENGINE == "openai":
|
||||
return [
|
||||
{"name": "alloy", "id": "alloy"},
|
||||
{"name": "echo", "id": "echo"},
|
||||
{"name": "fable", "id": "fable"},
|
||||
{"name": "onyx", "id": "onyx"},
|
||||
{"name": "nova", "id": "nova"},
|
||||
{"name": "shimmer", "id": "shimmer"},
|
||||
]
|
||||
elif app.state.config.TTS_ENGINE == "elevenlabs":
|
||||
headers = {
|
||||
"xi-api-key": app.state.config.TTS_API_KEY,
|
||||
"Content-Type": "application/json",
|
||||
ret = {
|
||||
"alloy": "alloy",
|
||||
"echo": "echo",
|
||||
"fable": "fable",
|
||||
"onyx": "onyx",
|
||||
"nova": "nova",
|
||||
"shimmer": "shimmer",
|
||||
}
|
||||
|
||||
elif app.state.config.TTS_ENGINE == "elevenlabs":
|
||||
try:
|
||||
response = requests.get(
|
||||
"https://api.elevenlabs.io/v1/voices", headers=headers
|
||||
)
|
||||
response.raise_for_status()
|
||||
voices_data = response.json()
|
||||
ret = get_elevenlabs_voices()
|
||||
except Exception as e:
|
||||
# Avoided @lru_cache with exception
|
||||
pass
|
||||
|
||||
voices = []
|
||||
for voice in voices_data.get("voices", []):
|
||||
voices.append({"name": voice["name"], "id": voice["voice_id"]})
|
||||
return voices
|
||||
except requests.RequestException as e:
|
||||
log.error(f"Error fetching voices: {str(e)}")
|
||||
return ret
|
||||
|
||||
return []
|
||||
|
||||
@lru_cache
|
||||
def get_elevenlabs_voices() -> dict:
|
||||
"""
|
||||
Note, set the following in your .env file to use Elevenlabs:
|
||||
AUDIO_TTS_ENGINE=elevenlabs
|
||||
AUDIO_TTS_API_KEY=sk_... # Your Elevenlabs API key
|
||||
AUDIO_TTS_VOICE=EXAVITQu4vr4xnSDxMaL # From https://api.elevenlabs.io/v1/voices
|
||||
AUDIO_TTS_MODEL=eleven_multilingual_v2
|
||||
"""
|
||||
headers = {
|
||||
"xi-api-key": app.state.config.TTS_API_KEY,
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
try:
|
||||
# TODO: Add retries
|
||||
response = requests.get("https://api.elevenlabs.io/v1/voices", headers=headers)
|
||||
response.raise_for_status()
|
||||
voices_data = response.json()
|
||||
|
||||
voices = {}
|
||||
for voice in voices_data.get("voices", []):
|
||||
voices[voice["voice_id"]] = voice["name"]
|
||||
except requests.RequestException as e:
|
||||
# Avoid @lru_cache with exception
|
||||
log.error(f"Error fetching voices: {str(e)}")
|
||||
raise RuntimeError(f"Error fetching voices: {str(e)}")
|
||||
|
||||
return voices
|
||||
|
||||
|
||||
@app.get("/voices")
|
||||
async def get_voices(user=Depends(get_verified_user)):
|
||||
return {"voices": get_available_voices()}
|
||||
return {"voices": [{"id": k, "name": v} for k, v in get_available_voices().items()]}
|
||||
|
||||
+243
-264
@@ -1,26 +1,10 @@
|
||||
import re
|
||||
import requests
|
||||
import base64
|
||||
from fastapi import (
|
||||
FastAPI,
|
||||
Request,
|
||||
Depends,
|
||||
HTTPException,
|
||||
status,
|
||||
UploadFile,
|
||||
File,
|
||||
Form,
|
||||
)
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
|
||||
from constants import ERROR_MESSAGES
|
||||
from utils.utils import (
|
||||
get_verified_user,
|
||||
get_admin_user,
|
||||
)
|
||||
|
||||
from apps.images.utils.comfyui import ImageGenerationPayload, comfyui_generate_image
|
||||
from utils.misc import calculate_sha256
|
||||
from typing import Optional
|
||||
from pydantic import BaseModel
|
||||
from pathlib import Path
|
||||
@@ -29,7 +13,21 @@ import uuid
|
||||
import base64
|
||||
import json
|
||||
import logging
|
||||
import re
|
||||
import requests
|
||||
|
||||
from utils.utils import (
|
||||
get_verified_user,
|
||||
get_admin_user,
|
||||
)
|
||||
|
||||
from apps.images.utils.comfyui import (
|
||||
ComfyUIWorkflow,
|
||||
ComfyUIGenerateImageForm,
|
||||
comfyui_generate_image,
|
||||
)
|
||||
|
||||
from constants import ERROR_MESSAGES
|
||||
from config import (
|
||||
SRC_LOG_LEVELS,
|
||||
CACHE_DIR,
|
||||
@@ -38,18 +36,14 @@ from config import (
|
||||
AUTOMATIC1111_BASE_URL,
|
||||
AUTOMATIC1111_API_AUTH,
|
||||
COMFYUI_BASE_URL,
|
||||
COMFYUI_CFG_SCALE,
|
||||
COMFYUI_SAMPLER,
|
||||
COMFYUI_SCHEDULER,
|
||||
COMFYUI_SD3,
|
||||
COMFYUI_FLUX,
|
||||
COMFYUI_FLUX_WEIGHT_DTYPE,
|
||||
COMFYUI_FLUX_FP8_CLIP,
|
||||
COMFYUI_WORKFLOW,
|
||||
COMFYUI_WORKFLOW_NODES,
|
||||
IMAGES_OPENAI_API_BASE_URL,
|
||||
IMAGES_OPENAI_API_KEY,
|
||||
IMAGE_GENERATION_MODEL,
|
||||
IMAGE_SIZE,
|
||||
IMAGE_STEPS,
|
||||
CORS_ALLOW_ORIGIN,
|
||||
AppConfig,
|
||||
)
|
||||
|
||||
@@ -62,7 +56,7 @@ IMAGE_CACHE_DIR.mkdir(parents=True, exist_ok=True)
|
||||
app = FastAPI()
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["*"],
|
||||
allow_origins=CORS_ALLOW_ORIGIN,
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
@@ -81,20 +75,98 @@ app.state.config.MODEL = IMAGE_GENERATION_MODEL
|
||||
app.state.config.AUTOMATIC1111_BASE_URL = AUTOMATIC1111_BASE_URL
|
||||
app.state.config.AUTOMATIC1111_API_AUTH = AUTOMATIC1111_API_AUTH
|
||||
app.state.config.COMFYUI_BASE_URL = COMFYUI_BASE_URL
|
||||
app.state.config.COMFYUI_WORKFLOW = COMFYUI_WORKFLOW
|
||||
app.state.config.COMFYUI_WORKFLOW_NODES = COMFYUI_WORKFLOW_NODES
|
||||
|
||||
app.state.config.IMAGE_SIZE = IMAGE_SIZE
|
||||
app.state.config.IMAGE_STEPS = IMAGE_STEPS
|
||||
app.state.config.COMFYUI_CFG_SCALE = COMFYUI_CFG_SCALE
|
||||
app.state.config.COMFYUI_SAMPLER = COMFYUI_SAMPLER
|
||||
app.state.config.COMFYUI_SCHEDULER = COMFYUI_SCHEDULER
|
||||
app.state.config.COMFYUI_SD3 = COMFYUI_SD3
|
||||
app.state.config.COMFYUI_FLUX = COMFYUI_FLUX
|
||||
app.state.config.COMFYUI_FLUX_WEIGHT_DTYPE = COMFYUI_FLUX_WEIGHT_DTYPE
|
||||
app.state.config.COMFYUI_FLUX_FP8_CLIP = COMFYUI_FLUX_FP8_CLIP
|
||||
|
||||
|
||||
@app.get("/config")
|
||||
async def get_config(request: Request, user=Depends(get_admin_user)):
|
||||
return {
|
||||
"enabled": app.state.config.ENABLED,
|
||||
"engine": app.state.config.ENGINE,
|
||||
"openai": {
|
||||
"OPENAI_API_BASE_URL": app.state.config.OPENAI_API_BASE_URL,
|
||||
"OPENAI_API_KEY": app.state.config.OPENAI_API_KEY,
|
||||
},
|
||||
"automatic1111": {
|
||||
"AUTOMATIC1111_BASE_URL": app.state.config.AUTOMATIC1111_BASE_URL,
|
||||
"AUTOMATIC1111_API_AUTH": app.state.config.AUTOMATIC1111_API_AUTH,
|
||||
},
|
||||
"comfyui": {
|
||||
"COMFYUI_BASE_URL": app.state.config.COMFYUI_BASE_URL,
|
||||
"COMFYUI_WORKFLOW": app.state.config.COMFYUI_WORKFLOW,
|
||||
"COMFYUI_WORKFLOW_NODES": app.state.config.COMFYUI_WORKFLOW_NODES,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
class OpenAIConfigForm(BaseModel):
|
||||
OPENAI_API_BASE_URL: str
|
||||
OPENAI_API_KEY: str
|
||||
|
||||
|
||||
class Automatic1111ConfigForm(BaseModel):
|
||||
AUTOMATIC1111_BASE_URL: str
|
||||
AUTOMATIC1111_API_AUTH: str
|
||||
|
||||
|
||||
class ComfyUIConfigForm(BaseModel):
|
||||
COMFYUI_BASE_URL: str
|
||||
COMFYUI_WORKFLOW: str
|
||||
COMFYUI_WORKFLOW_NODES: list[dict]
|
||||
|
||||
|
||||
class ConfigForm(BaseModel):
|
||||
enabled: bool
|
||||
engine: str
|
||||
openai: OpenAIConfigForm
|
||||
automatic1111: Automatic1111ConfigForm
|
||||
comfyui: ComfyUIConfigForm
|
||||
|
||||
|
||||
@app.post("/config/update")
|
||||
async def update_config(form_data: ConfigForm, user=Depends(get_admin_user)):
|
||||
app.state.config.ENGINE = form_data.engine
|
||||
app.state.config.ENABLED = form_data.enabled
|
||||
|
||||
app.state.config.OPENAI_API_BASE_URL = form_data.openai.OPENAI_API_BASE_URL
|
||||
app.state.config.OPENAI_API_KEY = form_data.openai.OPENAI_API_KEY
|
||||
|
||||
app.state.config.AUTOMATIC1111_BASE_URL = (
|
||||
form_data.automatic1111.AUTOMATIC1111_BASE_URL
|
||||
)
|
||||
app.state.config.AUTOMATIC1111_API_AUTH = (
|
||||
form_data.automatic1111.AUTOMATIC1111_API_AUTH
|
||||
)
|
||||
|
||||
app.state.config.COMFYUI_BASE_URL = form_data.comfyui.COMFYUI_BASE_URL
|
||||
app.state.config.COMFYUI_WORKFLOW = form_data.comfyui.COMFYUI_WORKFLOW
|
||||
app.state.config.COMFYUI_WORKFLOW_NODES = form_data.comfyui.COMFYUI_WORKFLOW_NODES
|
||||
|
||||
return {
|
||||
"enabled": app.state.config.ENABLED,
|
||||
"engine": app.state.config.ENGINE,
|
||||
"openai": {
|
||||
"OPENAI_API_BASE_URL": app.state.config.OPENAI_API_BASE_URL,
|
||||
"OPENAI_API_KEY": app.state.config.OPENAI_API_KEY,
|
||||
},
|
||||
"automatic1111": {
|
||||
"AUTOMATIC1111_BASE_URL": app.state.config.AUTOMATIC1111_BASE_URL,
|
||||
"AUTOMATIC1111_API_AUTH": app.state.config.AUTOMATIC1111_API_AUTH,
|
||||
},
|
||||
"comfyui": {
|
||||
"COMFYUI_BASE_URL": app.state.config.COMFYUI_BASE_URL,
|
||||
"COMFYUI_WORKFLOW": app.state.config.COMFYUI_WORKFLOW,
|
||||
"COMFYUI_WORKFLOW_NODES": app.state.config.COMFYUI_WORKFLOW_NODES,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def get_automatic1111_api_auth():
|
||||
if app.state.config.AUTOMATIC1111_API_AUTH == None:
|
||||
if app.state.config.AUTOMATIC1111_API_AUTH is None:
|
||||
return ""
|
||||
else:
|
||||
auth1111_byte_string = app.state.config.AUTOMATIC1111_API_AUTH.encode("utf-8")
|
||||
@@ -103,164 +175,110 @@ def get_automatic1111_api_auth():
|
||||
return f"Basic {auth1111_base64_encoded_string}"
|
||||
|
||||
|
||||
@app.get("/config")
|
||||
async def get_config(request: Request, user=Depends(get_admin_user)):
|
||||
return {
|
||||
"engine": app.state.config.ENGINE,
|
||||
"enabled": app.state.config.ENABLED,
|
||||
}
|
||||
|
||||
|
||||
class ConfigUpdateForm(BaseModel):
|
||||
engine: str
|
||||
enabled: bool
|
||||
|
||||
|
||||
@app.post("/config/update")
|
||||
async def update_config(form_data: ConfigUpdateForm, user=Depends(get_admin_user)):
|
||||
app.state.config.ENGINE = form_data.engine
|
||||
app.state.config.ENABLED = form_data.enabled
|
||||
return {
|
||||
"engine": app.state.config.ENGINE,
|
||||
"enabled": app.state.config.ENABLED,
|
||||
}
|
||||
|
||||
|
||||
class EngineUrlUpdateForm(BaseModel):
|
||||
AUTOMATIC1111_BASE_URL: Optional[str] = None
|
||||
AUTOMATIC1111_API_AUTH: Optional[str] = None
|
||||
COMFYUI_BASE_URL: Optional[str] = None
|
||||
|
||||
|
||||
@app.get("/url")
|
||||
async def get_engine_url(user=Depends(get_admin_user)):
|
||||
return {
|
||||
"AUTOMATIC1111_BASE_URL": app.state.config.AUTOMATIC1111_BASE_URL,
|
||||
"AUTOMATIC1111_API_AUTH": app.state.config.AUTOMATIC1111_API_AUTH,
|
||||
"COMFYUI_BASE_URL": app.state.config.COMFYUI_BASE_URL,
|
||||
}
|
||||
|
||||
|
||||
@app.post("/url/update")
|
||||
async def update_engine_url(
|
||||
form_data: EngineUrlUpdateForm, user=Depends(get_admin_user)
|
||||
):
|
||||
if form_data.AUTOMATIC1111_BASE_URL == None:
|
||||
app.state.config.AUTOMATIC1111_BASE_URL = AUTOMATIC1111_BASE_URL
|
||||
else:
|
||||
url = form_data.AUTOMATIC1111_BASE_URL.strip("/")
|
||||
@app.get("/config/url/verify")
|
||||
async def verify_url(user=Depends(get_admin_user)):
|
||||
if app.state.config.ENGINE == "automatic1111":
|
||||
try:
|
||||
r = requests.head(url)
|
||||
app.state.config.AUTOMATIC1111_BASE_URL = url
|
||||
r = requests.get(
|
||||
url=f"{app.state.config.AUTOMATIC1111_BASE_URL}/sdapi/v1/options",
|
||||
headers={"authorization": get_automatic1111_api_auth()},
|
||||
)
|
||||
r.raise_for_status()
|
||||
return True
|
||||
except Exception as e:
|
||||
app.state.config.ENABLED = False
|
||||
raise HTTPException(status_code=400, detail=ERROR_MESSAGES.INVALID_URL)
|
||||
elif app.state.config.ENGINE == "comfyui":
|
||||
try:
|
||||
r = requests.get(url=f"{app.state.config.COMFYUI_BASE_URL}/object_info")
|
||||
r.raise_for_status()
|
||||
return True
|
||||
except Exception as e:
|
||||
app.state.config.ENABLED = False
|
||||
raise HTTPException(status_code=400, detail=ERROR_MESSAGES.INVALID_URL)
|
||||
else:
|
||||
return True
|
||||
|
||||
|
||||
def set_image_model(model: str):
|
||||
app.state.config.MODEL = model
|
||||
if app.state.config.ENGINE in ["", "automatic1111"]:
|
||||
api_auth = get_automatic1111_api_auth()
|
||||
r = requests.get(
|
||||
url=f"{app.state.config.AUTOMATIC1111_BASE_URL}/sdapi/v1/options",
|
||||
headers={"authorization": api_auth},
|
||||
)
|
||||
options = r.json()
|
||||
if model != options["sd_model_checkpoint"]:
|
||||
options["sd_model_checkpoint"] = model
|
||||
r = requests.post(
|
||||
url=f"{app.state.config.AUTOMATIC1111_BASE_URL}/sdapi/v1/options",
|
||||
json=options,
|
||||
headers={"authorization": api_auth},
|
||||
)
|
||||
return app.state.config.MODEL
|
||||
|
||||
|
||||
def get_image_model():
|
||||
if app.state.config.ENGINE == "openai":
|
||||
return app.state.config.MODEL if app.state.config.MODEL else "dall-e-2"
|
||||
elif app.state.config.ENGINE == "comfyui":
|
||||
return app.state.config.MODEL if app.state.config.MODEL else ""
|
||||
elif app.state.config.ENGINE == "automatic1111" or app.state.config.ENGINE == "":
|
||||
try:
|
||||
r = requests.get(
|
||||
url=f"{app.state.config.AUTOMATIC1111_BASE_URL}/sdapi/v1/options",
|
||||
headers={"authorization": get_automatic1111_api_auth()},
|
||||
)
|
||||
options = r.json()
|
||||
return options["sd_model_checkpoint"]
|
||||
except Exception as e:
|
||||
app.state.config.ENABLED = False
|
||||
raise HTTPException(status_code=400, detail=ERROR_MESSAGES.DEFAULT(e))
|
||||
|
||||
if form_data.COMFYUI_BASE_URL == None:
|
||||
app.state.config.COMFYUI_BASE_URL = COMFYUI_BASE_URL
|
||||
else:
|
||||
url = form_data.COMFYUI_BASE_URL.strip("/")
|
||||
|
||||
try:
|
||||
r = requests.head(url)
|
||||
app.state.config.COMFYUI_BASE_URL = url
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=400, detail=ERROR_MESSAGES.DEFAULT(e))
|
||||
class ImageConfigForm(BaseModel):
|
||||
MODEL: str
|
||||
IMAGE_SIZE: str
|
||||
IMAGE_STEPS: int
|
||||
|
||||
if form_data.AUTOMATIC1111_API_AUTH == None:
|
||||
app.state.config.AUTOMATIC1111_API_AUTH = AUTOMATIC1111_API_AUTH
|
||||
else:
|
||||
app.state.config.AUTOMATIC1111_API_AUTH = form_data.AUTOMATIC1111_API_AUTH
|
||||
|
||||
@app.get("/image/config")
|
||||
async def get_image_config(user=Depends(get_admin_user)):
|
||||
return {
|
||||
"AUTOMATIC1111_BASE_URL": app.state.config.AUTOMATIC1111_BASE_URL,
|
||||
"AUTOMATIC1111_API_AUTH": app.state.config.AUTOMATIC1111_API_AUTH,
|
||||
"COMFYUI_BASE_URL": app.state.config.COMFYUI_BASE_URL,
|
||||
"status": True,
|
||||
"MODEL": app.state.config.MODEL,
|
||||
"IMAGE_SIZE": app.state.config.IMAGE_SIZE,
|
||||
"IMAGE_STEPS": app.state.config.IMAGE_STEPS,
|
||||
}
|
||||
|
||||
|
||||
class OpenAIConfigUpdateForm(BaseModel):
|
||||
url: str
|
||||
key: str
|
||||
@app.post("/image/config/update")
|
||||
async def update_image_config(form_data: ImageConfigForm, user=Depends(get_admin_user)):
|
||||
app.state.config.MODEL = form_data.MODEL
|
||||
|
||||
|
||||
@app.get("/openai/config")
|
||||
async def get_openai_config(user=Depends(get_admin_user)):
|
||||
return {
|
||||
"OPENAI_API_BASE_URL": app.state.config.OPENAI_API_BASE_URL,
|
||||
"OPENAI_API_KEY": app.state.config.OPENAI_API_KEY,
|
||||
}
|
||||
|
||||
|
||||
@app.post("/openai/config/update")
|
||||
async def update_openai_config(
|
||||
form_data: OpenAIConfigUpdateForm, user=Depends(get_admin_user)
|
||||
):
|
||||
if form_data.key == "":
|
||||
raise HTTPException(status_code=400, detail=ERROR_MESSAGES.API_KEY_NOT_FOUND)
|
||||
|
||||
app.state.config.OPENAI_API_BASE_URL = form_data.url
|
||||
app.state.config.OPENAI_API_KEY = form_data.key
|
||||
|
||||
return {
|
||||
"status": True,
|
||||
"OPENAI_API_BASE_URL": app.state.config.OPENAI_API_BASE_URL,
|
||||
"OPENAI_API_KEY": app.state.config.OPENAI_API_KEY,
|
||||
}
|
||||
|
||||
|
||||
class ImageSizeUpdateForm(BaseModel):
|
||||
size: str
|
||||
|
||||
|
||||
@app.get("/size")
|
||||
async def get_image_size(user=Depends(get_admin_user)):
|
||||
return {"IMAGE_SIZE": app.state.config.IMAGE_SIZE}
|
||||
|
||||
|
||||
@app.post("/size/update")
|
||||
async def update_image_size(
|
||||
form_data: ImageSizeUpdateForm, user=Depends(get_admin_user)
|
||||
):
|
||||
pattern = r"^\d+x\d+$" # Regular expression pattern
|
||||
if re.match(pattern, form_data.size):
|
||||
app.state.config.IMAGE_SIZE = form_data.size
|
||||
return {
|
||||
"IMAGE_SIZE": app.state.config.IMAGE_SIZE,
|
||||
"status": True,
|
||||
}
|
||||
pattern = r"^\d+x\d+$"
|
||||
if re.match(pattern, form_data.IMAGE_SIZE):
|
||||
app.state.config.IMAGE_SIZE = form_data.IMAGE_SIZE
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=ERROR_MESSAGES.INCORRECT_FORMAT(" (e.g., 512x512)."),
|
||||
)
|
||||
|
||||
|
||||
class ImageStepsUpdateForm(BaseModel):
|
||||
steps: int
|
||||
|
||||
|
||||
@app.get("/steps")
|
||||
async def get_image_size(user=Depends(get_admin_user)):
|
||||
return {"IMAGE_STEPS": app.state.config.IMAGE_STEPS}
|
||||
|
||||
|
||||
@app.post("/steps/update")
|
||||
async def update_image_size(
|
||||
form_data: ImageStepsUpdateForm, user=Depends(get_admin_user)
|
||||
):
|
||||
if form_data.steps >= 0:
|
||||
app.state.config.IMAGE_STEPS = form_data.steps
|
||||
return {
|
||||
"IMAGE_STEPS": app.state.config.IMAGE_STEPS,
|
||||
"status": True,
|
||||
}
|
||||
if form_data.IMAGE_STEPS >= 0:
|
||||
app.state.config.IMAGE_STEPS = form_data.IMAGE_STEPS
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=ERROR_MESSAGES.INCORRECT_FORMAT(" (e.g., 50)."),
|
||||
)
|
||||
|
||||
return {
|
||||
"MODEL": app.state.config.MODEL,
|
||||
"IMAGE_SIZE": app.state.config.IMAGE_SIZE,
|
||||
"IMAGE_STEPS": app.state.config.IMAGE_STEPS,
|
||||
}
|
||||
|
||||
|
||||
@app.get("/models")
|
||||
def get_models(user=Depends(get_verified_user)):
|
||||
@@ -271,18 +289,50 @@ def get_models(user=Depends(get_verified_user)):
|
||||
{"id": "dall-e-3", "name": "DALL·E 3"},
|
||||
]
|
||||
elif app.state.config.ENGINE == "comfyui":
|
||||
|
||||
# TODO - get models from comfyui
|
||||
r = requests.get(url=f"{app.state.config.COMFYUI_BASE_URL}/object_info")
|
||||
info = r.json()
|
||||
|
||||
return list(
|
||||
map(
|
||||
lambda model: {"id": model, "name": model},
|
||||
info["CheckpointLoaderSimple"]["input"]["required"]["ckpt_name"][0],
|
||||
)
|
||||
)
|
||||
workflow = json.loads(app.state.config.COMFYUI_WORKFLOW)
|
||||
model_node_id = None
|
||||
|
||||
else:
|
||||
for node in app.state.config.COMFYUI_WORKFLOW_NODES:
|
||||
if node["type"] == "model":
|
||||
model_node_id = node["node_ids"][0]
|
||||
break
|
||||
|
||||
if model_node_id:
|
||||
model_list_key = None
|
||||
|
||||
print(workflow[model_node_id]["class_type"])
|
||||
for key in info[workflow[model_node_id]["class_type"]]["input"][
|
||||
"required"
|
||||
]:
|
||||
if "_name" in key:
|
||||
model_list_key = key
|
||||
break
|
||||
|
||||
if model_list_key:
|
||||
return list(
|
||||
map(
|
||||
lambda model: {"id": model, "name": model},
|
||||
info[workflow[model_node_id]["class_type"]]["input"][
|
||||
"required"
|
||||
][model_list_key][0],
|
||||
)
|
||||
)
|
||||
else:
|
||||
return list(
|
||||
map(
|
||||
lambda model: {"id": model, "name": model},
|
||||
info["CheckpointLoaderSimple"]["input"]["required"][
|
||||
"ckpt_name"
|
||||
][0],
|
||||
)
|
||||
)
|
||||
elif (
|
||||
app.state.config.ENGINE == "automatic1111" or app.state.config.ENGINE == ""
|
||||
):
|
||||
r = requests.get(
|
||||
url=f"{app.state.config.AUTOMATIC1111_BASE_URL}/sdapi/v1/sd-models",
|
||||
headers={"authorization": get_automatic1111_api_auth()},
|
||||
@@ -299,69 +349,11 @@ def get_models(user=Depends(get_verified_user)):
|
||||
raise HTTPException(status_code=400, detail=ERROR_MESSAGES.DEFAULT(e))
|
||||
|
||||
|
||||
@app.get("/models/default")
|
||||
async def get_default_model(user=Depends(get_admin_user)):
|
||||
try:
|
||||
if app.state.config.ENGINE == "openai":
|
||||
return {
|
||||
"model": (
|
||||
app.state.config.MODEL if app.state.config.MODEL else "dall-e-2"
|
||||
)
|
||||
}
|
||||
elif app.state.config.ENGINE == "comfyui":
|
||||
return {"model": (app.state.config.MODEL if app.state.config.MODEL else "")}
|
||||
else:
|
||||
r = requests.get(
|
||||
url=f"{app.state.config.AUTOMATIC1111_BASE_URL}/sdapi/v1/options",
|
||||
headers={"authorization": get_automatic1111_api_auth()},
|
||||
)
|
||||
options = r.json()
|
||||
return {"model": options["sd_model_checkpoint"]}
|
||||
except Exception as e:
|
||||
app.state.config.ENABLED = False
|
||||
raise HTTPException(status_code=400, detail=ERROR_MESSAGES.DEFAULT(e))
|
||||
|
||||
|
||||
class UpdateModelForm(BaseModel):
|
||||
model: str
|
||||
|
||||
|
||||
def set_model_handler(model: str):
|
||||
if app.state.config.ENGINE in ["openai", "comfyui"]:
|
||||
app.state.config.MODEL = model
|
||||
return app.state.config.MODEL
|
||||
else:
|
||||
api_auth = get_automatic1111_api_auth()
|
||||
r = requests.get(
|
||||
url=f"{app.state.config.AUTOMATIC1111_BASE_URL}/sdapi/v1/options",
|
||||
headers={"authorization": api_auth},
|
||||
)
|
||||
options = r.json()
|
||||
|
||||
if model != options["sd_model_checkpoint"]:
|
||||
options["sd_model_checkpoint"] = model
|
||||
r = requests.post(
|
||||
url=f"{app.state.config.AUTOMATIC1111_BASE_URL}/sdapi/v1/options",
|
||||
json=options,
|
||||
headers={"authorization": api_auth},
|
||||
)
|
||||
|
||||
return options
|
||||
|
||||
|
||||
@app.post("/models/default/update")
|
||||
def update_default_model(
|
||||
form_data: UpdateModelForm,
|
||||
user=Depends(get_verified_user),
|
||||
):
|
||||
return set_model_handler(form_data.model)
|
||||
|
||||
|
||||
class GenerateImageForm(BaseModel):
|
||||
model: Optional[str] = None
|
||||
prompt: str
|
||||
n: int = 1
|
||||
size: Optional[str] = None
|
||||
n: int = 1
|
||||
negative_prompt: Optional[str] = None
|
||||
|
||||
|
||||
@@ -477,7 +469,6 @@ async def image_generations(
|
||||
return images
|
||||
|
||||
elif app.state.config.ENGINE == "comfyui":
|
||||
|
||||
data = {
|
||||
"prompt": form_data.prompt,
|
||||
"width": width,
|
||||
@@ -491,32 +482,20 @@ async def image_generations(
|
||||
if form_data.negative_prompt is not None:
|
||||
data["negative_prompt"] = form_data.negative_prompt
|
||||
|
||||
if app.state.config.COMFYUI_CFG_SCALE:
|
||||
data["cfg_scale"] = app.state.config.COMFYUI_CFG_SCALE
|
||||
|
||||
if app.state.config.COMFYUI_SAMPLER is not None:
|
||||
data["sampler"] = app.state.config.COMFYUI_SAMPLER
|
||||
|
||||
if app.state.config.COMFYUI_SCHEDULER is not None:
|
||||
data["scheduler"] = app.state.config.COMFYUI_SCHEDULER
|
||||
|
||||
if app.state.config.COMFYUI_SD3 is not None:
|
||||
data["sd3"] = app.state.config.COMFYUI_SD3
|
||||
|
||||
if app.state.config.COMFYUI_FLUX is not None:
|
||||
data["flux"] = app.state.config.COMFYUI_FLUX
|
||||
|
||||
if app.state.config.COMFYUI_FLUX_WEIGHT_DTYPE is not None:
|
||||
data["flux_weight_dtype"] = app.state.config.COMFYUI_FLUX_WEIGHT_DTYPE
|
||||
|
||||
if app.state.config.COMFYUI_FLUX_FP8_CLIP is not None:
|
||||
data["flux_fp8_clip"] = app.state.config.COMFYUI_FLUX_FP8_CLIP
|
||||
|
||||
data = ImageGenerationPayload(**data)
|
||||
|
||||
res = comfyui_generate_image(
|
||||
form_data = ComfyUIGenerateImageForm(
|
||||
**{
|
||||
"workflow": ComfyUIWorkflow(
|
||||
**{
|
||||
"workflow": app.state.config.COMFYUI_WORKFLOW,
|
||||
"nodes": app.state.config.COMFYUI_WORKFLOW_NODES,
|
||||
}
|
||||
),
|
||||
**data,
|
||||
}
|
||||
)
|
||||
res = await comfyui_generate_image(
|
||||
app.state.config.MODEL,
|
||||
data,
|
||||
form_data,
|
||||
user.id,
|
||||
app.state.config.COMFYUI_BASE_URL,
|
||||
)
|
||||
@@ -530,13 +509,15 @@ async def image_generations(
|
||||
file_body_path = IMAGE_CACHE_DIR.joinpath(f"{image_filename}.json")
|
||||
|
||||
with open(file_body_path, "w") as f:
|
||||
json.dump(data.model_dump(exclude_none=True), f)
|
||||
json.dump(form_data.model_dump(exclude_none=True), f)
|
||||
|
||||
log.debug(f"images: {images}")
|
||||
return images
|
||||
else:
|
||||
elif (
|
||||
app.state.config.ENGINE == "automatic1111" or app.state.config.ENGINE == ""
|
||||
):
|
||||
if form_data.model:
|
||||
set_model_handler(form_data.model)
|
||||
set_image_model(form_data.model)
|
||||
|
||||
data = {
|
||||
"prompt": form_data.prompt,
|
||||
@@ -558,7 +539,6 @@ async def image_generations(
|
||||
)
|
||||
|
||||
res = r.json()
|
||||
|
||||
log.debug(f"res: {res}")
|
||||
|
||||
images = []
|
||||
@@ -575,7 +555,6 @@ async def image_generations(
|
||||
|
||||
except Exception as e:
|
||||
error = e
|
||||
|
||||
if r != None:
|
||||
data = r.json()
|
||||
if "error" in data:
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import asyncio
|
||||
import websocket # NOTE: websocket-client (https://github.com/websocket-client/websocket-client)
|
||||
import uuid
|
||||
import json
|
||||
import urllib.request
|
||||
import urllib.parse
|
||||
@@ -15,245 +15,6 @@ from pydantic import BaseModel
|
||||
|
||||
from typing import Optional
|
||||
|
||||
COMFYUI_DEFAULT_PROMPT = """
|
||||
{
|
||||
"3": {
|
||||
"inputs": {
|
||||
"seed": 0,
|
||||
"steps": 20,
|
||||
"cfg": 8,
|
||||
"sampler_name": "euler",
|
||||
"scheduler": "normal",
|
||||
"denoise": 1,
|
||||
"model": [
|
||||
"4",
|
||||
0
|
||||
],
|
||||
"positive": [
|
||||
"6",
|
||||
0
|
||||
],
|
||||
"negative": [
|
||||
"7",
|
||||
0
|
||||
],
|
||||
"latent_image": [
|
||||
"5",
|
||||
0
|
||||
]
|
||||
},
|
||||
"class_type": "KSampler",
|
||||
"_meta": {
|
||||
"title": "KSampler"
|
||||
}
|
||||
},
|
||||
"4": {
|
||||
"inputs": {
|
||||
"ckpt_name": "model.safetensors"
|
||||
},
|
||||
"class_type": "CheckpointLoaderSimple",
|
||||
"_meta": {
|
||||
"title": "Load Checkpoint"
|
||||
}
|
||||
},
|
||||
"5": {
|
||||
"inputs": {
|
||||
"width": 512,
|
||||
"height": 512,
|
||||
"batch_size": 1
|
||||
},
|
||||
"class_type": "EmptyLatentImage",
|
||||
"_meta": {
|
||||
"title": "Empty Latent Image"
|
||||
}
|
||||
},
|
||||
"6": {
|
||||
"inputs": {
|
||||
"text": "Prompt",
|
||||
"clip": [
|
||||
"4",
|
||||
1
|
||||
]
|
||||
},
|
||||
"class_type": "CLIPTextEncode",
|
||||
"_meta": {
|
||||
"title": "CLIP Text Encode (Prompt)"
|
||||
}
|
||||
},
|
||||
"7": {
|
||||
"inputs": {
|
||||
"text": "Negative Prompt",
|
||||
"clip": [
|
||||
"4",
|
||||
1
|
||||
]
|
||||
},
|
||||
"class_type": "CLIPTextEncode",
|
||||
"_meta": {
|
||||
"title": "CLIP Text Encode (Prompt)"
|
||||
}
|
||||
},
|
||||
"8": {
|
||||
"inputs": {
|
||||
"samples": [
|
||||
"3",
|
||||
0
|
||||
],
|
||||
"vae": [
|
||||
"4",
|
||||
2
|
||||
]
|
||||
},
|
||||
"class_type": "VAEDecode",
|
||||
"_meta": {
|
||||
"title": "VAE Decode"
|
||||
}
|
||||
},
|
||||
"9": {
|
||||
"inputs": {
|
||||
"filename_prefix": "ComfyUI",
|
||||
"images": [
|
||||
"8",
|
||||
0
|
||||
]
|
||||
},
|
||||
"class_type": "SaveImage",
|
||||
"_meta": {
|
||||
"title": "Save Image"
|
||||
}
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
FLUX_DEFAULT_PROMPT = """
|
||||
{
|
||||
"5": {
|
||||
"inputs": {
|
||||
"width": 1024,
|
||||
"height": 1024,
|
||||
"batch_size": 1
|
||||
},
|
||||
"class_type": "EmptyLatentImage"
|
||||
},
|
||||
"6": {
|
||||
"inputs": {
|
||||
"text": "Input Text Here",
|
||||
"clip": [
|
||||
"11",
|
||||
0
|
||||
]
|
||||
},
|
||||
"class_type": "CLIPTextEncode"
|
||||
},
|
||||
"8": {
|
||||
"inputs": {
|
||||
"samples": [
|
||||
"13",
|
||||
0
|
||||
],
|
||||
"vae": [
|
||||
"10",
|
||||
0
|
||||
]
|
||||
},
|
||||
"class_type": "VAEDecode"
|
||||
},
|
||||
"9": {
|
||||
"inputs": {
|
||||
"filename_prefix": "ComfyUI",
|
||||
"images": [
|
||||
"8",
|
||||
0
|
||||
]
|
||||
},
|
||||
"class_type": "SaveImage"
|
||||
},
|
||||
"10": {
|
||||
"inputs": {
|
||||
"vae_name": "ae.sft"
|
||||
},
|
||||
"class_type": "VAELoader"
|
||||
},
|
||||
"11": {
|
||||
"inputs": {
|
||||
"clip_name1": "clip_l.safetensors",
|
||||
"clip_name2": "t5xxl_fp16.safetensors",
|
||||
"type": "flux"
|
||||
},
|
||||
"class_type": "DualCLIPLoader"
|
||||
},
|
||||
"12": {
|
||||
"inputs": {
|
||||
"unet_name": "flux1-dev.sft",
|
||||
"weight_dtype": "default"
|
||||
},
|
||||
"class_type": "UNETLoader"
|
||||
},
|
||||
"13": {
|
||||
"inputs": {
|
||||
"noise": [
|
||||
"25",
|
||||
0
|
||||
],
|
||||
"guider": [
|
||||
"22",
|
||||
0
|
||||
],
|
||||
"sampler": [
|
||||
"16",
|
||||
0
|
||||
],
|
||||
"sigmas": [
|
||||
"17",
|
||||
0
|
||||
],
|
||||
"latent_image": [
|
||||
"5",
|
||||
0
|
||||
]
|
||||
},
|
||||
"class_type": "SamplerCustomAdvanced"
|
||||
},
|
||||
"16": {
|
||||
"inputs": {
|
||||
"sampler_name": "euler"
|
||||
},
|
||||
"class_type": "KSamplerSelect"
|
||||
},
|
||||
"17": {
|
||||
"inputs": {
|
||||
"scheduler": "simple",
|
||||
"steps": 20,
|
||||
"denoise": 1,
|
||||
"model": [
|
||||
"12",
|
||||
0
|
||||
]
|
||||
},
|
||||
"class_type": "BasicScheduler"
|
||||
},
|
||||
"22": {
|
||||
"inputs": {
|
||||
"model": [
|
||||
"12",
|
||||
0
|
||||
],
|
||||
"conditioning": [
|
||||
"6",
|
||||
0
|
||||
]
|
||||
},
|
||||
"class_type": "BasicGuider"
|
||||
},
|
||||
"25": {
|
||||
"inputs": {
|
||||
"noise_seed": 778937779713005
|
||||
},
|
||||
"class_type": "RandomNoise"
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
|
||||
def queue_prompt(prompt, client_id, base_url):
|
||||
log.info("queue_prompt")
|
||||
@@ -311,82 +72,71 @@ def get_images(ws, prompt, client_id, base_url):
|
||||
return {"data": output_images}
|
||||
|
||||
|
||||
class ImageGenerationPayload(BaseModel):
|
||||
class ComfyUINodeInput(BaseModel):
|
||||
type: Optional[str] = None
|
||||
node_ids: list[str] = []
|
||||
key: Optional[str] = "text"
|
||||
value: Optional[str] = None
|
||||
|
||||
|
||||
class ComfyUIWorkflow(BaseModel):
|
||||
workflow: str
|
||||
nodes: list[ComfyUINodeInput]
|
||||
|
||||
|
||||
class ComfyUIGenerateImageForm(BaseModel):
|
||||
workflow: ComfyUIWorkflow
|
||||
|
||||
prompt: str
|
||||
negative_prompt: Optional[str] = ""
|
||||
steps: Optional[int] = None
|
||||
seed: Optional[int] = None
|
||||
negative_prompt: Optional[str] = None
|
||||
width: int
|
||||
height: int
|
||||
n: int = 1
|
||||
cfg_scale: Optional[float] = None
|
||||
sampler: Optional[str] = None
|
||||
scheduler: Optional[str] = None
|
||||
sd3: Optional[bool] = None
|
||||
flux: Optional[bool] = None
|
||||
flux_weight_dtype: Optional[str] = None
|
||||
flux_fp8_clip: Optional[bool] = None
|
||||
|
||||
steps: Optional[int] = None
|
||||
seed: Optional[int] = None
|
||||
|
||||
|
||||
def comfyui_generate_image(
|
||||
model: str, payload: ImageGenerationPayload, client_id, base_url
|
||||
async def comfyui_generate_image(
|
||||
model: str, payload: ComfyUIGenerateImageForm, client_id, base_url
|
||||
):
|
||||
ws_url = base_url.replace("http://", "ws://").replace("https://", "wss://")
|
||||
workflow = json.loads(payload.workflow.workflow)
|
||||
|
||||
comfyui_prompt = json.loads(COMFYUI_DEFAULT_PROMPT)
|
||||
|
||||
if payload.cfg_scale:
|
||||
comfyui_prompt["3"]["inputs"]["cfg"] = payload.cfg_scale
|
||||
|
||||
if payload.sampler:
|
||||
comfyui_prompt["3"]["inputs"]["sampler"] = payload.sampler
|
||||
|
||||
if payload.scheduler:
|
||||
comfyui_prompt["3"]["inputs"]["scheduler"] = payload.scheduler
|
||||
|
||||
if payload.sd3:
|
||||
comfyui_prompt["5"]["class_type"] = "EmptySD3LatentImage"
|
||||
|
||||
if payload.steps:
|
||||
comfyui_prompt["3"]["inputs"]["steps"] = payload.steps
|
||||
|
||||
comfyui_prompt["4"]["inputs"]["ckpt_name"] = model
|
||||
comfyui_prompt["7"]["inputs"]["text"] = payload.negative_prompt
|
||||
comfyui_prompt["3"]["inputs"]["seed"] = (
|
||||
payload.seed if payload.seed else random.randint(0, 18446744073709551614)
|
||||
)
|
||||
|
||||
# as Flux uses a completely different workflow, we must treat it specially
|
||||
if payload.flux:
|
||||
comfyui_prompt = json.loads(FLUX_DEFAULT_PROMPT)
|
||||
comfyui_prompt["12"]["inputs"]["unet_name"] = model
|
||||
comfyui_prompt["25"]["inputs"]["noise_seed"] = (
|
||||
payload.seed if payload.seed else random.randint(0, 18446744073709551614)
|
||||
)
|
||||
|
||||
if payload.sampler:
|
||||
comfyui_prompt["16"]["inputs"]["sampler_name"] = payload.sampler
|
||||
|
||||
if payload.steps:
|
||||
comfyui_prompt["17"]["inputs"]["steps"] = payload.steps
|
||||
|
||||
if payload.scheduler:
|
||||
comfyui_prompt["17"]["inputs"]["scheduler"] = payload.scheduler
|
||||
|
||||
if payload.flux_weight_dtype:
|
||||
comfyui_prompt["12"]["inputs"]["weight_dtype"] = payload.flux_weight_dtype
|
||||
|
||||
if payload.flux_fp8_clip:
|
||||
comfyui_prompt["11"]["inputs"][
|
||||
"clip_name2"
|
||||
] = "t5xxl_fp8_e4m3fn.safetensors"
|
||||
|
||||
comfyui_prompt["5"]["inputs"]["batch_size"] = payload.n
|
||||
comfyui_prompt["5"]["inputs"]["width"] = payload.width
|
||||
comfyui_prompt["5"]["inputs"]["height"] = payload.height
|
||||
|
||||
# set the text prompt for our positive CLIPTextEncode
|
||||
comfyui_prompt["6"]["inputs"]["text"] = payload.prompt
|
||||
for node in payload.workflow.nodes:
|
||||
if node.type:
|
||||
if node.type == "model":
|
||||
for node_id in node.node_ids:
|
||||
workflow[node_id]["inputs"][node.key] = model
|
||||
elif node.type == "prompt":
|
||||
for node_id in node.node_ids:
|
||||
workflow[node_id]["inputs"]["text"] = payload.prompt
|
||||
elif node.type == "negative_prompt":
|
||||
for node_id in node.node_ids:
|
||||
workflow[node_id]["inputs"]["text"] = payload.negative_prompt
|
||||
elif node.type == "width":
|
||||
for node_id in node.node_ids:
|
||||
workflow[node_id]["inputs"]["width"] = payload.width
|
||||
elif node.type == "height":
|
||||
for node_id in node.node_ids:
|
||||
workflow[node_id]["inputs"]["height"] = payload.height
|
||||
elif node.type == "n":
|
||||
for node_id in node.node_ids:
|
||||
workflow[node_id]["inputs"]["batch_size"] = payload.n
|
||||
elif node.type == "steps":
|
||||
for node_id in node.node_ids:
|
||||
workflow[node_id]["inputs"]["steps"] = payload.steps
|
||||
elif node.type == "seed":
|
||||
seed = (
|
||||
payload.seed
|
||||
if payload.seed
|
||||
else random.randint(0, 18446744073709551614)
|
||||
)
|
||||
for node_id in node.node_ids:
|
||||
workflow[node.node_id]["inputs"]["seed"] = seed
|
||||
else:
|
||||
for node_id in node.node_ids:
|
||||
workflow[node_id]["inputs"][node.key] = node.value
|
||||
|
||||
try:
|
||||
ws = websocket.WebSocket()
|
||||
@@ -397,7 +147,7 @@ def comfyui_generate_image(
|
||||
return None
|
||||
|
||||
try:
|
||||
images = get_images(ws, comfyui_prompt, client_id, base_url)
|
||||
images = await asyncio.to_thread(get_images, ws, workflow, client_id, base_url)
|
||||
except Exception as e:
|
||||
log.exception(f"Error while receiving images: {e}")
|
||||
images = None
|
||||
|
||||
+104
-322
@@ -1,47 +1,36 @@
|
||||
from fastapi import (
|
||||
FastAPI,
|
||||
Request,
|
||||
Response,
|
||||
HTTPException,
|
||||
Depends,
|
||||
status,
|
||||
UploadFile,
|
||||
File,
|
||||
BackgroundTasks,
|
||||
)
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.responses import StreamingResponse
|
||||
from fastapi.concurrency import run_in_threadpool
|
||||
|
||||
from pydantic import BaseModel, ConfigDict
|
||||
|
||||
import os
|
||||
import re
|
||||
import copy
|
||||
import random
|
||||
import requests
|
||||
import json
|
||||
import uuid
|
||||
import aiohttp
|
||||
import asyncio
|
||||
import logging
|
||||
import time
|
||||
from urllib.parse import urlparse
|
||||
from typing import Optional, List, Union
|
||||
from typing import Optional, Union
|
||||
|
||||
from starlette.background import BackgroundTask
|
||||
|
||||
from apps.webui.models.models import Models
|
||||
from apps.webui.models.users import Users
|
||||
from constants import ERROR_MESSAGES
|
||||
from utils.utils import (
|
||||
decode_token,
|
||||
get_current_user,
|
||||
get_verified_user,
|
||||
get_admin_user,
|
||||
)
|
||||
from utils.task import prompt_template
|
||||
|
||||
|
||||
from config import (
|
||||
SRC_LOG_LEVELS,
|
||||
@@ -52,8 +41,14 @@ from config import (
|
||||
MODEL_FILTER_LIST,
|
||||
UPLOAD_DIR,
|
||||
AppConfig,
|
||||
CORS_ALLOW_ORIGIN,
|
||||
)
|
||||
from utils.misc import (
|
||||
calculate_sha256,
|
||||
apply_model_params_to_body_ollama,
|
||||
apply_model_params_to_body_openai,
|
||||
apply_model_system_prompt_to_body,
|
||||
)
|
||||
from utils.misc import calculate_sha256, add_or_update_system_message
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
log.setLevel(SRC_LOG_LEVELS["OLLAMA"])
|
||||
@@ -61,7 +56,7 @@ log.setLevel(SRC_LOG_LEVELS["OLLAMA"])
|
||||
app = FastAPI()
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["*"],
|
||||
allow_origins=CORS_ALLOW_ORIGIN,
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
@@ -120,7 +115,7 @@ async def get_ollama_api_urls(user=Depends(get_admin_user)):
|
||||
|
||||
|
||||
class UrlUpdateForm(BaseModel):
|
||||
urls: List[str]
|
||||
urls: list[str]
|
||||
|
||||
|
||||
@app.post("/urls/update")
|
||||
@@ -153,13 +148,17 @@ async def cleanup_response(
|
||||
await session.close()
|
||||
|
||||
|
||||
async def post_streaming_url(url: str, payload: str, stream: bool = True):
|
||||
async def post_streaming_url(url: str, payload: Union[str, bytes], stream: bool = True):
|
||||
r = None
|
||||
try:
|
||||
session = aiohttp.ClientSession(
|
||||
trust_env=True, timeout=aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT)
|
||||
)
|
||||
r = await session.post(url, data=payload)
|
||||
r = await session.post(
|
||||
url,
|
||||
data=payload,
|
||||
headers={"Content-Type": "application/json"},
|
||||
)
|
||||
r.raise_for_status()
|
||||
|
||||
if stream:
|
||||
@@ -183,7 +182,7 @@ async def post_streaming_url(url: str, payload: str, stream: bool = True):
|
||||
res = await r.json()
|
||||
if "error" in res:
|
||||
error_detail = f"Ollama: {res['error']}"
|
||||
except:
|
||||
except Exception:
|
||||
error_detail = f"Ollama: {e}"
|
||||
|
||||
raise HTTPException(
|
||||
@@ -238,7 +237,7 @@ async def get_all_models():
|
||||
async def get_ollama_tags(
|
||||
url_idx: Optional[int] = None, user=Depends(get_verified_user)
|
||||
):
|
||||
if url_idx == None:
|
||||
if url_idx is None:
|
||||
models = await get_all_models()
|
||||
|
||||
if app.state.config.ENABLE_MODEL_FILTER:
|
||||
@@ -269,7 +268,7 @@ async def get_ollama_tags(
|
||||
res = r.json()
|
||||
if "error" in res:
|
||||
error_detail = f"Ollama: {res['error']}"
|
||||
except:
|
||||
except Exception:
|
||||
error_detail = f"Ollama: {e}"
|
||||
|
||||
raise HTTPException(
|
||||
@@ -282,8 +281,7 @@ async def get_ollama_tags(
|
||||
@app.get("/api/version/{url_idx}")
|
||||
async def get_ollama_versions(url_idx: Optional[int] = None):
|
||||
if app.state.config.ENABLE_OLLAMA_API:
|
||||
if url_idx == None:
|
||||
|
||||
if url_idx is None:
|
||||
# returns lowest version
|
||||
tasks = [
|
||||
fetch_url(f"{url}/api/version")
|
||||
@@ -323,7 +321,7 @@ async def get_ollama_versions(url_idx: Optional[int] = None):
|
||||
res = r.json()
|
||||
if "error" in res:
|
||||
error_detail = f"Ollama: {res['error']}"
|
||||
except:
|
||||
except Exception:
|
||||
error_detail = f"Ollama: {e}"
|
||||
|
||||
raise HTTPException(
|
||||
@@ -346,8 +344,6 @@ async def pull_model(
|
||||
url = app.state.config.OLLAMA_BASE_URLS[url_idx]
|
||||
log.info(f"url: {url}")
|
||||
|
||||
r = None
|
||||
|
||||
# Admin should be able to pull models from any source
|
||||
payload = {**form_data.model_dump(exclude_none=True), "insecure": True}
|
||||
|
||||
@@ -367,7 +363,7 @@ async def push_model(
|
||||
url_idx: Optional[int] = None,
|
||||
user=Depends(get_admin_user),
|
||||
):
|
||||
if url_idx == None:
|
||||
if url_idx is None:
|
||||
if form_data.name in app.state.MODELS:
|
||||
url_idx = app.state.MODELS[form_data.name]["urls"][0]
|
||||
else:
|
||||
@@ -417,7 +413,7 @@ async def copy_model(
|
||||
url_idx: Optional[int] = None,
|
||||
user=Depends(get_admin_user),
|
||||
):
|
||||
if url_idx == None:
|
||||
if url_idx is None:
|
||||
if form_data.source in app.state.MODELS:
|
||||
url_idx = app.state.MODELS[form_data.source]["urls"][0]
|
||||
else:
|
||||
@@ -428,13 +424,14 @@ async def copy_model(
|
||||
|
||||
url = app.state.config.OLLAMA_BASE_URLS[url_idx]
|
||||
log.info(f"url: {url}")
|
||||
r = requests.request(
|
||||
method="POST",
|
||||
url=f"{url}/api/copy",
|
||||
headers={"Content-Type": "application/json"},
|
||||
data=form_data.model_dump_json(exclude_none=True).encode(),
|
||||
)
|
||||
|
||||
try:
|
||||
r = requests.request(
|
||||
method="POST",
|
||||
url=f"{url}/api/copy",
|
||||
data=form_data.model_dump_json(exclude_none=True).encode(),
|
||||
)
|
||||
r.raise_for_status()
|
||||
|
||||
log.debug(f"r.text: {r.text}")
|
||||
@@ -448,7 +445,7 @@ async def copy_model(
|
||||
res = r.json()
|
||||
if "error" in res:
|
||||
error_detail = f"Ollama: {res['error']}"
|
||||
except:
|
||||
except Exception:
|
||||
error_detail = f"Ollama: {e}"
|
||||
|
||||
raise HTTPException(
|
||||
@@ -464,7 +461,7 @@ async def delete_model(
|
||||
url_idx: Optional[int] = None,
|
||||
user=Depends(get_admin_user),
|
||||
):
|
||||
if url_idx == None:
|
||||
if url_idx is None:
|
||||
if form_data.name in app.state.MODELS:
|
||||
url_idx = app.state.MODELS[form_data.name]["urls"][0]
|
||||
else:
|
||||
@@ -476,12 +473,13 @@ async def delete_model(
|
||||
url = app.state.config.OLLAMA_BASE_URLS[url_idx]
|
||||
log.info(f"url: {url}")
|
||||
|
||||
r = requests.request(
|
||||
method="DELETE",
|
||||
url=f"{url}/api/delete",
|
||||
headers={"Content-Type": "application/json"},
|
||||
data=form_data.model_dump_json(exclude_none=True).encode(),
|
||||
)
|
||||
try:
|
||||
r = requests.request(
|
||||
method="DELETE",
|
||||
url=f"{url}/api/delete",
|
||||
data=form_data.model_dump_json(exclude_none=True).encode(),
|
||||
)
|
||||
r.raise_for_status()
|
||||
|
||||
log.debug(f"r.text: {r.text}")
|
||||
@@ -495,7 +493,7 @@ async def delete_model(
|
||||
res = r.json()
|
||||
if "error" in res:
|
||||
error_detail = f"Ollama: {res['error']}"
|
||||
except:
|
||||
except Exception:
|
||||
error_detail = f"Ollama: {e}"
|
||||
|
||||
raise HTTPException(
|
||||
@@ -516,12 +514,13 @@ async def show_model_info(form_data: ModelNameForm, user=Depends(get_verified_us
|
||||
url = app.state.config.OLLAMA_BASE_URLS[url_idx]
|
||||
log.info(f"url: {url}")
|
||||
|
||||
r = requests.request(
|
||||
method="POST",
|
||||
url=f"{url}/api/show",
|
||||
headers={"Content-Type": "application/json"},
|
||||
data=form_data.model_dump_json(exclude_none=True).encode(),
|
||||
)
|
||||
try:
|
||||
r = requests.request(
|
||||
method="POST",
|
||||
url=f"{url}/api/show",
|
||||
data=form_data.model_dump_json(exclude_none=True).encode(),
|
||||
)
|
||||
r.raise_for_status()
|
||||
|
||||
return r.json()
|
||||
@@ -533,7 +532,7 @@ async def show_model_info(form_data: ModelNameForm, user=Depends(get_verified_us
|
||||
res = r.json()
|
||||
if "error" in res:
|
||||
error_detail = f"Ollama: {res['error']}"
|
||||
except:
|
||||
except Exception:
|
||||
error_detail = f"Ollama: {e}"
|
||||
|
||||
raise HTTPException(
|
||||
@@ -556,7 +555,7 @@ async def generate_embeddings(
|
||||
url_idx: Optional[int] = None,
|
||||
user=Depends(get_verified_user),
|
||||
):
|
||||
if url_idx == None:
|
||||
if url_idx is None:
|
||||
model = form_data.model
|
||||
|
||||
if ":" not in model:
|
||||
@@ -573,12 +572,13 @@ async def generate_embeddings(
|
||||
url = app.state.config.OLLAMA_BASE_URLS[url_idx]
|
||||
log.info(f"url: {url}")
|
||||
|
||||
r = requests.request(
|
||||
method="POST",
|
||||
url=f"{url}/api/embeddings",
|
||||
headers={"Content-Type": "application/json"},
|
||||
data=form_data.model_dump_json(exclude_none=True).encode(),
|
||||
)
|
||||
try:
|
||||
r = requests.request(
|
||||
method="POST",
|
||||
url=f"{url}/api/embeddings",
|
||||
data=form_data.model_dump_json(exclude_none=True).encode(),
|
||||
)
|
||||
r.raise_for_status()
|
||||
|
||||
return r.json()
|
||||
@@ -590,7 +590,7 @@ async def generate_embeddings(
|
||||
res = r.json()
|
||||
if "error" in res:
|
||||
error_detail = f"Ollama: {res['error']}"
|
||||
except:
|
||||
except Exception:
|
||||
error_detail = f"Ollama: {e}"
|
||||
|
||||
raise HTTPException(
|
||||
@@ -603,10 +603,9 @@ def generate_ollama_embeddings(
|
||||
form_data: GenerateEmbeddingsForm,
|
||||
url_idx: Optional[int] = None,
|
||||
):
|
||||
|
||||
log.info(f"generate_ollama_embeddings {form_data}")
|
||||
|
||||
if url_idx == None:
|
||||
if url_idx is None:
|
||||
model = form_data.model
|
||||
|
||||
if ":" not in model:
|
||||
@@ -623,12 +622,13 @@ def generate_ollama_embeddings(
|
||||
url = app.state.config.OLLAMA_BASE_URLS[url_idx]
|
||||
log.info(f"url: {url}")
|
||||
|
||||
r = requests.request(
|
||||
method="POST",
|
||||
url=f"{url}/api/embeddings",
|
||||
headers={"Content-Type": "application/json"},
|
||||
data=form_data.model_dump_json(exclude_none=True).encode(),
|
||||
)
|
||||
try:
|
||||
r = requests.request(
|
||||
method="POST",
|
||||
url=f"{url}/api/embeddings",
|
||||
data=form_data.model_dump_json(exclude_none=True).encode(),
|
||||
)
|
||||
r.raise_for_status()
|
||||
|
||||
data = r.json()
|
||||
@@ -638,7 +638,7 @@ def generate_ollama_embeddings(
|
||||
if "embedding" in data:
|
||||
return data["embedding"]
|
||||
else:
|
||||
raise "Something went wrong :/"
|
||||
raise Exception("Something went wrong :/")
|
||||
except Exception as e:
|
||||
log.exception(e)
|
||||
error_detail = "Open WebUI: Server Connection Error"
|
||||
@@ -647,16 +647,16 @@ def generate_ollama_embeddings(
|
||||
res = r.json()
|
||||
if "error" in res:
|
||||
error_detail = f"Ollama: {res['error']}"
|
||||
except:
|
||||
except Exception:
|
||||
error_detail = f"Ollama: {e}"
|
||||
|
||||
raise error_detail
|
||||
raise Exception(error_detail)
|
||||
|
||||
|
||||
class GenerateCompletionForm(BaseModel):
|
||||
model: str
|
||||
prompt: str
|
||||
images: Optional[List[str]] = None
|
||||
images: Optional[list[str]] = None
|
||||
format: Optional[str] = None
|
||||
options: Optional[dict] = None
|
||||
system: Optional[str] = None
|
||||
@@ -674,8 +674,7 @@ async def generate_completion(
|
||||
url_idx: Optional[int] = None,
|
||||
user=Depends(get_verified_user),
|
||||
):
|
||||
|
||||
if url_idx == None:
|
||||
if url_idx is None:
|
||||
model = form_data.model
|
||||
|
||||
if ":" not in model:
|
||||
@@ -700,12 +699,12 @@ async def generate_completion(
|
||||
class ChatMessage(BaseModel):
|
||||
role: str
|
||||
content: str
|
||||
images: Optional[List[str]] = None
|
||||
images: Optional[list[str]] = None
|
||||
|
||||
|
||||
class GenerateChatCompletionForm(BaseModel):
|
||||
model: str
|
||||
messages: List[ChatMessage]
|
||||
messages: list[ChatMessage]
|
||||
format: Optional[str] = None
|
||||
options: Optional[dict] = None
|
||||
template: Optional[str] = None
|
||||
@@ -713,6 +712,18 @@ class GenerateChatCompletionForm(BaseModel):
|
||||
keep_alive: Optional[Union[int, str]] = None
|
||||
|
||||
|
||||
def get_ollama_url(url_idx: Optional[int], model: str):
|
||||
if url_idx is None:
|
||||
if model not in app.state.MODELS:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=ERROR_MESSAGES.MODEL_NOT_FOUND(model),
|
||||
)
|
||||
url_idx = random.choice(app.state.MODELS[model]["urls"])
|
||||
url = app.state.config.OLLAMA_BASE_URLS[url_idx]
|
||||
return url
|
||||
|
||||
|
||||
@app.post("/api/chat")
|
||||
@app.post("/api/chat/{url_idx}")
|
||||
async def generate_chat_completion(
|
||||
@@ -720,16 +731,8 @@ async def generate_chat_completion(
|
||||
url_idx: Optional[int] = None,
|
||||
user=Depends(get_verified_user),
|
||||
):
|
||||
|
||||
log.debug(
|
||||
"form_data.model_dump_json(exclude_none=True).encode(): {0} ".format(
|
||||
form_data.model_dump_json(exclude_none=True).encode()
|
||||
)
|
||||
)
|
||||
|
||||
payload = {
|
||||
**form_data.model_dump(exclude_none=True, exclude=["metadata"]),
|
||||
}
|
||||
payload = {**form_data.model_dump(exclude_none=True)}
|
||||
log.debug(f"{payload = }")
|
||||
if "metadata" in payload:
|
||||
del payload["metadata"]
|
||||
|
||||
@@ -740,185 +743,21 @@ async def generate_chat_completion(
|
||||
if model_info.base_model_id:
|
||||
payload["model"] = model_info.base_model_id
|
||||
|
||||
model_info.params = model_info.params.model_dump()
|
||||
params = model_info.params.model_dump()
|
||||
|
||||
if model_info.params:
|
||||
if params:
|
||||
if payload.get("options") is None:
|
||||
payload["options"] = {}
|
||||
|
||||
if (
|
||||
model_info.params.get("mirostat", None)
|
||||
and payload["options"].get("mirostat") is None
|
||||
):
|
||||
payload["options"]["mirostat"] = model_info.params.get("mirostat", None)
|
||||
|
||||
if (
|
||||
model_info.params.get("mirostat_eta", None)
|
||||
and payload["options"].get("mirostat_eta") is None
|
||||
):
|
||||
payload["options"]["mirostat_eta"] = model_info.params.get(
|
||||
"mirostat_eta", None
|
||||
)
|
||||
|
||||
if (
|
||||
model_info.params.get("mirostat_tau", None)
|
||||
and payload["options"].get("mirostat_tau") is None
|
||||
):
|
||||
payload["options"]["mirostat_tau"] = model_info.params.get(
|
||||
"mirostat_tau", None
|
||||
)
|
||||
|
||||
if (
|
||||
model_info.params.get("num_ctx", None)
|
||||
and payload["options"].get("num_ctx") is None
|
||||
):
|
||||
payload["options"]["num_ctx"] = model_info.params.get("num_ctx", None)
|
||||
|
||||
if (
|
||||
model_info.params.get("num_batch", None)
|
||||
and payload["options"].get("num_batch") is None
|
||||
):
|
||||
payload["options"]["num_batch"] = model_info.params.get(
|
||||
"num_batch", None
|
||||
)
|
||||
|
||||
if (
|
||||
model_info.params.get("num_keep", None)
|
||||
and payload["options"].get("num_keep") is None
|
||||
):
|
||||
payload["options"]["num_keep"] = model_info.params.get("num_keep", None)
|
||||
|
||||
if (
|
||||
model_info.params.get("repeat_last_n", None)
|
||||
and payload["options"].get("repeat_last_n") is None
|
||||
):
|
||||
payload["options"]["repeat_last_n"] = model_info.params.get(
|
||||
"repeat_last_n", None
|
||||
)
|
||||
|
||||
if (
|
||||
model_info.params.get("frequency_penalty", None)
|
||||
and payload["options"].get("frequency_penalty") is None
|
||||
):
|
||||
payload["options"]["repeat_penalty"] = model_info.params.get(
|
||||
"frequency_penalty", None
|
||||
)
|
||||
|
||||
if (
|
||||
model_info.params.get("temperature", None) is not None
|
||||
and payload["options"].get("temperature") is None
|
||||
):
|
||||
payload["options"]["temperature"] = model_info.params.get(
|
||||
"temperature", None
|
||||
)
|
||||
|
||||
if (
|
||||
model_info.params.get("seed", None) is not None
|
||||
and payload["options"].get("seed") is None
|
||||
):
|
||||
payload["options"]["seed"] = model_info.params.get("seed", None)
|
||||
|
||||
if (
|
||||
model_info.params.get("stop", None)
|
||||
and payload["options"].get("stop") is None
|
||||
):
|
||||
payload["options"]["stop"] = (
|
||||
[
|
||||
bytes(stop, "utf-8").decode("unicode_escape")
|
||||
for stop in model_info.params["stop"]
|
||||
]
|
||||
if model_info.params.get("stop", None)
|
||||
else None
|
||||
)
|
||||
|
||||
if (
|
||||
model_info.params.get("tfs_z", None)
|
||||
and payload["options"].get("tfs_z") is None
|
||||
):
|
||||
payload["options"]["tfs_z"] = model_info.params.get("tfs_z", None)
|
||||
|
||||
if (
|
||||
model_info.params.get("max_tokens", None)
|
||||
and payload["options"].get("max_tokens") is None
|
||||
):
|
||||
payload["options"]["num_predict"] = model_info.params.get(
|
||||
"max_tokens", None
|
||||
)
|
||||
|
||||
if (
|
||||
model_info.params.get("top_k", None)
|
||||
and payload["options"].get("top_k") is None
|
||||
):
|
||||
payload["options"]["top_k"] = model_info.params.get("top_k", None)
|
||||
|
||||
if (
|
||||
model_info.params.get("top_p", None)
|
||||
and payload["options"].get("top_p") is None
|
||||
):
|
||||
payload["options"]["top_p"] = model_info.params.get("top_p", None)
|
||||
|
||||
if (
|
||||
model_info.params.get("min_p", None)
|
||||
and payload["options"].get("min_p") is None
|
||||
):
|
||||
payload["options"]["min_p"] = model_info.params.get("min_p", None)
|
||||
|
||||
if (
|
||||
model_info.params.get("use_mmap", None)
|
||||
and payload["options"].get("use_mmap") is None
|
||||
):
|
||||
payload["options"]["use_mmap"] = model_info.params.get("use_mmap", None)
|
||||
|
||||
if (
|
||||
model_info.params.get("use_mlock", None)
|
||||
and payload["options"].get("use_mlock") is None
|
||||
):
|
||||
payload["options"]["use_mlock"] = model_info.params.get(
|
||||
"use_mlock", None
|
||||
)
|
||||
|
||||
if (
|
||||
model_info.params.get("num_thread", None)
|
||||
and payload["options"].get("num_thread") is None
|
||||
):
|
||||
payload["options"]["num_thread"] = model_info.params.get(
|
||||
"num_thread", None
|
||||
)
|
||||
|
||||
system = model_info.params.get("system", None)
|
||||
if system:
|
||||
system = prompt_template(
|
||||
system,
|
||||
**(
|
||||
{
|
||||
"user_name": user.name,
|
||||
"user_location": (
|
||||
user.info.get("location") if user.info else None
|
||||
),
|
||||
}
|
||||
if user
|
||||
else {}
|
||||
),
|
||||
payload["options"] = apply_model_params_to_body_ollama(
|
||||
params, payload["options"]
|
||||
)
|
||||
payload = apply_model_system_prompt_to_body(params, payload, user)
|
||||
|
||||
if payload.get("messages"):
|
||||
payload["messages"] = add_or_update_system_message(
|
||||
system, payload["messages"]
|
||||
)
|
||||
if ":" not in payload["model"]:
|
||||
payload["model"] = f"{payload['model']}:latest"
|
||||
|
||||
if url_idx == None:
|
||||
if ":" not in payload["model"]:
|
||||
payload["model"] = f"{payload['model']}:latest"
|
||||
|
||||
if payload["model"] in app.state.MODELS:
|
||||
url_idx = random.choice(app.state.MODELS[payload["model"]]["urls"])
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=ERROR_MESSAGES.MODEL_NOT_FOUND(form_data.model),
|
||||
)
|
||||
|
||||
url = app.state.config.OLLAMA_BASE_URLS[url_idx]
|
||||
url = get_ollama_url(url_idx, payload["model"])
|
||||
log.info(f"url: {url}")
|
||||
log.debug(payload)
|
||||
|
||||
@@ -940,7 +779,7 @@ class OpenAIChatMessage(BaseModel):
|
||||
|
||||
class OpenAIChatCompletionForm(BaseModel):
|
||||
model: str
|
||||
messages: List[OpenAIChatMessage]
|
||||
messages: list[OpenAIChatMessage]
|
||||
|
||||
model_config = ConfigDict(extra="allow")
|
||||
|
||||
@@ -952,83 +791,28 @@ async def generate_openai_chat_completion(
|
||||
url_idx: Optional[int] = None,
|
||||
user=Depends(get_verified_user),
|
||||
):
|
||||
form_data = OpenAIChatCompletionForm(**form_data)
|
||||
payload = {**form_data.model_dump(exclude_none=True, exclude=["metadata"])}
|
||||
|
||||
completion_form = OpenAIChatCompletionForm(**form_data)
|
||||
payload = {**completion_form.model_dump(exclude_none=True, exclude=["metadata"])}
|
||||
if "metadata" in payload:
|
||||
del payload["metadata"]
|
||||
|
||||
model_id = form_data.model
|
||||
model_id = completion_form.model
|
||||
model_info = Models.get_model_by_id(model_id)
|
||||
|
||||
if model_info:
|
||||
if model_info.base_model_id:
|
||||
payload["model"] = model_info.base_model_id
|
||||
|
||||
model_info.params = model_info.params.model_dump()
|
||||
params = model_info.params.model_dump()
|
||||
|
||||
if model_info.params:
|
||||
payload["temperature"] = model_info.params.get("temperature", None)
|
||||
payload["top_p"] = model_info.params.get("top_p", None)
|
||||
payload["max_tokens"] = model_info.params.get("max_tokens", None)
|
||||
payload["frequency_penalty"] = model_info.params.get(
|
||||
"frequency_penalty", None
|
||||
)
|
||||
payload["seed"] = model_info.params.get("seed", None)
|
||||
payload["stop"] = (
|
||||
[
|
||||
bytes(stop, "utf-8").decode("unicode_escape")
|
||||
for stop in model_info.params["stop"]
|
||||
]
|
||||
if model_info.params.get("stop", None)
|
||||
else None
|
||||
)
|
||||
if params:
|
||||
payload = apply_model_params_to_body_openai(params, payload)
|
||||
payload = apply_model_system_prompt_to_body(params, payload, user)
|
||||
|
||||
system = model_info.params.get("system", None)
|
||||
if ":" not in payload["model"]:
|
||||
payload["model"] = f"{payload['model']}:latest"
|
||||
|
||||
if system:
|
||||
system = prompt_template(
|
||||
system,
|
||||
**(
|
||||
{
|
||||
"user_name": user.name,
|
||||
"user_location": (
|
||||
user.info.get("location") if user.info else None
|
||||
),
|
||||
}
|
||||
if user
|
||||
else {}
|
||||
),
|
||||
)
|
||||
# Check if the payload already has a system message
|
||||
# If not, add a system message to the payload
|
||||
if payload.get("messages"):
|
||||
for message in payload["messages"]:
|
||||
if message.get("role") == "system":
|
||||
message["content"] = system + message["content"]
|
||||
break
|
||||
else:
|
||||
payload["messages"].insert(
|
||||
0,
|
||||
{
|
||||
"role": "system",
|
||||
"content": system,
|
||||
},
|
||||
)
|
||||
|
||||
if url_idx == None:
|
||||
if ":" not in payload["model"]:
|
||||
payload["model"] = f"{payload['model']}:latest"
|
||||
|
||||
if payload["model"] in app.state.MODELS:
|
||||
url_idx = random.choice(app.state.MODELS[payload["model"]]["urls"])
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=ERROR_MESSAGES.MODEL_NOT_FOUND(form_data.model),
|
||||
)
|
||||
|
||||
url = app.state.config.OLLAMA_BASE_URLS[url_idx]
|
||||
url = get_ollama_url(url_idx, payload["model"])
|
||||
log.info(f"url: {url}")
|
||||
|
||||
return await post_streaming_url(
|
||||
@@ -1044,7 +828,7 @@ async def get_openai_models(
|
||||
url_idx: Optional[int] = None,
|
||||
user=Depends(get_verified_user),
|
||||
):
|
||||
if url_idx == None:
|
||||
if url_idx is None:
|
||||
models = await get_all_models()
|
||||
|
||||
if app.state.config.ENABLE_MODEL_FILTER:
|
||||
@@ -1099,7 +883,7 @@ async def get_openai_models(
|
||||
res = r.json()
|
||||
if "error" in res:
|
||||
error_detail = f"Ollama: {res['error']}"
|
||||
except:
|
||||
except Exception:
|
||||
error_detail = f"Ollama: {e}"
|
||||
|
||||
raise HTTPException(
|
||||
@@ -1125,7 +909,6 @@ def parse_huggingface_url(hf_url):
|
||||
path_components = parsed_url.path.split("/")
|
||||
|
||||
# Extract the desired output
|
||||
user_repo = "/".join(path_components[1:3])
|
||||
model_file = path_components[-1]
|
||||
|
||||
return model_file
|
||||
@@ -1190,7 +973,6 @@ async def download_model(
|
||||
url_idx: Optional[int] = None,
|
||||
user=Depends(get_admin_user),
|
||||
):
|
||||
|
||||
allowed_hosts = ["https://huggingface.co/", "https://github.com/"]
|
||||
|
||||
if not any(form_data.url.startswith(host) for host in allowed_hosts):
|
||||
@@ -1199,7 +981,7 @@ async def download_model(
|
||||
detail="Invalid file_url. Only URLs from allowed hosts are permitted.",
|
||||
)
|
||||
|
||||
if url_idx == None:
|
||||
if url_idx is None:
|
||||
url_idx = 0
|
||||
url = app.state.config.OLLAMA_BASE_URLS[url_idx]
|
||||
|
||||
@@ -1222,7 +1004,7 @@ def upload_model(
|
||||
url_idx: Optional[int] = None,
|
||||
user=Depends(get_admin_user),
|
||||
):
|
||||
if url_idx == None:
|
||||
if url_idx is None:
|
||||
url_idx = 0
|
||||
ollama_url = app.state.config.OLLAMA_BASE_URLS[url_idx]
|
||||
|
||||
|
||||
+77
-142
@@ -1,6 +1,6 @@
|
||||
from fastapi import FastAPI, Request, Response, HTTPException, Depends
|
||||
from fastapi import FastAPI, Request, HTTPException, Depends
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.responses import StreamingResponse, JSONResponse, FileResponse
|
||||
from fastapi.responses import StreamingResponse, FileResponse
|
||||
|
||||
import requests
|
||||
import aiohttp
|
||||
@@ -12,16 +12,15 @@ from pydantic import BaseModel
|
||||
from starlette.background import BackgroundTask
|
||||
|
||||
from apps.webui.models.models import Models
|
||||
from apps.webui.models.users import Users
|
||||
from constants import ERROR_MESSAGES
|
||||
from utils.utils import (
|
||||
decode_token,
|
||||
get_verified_user,
|
||||
get_verified_user,
|
||||
get_admin_user,
|
||||
)
|
||||
from utils.task import prompt_template
|
||||
from utils.misc import add_or_update_system_message
|
||||
from utils.misc import (
|
||||
apply_model_params_to_body_openai,
|
||||
apply_model_system_prompt_to_body,
|
||||
)
|
||||
|
||||
from config import (
|
||||
SRC_LOG_LEVELS,
|
||||
@@ -33,8 +32,9 @@ from config import (
|
||||
ENABLE_MODEL_FILTER,
|
||||
MODEL_FILTER_LIST,
|
||||
AppConfig,
|
||||
CORS_ALLOW_ORIGIN,
|
||||
)
|
||||
from typing import List, Optional
|
||||
from typing import Optional, Literal, overload
|
||||
|
||||
|
||||
import hashlib
|
||||
@@ -46,7 +46,7 @@ log.setLevel(SRC_LOG_LEVELS["OPENAI"])
|
||||
app = FastAPI()
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["*"],
|
||||
allow_origins=CORS_ALLOW_ORIGIN,
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
@@ -69,8 +69,6 @@ app.state.MODELS = {}
|
||||
async def check_url(request: Request, call_next):
|
||||
if len(app.state.MODELS) == 0:
|
||||
await get_all_models()
|
||||
else:
|
||||
pass
|
||||
|
||||
response = await call_next(request)
|
||||
return response
|
||||
@@ -92,11 +90,11 @@ async def update_config(form_data: OpenAIConfigForm, user=Depends(get_admin_user
|
||||
|
||||
|
||||
class UrlsUpdateForm(BaseModel):
|
||||
urls: List[str]
|
||||
urls: list[str]
|
||||
|
||||
|
||||
class KeysUpdateForm(BaseModel):
|
||||
keys: List[str]
|
||||
keys: list[str]
|
||||
|
||||
|
||||
@app.get("/urls")
|
||||
@@ -175,7 +173,7 @@ async def speech(request: Request, user=Depends(get_verified_user)):
|
||||
res = r.json()
|
||||
if "error" in res:
|
||||
error_detail = f"External: {res['error']}"
|
||||
except:
|
||||
except Exception:
|
||||
error_detail = f"External: {e}"
|
||||
|
||||
raise HTTPException(
|
||||
@@ -234,64 +232,68 @@ def merge_models_lists(model_lists):
|
||||
return merged_list
|
||||
|
||||
|
||||
async def get_all_models(raw: bool = False):
|
||||
def is_openai_api_disabled():
|
||||
api_keys = app.state.config.OPENAI_API_KEYS
|
||||
no_keys = len(api_keys) == 1 and api_keys[0] == ""
|
||||
return no_keys or not app.state.config.ENABLE_OPENAI_API
|
||||
|
||||
|
||||
async def get_all_models_raw() -> list:
|
||||
if is_openai_api_disabled():
|
||||
return []
|
||||
|
||||
# Check if API KEYS length is same than API URLS length
|
||||
num_urls = len(app.state.config.OPENAI_API_BASE_URLS)
|
||||
num_keys = len(app.state.config.OPENAI_API_KEYS)
|
||||
|
||||
if num_keys != num_urls:
|
||||
# if there are more keys than urls, remove the extra keys
|
||||
if num_keys > num_urls:
|
||||
new_keys = app.state.config.OPENAI_API_KEYS[:num_urls]
|
||||
app.state.config.OPENAI_API_KEYS = new_keys
|
||||
# if there are more urls than keys, add empty keys
|
||||
else:
|
||||
app.state.config.OPENAI_API_KEYS += [""] * (num_urls - num_keys)
|
||||
|
||||
tasks = [
|
||||
fetch_url(f"{url}/models", app.state.config.OPENAI_API_KEYS[idx])
|
||||
for idx, url in enumerate(app.state.config.OPENAI_API_BASE_URLS)
|
||||
]
|
||||
|
||||
responses = await asyncio.gather(*tasks)
|
||||
log.debug(f"get_all_models:responses() {responses}")
|
||||
|
||||
return responses
|
||||
|
||||
|
||||
@overload
|
||||
async def get_all_models(raw: Literal[True]) -> list: ...
|
||||
|
||||
|
||||
@overload
|
||||
async def get_all_models(raw: Literal[False] = False) -> dict[str, list]: ...
|
||||
|
||||
|
||||
async def get_all_models(raw=False) -> dict[str, list] | list:
|
||||
log.info("get_all_models()")
|
||||
if is_openai_api_disabled():
|
||||
return [] if raw else {"data": []}
|
||||
|
||||
if (
|
||||
len(app.state.config.OPENAI_API_KEYS) == 1
|
||||
and app.state.config.OPENAI_API_KEYS[0] == ""
|
||||
) or not app.state.config.ENABLE_OPENAI_API:
|
||||
models = {"data": []}
|
||||
else:
|
||||
# Check if API KEYS length is same than API URLS length
|
||||
if len(app.state.config.OPENAI_API_KEYS) != len(
|
||||
app.state.config.OPENAI_API_BASE_URLS
|
||||
):
|
||||
# if there are more keys than urls, remove the extra keys
|
||||
if len(app.state.config.OPENAI_API_KEYS) > len(
|
||||
app.state.config.OPENAI_API_BASE_URLS
|
||||
):
|
||||
app.state.config.OPENAI_API_KEYS = app.state.config.OPENAI_API_KEYS[
|
||||
: len(app.state.config.OPENAI_API_BASE_URLS)
|
||||
]
|
||||
# if there are more urls than keys, add empty keys
|
||||
else:
|
||||
app.state.config.OPENAI_API_KEYS += [
|
||||
""
|
||||
for _ in range(
|
||||
len(app.state.config.OPENAI_API_BASE_URLS)
|
||||
- len(app.state.config.OPENAI_API_KEYS)
|
||||
)
|
||||
]
|
||||
responses = await get_all_models_raw()
|
||||
if raw:
|
||||
return responses
|
||||
|
||||
tasks = [
|
||||
fetch_url(f"{url}/models", app.state.config.OPENAI_API_KEYS[idx])
|
||||
for idx, url in enumerate(app.state.config.OPENAI_API_BASE_URLS)
|
||||
]
|
||||
def extract_data(response):
|
||||
if response and "data" in response:
|
||||
return response["data"]
|
||||
if isinstance(response, list):
|
||||
return response
|
||||
return None
|
||||
|
||||
responses = await asyncio.gather(*tasks)
|
||||
log.debug(f"get_all_models:responses() {responses}")
|
||||
models = {"data": merge_models_lists(map(extract_data, responses))}
|
||||
|
||||
if raw:
|
||||
return responses
|
||||
|
||||
models = {
|
||||
"data": merge_models_lists(
|
||||
list(
|
||||
map(
|
||||
lambda response: (
|
||||
response["data"]
|
||||
if (response and "data" in response)
|
||||
else (response if isinstance(response, list) else None)
|
||||
),
|
||||
responses,
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
log.debug(f"models: {models}")
|
||||
app.state.MODELS = {model["id"]: model for model in models["data"]}
|
||||
log.debug(f"models: {models}")
|
||||
app.state.MODELS = {model["id"]: model for model in models["data"]}
|
||||
|
||||
return models
|
||||
|
||||
@@ -299,7 +301,7 @@ async def get_all_models(raw: bool = False):
|
||||
@app.get("/models")
|
||||
@app.get("/models/{url_idx}")
|
||||
async def get_models(url_idx: Optional[int] = None, user=Depends(get_verified_user)):
|
||||
if url_idx == None:
|
||||
if url_idx is None:
|
||||
models = await get_all_models()
|
||||
if app.state.config.ENABLE_MODEL_FILTER:
|
||||
if user.role == "user":
|
||||
@@ -340,7 +342,7 @@ async def get_models(url_idx: Optional[int] = None, user=Depends(get_verified_us
|
||||
res = r.json()
|
||||
if "error" in res:
|
||||
error_detail = f"External: {res['error']}"
|
||||
except:
|
||||
except Exception:
|
||||
error_detail = f"External: {e}"
|
||||
|
||||
raise HTTPException(
|
||||
@@ -358,6 +360,7 @@ async def generate_chat_completion(
|
||||
):
|
||||
idx = 0
|
||||
payload = {**form_data}
|
||||
|
||||
if "metadata" in payload:
|
||||
del payload["metadata"]
|
||||
|
||||
@@ -368,70 +371,9 @@ async def generate_chat_completion(
|
||||
if model_info.base_model_id:
|
||||
payload["model"] = model_info.base_model_id
|
||||
|
||||
model_info.params = model_info.params.model_dump()
|
||||
|
||||
if model_info.params:
|
||||
if (
|
||||
model_info.params.get("temperature", None) is not None
|
||||
and payload.get("temperature") is None
|
||||
):
|
||||
payload["temperature"] = float(model_info.params.get("temperature"))
|
||||
|
||||
if model_info.params.get("top_p", None) and payload.get("top_p") is None:
|
||||
payload["top_p"] = int(model_info.params.get("top_p", None))
|
||||
|
||||
if (
|
||||
model_info.params.get("max_tokens", None)
|
||||
and payload.get("max_tokens") is None
|
||||
):
|
||||
payload["max_tokens"] = int(model_info.params.get("max_tokens", None))
|
||||
|
||||
if (
|
||||
model_info.params.get("frequency_penalty", None)
|
||||
and payload.get("frequency_penalty") is None
|
||||
):
|
||||
payload["frequency_penalty"] = int(
|
||||
model_info.params.get("frequency_penalty", None)
|
||||
)
|
||||
|
||||
if (
|
||||
model_info.params.get("seed", None) is not None
|
||||
and payload.get("seed") is None
|
||||
):
|
||||
payload["seed"] = model_info.params.get("seed", None)
|
||||
|
||||
if model_info.params.get("stop", None) and payload.get("stop") is None:
|
||||
payload["stop"] = (
|
||||
[
|
||||
bytes(stop, "utf-8").decode("unicode_escape")
|
||||
for stop in model_info.params["stop"]
|
||||
]
|
||||
if model_info.params.get("stop", None)
|
||||
else None
|
||||
)
|
||||
|
||||
system = model_info.params.get("system", None)
|
||||
if system:
|
||||
system = prompt_template(
|
||||
system,
|
||||
**(
|
||||
{
|
||||
"user_name": user.name,
|
||||
"user_location": (
|
||||
user.info.get("location") if user.info else None
|
||||
),
|
||||
}
|
||||
if user
|
||||
else {}
|
||||
),
|
||||
)
|
||||
if payload.get("messages"):
|
||||
payload["messages"] = add_or_update_system_message(
|
||||
system, payload["messages"]
|
||||
)
|
||||
|
||||
else:
|
||||
pass
|
||||
params = model_info.params.model_dump()
|
||||
payload = apply_model_params_to_body_openai(params, payload)
|
||||
payload = apply_model_system_prompt_to_body(params, payload, user)
|
||||
|
||||
model = app.state.MODELS[payload.get("model")]
|
||||
idx = model["urlIdx"]
|
||||
@@ -444,13 +386,6 @@ async def generate_chat_completion(
|
||||
"role": user.role,
|
||||
}
|
||||
|
||||
# Check if the model is "gpt-4-vision-preview" and set "max_tokens" to 4000
|
||||
# This is a workaround until OpenAI fixes the issue with this model
|
||||
if payload.get("model") == "gpt-4-vision-preview":
|
||||
if "max_tokens" not in payload:
|
||||
payload["max_tokens"] = 4000
|
||||
log.debug("Modified payload:", payload)
|
||||
|
||||
# Convert the modified body back to JSON
|
||||
payload = json.dumps(payload)
|
||||
|
||||
@@ -506,7 +441,7 @@ async def generate_chat_completion(
|
||||
print(res)
|
||||
if "error" in res:
|
||||
error_detail = f"External: {res['error']['message'] if 'message' in res['error'] else res['error']}"
|
||||
except:
|
||||
except Exception:
|
||||
error_detail = f"External: {e}"
|
||||
raise HTTPException(status_code=r.status if r else 500, detail=error_detail)
|
||||
finally:
|
||||
@@ -569,7 +504,7 @@ async def proxy(path: str, request: Request, user=Depends(get_verified_user)):
|
||||
print(res)
|
||||
if "error" in res:
|
||||
error_detail = f"External: {res['error']['message'] if 'message' in res['error'] else res['error']}"
|
||||
except:
|
||||
except Exception:
|
||||
error_detail = f"External: {e}"
|
||||
raise HTTPException(status_code=r.status if r else 500, detail=error_detail)
|
||||
finally:
|
||||
|
||||
+11
-13
@@ -13,7 +13,7 @@ import os, shutil, logging, re
|
||||
from datetime import datetime
|
||||
|
||||
from pathlib import Path
|
||||
from typing import List, Union, Sequence, Iterator, Any
|
||||
from typing import Union, Sequence, Iterator, Any
|
||||
|
||||
from chromadb.utils.batch_utils import create_batches
|
||||
from langchain_core.documents import Document
|
||||
@@ -129,6 +129,7 @@ from config import (
|
||||
RAG_WEB_SEARCH_RESULT_COUNT,
|
||||
RAG_WEB_SEARCH_CONCURRENT_REQUESTS,
|
||||
RAG_EMBEDDING_OPENAI_BATCH_SIZE,
|
||||
CORS_ALLOW_ORIGIN,
|
||||
)
|
||||
|
||||
from constants import ERROR_MESSAGES
|
||||
@@ -240,12 +241,9 @@ app.state.EMBEDDING_FUNCTION = get_embedding_function(
|
||||
app.state.config.RAG_EMBEDDING_OPENAI_BATCH_SIZE,
|
||||
)
|
||||
|
||||
origins = ["*"]
|
||||
|
||||
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=origins,
|
||||
allow_origins=CORS_ALLOW_ORIGIN,
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
@@ -376,7 +374,7 @@ async def update_reranking_config(
|
||||
try:
|
||||
app.state.config.RAG_RERANKING_MODEL = form_data.reranking_model
|
||||
|
||||
update_reranking_model(app.state.config.RAG_RERANKING_MODEL), True
|
||||
update_reranking_model(app.state.config.RAG_RERANKING_MODEL, True)
|
||||
|
||||
return {
|
||||
"status": True,
|
||||
@@ -439,7 +437,7 @@ class ChunkParamUpdateForm(BaseModel):
|
||||
|
||||
|
||||
class YoutubeLoaderConfig(BaseModel):
|
||||
language: List[str]
|
||||
language: list[str]
|
||||
translation: Optional[str] = None
|
||||
|
||||
|
||||
@@ -642,7 +640,7 @@ def query_doc_handler(
|
||||
|
||||
|
||||
class QueryCollectionsForm(BaseModel):
|
||||
collection_names: List[str]
|
||||
collection_names: list[str]
|
||||
query: str
|
||||
k: Optional[int] = None
|
||||
r: Optional[float] = None
|
||||
@@ -1021,7 +1019,7 @@ class TikaLoader:
|
||||
self.file_path = file_path
|
||||
self.mime_type = mime_type
|
||||
|
||||
def load(self) -> List[Document]:
|
||||
def load(self) -> list[Document]:
|
||||
with open(self.file_path, "rb") as f:
|
||||
data = f.read()
|
||||
|
||||
@@ -1185,7 +1183,7 @@ def store_doc(
|
||||
f.close()
|
||||
|
||||
f = open(file_path, "rb")
|
||||
if collection_name == None:
|
||||
if collection_name is None:
|
||||
collection_name = calculate_sha256(f)[:63]
|
||||
f.close()
|
||||
|
||||
@@ -1238,7 +1236,7 @@ def process_doc(
|
||||
f = open(file_path, "rb")
|
||||
|
||||
collection_name = form_data.collection_name
|
||||
if collection_name == None:
|
||||
if collection_name is None:
|
||||
collection_name = calculate_sha256(f)[:63]
|
||||
f.close()
|
||||
|
||||
@@ -1296,7 +1294,7 @@ def store_text(
|
||||
):
|
||||
|
||||
collection_name = form_data.collection_name
|
||||
if collection_name == None:
|
||||
if collection_name is None:
|
||||
collection_name = calculate_sha256_string(form_data.content)
|
||||
|
||||
result = store_text_in_vector_db(
|
||||
@@ -1339,7 +1337,7 @@ def scan_docs_dir(user=Depends(get_admin_user)):
|
||||
sanitized_filename = sanitize_filename(filename)
|
||||
doc = Documents.get_doc_by_name(sanitized_filename)
|
||||
|
||||
if doc == None:
|
||||
if doc is None:
|
||||
doc = Documents.insert_new_doc(
|
||||
user.id,
|
||||
DocumentForm(
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import logging
|
||||
from typing import List, Optional
|
||||
from typing import Optional
|
||||
import requests
|
||||
|
||||
from apps.rag.search.main import SearchResult, get_filtered_results
|
||||
@@ -10,7 +10,7 @@ log.setLevel(SRC_LOG_LEVELS["RAG"])
|
||||
|
||||
|
||||
def search_brave(
|
||||
api_key: str, query: str, count: int, filter_list: Optional[List[str]] = None
|
||||
api_key: str, query: str, count: int, filter_list: Optional[list[str]] = None
|
||||
) -> list[SearchResult]:
|
||||
"""Search using Brave's Search API and return the results as a list of SearchResult objects.
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import logging
|
||||
from typing import List, Optional
|
||||
from typing import Optional
|
||||
from apps.rag.search.main import SearchResult, get_filtered_results
|
||||
from duckduckgo_search import DDGS
|
||||
from config import SRC_LOG_LEVELS
|
||||
@@ -9,7 +9,7 @@ log.setLevel(SRC_LOG_LEVELS["RAG"])
|
||||
|
||||
|
||||
def search_duckduckgo(
|
||||
query: str, count: int, filter_list: Optional[List[str]] = None
|
||||
query: str, count: int, filter_list: Optional[list[str]] = None
|
||||
) -> list[SearchResult]:
|
||||
"""
|
||||
Search using DuckDuckGo's Search API and return the results as a list of SearchResult objects.
|
||||
@@ -18,7 +18,7 @@ def search_duckduckgo(
|
||||
count (int): The number of results to return
|
||||
|
||||
Returns:
|
||||
List[SearchResult]: A list of search results
|
||||
list[SearchResult]: A list of search results
|
||||
"""
|
||||
# Use the DDGS context manager to create a DDGS object
|
||||
with DDGS() as ddgs:
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import json
|
||||
import logging
|
||||
from typing import List, Optional
|
||||
from typing import Optional
|
||||
import requests
|
||||
|
||||
from apps.rag.search.main import SearchResult, get_filtered_results
|
||||
@@ -15,7 +15,7 @@ def search_google_pse(
|
||||
search_engine_id: str,
|
||||
query: str,
|
||||
count: int,
|
||||
filter_list: Optional[List[str]] = None,
|
||||
filter_list: Optional[list[str]] = None,
|
||||
) -> list[SearchResult]:
|
||||
"""Search using Google's Programmable Search Engine API and return the results as a list of SearchResult objects.
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ def search_jina(query: str, count: int) -> list[SearchResult]:
|
||||
count (int): The number of results to return
|
||||
|
||||
Returns:
|
||||
List[SearchResult]: A list of search results
|
||||
list[SearchResult]: A list of search results
|
||||
"""
|
||||
jina_search_endpoint = "https://s.jina.ai/"
|
||||
headers = {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import logging
|
||||
import requests
|
||||
|
||||
from typing import List, Optional
|
||||
from typing import Optional
|
||||
|
||||
from apps.rag.search.main import SearchResult, get_filtered_results
|
||||
from config import SRC_LOG_LEVELS
|
||||
@@ -14,9 +14,9 @@ def search_searxng(
|
||||
query_url: str,
|
||||
query: str,
|
||||
count: int,
|
||||
filter_list: Optional[List[str]] = None,
|
||||
filter_list: Optional[list[str]] = None,
|
||||
**kwargs,
|
||||
) -> List[SearchResult]:
|
||||
) -> list[SearchResult]:
|
||||
"""
|
||||
Search a SearXNG instance for a given query and return the results as a list of SearchResult objects.
|
||||
|
||||
@@ -31,10 +31,10 @@ def search_searxng(
|
||||
language (str): Language filter for the search results; e.g., "en-US". Defaults to an empty string.
|
||||
safesearch (int): Safe search filter for safer web results; 0 = off, 1 = moderate, 2 = strict. Defaults to 1 (moderate).
|
||||
time_range (str): Time range for filtering results by date; e.g., "2023-04-05..today" or "all-time". Defaults to ''.
|
||||
categories: (Optional[List[str]]): Specific categories within which the search should be performed, defaulting to an empty string if not provided.
|
||||
categories: (Optional[list[str]]): Specific categories within which the search should be performed, defaulting to an empty string if not provided.
|
||||
|
||||
Returns:
|
||||
List[SearchResult]: A list of SearchResults sorted by relevance score in descending order.
|
||||
list[SearchResult]: A list of SearchResults sorted by relevance score in descending order.
|
||||
|
||||
Raise:
|
||||
requests.exceptions.RequestException: If a request error occurs during the search process.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import json
|
||||
import logging
|
||||
from typing import List, Optional
|
||||
from typing import Optional
|
||||
import requests
|
||||
|
||||
from apps.rag.search.main import SearchResult, get_filtered_results
|
||||
@@ -11,7 +11,7 @@ log.setLevel(SRC_LOG_LEVELS["RAG"])
|
||||
|
||||
|
||||
def search_serper(
|
||||
api_key: str, query: str, count: int, filter_list: Optional[List[str]] = None
|
||||
api_key: str, query: str, count: int, filter_list: Optional[list[str]] = None
|
||||
) -> list[SearchResult]:
|
||||
"""Search using serper.dev's API and return the results as a list of SearchResult objects.
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import json
|
||||
import logging
|
||||
from typing import List, Optional
|
||||
from typing import Optional
|
||||
import requests
|
||||
from urllib.parse import urlencode
|
||||
|
||||
@@ -19,7 +19,7 @@ def search_serply(
|
||||
limit: int = 10,
|
||||
device_type: str = "desktop",
|
||||
proxy_location: str = "US",
|
||||
filter_list: Optional[List[str]] = None,
|
||||
filter_list: Optional[list[str]] = None,
|
||||
) -> list[SearchResult]:
|
||||
"""Search using serper.dev's API and return the results as a list of SearchResult objects.
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import json
|
||||
import logging
|
||||
from typing import List, Optional
|
||||
from typing import Optional
|
||||
import requests
|
||||
|
||||
from apps.rag.search.main import SearchResult, get_filtered_results
|
||||
@@ -14,7 +14,7 @@ def search_serpstack(
|
||||
api_key: str,
|
||||
query: str,
|
||||
count: int,
|
||||
filter_list: Optional[List[str]] = None,
|
||||
filter_list: Optional[list[str]] = None,
|
||||
https_enabled: bool = True,
|
||||
) -> list[SearchResult]:
|
||||
"""Search using serpstack.com's and return the results as a list of SearchResult objects.
|
||||
|
||||
@@ -17,7 +17,7 @@ def search_tavily(api_key: str, query: str, count: int) -> list[SearchResult]:
|
||||
query (str): The query to search for
|
||||
|
||||
Returns:
|
||||
List[SearchResult]: A list of search results
|
||||
list[SearchResult]: A list of search results
|
||||
"""
|
||||
url = "https://api.tavily.com/search"
|
||||
data = {"query": query, "api_key": api_key}
|
||||
|
||||
@@ -2,7 +2,7 @@ import os
|
||||
import logging
|
||||
import requests
|
||||
|
||||
from typing import List, Union
|
||||
from typing import Union
|
||||
|
||||
from apps.ollama.main import (
|
||||
generate_ollama_embeddings,
|
||||
@@ -142,7 +142,7 @@ def merge_and_sort_query_results(query_results, k, reverse=False):
|
||||
|
||||
|
||||
def query_collection(
|
||||
collection_names: List[str],
|
||||
collection_names: list[str],
|
||||
query: str,
|
||||
embedding_function,
|
||||
k: int,
|
||||
@@ -157,13 +157,13 @@ def query_collection(
|
||||
embedding_function=embedding_function,
|
||||
)
|
||||
results.append(result)
|
||||
except:
|
||||
except Exception:
|
||||
pass
|
||||
return merge_and_sort_query_results(results, k=k)
|
||||
|
||||
|
||||
def query_collection_with_hybrid_search(
|
||||
collection_names: List[str],
|
||||
collection_names: list[str],
|
||||
query: str,
|
||||
embedding_function,
|
||||
k: int,
|
||||
@@ -182,7 +182,7 @@ def query_collection_with_hybrid_search(
|
||||
r=r,
|
||||
)
|
||||
results.append(result)
|
||||
except:
|
||||
except Exception:
|
||||
pass
|
||||
return merge_and_sort_query_results(results, k=k, reverse=True)
|
||||
|
||||
@@ -411,7 +411,7 @@ class ChromaRetriever(BaseRetriever):
|
||||
query: str,
|
||||
*,
|
||||
run_manager: CallbackManagerForRetrieverRun,
|
||||
) -> List[Document]:
|
||||
) -> list[Document]:
|
||||
query_embeddings = self.embedding_function(query)
|
||||
|
||||
results = self.collection.query(
|
||||
|
||||
+15
-12
@@ -44,23 +44,26 @@ async def user_join(sid, data):
|
||||
print("user-join", sid, data)
|
||||
|
||||
auth = data["auth"] if "auth" in data else None
|
||||
if not auth or "token" not in auth:
|
||||
return
|
||||
|
||||
if auth and "token" in auth:
|
||||
data = decode_token(auth["token"])
|
||||
data = decode_token(auth["token"])
|
||||
if data is None or "id" not in data:
|
||||
return
|
||||
|
||||
if data is not None and "id" in data:
|
||||
user = Users.get_user_by_id(data["id"])
|
||||
user = Users.get_user_by_id(data["id"])
|
||||
if not user:
|
||||
return
|
||||
|
||||
if user:
|
||||
SESSION_POOL[sid] = user.id
|
||||
if user.id in USER_POOL:
|
||||
USER_POOL[user.id].append(sid)
|
||||
else:
|
||||
USER_POOL[user.id] = [sid]
|
||||
SESSION_POOL[sid] = user.id
|
||||
if user.id in USER_POOL:
|
||||
USER_POOL[user.id].append(sid)
|
||||
else:
|
||||
USER_POOL[user.id] = [sid]
|
||||
|
||||
print(f"user {user.name}({user.id}) connected with session ID {sid}")
|
||||
print(f"user {user.name}({user.id}) connected with session ID {sid}")
|
||||
|
||||
await sio.emit("user-count", {"count": len(set(USER_POOL))})
|
||||
await sio.emit("user-count", {"count": len(set(USER_POOL))})
|
||||
|
||||
|
||||
@sio.on("user-count")
|
||||
|
||||
+28
-58
@@ -22,10 +22,11 @@ from apps.webui.utils import load_function_module_by_id
|
||||
from utils.misc import (
|
||||
openai_chat_chunk_message_template,
|
||||
openai_chat_completion_message_template,
|
||||
add_or_update_system_message,
|
||||
apply_model_params_to_body_openai,
|
||||
apply_model_system_prompt_to_body,
|
||||
)
|
||||
from utils.task import prompt_template
|
||||
|
||||
from utils.tools import get_tools
|
||||
|
||||
from config import (
|
||||
SHOW_ADMIN_DETAILS,
|
||||
@@ -43,9 +44,12 @@ from config import (
|
||||
JWT_EXPIRES_IN,
|
||||
WEBUI_BANNERS,
|
||||
ENABLE_COMMUNITY_SHARING,
|
||||
ENABLE_MESSAGE_RATING,
|
||||
AppConfig,
|
||||
OAUTH_USERNAME_CLAIM,
|
||||
OAUTH_PICTURE_CLAIM,
|
||||
OAUTH_EMAIL_CLAIM,
|
||||
CORS_ALLOW_ORIGIN,
|
||||
)
|
||||
|
||||
from apps.socket.main import get_event_call, get_event_emitter
|
||||
@@ -58,8 +62,6 @@ from pydantic import BaseModel
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
origins = ["*"]
|
||||
|
||||
app.state.config = AppConfig()
|
||||
|
||||
app.state.config.ENABLE_SIGNUP = ENABLE_SIGNUP
|
||||
@@ -81,9 +83,11 @@ app.state.config.WEBHOOK_URL = WEBHOOK_URL
|
||||
app.state.config.BANNERS = WEBUI_BANNERS
|
||||
|
||||
app.state.config.ENABLE_COMMUNITY_SHARING = ENABLE_COMMUNITY_SHARING
|
||||
app.state.config.ENABLE_MESSAGE_RATING = ENABLE_MESSAGE_RATING
|
||||
|
||||
app.state.config.OAUTH_USERNAME_CLAIM = OAUTH_USERNAME_CLAIM
|
||||
app.state.config.OAUTH_PICTURE_CLAIM = OAUTH_PICTURE_CLAIM
|
||||
app.state.config.OAUTH_EMAIL_CLAIM = OAUTH_EMAIL_CLAIM
|
||||
|
||||
app.state.MODELS = {}
|
||||
app.state.TOOLS = {}
|
||||
@@ -91,7 +95,7 @@ app.state.FUNCTIONS = {}
|
||||
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=origins,
|
||||
allow_origins=CORS_ALLOW_ORIGIN,
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
@@ -269,51 +273,12 @@ def get_function_params(function_module, form_data, user, extra_params={}):
|
||||
return params
|
||||
|
||||
|
||||
# inplace function: form_data is modified
|
||||
def apply_model_params_to_body(params: dict, form_data: dict) -> dict:
|
||||
if not params:
|
||||
return form_data
|
||||
|
||||
mappings = {
|
||||
"temperature": float,
|
||||
"top_p": int,
|
||||
"max_tokens": int,
|
||||
"frequency_penalty": int,
|
||||
"seed": lambda x: x,
|
||||
"stop": lambda x: [bytes(s, "utf-8").decode("unicode_escape") for s in x],
|
||||
}
|
||||
|
||||
for key, cast_func in mappings.items():
|
||||
if (value := params.get(key)) is not None:
|
||||
form_data[key] = cast_func(value)
|
||||
|
||||
return form_data
|
||||
|
||||
|
||||
# inplace function: form_data is modified
|
||||
def apply_model_system_prompt_to_body(params: dict, form_data: dict, user) -> dict:
|
||||
system = params.get("system", None)
|
||||
if not system:
|
||||
return form_data
|
||||
|
||||
if user:
|
||||
template_params = {
|
||||
"user_name": user.name,
|
||||
"user_location": user.info.get("location") if user.info else None,
|
||||
}
|
||||
else:
|
||||
template_params = {}
|
||||
system = prompt_template(system, **template_params)
|
||||
form_data["messages"] = add_or_update_system_message(
|
||||
system, form_data.get("messages", [])
|
||||
)
|
||||
return form_data
|
||||
|
||||
|
||||
async def generate_function_chat_completion(form_data, user):
|
||||
model_id = form_data.get("model")
|
||||
model_info = Models.get_model_by_id(model_id)
|
||||
metadata = form_data.pop("metadata", None)
|
||||
metadata = form_data.pop("metadata", {})
|
||||
files = metadata.get("files", [])
|
||||
tool_ids = metadata.get("tool_ids", [])
|
||||
|
||||
__event_emitter__ = None
|
||||
__event_call__ = None
|
||||
@@ -325,28 +290,33 @@ async def generate_function_chat_completion(form_data, user):
|
||||
__event_call__ = get_event_call(metadata)
|
||||
__task__ = metadata.get("task", None)
|
||||
|
||||
extra_params = {
|
||||
"__event_emitter__": __event_emitter__,
|
||||
"__event_call__": __event_call__,
|
||||
"__task__": __task__,
|
||||
}
|
||||
tools_params = {
|
||||
**extra_params,
|
||||
"__model__": app.state.MODELS[form_data["model"]],
|
||||
"__messages__": form_data["messages"],
|
||||
"__files__": files,
|
||||
}
|
||||
configured_tools = get_tools(app, tool_ids, user, tools_params)
|
||||
|
||||
extra_params["__tools__"] = configured_tools
|
||||
if model_info:
|
||||
if model_info.base_model_id:
|
||||
form_data["model"] = model_info.base_model_id
|
||||
|
||||
params = model_info.params.model_dump()
|
||||
form_data = apply_model_params_to_body(params, form_data)
|
||||
form_data = apply_model_params_to_body_openai(params, form_data)
|
||||
form_data = apply_model_system_prompt_to_body(params, form_data, user)
|
||||
|
||||
pipe_id = get_pipe_id(form_data)
|
||||
function_module = get_function_module(pipe_id)
|
||||
|
||||
pipe = function_module.pipe
|
||||
params = get_function_params(
|
||||
function_module,
|
||||
form_data,
|
||||
user,
|
||||
{
|
||||
"__event_emitter__": __event_emitter__,
|
||||
"__event_call__": __event_call__,
|
||||
"__task__": __task__,
|
||||
},
|
||||
)
|
||||
params = get_function_params(function_module, form_data, user, extra_params)
|
||||
|
||||
if form_data["stream"]:
|
||||
|
||||
|
||||
@@ -140,7 +140,7 @@ class AuthsTable:
|
||||
return None
|
||||
else:
|
||||
return None
|
||||
except:
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def authenticate_user_by_api_key(self, api_key: str) -> Optional[UserModel]:
|
||||
@@ -152,7 +152,7 @@ class AuthsTable:
|
||||
try:
|
||||
user = Users.get_user_by_api_key(api_key)
|
||||
return user if user else None
|
||||
except:
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
def authenticate_user_by_trusted_header(self, email: str) -> Optional[UserModel]:
|
||||
@@ -163,7 +163,7 @@ class AuthsTable:
|
||||
if auth:
|
||||
user = Users.get_user_by_id(auth.id)
|
||||
return user
|
||||
except:
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def update_user_password_by_id(self, id: str, new_password: str) -> bool:
|
||||
@@ -174,7 +174,7 @@ class AuthsTable:
|
||||
)
|
||||
db.commit()
|
||||
return True if result == 1 else False
|
||||
except:
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
def update_email_by_id(self, id: str, email: str) -> bool:
|
||||
@@ -183,7 +183,7 @@ class AuthsTable:
|
||||
result = db.query(Auth).filter_by(id=id).update({"email": email})
|
||||
db.commit()
|
||||
return True if result == 1 else False
|
||||
except:
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
def delete_auth_by_id(self, id: str) -> bool:
|
||||
@@ -200,7 +200,7 @@ class AuthsTable:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
except:
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
from pydantic import BaseModel, ConfigDict
|
||||
from typing import List, Union, Optional
|
||||
from typing import Union, Optional
|
||||
|
||||
import json
|
||||
import uuid
|
||||
@@ -164,7 +164,7 @@ class ChatTable:
|
||||
db.refresh(chat)
|
||||
|
||||
return self.get_chat_by_id(chat.share_id)
|
||||
except:
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def delete_shared_chat_by_chat_id(self, chat_id: str) -> bool:
|
||||
@@ -175,7 +175,7 @@ class ChatTable:
|
||||
db.commit()
|
||||
|
||||
return True
|
||||
except:
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
def update_chat_share_id_by_id(
|
||||
@@ -189,7 +189,7 @@ class ChatTable:
|
||||
db.commit()
|
||||
db.refresh(chat)
|
||||
return ChatModel.model_validate(chat)
|
||||
except:
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def toggle_chat_archive_by_id(self, id: str) -> Optional[ChatModel]:
|
||||
@@ -201,7 +201,7 @@ class ChatTable:
|
||||
db.commit()
|
||||
db.refresh(chat)
|
||||
return ChatModel.model_validate(chat)
|
||||
except:
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def archive_all_chats_by_user_id(self, user_id: str) -> bool:
|
||||
@@ -210,12 +210,12 @@ class ChatTable:
|
||||
db.query(Chat).filter_by(user_id=user_id).update({"archived": True})
|
||||
db.commit()
|
||||
return True
|
||||
except:
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
def get_archived_chat_list_by_user_id(
|
||||
self, user_id: str, skip: int = 0, limit: int = 50
|
||||
) -> List[ChatModel]:
|
||||
) -> list[ChatModel]:
|
||||
with get_db() as db:
|
||||
|
||||
all_chats = (
|
||||
@@ -233,7 +233,7 @@ class ChatTable:
|
||||
include_archived: bool = False,
|
||||
skip: int = 0,
|
||||
limit: int = 50,
|
||||
) -> List[ChatModel]:
|
||||
) -> list[ChatModel]:
|
||||
with get_db() as db:
|
||||
query = db.query(Chat).filter_by(user_id=user_id)
|
||||
if not include_archived:
|
||||
@@ -250,8 +250,8 @@ class ChatTable:
|
||||
user_id: str,
|
||||
include_archived: bool = False,
|
||||
skip: int = 0,
|
||||
limit: int = 50,
|
||||
) -> List[ChatTitleIdResponse]:
|
||||
limit: int = -1,
|
||||
) -> list[ChatTitleIdResponse]:
|
||||
with get_db() as db:
|
||||
query = db.query(Chat).filter_by(user_id=user_id)
|
||||
if not include_archived:
|
||||
@@ -260,9 +260,10 @@ class ChatTable:
|
||||
all_chats = (
|
||||
query.order_by(Chat.updated_at.desc())
|
||||
# limit cols
|
||||
.with_entities(
|
||||
Chat.id, Chat.title, Chat.updated_at, Chat.created_at
|
||||
).all()
|
||||
.with_entities(Chat.id, Chat.title, Chat.updated_at, Chat.created_at)
|
||||
.limit(limit)
|
||||
.offset(skip)
|
||||
.all()
|
||||
)
|
||||
# result has to be destrctured from sqlalchemy `row` and mapped to a dict since the `ChatModel`is not the returned dataclass.
|
||||
return [
|
||||
@@ -278,8 +279,8 @@ class ChatTable:
|
||||
]
|
||||
|
||||
def get_chat_list_by_chat_ids(
|
||||
self, chat_ids: List[str], skip: int = 0, limit: int = 50
|
||||
) -> List[ChatModel]:
|
||||
self, chat_ids: list[str], skip: int = 0, limit: int = 50
|
||||
) -> list[ChatModel]:
|
||||
with get_db() as db:
|
||||
all_chats = (
|
||||
db.query(Chat)
|
||||
@@ -296,7 +297,7 @@ class ChatTable:
|
||||
|
||||
chat = db.get(Chat, id)
|
||||
return ChatModel.model_validate(chat)
|
||||
except:
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def get_chat_by_share_id(self, id: str) -> Optional[ChatModel]:
|
||||
@@ -318,10 +319,10 @@ class ChatTable:
|
||||
|
||||
chat = db.query(Chat).filter_by(id=id, user_id=user_id).first()
|
||||
return ChatModel.model_validate(chat)
|
||||
except:
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def get_chats(self, skip: int = 0, limit: int = 50) -> List[ChatModel]:
|
||||
def get_chats(self, skip: int = 0, limit: int = 50) -> list[ChatModel]:
|
||||
with get_db() as db:
|
||||
|
||||
all_chats = (
|
||||
@@ -331,7 +332,7 @@ class ChatTable:
|
||||
)
|
||||
return [ChatModel.model_validate(chat) for chat in all_chats]
|
||||
|
||||
def get_chats_by_user_id(self, user_id: str) -> List[ChatModel]:
|
||||
def get_chats_by_user_id(self, user_id: str) -> list[ChatModel]:
|
||||
with get_db() as db:
|
||||
|
||||
all_chats = (
|
||||
@@ -341,7 +342,7 @@ class ChatTable:
|
||||
)
|
||||
return [ChatModel.model_validate(chat) for chat in all_chats]
|
||||
|
||||
def get_archived_chats_by_user_id(self, user_id: str) -> List[ChatModel]:
|
||||
def get_archived_chats_by_user_id(self, user_id: str) -> list[ChatModel]:
|
||||
with get_db() as db:
|
||||
|
||||
all_chats = (
|
||||
@@ -359,7 +360,7 @@ class ChatTable:
|
||||
db.commit()
|
||||
|
||||
return True and self.delete_shared_chat_by_chat_id(id)
|
||||
except:
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
def delete_chat_by_id_and_user_id(self, id: str, user_id: str) -> bool:
|
||||
@@ -370,7 +371,7 @@ class ChatTable:
|
||||
db.commit()
|
||||
|
||||
return True and self.delete_shared_chat_by_chat_id(id)
|
||||
except:
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
def delete_chats_by_user_id(self, user_id: str) -> bool:
|
||||
@@ -384,7 +385,7 @@ class ChatTable:
|
||||
db.commit()
|
||||
|
||||
return True
|
||||
except:
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
def delete_shared_chats_by_user_id(self, user_id: str) -> bool:
|
||||
@@ -399,7 +400,7 @@ class ChatTable:
|
||||
db.commit()
|
||||
|
||||
return True
|
||||
except:
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
from pydantic import BaseModel, ConfigDict
|
||||
from typing import List, Optional
|
||||
from typing import Optional
|
||||
import time
|
||||
import logging
|
||||
|
||||
@@ -93,7 +93,7 @@ class DocumentsTable:
|
||||
return DocumentModel.model_validate(result)
|
||||
else:
|
||||
return None
|
||||
except:
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def get_doc_by_name(self, name: str) -> Optional[DocumentModel]:
|
||||
@@ -102,10 +102,10 @@ class DocumentsTable:
|
||||
|
||||
document = db.query(Document).filter_by(name=name).first()
|
||||
return DocumentModel.model_validate(document) if document else None
|
||||
except:
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def get_docs(self) -> List[DocumentModel]:
|
||||
def get_docs(self) -> list[DocumentModel]:
|
||||
with get_db() as db:
|
||||
|
||||
return [
|
||||
@@ -160,7 +160,7 @@ class DocumentsTable:
|
||||
db.query(Document).filter_by(name=name).delete()
|
||||
db.commit()
|
||||
return True
|
||||
except:
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
from pydantic import BaseModel, ConfigDict
|
||||
from typing import List, Union, Optional
|
||||
from typing import Union, Optional
|
||||
import time
|
||||
import logging
|
||||
|
||||
@@ -90,10 +90,10 @@ class FilesTable:
|
||||
try:
|
||||
file = db.get(File, id)
|
||||
return FileModel.model_validate(file)
|
||||
except:
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def get_files(self) -> List[FileModel]:
|
||||
def get_files(self) -> list[FileModel]:
|
||||
with get_db() as db:
|
||||
|
||||
return [FileModel.model_validate(file) for file in db.query(File).all()]
|
||||
@@ -107,7 +107,7 @@ class FilesTable:
|
||||
db.commit()
|
||||
|
||||
return True
|
||||
except:
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
def delete_all_files(self) -> bool:
|
||||
@@ -119,7 +119,7 @@ class FilesTable:
|
||||
db.commit()
|
||||
|
||||
return True
|
||||
except:
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
from pydantic import BaseModel, ConfigDict
|
||||
from typing import List, Union, Optional
|
||||
from typing import Union, Optional
|
||||
import time
|
||||
import logging
|
||||
|
||||
@@ -122,10 +122,10 @@ class FunctionsTable:
|
||||
|
||||
function = db.get(Function, id)
|
||||
return FunctionModel.model_validate(function)
|
||||
except:
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def get_functions(self, active_only=False) -> List[FunctionModel]:
|
||||
def get_functions(self, active_only=False) -> list[FunctionModel]:
|
||||
with get_db() as db:
|
||||
|
||||
if active_only:
|
||||
@@ -141,7 +141,7 @@ class FunctionsTable:
|
||||
|
||||
def get_functions_by_type(
|
||||
self, type: str, active_only=False
|
||||
) -> List[FunctionModel]:
|
||||
) -> list[FunctionModel]:
|
||||
with get_db() as db:
|
||||
|
||||
if active_only:
|
||||
@@ -157,7 +157,7 @@ class FunctionsTable:
|
||||
for function in db.query(Function).filter_by(type=type).all()
|
||||
]
|
||||
|
||||
def get_global_filter_functions(self) -> List[FunctionModel]:
|
||||
def get_global_filter_functions(self) -> list[FunctionModel]:
|
||||
with get_db() as db:
|
||||
|
||||
return [
|
||||
@@ -167,7 +167,7 @@ class FunctionsTable:
|
||||
.all()
|
||||
]
|
||||
|
||||
def get_global_action_functions(self) -> List[FunctionModel]:
|
||||
def get_global_action_functions(self) -> list[FunctionModel]:
|
||||
with get_db() as db:
|
||||
return [
|
||||
FunctionModel.model_validate(function)
|
||||
@@ -198,7 +198,7 @@ class FunctionsTable:
|
||||
db.commit()
|
||||
db.refresh(function)
|
||||
return self.get_function_by_id(id)
|
||||
except:
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def get_user_valves_by_id_and_user_id(
|
||||
@@ -256,7 +256,7 @@ class FunctionsTable:
|
||||
)
|
||||
db.commit()
|
||||
return self.get_function_by_id(id)
|
||||
except:
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def deactivate_all_functions(self) -> Optional[bool]:
|
||||
@@ -271,7 +271,7 @@ class FunctionsTable:
|
||||
)
|
||||
db.commit()
|
||||
return True
|
||||
except:
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def delete_function_by_id(self, id: str) -> bool:
|
||||
@@ -281,7 +281,7 @@ class FunctionsTable:
|
||||
db.commit()
|
||||
|
||||
return True
|
||||
except:
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
from pydantic import BaseModel, ConfigDict
|
||||
from typing import List, Union, Optional
|
||||
from typing import Union, Optional
|
||||
|
||||
from sqlalchemy import Column, String, BigInteger, Text
|
||||
|
||||
@@ -80,25 +80,25 @@ class MemoriesTable:
|
||||
)
|
||||
db.commit()
|
||||
return self.get_memory_by_id(id)
|
||||
except:
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def get_memories(self) -> List[MemoryModel]:
|
||||
def get_memories(self) -> list[MemoryModel]:
|
||||
with get_db() as db:
|
||||
|
||||
try:
|
||||
memories = db.query(Memory).all()
|
||||
return [MemoryModel.model_validate(memory) for memory in memories]
|
||||
except:
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def get_memories_by_user_id(self, user_id: str) -> List[MemoryModel]:
|
||||
def get_memories_by_user_id(self, user_id: str) -> list[MemoryModel]:
|
||||
with get_db() as db:
|
||||
|
||||
try:
|
||||
memories = db.query(Memory).filter_by(user_id=user_id).all()
|
||||
return [MemoryModel.model_validate(memory) for memory in memories]
|
||||
except:
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def get_memory_by_id(self, id: str) -> Optional[MemoryModel]:
|
||||
@@ -107,7 +107,7 @@ class MemoriesTable:
|
||||
try:
|
||||
memory = db.get(Memory, id)
|
||||
return MemoryModel.model_validate(memory)
|
||||
except:
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def delete_memory_by_id(self, id: str) -> bool:
|
||||
@@ -119,7 +119,7 @@ class MemoriesTable:
|
||||
|
||||
return True
|
||||
|
||||
except:
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
def delete_memories_by_user_id(self, user_id: str) -> bool:
|
||||
@@ -130,7 +130,7 @@ class MemoriesTable:
|
||||
db.commit()
|
||||
|
||||
return True
|
||||
except:
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
def delete_memory_by_id_and_user_id(self, id: str, user_id: str) -> bool:
|
||||
@@ -141,7 +141,7 @@ class MemoriesTable:
|
||||
db.commit()
|
||||
|
||||
return True
|
||||
except:
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
|
||||
@@ -137,7 +137,7 @@ class ModelsTable:
|
||||
print(e)
|
||||
return None
|
||||
|
||||
def get_all_models(self) -> List[ModelModel]:
|
||||
def get_all_models(self) -> list[ModelModel]:
|
||||
with get_db() as db:
|
||||
return [ModelModel.model_validate(model) for model in db.query(Model).all()]
|
||||
|
||||
@@ -146,7 +146,7 @@ class ModelsTable:
|
||||
with get_db() as db:
|
||||
model = db.get(Model, id)
|
||||
return ModelModel.model_validate(model)
|
||||
except:
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def update_model_by_id(self, id: str, model: ModelForm) -> Optional[ModelModel]:
|
||||
@@ -175,7 +175,7 @@ class ModelsTable:
|
||||
db.commit()
|
||||
|
||||
return True
|
||||
except:
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
from pydantic import BaseModel, ConfigDict
|
||||
from typing import List, Optional
|
||||
from typing import Optional
|
||||
import time
|
||||
|
||||
from sqlalchemy import String, Column, BigInteger, Text
|
||||
@@ -79,10 +79,10 @@ class PromptsTable:
|
||||
|
||||
prompt = db.query(Prompt).filter_by(command=command).first()
|
||||
return PromptModel.model_validate(prompt)
|
||||
except:
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def get_prompts(self) -> List[PromptModel]:
|
||||
def get_prompts(self) -> list[PromptModel]:
|
||||
with get_db() as db:
|
||||
|
||||
return [
|
||||
@@ -101,7 +101,7 @@ class PromptsTable:
|
||||
prompt.timestamp = int(time.time())
|
||||
db.commit()
|
||||
return PromptModel.model_validate(prompt)
|
||||
except:
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def delete_prompt_by_command(self, command: str) -> bool:
|
||||
@@ -112,7 +112,7 @@ class PromptsTable:
|
||||
db.commit()
|
||||
|
||||
return True
|
||||
except:
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
from pydantic import BaseModel, ConfigDict
|
||||
from typing import List, Optional
|
||||
from typing import Optional
|
||||
|
||||
import json
|
||||
import uuid
|
||||
@@ -69,11 +69,11 @@ class ChatIdTagForm(BaseModel):
|
||||
|
||||
|
||||
class TagChatIdsResponse(BaseModel):
|
||||
chat_ids: List[str]
|
||||
chat_ids: list[str]
|
||||
|
||||
|
||||
class ChatTagsResponse(BaseModel):
|
||||
tags: List[str]
|
||||
tags: list[str]
|
||||
|
||||
|
||||
class TagTable:
|
||||
@@ -109,7 +109,7 @@ class TagTable:
|
||||
self, user_id: str, form_data: ChatIdTagForm
|
||||
) -> Optional[ChatIdTagModel]:
|
||||
tag = self.get_tag_by_name_and_user_id(form_data.tag_name, user_id)
|
||||
if tag == None:
|
||||
if tag is None:
|
||||
tag = self.insert_new_tag(form_data.tag_name, user_id)
|
||||
|
||||
id = str(uuid.uuid4())
|
||||
@@ -132,10 +132,10 @@ class TagTable:
|
||||
return ChatIdTagModel.model_validate(result)
|
||||
else:
|
||||
return None
|
||||
except:
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def get_tags_by_user_id(self, user_id: str) -> List[TagModel]:
|
||||
def get_tags_by_user_id(self, user_id: str) -> list[TagModel]:
|
||||
with get_db() as db:
|
||||
tag_names = [
|
||||
chat_id_tag.tag_name
|
||||
@@ -159,7 +159,7 @@ class TagTable:
|
||||
|
||||
def get_tags_by_chat_id_and_user_id(
|
||||
self, chat_id: str, user_id: str
|
||||
) -> List[TagModel]:
|
||||
) -> list[TagModel]:
|
||||
with get_db() as db:
|
||||
|
||||
tag_names = [
|
||||
@@ -184,7 +184,7 @@ class TagTable:
|
||||
|
||||
def get_chat_ids_by_tag_name_and_user_id(
|
||||
self, tag_name: str, user_id: str
|
||||
) -> List[ChatIdTagModel]:
|
||||
) -> list[ChatIdTagModel]:
|
||||
with get_db() as db:
|
||||
|
||||
return [
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
from pydantic import BaseModel, ConfigDict
|
||||
from typing import List, Optional
|
||||
from typing import Optional
|
||||
import time
|
||||
import logging
|
||||
from sqlalchemy import String, Column, BigInteger, Text
|
||||
@@ -45,7 +45,7 @@ class ToolModel(BaseModel):
|
||||
user_id: str
|
||||
name: str
|
||||
content: str
|
||||
specs: List[dict]
|
||||
specs: list[dict]
|
||||
meta: ToolMeta
|
||||
updated_at: int # timestamp in epoch
|
||||
created_at: int # timestamp in epoch
|
||||
@@ -81,7 +81,7 @@ class ToolValves(BaseModel):
|
||||
class ToolsTable:
|
||||
|
||||
def insert_new_tool(
|
||||
self, user_id: str, form_data: ToolForm, specs: List[dict]
|
||||
self, user_id: str, form_data: ToolForm, specs: list[dict]
|
||||
) -> Optional[ToolModel]:
|
||||
|
||||
with get_db() as db:
|
||||
@@ -115,10 +115,10 @@ class ToolsTable:
|
||||
|
||||
tool = db.get(Tool, id)
|
||||
return ToolModel.model_validate(tool)
|
||||
except:
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def get_tools(self) -> List[ToolModel]:
|
||||
def get_tools(self) -> list[ToolModel]:
|
||||
with get_db() as db:
|
||||
return [ToolModel.model_validate(tool) for tool in db.query(Tool).all()]
|
||||
|
||||
@@ -141,7 +141,7 @@ class ToolsTable:
|
||||
)
|
||||
db.commit()
|
||||
return self.get_tool_by_id(id)
|
||||
except:
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def get_user_valves_by_id_and_user_id(
|
||||
@@ -196,7 +196,7 @@ class ToolsTable:
|
||||
tool = db.query(Tool).get(id)
|
||||
db.refresh(tool)
|
||||
return ToolModel.model_validate(tool)
|
||||
except:
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def delete_tool_by_id(self, id: str) -> bool:
|
||||
@@ -206,7 +206,7 @@ class ToolsTable:
|
||||
db.commit()
|
||||
|
||||
return True
|
||||
except:
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
from pydantic import BaseModel, ConfigDict, parse_obj_as
|
||||
from typing import List, Union, Optional
|
||||
from pydantic import BaseModel, ConfigDict
|
||||
from typing import Optional
|
||||
import time
|
||||
|
||||
from sqlalchemy import String, Column, BigInteger, Text
|
||||
|
||||
from utils.misc import get_gravatar_url
|
||||
|
||||
from apps.webui.internal.db import Base, JSONField, Session, get_db
|
||||
from apps.webui.internal.db import Base, JSONField, get_db
|
||||
from apps.webui.models.chats import Chats
|
||||
|
||||
####################
|
||||
@@ -78,7 +76,6 @@ class UserUpdateForm(BaseModel):
|
||||
|
||||
|
||||
class UsersTable:
|
||||
|
||||
def insert_new_user(
|
||||
self,
|
||||
id: str,
|
||||
@@ -122,31 +119,28 @@ class UsersTable:
|
||||
def get_user_by_api_key(self, api_key: str) -> Optional[UserModel]:
|
||||
try:
|
||||
with get_db() as db:
|
||||
|
||||
user = db.query(User).filter_by(api_key=api_key).first()
|
||||
return UserModel.model_validate(user)
|
||||
except:
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def get_user_by_email(self, email: str) -> Optional[UserModel]:
|
||||
try:
|
||||
with get_db() as db:
|
||||
|
||||
user = db.query(User).filter_by(email=email).first()
|
||||
return UserModel.model_validate(user)
|
||||
except:
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def get_user_by_oauth_sub(self, sub: str) -> Optional[UserModel]:
|
||||
try:
|
||||
with get_db() as db:
|
||||
|
||||
user = db.query(User).filter_by(oauth_sub=sub).first()
|
||||
return UserModel.model_validate(user)
|
||||
except:
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def get_users(self, skip: int = 0, limit: int = 50) -> List[UserModel]:
|
||||
def get_users(self, skip: int = 0, limit: int = 50) -> list[UserModel]:
|
||||
with get_db() as db:
|
||||
users = (
|
||||
db.query(User)
|
||||
@@ -164,7 +158,7 @@ class UsersTable:
|
||||
with get_db() as db:
|
||||
user = db.query(User).order_by(User.created_at).first()
|
||||
return UserModel.model_validate(user)
|
||||
except:
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def update_user_role_by_id(self, id: str, role: str) -> Optional[UserModel]:
|
||||
@@ -174,7 +168,7 @@ class UsersTable:
|
||||
db.commit()
|
||||
user = db.query(User).filter_by(id=id).first()
|
||||
return UserModel.model_validate(user)
|
||||
except:
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def update_user_profile_image_url_by_id(
|
||||
@@ -189,13 +183,12 @@ class UsersTable:
|
||||
|
||||
user = db.query(User).filter_by(id=id).first()
|
||||
return UserModel.model_validate(user)
|
||||
except:
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def update_user_last_active_by_id(self, id: str) -> Optional[UserModel]:
|
||||
try:
|
||||
with get_db() as db:
|
||||
|
||||
db.query(User).filter_by(id=id).update(
|
||||
{"last_active_at": int(time.time())}
|
||||
)
|
||||
@@ -203,7 +196,7 @@ class UsersTable:
|
||||
|
||||
user = db.query(User).filter_by(id=id).first()
|
||||
return UserModel.model_validate(user)
|
||||
except:
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def update_user_oauth_sub_by_id(
|
||||
@@ -216,7 +209,7 @@ class UsersTable:
|
||||
|
||||
user = db.query(User).filter_by(id=id).first()
|
||||
return UserModel.model_validate(user)
|
||||
except:
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def update_user_by_id(self, id: str, updated: dict) -> Optional[UserModel]:
|
||||
@@ -245,7 +238,7 @@ class UsersTable:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
except:
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
def update_user_api_key_by_id(self, id: str, api_key: str) -> str:
|
||||
@@ -254,7 +247,7 @@ class UsersTable:
|
||||
result = db.query(User).filter_by(id=id).update({"api_key": api_key})
|
||||
db.commit()
|
||||
return True if result == 1 else False
|
||||
except:
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
def get_user_api_key_by_id(self, id: str) -> Optional[str]:
|
||||
|
||||
@@ -352,6 +352,7 @@ async def get_admin_config(request: Request, user=Depends(get_admin_user)):
|
||||
"DEFAULT_USER_ROLE": request.app.state.config.DEFAULT_USER_ROLE,
|
||||
"JWT_EXPIRES_IN": request.app.state.config.JWT_EXPIRES_IN,
|
||||
"ENABLE_COMMUNITY_SHARING": request.app.state.config.ENABLE_COMMUNITY_SHARING,
|
||||
"ENABLE_MESSAGE_RATING": request.app.state.config.ENABLE_MESSAGE_RATING,
|
||||
}
|
||||
|
||||
|
||||
@@ -361,6 +362,7 @@ class AdminConfig(BaseModel):
|
||||
DEFAULT_USER_ROLE: str
|
||||
JWT_EXPIRES_IN: str
|
||||
ENABLE_COMMUNITY_SHARING: bool
|
||||
ENABLE_MESSAGE_RATING: bool
|
||||
|
||||
|
||||
@router.post("/admin/config")
|
||||
@@ -382,6 +384,7 @@ async def update_admin_config(
|
||||
request.app.state.config.ENABLE_COMMUNITY_SHARING = (
|
||||
form_data.ENABLE_COMMUNITY_SHARING
|
||||
)
|
||||
request.app.state.config.ENABLE_MESSAGE_RATING = form_data.ENABLE_MESSAGE_RATING
|
||||
|
||||
return {
|
||||
"SHOW_ADMIN_DETAILS": request.app.state.config.SHOW_ADMIN_DETAILS,
|
||||
@@ -389,6 +392,7 @@ async def update_admin_config(
|
||||
"DEFAULT_USER_ROLE": request.app.state.config.DEFAULT_USER_ROLE,
|
||||
"JWT_EXPIRES_IN": request.app.state.config.JWT_EXPIRES_IN,
|
||||
"ENABLE_COMMUNITY_SHARING": request.app.state.config.ENABLE_COMMUNITY_SHARING,
|
||||
"ENABLE_MESSAGE_RATING": request.app.state.config.ENABLE_MESSAGE_RATING,
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from fastapi import Depends, Request, HTTPException, status
|
||||
from datetime import datetime, timedelta
|
||||
from typing import List, Union, Optional
|
||||
from typing import Union, Optional
|
||||
from utils.utils import get_verified_user, get_admin_user
|
||||
from fastapi import APIRouter
|
||||
from pydantic import BaseModel
|
||||
@@ -28,7 +28,7 @@ from apps.webui.models.tags import (
|
||||
|
||||
from constants import ERROR_MESSAGES
|
||||
|
||||
from config import SRC_LOG_LEVELS, ENABLE_ADMIN_EXPORT
|
||||
from config import SRC_LOG_LEVELS, ENABLE_ADMIN_EXPORT, ENABLE_ADMIN_CHAT_ACCESS
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
log.setLevel(SRC_LOG_LEVELS["MODELS"])
|
||||
@@ -40,12 +40,18 @@ router = APIRouter()
|
||||
############################
|
||||
|
||||
|
||||
@router.get("/", response_model=List[ChatTitleIdResponse])
|
||||
@router.get("/list", response_model=List[ChatTitleIdResponse])
|
||||
@router.get("/", response_model=list[ChatTitleIdResponse])
|
||||
@router.get("/list", response_model=list[ChatTitleIdResponse])
|
||||
async def get_session_user_chat_list(
|
||||
user=Depends(get_verified_user), skip: int = 0, limit: int = 50
|
||||
user=Depends(get_verified_user), page: Optional[int] = None
|
||||
):
|
||||
return Chats.get_chat_title_id_list_by_user_id(user.id, skip=skip, limit=limit)
|
||||
if page is not None:
|
||||
limit = 60
|
||||
skip = (page - 1) * limit
|
||||
|
||||
return Chats.get_chat_title_id_list_by_user_id(user.id, skip=skip, limit=limit)
|
||||
else:
|
||||
return Chats.get_chat_title_id_list_by_user_id(user.id)
|
||||
|
||||
|
||||
############################
|
||||
@@ -74,13 +80,18 @@ async def delete_all_user_chats(request: Request, user=Depends(get_verified_user
|
||||
############################
|
||||
|
||||
|
||||
@router.get("/list/user/{user_id}", response_model=List[ChatTitleIdResponse])
|
||||
@router.get("/list/user/{user_id}", response_model=list[ChatTitleIdResponse])
|
||||
async def get_user_chat_list_by_user_id(
|
||||
user_id: str,
|
||||
user=Depends(get_admin_user),
|
||||
skip: int = 0,
|
||||
limit: int = 50,
|
||||
):
|
||||
if not ENABLE_ADMIN_CHAT_ACCESS:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
|
||||
)
|
||||
return Chats.get_chat_list_by_user_id(
|
||||
user_id, include_archived=True, skip=skip, limit=limit
|
||||
)
|
||||
@@ -108,7 +119,7 @@ async def create_new_chat(form_data: ChatForm, user=Depends(get_verified_user)):
|
||||
############################
|
||||
|
||||
|
||||
@router.get("/all", response_model=List[ChatResponse])
|
||||
@router.get("/all", response_model=list[ChatResponse])
|
||||
async def get_user_chats(user=Depends(get_verified_user)):
|
||||
return [
|
||||
ChatResponse(**{**chat.model_dump(), "chat": json.loads(chat.chat)})
|
||||
@@ -121,7 +132,7 @@ async def get_user_chats(user=Depends(get_verified_user)):
|
||||
############################
|
||||
|
||||
|
||||
@router.get("/all/archived", response_model=List[ChatResponse])
|
||||
@router.get("/all/archived", response_model=list[ChatResponse])
|
||||
async def get_user_archived_chats(user=Depends(get_verified_user)):
|
||||
return [
|
||||
ChatResponse(**{**chat.model_dump(), "chat": json.loads(chat.chat)})
|
||||
@@ -134,7 +145,7 @@ async def get_user_archived_chats(user=Depends(get_verified_user)):
|
||||
############################
|
||||
|
||||
|
||||
@router.get("/all/db", response_model=List[ChatResponse])
|
||||
@router.get("/all/db", response_model=list[ChatResponse])
|
||||
async def get_all_user_chats_in_db(user=Depends(get_admin_user)):
|
||||
if not ENABLE_ADMIN_EXPORT:
|
||||
raise HTTPException(
|
||||
@@ -152,7 +163,7 @@ async def get_all_user_chats_in_db(user=Depends(get_admin_user)):
|
||||
############################
|
||||
|
||||
|
||||
@router.get("/archived", response_model=List[ChatTitleIdResponse])
|
||||
@router.get("/archived", response_model=list[ChatTitleIdResponse])
|
||||
async def get_archived_session_user_chat_list(
|
||||
user=Depends(get_verified_user), skip: int = 0, limit: int = 50
|
||||
):
|
||||
@@ -181,9 +192,9 @@ async def get_shared_chat_by_id(share_id: str, user=Depends(get_verified_user)):
|
||||
status_code=status.HTTP_401_UNAUTHORIZED, detail=ERROR_MESSAGES.NOT_FOUND
|
||||
)
|
||||
|
||||
if user.role == "user":
|
||||
if user.role == "user" or (user.role == "admin" and not ENABLE_ADMIN_CHAT_ACCESS):
|
||||
chat = Chats.get_chat_by_share_id(share_id)
|
||||
elif user.role == "admin":
|
||||
elif user.role == "admin" and ENABLE_ADMIN_CHAT_ACCESS:
|
||||
chat = Chats.get_chat_by_id(share_id)
|
||||
|
||||
if chat:
|
||||
@@ -205,7 +216,7 @@ class TagNameForm(BaseModel):
|
||||
limit: Optional[int] = 50
|
||||
|
||||
|
||||
@router.post("/tags", response_model=List[ChatTitleIdResponse])
|
||||
@router.post("/tags", response_model=list[ChatTitleIdResponse])
|
||||
async def get_user_chat_list_by_tag_name(
|
||||
form_data: TagNameForm, user=Depends(get_verified_user)
|
||||
):
|
||||
@@ -230,7 +241,7 @@ async def get_user_chat_list_by_tag_name(
|
||||
############################
|
||||
|
||||
|
||||
@router.get("/tags/all", response_model=List[TagModel])
|
||||
@router.get("/tags/all", response_model=list[TagModel])
|
||||
async def get_all_tags(user=Depends(get_verified_user)):
|
||||
try:
|
||||
tags = Tags.get_tags_by_user_id(user.id)
|
||||
@@ -406,7 +417,7 @@ async def delete_shared_chat_by_id(id: str, user=Depends(get_verified_user)):
|
||||
############################
|
||||
|
||||
|
||||
@router.get("/{id}/tags", response_model=List[TagModel])
|
||||
@router.get("/{id}/tags", response_model=list[TagModel])
|
||||
async def get_chat_tags_by_id(id: str, user=Depends(get_verified_user)):
|
||||
tags = Tags.get_tags_by_chat_id_and_user_id(id, user.id)
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from fastapi import Response, Request
|
||||
from fastapi import Depends, FastAPI, HTTPException, status
|
||||
from datetime import datetime, timedelta
|
||||
from typing import List, Union
|
||||
from typing import Union
|
||||
|
||||
from fastapi import APIRouter
|
||||
from pydantic import BaseModel
|
||||
@@ -29,12 +29,12 @@ class SetDefaultModelsForm(BaseModel):
|
||||
|
||||
|
||||
class PromptSuggestion(BaseModel):
|
||||
title: List[str]
|
||||
title: list[str]
|
||||
content: str
|
||||
|
||||
|
||||
class SetDefaultSuggestionsForm(BaseModel):
|
||||
suggestions: List[PromptSuggestion]
|
||||
suggestions: list[PromptSuggestion]
|
||||
|
||||
|
||||
############################
|
||||
@@ -50,7 +50,7 @@ async def set_global_default_models(
|
||||
return request.app.state.config.DEFAULT_MODELS
|
||||
|
||||
|
||||
@router.post("/default/suggestions", response_model=List[PromptSuggestion])
|
||||
@router.post("/default/suggestions", response_model=list[PromptSuggestion])
|
||||
async def set_global_default_suggestions(
|
||||
request: Request,
|
||||
form_data: SetDefaultSuggestionsForm,
|
||||
@@ -67,10 +67,10 @@ async def set_global_default_suggestions(
|
||||
|
||||
|
||||
class SetBannersForm(BaseModel):
|
||||
banners: List[BannerModel]
|
||||
banners: list[BannerModel]
|
||||
|
||||
|
||||
@router.post("/banners", response_model=List[BannerModel])
|
||||
@router.post("/banners", response_model=list[BannerModel])
|
||||
async def set_banners(
|
||||
request: Request,
|
||||
form_data: SetBannersForm,
|
||||
@@ -81,7 +81,7 @@ async def set_banners(
|
||||
return request.app.state.config.BANNERS
|
||||
|
||||
|
||||
@router.get("/banners", response_model=List[BannerModel])
|
||||
@router.get("/banners", response_model=list[BannerModel])
|
||||
async def get_banners(
|
||||
request: Request,
|
||||
user=Depends(get_verified_user),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from fastapi import Depends, FastAPI, HTTPException, status
|
||||
from datetime import datetime, timedelta
|
||||
from typing import List, Union, Optional
|
||||
from typing import Union, Optional
|
||||
|
||||
from fastapi import APIRouter
|
||||
from pydantic import BaseModel
|
||||
@@ -24,7 +24,7 @@ router = APIRouter()
|
||||
############################
|
||||
|
||||
|
||||
@router.get("/", response_model=List[DocumentResponse])
|
||||
@router.get("/", response_model=list[DocumentResponse])
|
||||
async def get_documents(user=Depends(get_verified_user)):
|
||||
docs = [
|
||||
DocumentResponse(
|
||||
@@ -46,7 +46,7 @@ async def get_documents(user=Depends(get_verified_user)):
|
||||
@router.post("/create", response_model=Optional[DocumentResponse])
|
||||
async def create_new_doc(form_data: DocumentForm, user=Depends(get_admin_user)):
|
||||
doc = Documents.get_doc_by_name(form_data.name)
|
||||
if doc == None:
|
||||
if doc is None:
|
||||
doc = Documents.insert_new_doc(user.id, form_data)
|
||||
|
||||
if doc:
|
||||
@@ -102,7 +102,7 @@ class TagItem(BaseModel):
|
||||
|
||||
class TagDocumentForm(BaseModel):
|
||||
name: str
|
||||
tags: List[dict]
|
||||
tags: list[dict]
|
||||
|
||||
|
||||
@router.post("/doc/tags", response_model=Optional[DocumentResponse])
|
||||
|
||||
@@ -11,7 +11,7 @@ from fastapi import (
|
||||
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
from typing import List, Union, Optional
|
||||
from typing import Union, Optional
|
||||
from pathlib import Path
|
||||
|
||||
from fastapi import APIRouter
|
||||
@@ -104,7 +104,7 @@ def upload_file(file: UploadFile = File(...), user=Depends(get_verified_user)):
|
||||
############################
|
||||
|
||||
|
||||
@router.get("/", response_model=List[FileModel])
|
||||
@router.get("/", response_model=list[FileModel])
|
||||
async def list_files(user=Depends(get_verified_user)):
|
||||
files = Files.get_files()
|
||||
return files
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from fastapi import Depends, FastAPI, HTTPException, status, Request
|
||||
from datetime import datetime, timedelta
|
||||
from typing import List, Union, Optional
|
||||
from typing import Union, Optional
|
||||
|
||||
from fastapi import APIRouter
|
||||
from pydantic import BaseModel
|
||||
@@ -30,7 +30,7 @@ router = APIRouter()
|
||||
############################
|
||||
|
||||
|
||||
@router.get("/", response_model=List[FunctionResponse])
|
||||
@router.get("/", response_model=list[FunctionResponse])
|
||||
async def get_functions(user=Depends(get_verified_user)):
|
||||
return Functions.get_functions()
|
||||
|
||||
@@ -40,7 +40,7 @@ async def get_functions(user=Depends(get_verified_user)):
|
||||
############################
|
||||
|
||||
|
||||
@router.get("/export", response_model=List[FunctionModel])
|
||||
@router.get("/export", response_model=list[FunctionModel])
|
||||
async def get_functions(user=Depends(get_admin_user)):
|
||||
return Functions.get_functions()
|
||||
|
||||
@@ -63,7 +63,7 @@ async def create_new_function(
|
||||
form_data.id = form_data.id.lower()
|
||||
|
||||
function = Functions.get_function_by_id(form_data.id)
|
||||
if function == None:
|
||||
if function is None:
|
||||
function_path = os.path.join(FUNCTIONS_DIR, f"{form_data.id}.py")
|
||||
try:
|
||||
with open(function_path, "w") as function_file:
|
||||
@@ -235,7 +235,7 @@ async def delete_function_by_id(
|
||||
function_path = os.path.join(FUNCTIONS_DIR, f"{id}.py")
|
||||
try:
|
||||
os.remove(function_path)
|
||||
except:
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return result
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from fastapi import Response, Request
|
||||
from fastapi import Depends, FastAPI, HTTPException, status
|
||||
from datetime import datetime, timedelta
|
||||
from typing import List, Union, Optional
|
||||
from typing import Union, Optional
|
||||
|
||||
from fastapi import APIRouter
|
||||
from pydantic import BaseModel
|
||||
@@ -30,7 +30,7 @@ async def get_embeddings(request: Request):
|
||||
############################
|
||||
|
||||
|
||||
@router.get("/", response_model=List[MemoryModel])
|
||||
@router.get("/", response_model=list[MemoryModel])
|
||||
async def get_memories(user=Depends(get_verified_user)):
|
||||
return Memories.get_memories_by_user_id(user.id)
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from fastapi import Depends, FastAPI, HTTPException, status, Request
|
||||
from datetime import datetime, timedelta
|
||||
from typing import List, Union, Optional
|
||||
from typing import Union, Optional
|
||||
|
||||
from fastapi import APIRouter
|
||||
from pydantic import BaseModel
|
||||
@@ -18,7 +18,7 @@ router = APIRouter()
|
||||
###########################
|
||||
|
||||
|
||||
@router.get("/", response_model=List[ModelResponse])
|
||||
@router.get("/", response_model=list[ModelResponse])
|
||||
async def get_models(user=Depends(get_verified_user)):
|
||||
return Models.get_all_models()
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from fastapi import Depends, FastAPI, HTTPException, status
|
||||
from datetime import datetime, timedelta
|
||||
from typing import List, Union, Optional
|
||||
from typing import Union, Optional
|
||||
|
||||
from fastapi import APIRouter
|
||||
from pydantic import BaseModel
|
||||
@@ -18,7 +18,7 @@ router = APIRouter()
|
||||
############################
|
||||
|
||||
|
||||
@router.get("/", response_model=List[PromptModel])
|
||||
@router.get("/", response_model=list[PromptModel])
|
||||
async def get_prompts(user=Depends(get_verified_user)):
|
||||
return Prompts.get_prompts()
|
||||
|
||||
@@ -31,7 +31,7 @@ async def get_prompts(user=Depends(get_verified_user)):
|
||||
@router.post("/create", response_model=Optional[PromptModel])
|
||||
async def create_new_prompt(form_data: PromptForm, user=Depends(get_admin_user)):
|
||||
prompt = Prompts.get_prompt_by_command(form_data.command)
|
||||
if prompt == None:
|
||||
if prompt is None:
|
||||
prompt = Prompts.insert_new_prompt(user.id, form_data)
|
||||
|
||||
if prompt:
|
||||
|
||||
@@ -1,12 +1,8 @@
|
||||
from fastapi import Depends, FastAPI, HTTPException, status, Request
|
||||
from datetime import datetime, timedelta
|
||||
from typing import List, Union, Optional
|
||||
from fastapi import Depends, HTTPException, status, Request
|
||||
from typing import Optional
|
||||
|
||||
from fastapi import APIRouter
|
||||
from pydantic import BaseModel
|
||||
import json
|
||||
|
||||
from apps.webui.models.users import Users
|
||||
from apps.webui.models.tools import Tools, ToolForm, ToolModel, ToolResponse
|
||||
from apps.webui.utils import load_toolkit_module_by_id
|
||||
|
||||
@@ -14,7 +10,6 @@ from utils.utils import get_admin_user, get_verified_user
|
||||
from utils.tools import get_tools_specs
|
||||
from constants import ERROR_MESSAGES
|
||||
|
||||
from importlib import util
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
@@ -32,7 +27,7 @@ router = APIRouter()
|
||||
############################
|
||||
|
||||
|
||||
@router.get("/", response_model=List[ToolResponse])
|
||||
@router.get("/", response_model=list[ToolResponse])
|
||||
async def get_toolkits(user=Depends(get_verified_user)):
|
||||
toolkits = [toolkit for toolkit in Tools.get_tools()]
|
||||
return toolkits
|
||||
@@ -43,7 +38,7 @@ async def get_toolkits(user=Depends(get_verified_user)):
|
||||
############################
|
||||
|
||||
|
||||
@router.get("/export", response_model=List[ToolModel])
|
||||
@router.get("/export", response_model=list[ToolModel])
|
||||
async def get_toolkits(user=Depends(get_admin_user)):
|
||||
toolkits = [toolkit for toolkit in Tools.get_tools()]
|
||||
return toolkits
|
||||
@@ -69,7 +64,7 @@ async def create_new_toolkit(
|
||||
form_data.id = form_data.id.lower()
|
||||
|
||||
toolkit = Tools.get_tool_by_id(form_data.id)
|
||||
if toolkit == None:
|
||||
if toolkit is None:
|
||||
toolkit_path = os.path.join(TOOLS_DIR, f"{form_data.id}.py")
|
||||
try:
|
||||
with open(toolkit_path, "w") as tool_file:
|
||||
@@ -98,7 +93,7 @@ async def create_new_toolkit(
|
||||
print(e)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=ERROR_MESSAGES.DEFAULT(e),
|
||||
detail=ERROR_MESSAGES.DEFAULT(str(e)),
|
||||
)
|
||||
else:
|
||||
raise HTTPException(
|
||||
@@ -170,7 +165,7 @@ async def update_toolkit_by_id(
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=ERROR_MESSAGES.DEFAULT(e),
|
||||
detail=ERROR_MESSAGES.DEFAULT(str(e)),
|
||||
)
|
||||
|
||||
|
||||
@@ -210,7 +205,7 @@ async def get_toolkit_valves_by_id(id: str, user=Depends(get_admin_user)):
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=ERROR_MESSAGES.DEFAULT(e),
|
||||
detail=ERROR_MESSAGES.DEFAULT(str(e)),
|
||||
)
|
||||
else:
|
||||
raise HTTPException(
|
||||
@@ -233,7 +228,7 @@ async def get_toolkit_valves_spec_by_id(
|
||||
if id in request.app.state.TOOLS:
|
||||
toolkit_module = request.app.state.TOOLS[id]
|
||||
else:
|
||||
toolkit_module, frontmatter = load_toolkit_module_by_id(id)
|
||||
toolkit_module, _ = load_toolkit_module_by_id(id)
|
||||
request.app.state.TOOLS[id] = toolkit_module
|
||||
|
||||
if hasattr(toolkit_module, "Valves"):
|
||||
@@ -261,7 +256,7 @@ async def update_toolkit_valves_by_id(
|
||||
if id in request.app.state.TOOLS:
|
||||
toolkit_module = request.app.state.TOOLS[id]
|
||||
else:
|
||||
toolkit_module, frontmatter = load_toolkit_module_by_id(id)
|
||||
toolkit_module, _ = load_toolkit_module_by_id(id)
|
||||
request.app.state.TOOLS[id] = toolkit_module
|
||||
|
||||
if hasattr(toolkit_module, "Valves"):
|
||||
@@ -276,7 +271,7 @@ async def update_toolkit_valves_by_id(
|
||||
print(e)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=ERROR_MESSAGES.DEFAULT(e),
|
||||
detail=ERROR_MESSAGES.DEFAULT(str(e)),
|
||||
)
|
||||
else:
|
||||
raise HTTPException(
|
||||
@@ -306,7 +301,7 @@ async def get_toolkit_user_valves_by_id(id: str, user=Depends(get_verified_user)
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=ERROR_MESSAGES.DEFAULT(e),
|
||||
detail=ERROR_MESSAGES.DEFAULT(str(e)),
|
||||
)
|
||||
else:
|
||||
raise HTTPException(
|
||||
@@ -324,7 +319,7 @@ async def get_toolkit_user_valves_spec_by_id(
|
||||
if id in request.app.state.TOOLS:
|
||||
toolkit_module = request.app.state.TOOLS[id]
|
||||
else:
|
||||
toolkit_module, frontmatter = load_toolkit_module_by_id(id)
|
||||
toolkit_module, _ = load_toolkit_module_by_id(id)
|
||||
request.app.state.TOOLS[id] = toolkit_module
|
||||
|
||||
if hasattr(toolkit_module, "UserValves"):
|
||||
@@ -348,7 +343,7 @@ async def update_toolkit_user_valves_by_id(
|
||||
if id in request.app.state.TOOLS:
|
||||
toolkit_module = request.app.state.TOOLS[id]
|
||||
else:
|
||||
toolkit_module, frontmatter = load_toolkit_module_by_id(id)
|
||||
toolkit_module, _ = load_toolkit_module_by_id(id)
|
||||
request.app.state.TOOLS[id] = toolkit_module
|
||||
|
||||
if hasattr(toolkit_module, "UserValves"):
|
||||
@@ -365,7 +360,7 @@ async def update_toolkit_user_valves_by_id(
|
||||
print(e)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=ERROR_MESSAGES.DEFAULT(e),
|
||||
detail=ERROR_MESSAGES.DEFAULT(str(e)),
|
||||
)
|
||||
else:
|
||||
raise HTTPException(
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from fastapi import Response, Request
|
||||
from fastapi import Depends, FastAPI, HTTPException, status
|
||||
from datetime import datetime, timedelta
|
||||
from typing import List, Union, Optional
|
||||
from typing import Union, Optional
|
||||
|
||||
from fastapi import APIRouter
|
||||
from pydantic import BaseModel
|
||||
@@ -39,7 +39,7 @@ router = APIRouter()
|
||||
############################
|
||||
|
||||
|
||||
@router.get("/", response_model=List[UserModel])
|
||||
@router.get("/", response_model=list[UserModel])
|
||||
async def get_users(skip: int = 0, limit: int = 50, user=Depends(get_admin_user)):
|
||||
return Users.get_users(skip, limit)
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ from utils.misc import calculate_sha256, get_gravatar_url
|
||||
|
||||
from config import OLLAMA_BASE_URLS, DATA_DIR, UPLOAD_DIR, ENABLE_ADMIN_EXPORT
|
||||
from constants import ERROR_MESSAGES
|
||||
from typing import List
|
||||
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@@ -57,7 +57,7 @@ async def get_html_from_markdown(
|
||||
|
||||
class ChatForm(BaseModel):
|
||||
title: str
|
||||
messages: List[dict]
|
||||
messages: list[dict]
|
||||
|
||||
|
||||
@router.post("/pdf")
|
||||
@@ -85,9 +85,10 @@ async def download_chat_as_pdf(
|
||||
pdf.add_font("NotoSans", "i", f"{FONTS_DIR}/NotoSans-Italic.ttf")
|
||||
pdf.add_font("NotoSansKR", "", f"{FONTS_DIR}/NotoSansKR-Regular.ttf")
|
||||
pdf.add_font("NotoSansJP", "", f"{FONTS_DIR}/NotoSansJP-Regular.ttf")
|
||||
pdf.add_font("NotoSansSC", "", f"{FONTS_DIR}/NotoSansSC-Regular.ttf")
|
||||
|
||||
pdf.set_font("NotoSans", size=12)
|
||||
pdf.set_fallback_fonts(["NotoSansKR", "NotoSansJP"])
|
||||
pdf.set_fallback_fonts(["NotoSansKR", "NotoSansJP", "NotoSansSC"])
|
||||
|
||||
pdf.set_auto_page_break(auto=True, margin=15)
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
from importlib import util
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import subprocess
|
||||
|
||||
from config import TOOLS_DIR, FUNCTIONS_DIR
|
||||
|
||||
@@ -52,6 +54,7 @@ def load_toolkit_module_by_id(toolkit_id):
|
||||
frontmatter = extract_frontmatter(toolkit_path)
|
||||
|
||||
try:
|
||||
install_frontmatter_requirements(frontmatter.get("requirements", ""))
|
||||
spec.loader.exec_module(module)
|
||||
print(f"Loaded module: {module.__name__}")
|
||||
if hasattr(module, "Tools"):
|
||||
@@ -73,6 +76,7 @@ def load_function_module_by_id(function_id):
|
||||
frontmatter = extract_frontmatter(function_path)
|
||||
|
||||
try:
|
||||
install_frontmatter_requirements(frontmatter.get("requirements", ""))
|
||||
spec.loader.exec_module(module)
|
||||
print(f"Loaded module: {module.__name__}")
|
||||
if hasattr(module, "Pipe"):
|
||||
@@ -88,3 +92,13 @@ def load_function_module_by_id(function_id):
|
||||
# Move the file to the error folder
|
||||
os.rename(function_path, f"{function_path}.error")
|
||||
raise e
|
||||
|
||||
|
||||
def install_frontmatter_requirements(requirements):
|
||||
if requirements:
|
||||
req_list = [req.strip() for req in requirements.split(",")]
|
||||
for req in req_list:
|
||||
print(f"Installing requirement: {req}")
|
||||
subprocess.check_call([sys.executable, "-m", "pip", "install", req])
|
||||
else:
|
||||
print("No requirements found in frontmatter.")
|
||||
|
||||
+203
-58
@@ -3,6 +3,8 @@ import sys
|
||||
import logging
|
||||
import importlib.metadata
|
||||
import pkgutil
|
||||
from urllib.parse import urlparse
|
||||
|
||||
import chromadb
|
||||
from chromadb import Settings
|
||||
from bs4 import BeautifulSoup
|
||||
@@ -104,7 +106,7 @@ ENV = os.environ.get("ENV", "dev")
|
||||
|
||||
try:
|
||||
PACKAGE_DATA = json.loads((BASE_DIR / "package.json").read_text())
|
||||
except:
|
||||
except Exception:
|
||||
try:
|
||||
PACKAGE_DATA = {"version": importlib.metadata.version("open-webui")}
|
||||
except importlib.metadata.PackageNotFoundError:
|
||||
@@ -137,7 +139,7 @@ try:
|
||||
with open(str(changelog_path.absolute()), "r", encoding="utf8") as file:
|
||||
changelog_content = file.read()
|
||||
|
||||
except:
|
||||
except Exception:
|
||||
changelog_content = (pkgutil.get_data("open_webui", "CHANGELOG.md") or b"").decode()
|
||||
|
||||
|
||||
@@ -174,7 +176,6 @@ for version in soup.find_all("h2"):
|
||||
|
||||
CHANGELOG = changelog_json
|
||||
|
||||
|
||||
####################################
|
||||
# SAFE_MODE
|
||||
####################################
|
||||
@@ -202,12 +203,12 @@ if RESET_CONFIG_ON_START:
|
||||
os.remove(f"{DATA_DIR}/config.json")
|
||||
with open(f"{DATA_DIR}/config.json", "w") as f:
|
||||
f.write("{}")
|
||||
except:
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
try:
|
||||
CONFIG_DATA = json.loads((DATA_DIR / "config.json").read_text())
|
||||
except:
|
||||
except Exception:
|
||||
CONFIG_DATA = {}
|
||||
|
||||
|
||||
@@ -433,6 +434,12 @@ OAUTH_PICTURE_CLAIM = PersistentConfig(
|
||||
os.environ.get("OAUTH_PICTURE_CLAIM", "picture"),
|
||||
)
|
||||
|
||||
OAUTH_EMAIL_CLAIM = PersistentConfig(
|
||||
"OAUTH_EMAIL_CLAIM",
|
||||
"oauth.oidc.email_claim",
|
||||
os.environ.get("OAUTH_EMAIL_CLAIM", "email"),
|
||||
)
|
||||
|
||||
|
||||
def load_oauth_providers():
|
||||
OAUTH_PROVIDERS.clear()
|
||||
@@ -641,7 +648,7 @@ if AIOHTTP_CLIENT_TIMEOUT == "":
|
||||
else:
|
||||
try:
|
||||
AIOHTTP_CLIENT_TIMEOUT = int(AIOHTTP_CLIENT_TIMEOUT)
|
||||
except:
|
||||
except Exception:
|
||||
AIOHTTP_CLIENT_TIMEOUT = 300
|
||||
|
||||
|
||||
@@ -721,7 +728,7 @@ try:
|
||||
OPENAI_API_KEY = OPENAI_API_KEYS.value[
|
||||
OPENAI_API_BASE_URLS.value.index("https://api.openai.com/v1")
|
||||
]
|
||||
except:
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
OPENAI_API_BASE_URL = "https://api.openai.com/v1"
|
||||
@@ -800,10 +807,24 @@ USER_PERMISSIONS_CHAT_DELETION = (
|
||||
os.environ.get("USER_PERMISSIONS_CHAT_DELETION", "True").lower() == "true"
|
||||
)
|
||||
|
||||
USER_PERMISSIONS_CHAT_EDITING = (
|
||||
os.environ.get("USER_PERMISSIONS_CHAT_EDITING", "True").lower() == "true"
|
||||
)
|
||||
|
||||
USER_PERMISSIONS_CHAT_TEMPORARY = (
|
||||
os.environ.get("USER_PERMISSIONS_CHAT_TEMPORARY", "True").lower() == "true"
|
||||
)
|
||||
|
||||
USER_PERMISSIONS = PersistentConfig(
|
||||
"USER_PERMISSIONS",
|
||||
"ui.user_permissions",
|
||||
{"chat": {"deletion": USER_PERMISSIONS_CHAT_DELETION}},
|
||||
{
|
||||
"chat": {
|
||||
"deletion": USER_PERMISSIONS_CHAT_DELETION,
|
||||
"editing": USER_PERMISSIONS_CHAT_EDITING,
|
||||
"temporary": USER_PERMISSIONS_CHAT_TEMPORARY,
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
ENABLE_MODEL_FILTER = PersistentConfig(
|
||||
@@ -824,12 +845,57 @@ WEBHOOK_URL = PersistentConfig(
|
||||
|
||||
ENABLE_ADMIN_EXPORT = os.environ.get("ENABLE_ADMIN_EXPORT", "True").lower() == "true"
|
||||
|
||||
ENABLE_ADMIN_CHAT_ACCESS = (
|
||||
os.environ.get("ENABLE_ADMIN_CHAT_ACCESS", "True").lower() == "true"
|
||||
)
|
||||
|
||||
ENABLE_COMMUNITY_SHARING = PersistentConfig(
|
||||
"ENABLE_COMMUNITY_SHARING",
|
||||
"ui.enable_community_sharing",
|
||||
os.environ.get("ENABLE_COMMUNITY_SHARING", "True").lower() == "true",
|
||||
)
|
||||
|
||||
ENABLE_MESSAGE_RATING = PersistentConfig(
|
||||
"ENABLE_MESSAGE_RATING",
|
||||
"ui.enable_message_rating",
|
||||
os.environ.get("ENABLE_MESSAGE_RATING", "True").lower() == "true",
|
||||
)
|
||||
|
||||
|
||||
def validate_cors_origins(origins):
|
||||
for origin in origins:
|
||||
if origin != "*":
|
||||
validate_cors_origin(origin)
|
||||
|
||||
|
||||
def validate_cors_origin(origin):
|
||||
parsed_url = urlparse(origin)
|
||||
|
||||
# Check if the scheme is either http or https
|
||||
if parsed_url.scheme not in ["http", "https"]:
|
||||
raise ValueError(
|
||||
f"Invalid scheme in CORS_ALLOW_ORIGIN: '{origin}'. Only 'http' and 'https' are allowed."
|
||||
)
|
||||
|
||||
# Ensure that the netloc (domain + port) is present, indicating it's a valid URL
|
||||
if not parsed_url.netloc:
|
||||
raise ValueError(f"Invalid URL structure in CORS_ALLOW_ORIGIN: '{origin}'.")
|
||||
|
||||
|
||||
# For production, you should only need one host as
|
||||
# fastapi serves the svelte-kit built frontend and backend from the same host and port.
|
||||
# To test CORS_ALLOW_ORIGIN locally, you can set something like
|
||||
# CORS_ALLOW_ORIGIN=http://localhost:5173;http://localhost:8080
|
||||
# in your .env file depending on your frontend port, 5173 in this case.
|
||||
CORS_ALLOW_ORIGIN = os.environ.get("CORS_ALLOW_ORIGIN", "*").split(";")
|
||||
|
||||
if "*" in CORS_ALLOW_ORIGIN:
|
||||
log.warning(
|
||||
"\n\nWARNING: CORS_ALLOW_ORIGIN IS SET TO '*' - NOT RECOMMENDED FOR PRODUCTION DEPLOYMENTS.\n"
|
||||
)
|
||||
|
||||
validate_cors_origins(CORS_ALLOW_ORIGIN)
|
||||
|
||||
|
||||
class BannerModel(BaseModel):
|
||||
id: str
|
||||
@@ -885,10 +951,7 @@ TITLE_GENERATION_PROMPT_TEMPLATE = PersistentConfig(
|
||||
"task.title.prompt_template",
|
||||
os.environ.get(
|
||||
"TITLE_GENERATION_PROMPT_TEMPLATE",
|
||||
"""Here is the query:
|
||||
{{prompt:middletruncate:8000}}
|
||||
|
||||
Create a concise, 3-5 word phrase with an emoji as a title for the previous query. Suitable Emojis for the summary can be used to enhance understanding but avoid quotation marks or special formatting. RESPOND ONLY WITH THE TITLE TEXT.
|
||||
"""Create a concise, 3-5 word title with an emoji as a title for the prompt in the given language. Suitable Emojis for the summary can be used to enhance understanding but avoid quotation marks or special formatting. RESPOND ONLY WITH THE TITLE TEXT.
|
||||
|
||||
Examples of titles:
|
||||
📉 Stock Market Trends
|
||||
@@ -896,7 +959,9 @@ Examples of titles:
|
||||
Evolution of Music Streaming
|
||||
Remote Work Productivity Tips
|
||||
Artificial Intelligence in Healthcare
|
||||
🎮 Video Game Development Insights""",
|
||||
🎮 Video Game Development Insights
|
||||
|
||||
Prompt: {{prompt:middletruncate:8000}}""",
|
||||
),
|
||||
)
|
||||
|
||||
@@ -929,8 +994,7 @@ TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE = PersistentConfig(
|
||||
"task.tools.prompt_template",
|
||||
os.environ.get(
|
||||
"TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE",
|
||||
"""Tools: {{TOOLS}}
|
||||
If a function tool doesn't match the query, return an empty string. Else, pick a function tool, fill in the parameters from the function tool's schema, and return it in the format { "name": \"functionName\", "parameters": { "key": "value" } }. Only pick a function if the user asks. Only return the object. Do not return any other text.""",
|
||||
"""Available Tools: {{TOOLS}}\nReturn an empty string if no tools match the query. If a function tool matches, construct and return a JSON object in the format {\"name\": \"functionName\", \"parameters\": {\"requiredFunctionParamKey\": \"requiredFunctionParamValue\"}} using the appropriate tool and its parameters. Only return the object and limit the response to the JSON object without additional text.""",
|
||||
),
|
||||
)
|
||||
|
||||
@@ -1033,7 +1097,7 @@ RAG_EMBEDDING_MODEL = PersistentConfig(
|
||||
"rag.embedding_model",
|
||||
os.environ.get("RAG_EMBEDDING_MODEL", "sentence-transformers/all-MiniLM-L6-v2"),
|
||||
)
|
||||
log.info(f"Embedding model set: {RAG_EMBEDDING_MODEL.value}"),
|
||||
log.info(f"Embedding model set: {RAG_EMBEDDING_MODEL.value}")
|
||||
|
||||
RAG_EMBEDDING_MODEL_AUTO_UPDATE = (
|
||||
os.environ.get("RAG_EMBEDDING_MODEL_AUTO_UPDATE", "").lower() == "true"
|
||||
@@ -1046,7 +1110,7 @@ RAG_EMBEDDING_MODEL_TRUST_REMOTE_CODE = (
|
||||
RAG_EMBEDDING_OPENAI_BATCH_SIZE = PersistentConfig(
|
||||
"RAG_EMBEDDING_OPENAI_BATCH_SIZE",
|
||||
"rag.embedding_openai_batch_size",
|
||||
os.environ.get("RAG_EMBEDDING_OPENAI_BATCH_SIZE", 1),
|
||||
int(os.environ.get("RAG_EMBEDDING_OPENAI_BATCH_SIZE", "1")),
|
||||
)
|
||||
|
||||
RAG_RERANKING_MODEL = PersistentConfig(
|
||||
@@ -1055,7 +1119,7 @@ RAG_RERANKING_MODEL = PersistentConfig(
|
||||
os.environ.get("RAG_RERANKING_MODEL", ""),
|
||||
)
|
||||
if RAG_RERANKING_MODEL.value != "":
|
||||
log.info(f"Reranking model set: {RAG_RERANKING_MODEL.value}"),
|
||||
log.info(f"Reranking model set: {RAG_RERANKING_MODEL.value}")
|
||||
|
||||
RAG_RERANKING_MODEL_AUTO_UPDATE = (
|
||||
os.environ.get("RAG_RERANKING_MODEL_AUTO_UPDATE", "").lower() == "true"
|
||||
@@ -1278,46 +1342,127 @@ COMFYUI_BASE_URL = PersistentConfig(
|
||||
os.getenv("COMFYUI_BASE_URL", ""),
|
||||
)
|
||||
|
||||
COMFYUI_CFG_SCALE = PersistentConfig(
|
||||
"COMFYUI_CFG_SCALE",
|
||||
"image_generation.comfyui.cfg_scale",
|
||||
os.getenv("COMFYUI_CFG_SCALE", ""),
|
||||
COMFYUI_DEFAULT_WORKFLOW = """
|
||||
{
|
||||
"3": {
|
||||
"inputs": {
|
||||
"seed": 0,
|
||||
"steps": 20,
|
||||
"cfg": 8,
|
||||
"sampler_name": "euler",
|
||||
"scheduler": "normal",
|
||||
"denoise": 1,
|
||||
"model": [
|
||||
"4",
|
||||
0
|
||||
],
|
||||
"positive": [
|
||||
"6",
|
||||
0
|
||||
],
|
||||
"negative": [
|
||||
"7",
|
||||
0
|
||||
],
|
||||
"latent_image": [
|
||||
"5",
|
||||
0
|
||||
]
|
||||
},
|
||||
"class_type": "KSampler",
|
||||
"_meta": {
|
||||
"title": "KSampler"
|
||||
}
|
||||
},
|
||||
"4": {
|
||||
"inputs": {
|
||||
"ckpt_name": "model.safetensors"
|
||||
},
|
||||
"class_type": "CheckpointLoaderSimple",
|
||||
"_meta": {
|
||||
"title": "Load Checkpoint"
|
||||
}
|
||||
},
|
||||
"5": {
|
||||
"inputs": {
|
||||
"width": 512,
|
||||
"height": 512,
|
||||
"batch_size": 1
|
||||
},
|
||||
"class_type": "EmptyLatentImage",
|
||||
"_meta": {
|
||||
"title": "Empty Latent Image"
|
||||
}
|
||||
},
|
||||
"6": {
|
||||
"inputs": {
|
||||
"text": "Prompt",
|
||||
"clip": [
|
||||
"4",
|
||||
1
|
||||
]
|
||||
},
|
||||
"class_type": "CLIPTextEncode",
|
||||
"_meta": {
|
||||
"title": "CLIP Text Encode (Prompt)"
|
||||
}
|
||||
},
|
||||
"7": {
|
||||
"inputs": {
|
||||
"text": "",
|
||||
"clip": [
|
||||
"4",
|
||||
1
|
||||
]
|
||||
},
|
||||
"class_type": "CLIPTextEncode",
|
||||
"_meta": {
|
||||
"title": "CLIP Text Encode (Prompt)"
|
||||
}
|
||||
},
|
||||
"8": {
|
||||
"inputs": {
|
||||
"samples": [
|
||||
"3",
|
||||
0
|
||||
],
|
||||
"vae": [
|
||||
"4",
|
||||
2
|
||||
]
|
||||
},
|
||||
"class_type": "VAEDecode",
|
||||
"_meta": {
|
||||
"title": "VAE Decode"
|
||||
}
|
||||
},
|
||||
"9": {
|
||||
"inputs": {
|
||||
"filename_prefix": "ComfyUI",
|
||||
"images": [
|
||||
"8",
|
||||
0
|
||||
]
|
||||
},
|
||||
"class_type": "SaveImage",
|
||||
"_meta": {
|
||||
"title": "Save Image"
|
||||
}
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
|
||||
COMFYUI_WORKFLOW = PersistentConfig(
|
||||
"COMFYUI_WORKFLOW",
|
||||
"image_generation.comfyui.workflow",
|
||||
os.getenv("COMFYUI_WORKFLOW", COMFYUI_DEFAULT_WORKFLOW),
|
||||
)
|
||||
|
||||
COMFYUI_SAMPLER = PersistentConfig(
|
||||
"COMFYUI_SAMPLER",
|
||||
"image_generation.comfyui.sampler",
|
||||
os.getenv("COMFYUI_SAMPLER", ""),
|
||||
)
|
||||
|
||||
COMFYUI_SCHEDULER = PersistentConfig(
|
||||
"COMFYUI_SCHEDULER",
|
||||
"image_generation.comfyui.scheduler",
|
||||
os.getenv("COMFYUI_SCHEDULER", ""),
|
||||
)
|
||||
|
||||
COMFYUI_SD3 = PersistentConfig(
|
||||
"COMFYUI_SD3",
|
||||
"image_generation.comfyui.sd3",
|
||||
os.environ.get("COMFYUI_SD3", "").lower() == "true",
|
||||
)
|
||||
|
||||
COMFYUI_FLUX = PersistentConfig(
|
||||
"COMFYUI_FLUX",
|
||||
"image_generation.comfyui.flux",
|
||||
os.environ.get("COMFYUI_FLUX", "").lower() == "true",
|
||||
)
|
||||
|
||||
COMFYUI_FLUX_WEIGHT_DTYPE = PersistentConfig(
|
||||
"COMFYUI_FLUX_WEIGHT_DTYPE",
|
||||
"image_generation.comfyui.flux_weight_dtype",
|
||||
os.getenv("COMFYUI_FLUX_WEIGHT_DTYPE", ""),
|
||||
)
|
||||
|
||||
COMFYUI_FLUX_FP8_CLIP = PersistentConfig(
|
||||
"COMFYUI_FLUX_FP8_CLIP",
|
||||
"image_generation.comfyui.flux_fp8_clip",
|
||||
os.getenv("COMFYUI_FLUX_FP8_CLIP", ""),
|
||||
COMFYUI_WORKFLOW_NODES = PersistentConfig(
|
||||
"COMFYUI_WORKFLOW",
|
||||
"image_generation.comfyui.nodes",
|
||||
[],
|
||||
)
|
||||
|
||||
IMAGES_OPENAI_API_BASE_URL = PersistentConfig(
|
||||
@@ -1400,13 +1545,13 @@ AUDIO_TTS_ENGINE = PersistentConfig(
|
||||
AUDIO_TTS_MODEL = PersistentConfig(
|
||||
"AUDIO_TTS_MODEL",
|
||||
"audio.tts.model",
|
||||
os.getenv("AUDIO_TTS_MODEL", "tts-1"),
|
||||
os.getenv("AUDIO_TTS_MODEL", "tts-1"), # OpenAI default model
|
||||
)
|
||||
|
||||
AUDIO_TTS_VOICE = PersistentConfig(
|
||||
"AUDIO_TTS_VOICE",
|
||||
"audio.tts.voice",
|
||||
os.getenv("AUDIO_TTS_VOICE", "alloy"),
|
||||
os.getenv("AUDIO_TTS_VOICE", "alloy"), # OpenAI default voice
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -100,3 +100,4 @@ class TASKS(str, Enum):
|
||||
EMOJI_GENERATION = "emoji_generation"
|
||||
QUERY_GENERATION = "query_generation"
|
||||
FUNCTION_CALLING = "function_calling"
|
||||
MOA_RESPONSE_GENERATION = "moa_response_generation"
|
||||
|
||||
+457
-521
File diff suppressed because it is too large
Load Diff
+18
-17
@@ -1,5 +1,5 @@
|
||||
fastapi==0.111.0
|
||||
uvicorn[standard]==0.22.0
|
||||
uvicorn[standard]==0.30.6
|
||||
pydantic==2.8.2
|
||||
python-multipart==0.0.9
|
||||
|
||||
@@ -11,18 +11,19 @@ python-jose==3.3.0
|
||||
passlib[bcrypt]==1.7.4
|
||||
|
||||
requests==2.32.3
|
||||
aiohttp==3.9.5
|
||||
sqlalchemy==2.0.31
|
||||
aiohttp==3.10.2
|
||||
|
||||
sqlalchemy==2.0.32
|
||||
alembic==1.13.2
|
||||
peewee==3.17.6
|
||||
peewee-migrate==1.12.2
|
||||
psycopg2-binary==2.9.9
|
||||
PyMySQL==1.1.1
|
||||
bcrypt==4.1.3
|
||||
SQLAlchemy
|
||||
bcrypt==4.2.0
|
||||
|
||||
pymongo
|
||||
redis
|
||||
boto3==1.34.110
|
||||
boto3==1.35.0
|
||||
|
||||
argon2-cffi==23.1.0
|
||||
APScheduler==3.10.4
|
||||
@@ -33,24 +34,24 @@ anthropic
|
||||
google-generativeai==0.7.2
|
||||
tiktoken
|
||||
|
||||
langchain==0.2.11
|
||||
langchain==0.2.12
|
||||
langchain-community==0.2.10
|
||||
langchain-chroma==0.1.2
|
||||
|
||||
fake-useragent==1.5.1
|
||||
chromadb==0.5.4
|
||||
chromadb==0.5.5
|
||||
sentence-transformers==3.0.1
|
||||
pypdf==4.2.0
|
||||
pypdf==4.3.1
|
||||
docx2txt==0.8
|
||||
python-pptx==0.6.23
|
||||
unstructured==0.15.0
|
||||
python-pptx==1.0.0
|
||||
unstructured==0.15.5
|
||||
Markdown==3.6
|
||||
pypandoc==1.13
|
||||
pandas==2.2.2
|
||||
openpyxl==3.1.5
|
||||
pyxlsb==1.0.10
|
||||
xlrd==2.0.1
|
||||
validators==0.28.1
|
||||
validators==0.33.0
|
||||
psutil
|
||||
|
||||
opencv-python-headless==4.10.0.84
|
||||
@@ -59,13 +60,13 @@ rapidocr-onnxruntime==1.3.24
|
||||
fpdf2==2.7.9
|
||||
rank-bm25==0.2.2
|
||||
|
||||
faster-whisper==1.0.2
|
||||
faster-whisper==1.0.3
|
||||
|
||||
PyJWT[crypto]==2.8.0
|
||||
PyJWT[crypto]==2.9.0
|
||||
authlib==1.3.1
|
||||
|
||||
black==24.4.2
|
||||
langfuse==2.39.2
|
||||
black==24.8.0
|
||||
langfuse==2.43.3
|
||||
youtube-transcript-api==0.6.2
|
||||
pytube==15.0.0
|
||||
|
||||
@@ -75,5 +76,5 @@ duckduckgo-search~=6.2.1
|
||||
|
||||
## Tests
|
||||
docker~=7.1.0
|
||||
pytest~=8.2.2
|
||||
pytest~=8.3.2
|
||||
pytest-docker~=3.1.1
|
||||
|
||||
@@ -30,7 +30,6 @@ if [[ "${USE_CUDA_DOCKER,,}" == "true" ]]; then
|
||||
export LD_LIBRARY_PATH="$LD_LIBRARY_PATH:/usr/local/lib/python3.11/site-packages/torch/lib:/usr/local/lib/python3.11/site-packages/nvidia/cudnn/lib"
|
||||
fi
|
||||
|
||||
|
||||
# Check if SPACE_ID is set, if so, configure for space
|
||||
if [ -n "$SPACE_ID" ]; then
|
||||
echo "Configuring for HuggingFace Space deployment"
|
||||
|
||||
Binary file not shown.
+97
-12
@@ -2,12 +2,14 @@ from pathlib import Path
|
||||
import hashlib
|
||||
import re
|
||||
from datetime import timedelta
|
||||
from typing import Optional, List, Tuple
|
||||
from typing import Optional, Callable
|
||||
import uuid
|
||||
import time
|
||||
|
||||
from utils.task import prompt_template
|
||||
|
||||
def get_last_user_message_item(messages: List[dict]) -> Optional[dict]:
|
||||
|
||||
def get_last_user_message_item(messages: list[dict]) -> Optional[dict]:
|
||||
for message in reversed(messages):
|
||||
if message["role"] == "user":
|
||||
return message
|
||||
@@ -24,7 +26,7 @@ def get_content_from_message(message: dict) -> Optional[str]:
|
||||
return None
|
||||
|
||||
|
||||
def get_last_user_message(messages: List[dict]) -> Optional[str]:
|
||||
def get_last_user_message(messages: list[dict]) -> Optional[str]:
|
||||
message = get_last_user_message_item(messages)
|
||||
if message is None:
|
||||
return None
|
||||
@@ -32,31 +34,31 @@ def get_last_user_message(messages: List[dict]) -> Optional[str]:
|
||||
return get_content_from_message(message)
|
||||
|
||||
|
||||
def get_last_assistant_message(messages: List[dict]) -> Optional[str]:
|
||||
def get_last_assistant_message(messages: list[dict]) -> Optional[str]:
|
||||
for message in reversed(messages):
|
||||
if message["role"] == "assistant":
|
||||
return get_content_from_message(message)
|
||||
return None
|
||||
|
||||
|
||||
def get_system_message(messages: List[dict]) -> Optional[dict]:
|
||||
def get_system_message(messages: list[dict]) -> Optional[dict]:
|
||||
for message in messages:
|
||||
if message["role"] == "system":
|
||||
return message
|
||||
return None
|
||||
|
||||
|
||||
def remove_system_message(messages: List[dict]) -> List[dict]:
|
||||
def remove_system_message(messages: list[dict]) -> list[dict]:
|
||||
return [message for message in messages if message["role"] != "system"]
|
||||
|
||||
|
||||
def pop_system_message(messages: List[dict]) -> Tuple[Optional[dict], List[dict]]:
|
||||
def pop_system_message(messages: list[dict]) -> tuple[Optional[dict], list[dict]]:
|
||||
return get_system_message(messages), remove_system_message(messages)
|
||||
|
||||
|
||||
def prepend_to_first_user_message_content(
|
||||
content: str, messages: List[dict]
|
||||
) -> List[dict]:
|
||||
content: str, messages: list[dict]
|
||||
) -> list[dict]:
|
||||
for message in messages:
|
||||
if message["role"] == "user":
|
||||
if isinstance(message["content"], list):
|
||||
@@ -69,7 +71,7 @@ def prepend_to_first_user_message_content(
|
||||
return messages
|
||||
|
||||
|
||||
def add_or_update_system_message(content: str, messages: List[dict]):
|
||||
def add_or_update_system_message(content: str, messages: list[dict]):
|
||||
"""
|
||||
Adds a new system message at the beginning of the messages list
|
||||
or updates the existing system message at the beginning.
|
||||
@@ -97,18 +99,101 @@ def openai_chat_message_template(model: str):
|
||||
}
|
||||
|
||||
|
||||
def openai_chat_chunk_message_template(model: str, message: str):
|
||||
def openai_chat_chunk_message_template(model: str, message: str) -> dict:
|
||||
template = openai_chat_message_template(model)
|
||||
template["object"] = "chat.completion.chunk"
|
||||
template["choices"][0]["delta"] = {"content": message}
|
||||
return template
|
||||
|
||||
|
||||
def openai_chat_completion_message_template(model: str, message: str):
|
||||
def openai_chat_completion_message_template(model: str, message: str) -> dict:
|
||||
template = openai_chat_message_template(model)
|
||||
template["object"] = "chat.completion"
|
||||
template["choices"][0]["message"] = {"content": message, "role": "assistant"}
|
||||
template["choices"][0]["finish_reason"] = "stop"
|
||||
return template
|
||||
|
||||
|
||||
# inplace function: form_data is modified
|
||||
def apply_model_system_prompt_to_body(params: dict, form_data: dict, user) -> dict:
|
||||
system = params.get("system", None)
|
||||
if not system:
|
||||
return form_data
|
||||
|
||||
if user:
|
||||
template_params = {
|
||||
"user_name": user.name,
|
||||
"user_location": user.info.get("location") if user.info else None,
|
||||
}
|
||||
else:
|
||||
template_params = {}
|
||||
system = prompt_template(system, **template_params)
|
||||
form_data["messages"] = add_or_update_system_message(
|
||||
system, form_data.get("messages", [])
|
||||
)
|
||||
return form_data
|
||||
|
||||
|
||||
# inplace function: form_data is modified
|
||||
def apply_model_params_to_body(
|
||||
params: dict, form_data: dict, mappings: dict[str, Callable]
|
||||
) -> dict:
|
||||
if not params:
|
||||
return form_data
|
||||
|
||||
for key, cast_func in mappings.items():
|
||||
if (value := params.get(key)) is not None:
|
||||
form_data[key] = cast_func(value)
|
||||
|
||||
return form_data
|
||||
|
||||
|
||||
# inplace function: form_data is modified
|
||||
def apply_model_params_to_body_openai(params: dict, form_data: dict) -> dict:
|
||||
mappings = {
|
||||
"temperature": float,
|
||||
"top_p": int,
|
||||
"max_tokens": int,
|
||||
"frequency_penalty": int,
|
||||
"seed": lambda x: x,
|
||||
"stop": lambda x: [bytes(s, "utf-8").decode("unicode_escape") for s in x],
|
||||
}
|
||||
return apply_model_params_to_body(params, form_data, mappings)
|
||||
|
||||
|
||||
def apply_model_params_to_body_ollama(params: dict, form_data: dict) -> dict:
|
||||
opts = [
|
||||
"temperature",
|
||||
"top_p",
|
||||
"seed",
|
||||
"mirostat",
|
||||
"mirostat_eta",
|
||||
"mirostat_tau",
|
||||
"num_ctx",
|
||||
"num_batch",
|
||||
"num_keep",
|
||||
"repeat_last_n",
|
||||
"tfs_z",
|
||||
"top_k",
|
||||
"min_p",
|
||||
"use_mmap",
|
||||
"use_mlock",
|
||||
"num_thread",
|
||||
"num_gpu",
|
||||
]
|
||||
mappings = {i: lambda x: x for i in opts}
|
||||
form_data = apply_model_params_to_body(params, form_data, mappings)
|
||||
|
||||
name_differences = {
|
||||
"max_tokens": "num_predict",
|
||||
"frequency_penalty": "repeat_penalty",
|
||||
}
|
||||
|
||||
for key, value in name_differences.items():
|
||||
if (param := params.get(key, None)) is not None:
|
||||
form_data[value] = param
|
||||
|
||||
return form_data
|
||||
|
||||
|
||||
def get_gravatar_url(email):
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
from pydantic import BaseModel, Field, create_model
|
||||
from typing import Any, Optional, Type
|
||||
|
||||
|
||||
def json_schema_to_model(tool_dict: dict[str, Any]) -> Type[BaseModel]:
|
||||
"""
|
||||
Converts a JSON schema to a Pydantic BaseModel class.
|
||||
|
||||
Args:
|
||||
json_schema: The JSON schema to convert.
|
||||
|
||||
Returns:
|
||||
A Pydantic BaseModel class.
|
||||
"""
|
||||
|
||||
# Extract the model name from the schema title.
|
||||
model_name = tool_dict["name"]
|
||||
schema = tool_dict["parameters"]
|
||||
|
||||
# Extract the field definitions from the schema properties.
|
||||
field_definitions = {
|
||||
name: json_schema_to_pydantic_field(name, prop, schema.get("required", []))
|
||||
for name, prop in schema.get("properties", {}).items()
|
||||
}
|
||||
|
||||
# Create the BaseModel class using create_model().
|
||||
return create_model(model_name, **field_definitions)
|
||||
|
||||
|
||||
def json_schema_to_pydantic_field(
|
||||
name: str, json_schema: dict[str, Any], required: list[str]
|
||||
) -> Any:
|
||||
"""
|
||||
Converts a JSON schema property to a Pydantic field definition.
|
||||
|
||||
Args:
|
||||
name: The field name.
|
||||
json_schema: The JSON schema property.
|
||||
|
||||
Returns:
|
||||
A Pydantic field definition.
|
||||
"""
|
||||
|
||||
# Get the field type.
|
||||
type_ = json_schema_to_pydantic_type(json_schema)
|
||||
|
||||
# Get the field description.
|
||||
description = json_schema.get("description")
|
||||
|
||||
# Get the field examples.
|
||||
examples = json_schema.get("examples")
|
||||
|
||||
# Create a Field object with the type, description, and examples.
|
||||
# The 'required' flag will be set later when creating the model.
|
||||
return (
|
||||
type_,
|
||||
Field(
|
||||
description=description,
|
||||
examples=examples,
|
||||
default=... if name in required else None,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def json_schema_to_pydantic_type(json_schema: dict[str, Any]) -> Any:
|
||||
"""
|
||||
Converts a JSON schema type to a Pydantic type.
|
||||
|
||||
Args:
|
||||
json_schema: The JSON schema to convert.
|
||||
|
||||
Returns:
|
||||
A Pydantic type.
|
||||
"""
|
||||
|
||||
type_ = json_schema.get("type")
|
||||
|
||||
if type_ == "string" or type_ == "str":
|
||||
return str
|
||||
elif type_ == "integer" or type_ == "int":
|
||||
return int
|
||||
elif type_ == "number" or type_ == "float":
|
||||
return float
|
||||
elif type_ == "boolean" or type_ == "bool":
|
||||
return bool
|
||||
elif type_ == "array":
|
||||
items_schema = json_schema.get("items")
|
||||
if items_schema:
|
||||
item_type = json_schema_to_pydantic_type(items_schema)
|
||||
return list[item_type]
|
||||
else:
|
||||
return list
|
||||
elif type_ == "object":
|
||||
# Handle nested models.
|
||||
properties = json_schema.get("properties")
|
||||
if properties:
|
||||
nested_model = json_schema_to_model(json_schema)
|
||||
return nested_model
|
||||
else:
|
||||
return dict
|
||||
elif type_ == "null":
|
||||
return Optional[Any] # Use Optional[Any] for nullable fields
|
||||
else:
|
||||
raise ValueError(f"Unsupported JSON schema type: {type_}")
|
||||
+38
-2
@@ -6,7 +6,7 @@ from typing import Optional
|
||||
|
||||
|
||||
def prompt_template(
|
||||
template: str, user_name: str = None, user_location: str = None
|
||||
template: str, user_name: Optional[str] = None, user_location: Optional[str] = None
|
||||
) -> str:
|
||||
# Get the current date
|
||||
current_date = datetime.now()
|
||||
@@ -83,7 +83,6 @@ def title_generation_template(
|
||||
def search_query_generation_template(
|
||||
template: str, prompt: str, user: Optional[dict] = None
|
||||
) -> str:
|
||||
|
||||
def replacement_function(match):
|
||||
full_match = match.group(0)
|
||||
start_length = match.group(1)
|
||||
@@ -122,6 +121,43 @@ def search_query_generation_template(
|
||||
return template
|
||||
|
||||
|
||||
def moa_response_generation_template(
|
||||
template: str, prompt: str, responses: list[str]
|
||||
) -> str:
|
||||
def replacement_function(match):
|
||||
full_match = match.group(0)
|
||||
start_length = match.group(1)
|
||||
end_length = match.group(2)
|
||||
middle_length = match.group(3)
|
||||
|
||||
if full_match == "{{prompt}}":
|
||||
return prompt
|
||||
elif start_length is not None:
|
||||
return prompt[: int(start_length)]
|
||||
elif end_length is not None:
|
||||
return prompt[-int(end_length) :]
|
||||
elif middle_length is not None:
|
||||
middle_length = int(middle_length)
|
||||
if len(prompt) <= middle_length:
|
||||
return prompt
|
||||
start = prompt[: math.ceil(middle_length / 2)]
|
||||
end = prompt[-math.floor(middle_length / 2) :]
|
||||
return f"{start}...{end}"
|
||||
return ""
|
||||
|
||||
template = re.sub(
|
||||
r"{{prompt}}|{{prompt:start:(\d+)}}|{{prompt:end:(\d+)}}|{{prompt:middletruncate:(\d+)}}",
|
||||
replacement_function,
|
||||
template,
|
||||
)
|
||||
|
||||
responses = [f'"""{response}"""' for response in responses]
|
||||
responses = "\n\n".join(responses)
|
||||
|
||||
template = template.replace("{{responses}}", responses)
|
||||
return template
|
||||
|
||||
|
||||
def tools_function_calling_generation_template(template: str, tools_specs: str) -> str:
|
||||
template = template.replace("{{TOOLS}}", tools_specs)
|
||||
return template
|
||||
|
||||
+87
-2
@@ -1,5 +1,90 @@
|
||||
import inspect
|
||||
from typing import get_type_hints, List, Dict, Any
|
||||
import logging
|
||||
from typing import Awaitable, Callable, get_type_hints
|
||||
|
||||
from apps.webui.models.tools import Tools
|
||||
from apps.webui.models.users import UserModel
|
||||
from apps.webui.utils import load_toolkit_module_by_id
|
||||
|
||||
from utils.schemas import json_schema_to_model
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def apply_extra_params_to_tool_function(
|
||||
function: Callable, extra_params: dict
|
||||
) -> Callable[..., Awaitable]:
|
||||
sig = inspect.signature(function)
|
||||
extra_params = {
|
||||
key: value for key, value in extra_params.items() if key in sig.parameters
|
||||
}
|
||||
is_coroutine = inspect.iscoroutinefunction(function)
|
||||
|
||||
async def new_function(**kwargs):
|
||||
extra_kwargs = kwargs | extra_params
|
||||
if is_coroutine:
|
||||
return await function(**extra_kwargs)
|
||||
return function(**extra_kwargs)
|
||||
|
||||
return new_function
|
||||
|
||||
|
||||
# Mutation on extra_params
|
||||
def get_tools(
|
||||
webui_app, tool_ids: list[str], user: UserModel, extra_params: dict
|
||||
) -> dict[str, dict]:
|
||||
tools = {}
|
||||
for tool_id in tool_ids:
|
||||
toolkit = Tools.get_tool_by_id(tool_id)
|
||||
if toolkit is None:
|
||||
continue
|
||||
|
||||
module = webui_app.state.TOOLS.get(tool_id, None)
|
||||
if module is None:
|
||||
module, _ = load_toolkit_module_by_id(tool_id)
|
||||
webui_app.state.TOOLS[tool_id] = module
|
||||
|
||||
extra_params["__id__"] = tool_id
|
||||
if hasattr(module, "valves") and hasattr(module, "Valves"):
|
||||
valves = Tools.get_tool_valves_by_id(tool_id) or {}
|
||||
module.valves = module.Valves(**valves)
|
||||
|
||||
if hasattr(module, "UserValves"):
|
||||
extra_params["__user__"]["valves"] = module.UserValves( # type: ignore
|
||||
**Tools.get_user_valves_by_id_and_user_id(tool_id, user.id)
|
||||
)
|
||||
|
||||
for spec in toolkit.specs:
|
||||
# TODO: Fix hack for OpenAI API
|
||||
for val in spec.get("parameters", {}).get("properties", {}).values():
|
||||
if val["type"] == "str":
|
||||
val["type"] = "string"
|
||||
function_name = spec["name"]
|
||||
|
||||
# convert to function that takes only model params and inserts custom params
|
||||
original_func = getattr(module, function_name)
|
||||
callable = apply_extra_params_to_tool_function(original_func, extra_params)
|
||||
if hasattr(original_func, "__doc__"):
|
||||
callable.__doc__ = original_func.__doc__
|
||||
|
||||
# TODO: This needs to be a pydantic model
|
||||
tool_dict = {
|
||||
"toolkit_id": tool_id,
|
||||
"callable": callable,
|
||||
"spec": spec,
|
||||
"pydantic_model": json_schema_to_model(spec),
|
||||
"file_handler": hasattr(module, "file_handler") and module.file_handler,
|
||||
"citation": hasattr(module, "citation") and module.citation,
|
||||
}
|
||||
|
||||
# TODO: if collision, prepend toolkit name
|
||||
if function_name in tools:
|
||||
log.warning(f"Tool {function_name} already exists in another toolkit!")
|
||||
log.warning(f"Collision between {toolkit} and {tool_id}.")
|
||||
log.warning(f"Discarding {toolkit}.{function_name}")
|
||||
else:
|
||||
tools[function_name] = tool_dict
|
||||
return tools
|
||||
|
||||
|
||||
def doc_to_dict(docstring):
|
||||
@@ -16,7 +101,7 @@ def doc_to_dict(docstring):
|
||||
return ret_dict
|
||||
|
||||
|
||||
def get_tools_specs(tools) -> List[dict]:
|
||||
def get_tools_specs(tools) -> list[dict]:
|
||||
function_list = [
|
||||
{"name": func, "function": getattr(tools, func)}
|
||||
for func in dir(tools)
|
||||
|
||||
@@ -1,15 +1,12 @@
|
||||
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
|
||||
from fastapi import HTTPException, status, Depends, Request
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from apps.webui.models.users import Users
|
||||
|
||||
from pydantic import BaseModel
|
||||
from typing import Union, Optional
|
||||
from constants import ERROR_MESSAGES
|
||||
from passlib.context import CryptContext
|
||||
from datetime import datetime, timedelta
|
||||
import requests
|
||||
import jwt
|
||||
import uuid
|
||||
import logging
|
||||
@@ -54,7 +51,7 @@ def decode_token(token: str) -> Optional[dict]:
|
||||
try:
|
||||
decoded = jwt.decode(token, SESSION_SECRET, algorithms=[ALGORITHM])
|
||||
return decoded
|
||||
except Exception as e:
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
@@ -71,7 +68,7 @@ def get_http_authorization_cred(auth_header: str):
|
||||
try:
|
||||
scheme, credentials = auth_header.split(" ")
|
||||
return HTTPAuthorizationCredentials(scheme=scheme, credentials=credentials)
|
||||
except:
|
||||
except Exception:
|
||||
raise ValueError(ERROR_MESSAGES.INVALID_TOKEN)
|
||||
|
||||
|
||||
@@ -96,7 +93,7 @@ def get_current_user(
|
||||
|
||||
# auth by jwt token
|
||||
data = decode_token(token)
|
||||
if data != None and "id" in data:
|
||||
if data is not None and "id" in data:
|
||||
user = Users.get_user_by_id(data["id"])
|
||||
if user is None:
|
||||
raise HTTPException(
|
||||
|
||||
+18
-13
@@ -38,9 +38,10 @@ describe('Settings', () => {
|
||||
// User's message should be visible
|
||||
cy.get('.chat-user').should('exist');
|
||||
// Wait for the response
|
||||
cy.get('.chat-assistant', { timeout: 120_000 }) // .chat-assistant is created after the first token is received
|
||||
.find('div[aria-label="Generation Info"]', { timeout: 120_000 }) // Generation Info is created after the stop token is received
|
||||
.should('exist');
|
||||
// .chat-assistant is created after the first token is received
|
||||
cy.get('.chat-assistant', { timeout: 10_000 }).should('exist');
|
||||
// Generation Info is created after the stop token is received
|
||||
cy.get('div[aria-label="Generation Info"]', { timeout: 120_000 }).should('exist');
|
||||
});
|
||||
|
||||
it('user can share chat', () => {
|
||||
@@ -57,21 +58,24 @@ describe('Settings', () => {
|
||||
// User's message should be visible
|
||||
cy.get('.chat-user').should('exist');
|
||||
// Wait for the response
|
||||
cy.get('.chat-assistant', { timeout: 120_000 }) // .chat-assistant is created after the first token is received
|
||||
.find('div[aria-label="Generation Info"]', { timeout: 120_000 }) // Generation Info is created after the stop token is received
|
||||
.should('exist');
|
||||
// .chat-assistant is created after the first token is received
|
||||
cy.get('.chat-assistant', { timeout: 10_000 }).should('exist');
|
||||
// Generation Info is created after the stop token is received
|
||||
cy.get('div[aria-label="Generation Info"]', { timeout: 120_000 }).should('exist');
|
||||
// spy on requests
|
||||
const spy = cy.spy();
|
||||
cy.intercept('GET', '/api/v1/chats/*', spy);
|
||||
cy.intercept('POST', '/api/v1/chats/**/share', spy);
|
||||
// Open context menu
|
||||
cy.get('#chat-context-menu-button').click();
|
||||
// Click share button
|
||||
cy.get('#chat-share-button').click();
|
||||
// Check if the share dialog is visible
|
||||
cy.get('#copy-and-share-chat-button').should('exist');
|
||||
cy.wrap({}, { timeout: 5000 }).should(() => {
|
||||
// Check if the request was made twice (once for to replace chat object and once more due to change event)
|
||||
expect(spy).to.be.callCount(2);
|
||||
// Click the copy button
|
||||
cy.get('#copy-and-share-chat-button').click();
|
||||
cy.wrap({}, { timeout: 5_000 }).should(() => {
|
||||
// Check if the share request was made
|
||||
expect(spy).to.be.callCount(1);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -89,9 +93,10 @@ describe('Settings', () => {
|
||||
// User's message should be visible
|
||||
cy.get('.chat-user').should('exist');
|
||||
// Wait for the response
|
||||
cy.get('.chat-assistant', { timeout: 120_000 }) // .chat-assistant is created after the first token is received
|
||||
.find('div[aria-label="Generation Info"]', { timeout: 120_000 }) // Generation Info is created after the stop token is received
|
||||
.should('exist');
|
||||
// .chat-assistant is created after the first token is received
|
||||
cy.get('.chat-assistant', { timeout: 10_000 }).should('exist');
|
||||
// Generation Info is created after the stop token is received
|
||||
cy.get('div[aria-label="Generation Info"]', { timeout: 120_000 }).should('exist');
|
||||
// Click on the generate image button
|
||||
cy.get('[aria-label="Generate Image"]').click();
|
||||
// Wait for image to be visible
|
||||
|
||||
@@ -22,7 +22,6 @@ Noticed something off? Have an idea? Check our [Issues tab](https://github.com/o
|
||||
> [!IMPORTANT]
|
||||
>
|
||||
> - **Template Compliance:** Please be aware that failure to follow the provided issue template, or not providing the requested information at all, will likely result in your issue being closed without further consideration. This approach is critical for maintaining the manageability and integrity of issue tracking.
|
||||
>
|
||||
> - **Detail is Key:** To ensure your issue is understood and can be effectively addressed, it's imperative to include comprehensive details. Descriptions should be clear, including steps to reproduce, expected outcomes, and actual results. Lack of sufficient detail may hinder our ability to resolve your issue.
|
||||
|
||||
### 🧭 Scope of Support
|
||||
|
||||
+27
-3
@@ -9,12 +9,36 @@ Our primary goal is to ensure the protection and confidentiality of sensitive da
|
||||
| main | :white_check_mark: |
|
||||
| others | :x: |
|
||||
|
||||
## Zero Tolerance for External Platforms
|
||||
|
||||
Based on a precedent of an unacceptable degree of spamming and unsolicited communications from third-party platforms, we forcefully reaffirm our stance. **We refuse to engage with, join, or monitor any platforms outside of GitHub for vulnerability reporting.** Our reasons are not just procedural but are deep-seated in the ethos of our project, which champions transparency and direct community interaction inherent in the open-source culture. Any attempts to divert our processes to external platforms will be met with outright rejection. This policy is non-negotiable and understands no exceptions.
|
||||
|
||||
Any reports or solicitations arriving from sources other than our designated GitHub repository will be dismissed without consideration. We’ve seen how external engagements can dilute and compromise the integrity of community-driven projects, and we’re not here to gamble with the security and privacy of our user community.
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
If you discover a security issue within our system, please notify us immediately via a pull request or contact us on discord.
|
||||
We appreciate the community's interest in identifying potential vulnerabilities. However, effective immediately, we will **not** accept low-effort vulnerability reports. To ensure that submissions are constructive and actionable, please adhere to the following guidelines:
|
||||
|
||||
Reports not submitted through our designated GitHub repository will be disregarded, and we will categorically reject invitations to collaborate on external platforms. Our aggressive stance on this matter underscores our commitment to a secure, transparent, and open community where all operations are visible and contributors are accountable.
|
||||
|
||||
1. **No Vague Reports**: Submissions such as "I found a vulnerability" without any details will be treated as spam and will not be accepted.
|
||||
|
||||
2. **In-Depth Understanding Required**: Reports must reflect a clear understanding of the codebase and provide specific details about the vulnerability, including the affected components and potential impacts.
|
||||
|
||||
3. **Proof of Concept (PoC) is Mandatory**: Each submission must include a well-documented proof of concept (PoC) that demonstrates the vulnerability. If confidentiality is a concern, reporters are encouraged to create a private fork of the repository and share access with the maintainers. Reports lacking valid evidence will be disregarded.
|
||||
|
||||
4. **Required Patch Submission**: Along with the PoC, reporters must provide a patch or actionable steps to remediate the identified vulnerability. This helps us evaluate and implement fixes rapidly.
|
||||
|
||||
5. **Streamlined Merging Process**: When vulnerability reports meet the above criteria, we can consider them for immediate merging, similar to regular pull requests. Well-structured and thorough submissions will expedite the process of enhancing our security.
|
||||
|
||||
**Non-compliant submissions will be closed, and repeat violators may be banned.** Our goal is to foster a constructive reporting environment where quality submissions promote better security for all users.
|
||||
|
||||
## Product Security
|
||||
|
||||
We regularly audit our internal processes and system's architecture for vulnerabilities using a combination of automated and manual testing techniques.
|
||||
We regularly audit our internal processes and system architecture for vulnerabilities using a combination of automated and manual testing techniques. We are also planning to implement SAST and SCA scans in our project soon.
|
||||
|
||||
We are planning on implementing SAST and SCA scans in our project soon.
|
||||
For immediate concerns or detailed reports that meet our guidelines, please create an issue in our [issue tracker](/open-webui/open-webui/issues) or contact us on [Discord](https://discord.gg/5rJgQTnV4s).
|
||||
|
||||
---
|
||||
|
||||
_Last updated on **2024-08-19**._
|
||||
|
||||
Generated
+371
-600
File diff suppressed because it is too large
Load Diff
+22
-16
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "open-webui",
|
||||
"version": "0.3.11",
|
||||
"version": "0.3.14",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "npm run pyodide:fetch && vite dev --host",
|
||||
@@ -20,30 +20,31 @@
|
||||
"pyodide:fetch": "node scripts/prepare-pyodide.js"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@sveltejs/adapter-auto": "^2.0.0",
|
||||
"@sveltejs/adapter-static": "^2.0.3",
|
||||
"@sveltejs/kit": "^1.30.0",
|
||||
"@tailwindcss/typography": "^0.5.10",
|
||||
"@sveltejs/adapter-auto": "3.2.2",
|
||||
"@sveltejs/adapter-static": "^3.0.2",
|
||||
"@sveltejs/kit": "^2.5.20",
|
||||
"@sveltejs/vite-plugin-svelte": "^3.1.1",
|
||||
"@tailwindcss/typography": "^0.5.13",
|
||||
"@types/bun": "latest",
|
||||
"@typescript-eslint/eslint-plugin": "^6.17.0",
|
||||
"@typescript-eslint/parser": "^6.17.0",
|
||||
"autoprefixer": "^10.4.16",
|
||||
"cypress": "^13.8.1",
|
||||
"eslint": "^8.56.0",
|
||||
"eslint-config-prettier": "^8.5.0",
|
||||
"eslint-plugin-cypress": "^3.0.2",
|
||||
"eslint-plugin-svelte": "^2.30.0",
|
||||
"i18next-parser": "^8.13.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-plugin-cypress": "^3.4.0",
|
||||
"eslint-plugin-svelte": "^2.43.0",
|
||||
"i18next-parser": "^9.0.1",
|
||||
"postcss": "^8.4.31",
|
||||
"prettier": "^2.8.0",
|
||||
"prettier-plugin-svelte": "^2.10.1",
|
||||
"svelte": "^4.0.5",
|
||||
"svelte-check": "^3.4.3",
|
||||
"prettier": "^3.3.3",
|
||||
"prettier-plugin-svelte": "^3.2.6",
|
||||
"svelte": "^4.2.18",
|
||||
"svelte-check": "^3.8.5",
|
||||
"svelte-confetti": "^1.3.2",
|
||||
"tailwindcss": "^3.3.3",
|
||||
"tslib": "^2.4.1",
|
||||
"typescript": "^5.0.0",
|
||||
"vite": "^4.4.2",
|
||||
"typescript": "^5.5.4",
|
||||
"vite": "^5.3.5",
|
||||
"vitest": "^1.6.0"
|
||||
},
|
||||
"type": "module",
|
||||
@@ -52,12 +53,13 @@
|
||||
"@codemirror/lang-python": "^6.1.6",
|
||||
"@codemirror/theme-one-dark": "^6.1.2",
|
||||
"@pyscript/core": "^0.4.32",
|
||||
"@sveltejs/adapter-node": "^1.3.1",
|
||||
"@sveltejs/adapter-node": "^2.0.0",
|
||||
"async": "^3.2.5",
|
||||
"bits-ui": "^0.19.7",
|
||||
"codemirror": "^6.0.1",
|
||||
"crc-32": "^1.2.2",
|
||||
"dayjs": "^1.11.10",
|
||||
"dompurify": "^3.1.6",
|
||||
"eventsource-parser": "^1.1.2",
|
||||
"file-saver": "^2.0.5",
|
||||
"fuse.js": "^7.0.0",
|
||||
@@ -77,5 +79,9 @@
|
||||
"tippy.js": "^6.3.7",
|
||||
"turndown": "^7.2.0",
|
||||
"uuid": "^9.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.13.0 <=21.x.x",
|
||||
"npm": ">=6.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
+41
-26
@@ -1,74 +1,89 @@
|
||||
[project]
|
||||
name = "open-webui"
|
||||
description = "Open WebUI (Formerly Ollama WebUI)"
|
||||
description = "Open WebUI"
|
||||
authors = [
|
||||
{ name = "Timothy Jaeryang Baek", email = "tim@openwebui.com" }
|
||||
]
|
||||
license = { file = "LICENSE" }
|
||||
dependencies = [
|
||||
"fastapi==0.111.0",
|
||||
"uvicorn[standard]==0.22.0",
|
||||
"pydantic==2.7.1",
|
||||
"uvicorn[standard]==0.30.6",
|
||||
"pydantic==2.8.2",
|
||||
"python-multipart==0.0.9",
|
||||
|
||||
"Flask==3.0.3",
|
||||
"Flask-Cors==4.0.1",
|
||||
|
||||
"python-socketio==5.11.2",
|
||||
"python-socketio==5.11.3",
|
||||
"python-jose==3.3.0",
|
||||
"passlib[bcrypt]==1.7.4",
|
||||
|
||||
"requests==2.32.2",
|
||||
"aiohttp==3.9.5",
|
||||
"peewee==3.17.5",
|
||||
"requests==2.32.3",
|
||||
"aiohttp==3.10.2",
|
||||
|
||||
"sqlalchemy==2.0.32",
|
||||
"alembic==1.13.2",
|
||||
"peewee==3.17.6",
|
||||
"peewee-migrate==1.12.2",
|
||||
"psycopg2-binary==2.9.9",
|
||||
"PyMySQL==1.1.1",
|
||||
"bcrypt==4.1.3",
|
||||
"bcrypt==4.2.0",
|
||||
|
||||
"boto3==1.34.110",
|
||||
"pymongo",
|
||||
"redis",
|
||||
"boto3==1.35.0",
|
||||
|
||||
"argon2-cffi==23.1.0",
|
||||
"APScheduler==3.10.4",
|
||||
"google-generativeai==0.5.4",
|
||||
|
||||
"langchain==0.2.0",
|
||||
"langchain-community==0.2.9",
|
||||
"langchain-chroma==0.1.1",
|
||||
"openai",
|
||||
"anthropic",
|
||||
"google-generativeai==0.7.2",
|
||||
"tiktoken",
|
||||
|
||||
"langchain==0.2.12",
|
||||
"langchain-community==0.2.10",
|
||||
"langchain-chroma==0.1.2",
|
||||
|
||||
"fake-useragent==1.5.1",
|
||||
"chromadb==0.5.0",
|
||||
"sentence-transformers==2.7.0",
|
||||
"pypdf==4.2.0",
|
||||
"chromadb==0.5.5",
|
||||
"sentence-transformers==3.0.1",
|
||||
"pypdf==4.3.1",
|
||||
"docx2txt==0.8",
|
||||
"unstructured==0.14.0",
|
||||
"python-pptx==1.0.0",
|
||||
"unstructured==0.15.5",
|
||||
"Markdown==3.6",
|
||||
"pypandoc==1.13",
|
||||
"pandas==2.2.2",
|
||||
"openpyxl==3.1.2",
|
||||
"openpyxl==3.1.5",
|
||||
"pyxlsb==1.0.10",
|
||||
"xlrd==2.0.1",
|
||||
"validators==0.28.1",
|
||||
"validators==0.33.0",
|
||||
"psutil",
|
||||
|
||||
"opencv-python-headless==4.9.0.80",
|
||||
"rapidocr-onnxruntime==1.3.22",
|
||||
"opencv-python-headless==4.10.0.84",
|
||||
"rapidocr-onnxruntime==1.3.24",
|
||||
|
||||
"fpdf2==2.7.9",
|
||||
"rank-bm25==0.2.2",
|
||||
|
||||
"faster-whisper==1.0.2",
|
||||
"faster-whisper==1.0.3",
|
||||
|
||||
"PyJWT[crypto]==2.8.0",
|
||||
"PyJWT[crypto]==2.9.0",
|
||||
"authlib==1.3.1",
|
||||
|
||||
"black==24.4.2",
|
||||
"langfuse==2.33.0",
|
||||
"black==24.8.0",
|
||||
"langfuse==2.43.3",
|
||||
"youtube-transcript-api==0.6.2",
|
||||
"pytube==15.0.0",
|
||||
|
||||
"extract_msg",
|
||||
"pydub",
|
||||
"duckduckgo-search~=6.1.5"
|
||||
"duckduckgo-search~=6.2.1",
|
||||
|
||||
"docker~=7.1.0",
|
||||
"pytest~=8.2.2",
|
||||
"pytest-docker~=3.1.1"
|
||||
]
|
||||
readme = "README.md"
|
||||
requires-python = ">= 3.11, < 3.12.0a1"
|
||||
|
||||
+106
-40
@@ -10,16 +10,25 @@
|
||||
# universal: false
|
||||
|
||||
-e file:.
|
||||
aiohttp==3.9.5
|
||||
aiohappyeyeballs==2.3.5
|
||||
# via aiohttp
|
||||
aiohttp==3.10.2
|
||||
# via langchain
|
||||
# via langchain-community
|
||||
# via open-webui
|
||||
aiosignal==1.3.1
|
||||
# via aiohttp
|
||||
alembic==1.13.2
|
||||
# via open-webui
|
||||
annotated-types==0.6.0
|
||||
# via pydantic
|
||||
anyio==4.3.0
|
||||
anthropic==0.32.0
|
||||
# via open-webui
|
||||
anyio==4.4.0
|
||||
# via anthropic
|
||||
# via httpx
|
||||
# via langfuse
|
||||
# via openai
|
||||
# via starlette
|
||||
# via watchfiles
|
||||
apscheduler==3.10.4
|
||||
@@ -32,6 +41,7 @@ asgiref==3.8.1
|
||||
# via opentelemetry-instrumentation-asgi
|
||||
attrs==23.2.0
|
||||
# via aiohttp
|
||||
# via pytest-docker
|
||||
authlib==1.3.1
|
||||
# via open-webui
|
||||
av==11.0.0
|
||||
@@ -40,7 +50,7 @@ backoff==2.2.1
|
||||
# via langfuse
|
||||
# via posthog
|
||||
# via unstructured
|
||||
bcrypt==4.1.3
|
||||
bcrypt==4.2.0
|
||||
# via chromadb
|
||||
# via open-webui
|
||||
# via passlib
|
||||
@@ -49,13 +59,13 @@ beautifulsoup4==4.12.3
|
||||
# via unstructured
|
||||
bidict==0.23.1
|
||||
# via python-socketio
|
||||
black==24.4.2
|
||||
black==24.8.0
|
||||
# via open-webui
|
||||
blinker==1.8.2
|
||||
# via flask
|
||||
boto3==1.34.110
|
||||
boto3==1.35.0
|
||||
# via open-webui
|
||||
botocore==1.34.110
|
||||
botocore==1.35.2
|
||||
# via boto3
|
||||
# via s3transfer
|
||||
build==1.2.1
|
||||
@@ -76,9 +86,9 @@ chardet==5.2.0
|
||||
charset-normalizer==3.3.2
|
||||
# via requests
|
||||
# via unstructured-client
|
||||
chroma-hnswlib==0.7.3
|
||||
chroma-hnswlib==0.7.6
|
||||
# via chromadb
|
||||
chromadb==0.5.0
|
||||
chromadb==0.5.5
|
||||
# via langchain-chroma
|
||||
# via open-webui
|
||||
click==8.1.7
|
||||
@@ -102,7 +112,6 @@ cryptography==42.0.7
|
||||
ctranslate2==4.2.1
|
||||
# via faster-whisper
|
||||
dataclasses-json==0.6.6
|
||||
# via langchain
|
||||
# via langchain-community
|
||||
# via unstructured
|
||||
# via unstructured-client
|
||||
@@ -113,11 +122,17 @@ defusedxml==0.7.1
|
||||
deprecated==1.2.14
|
||||
# via opentelemetry-api
|
||||
# via opentelemetry-exporter-otlp-proto-grpc
|
||||
distro==1.9.0
|
||||
# via anthropic
|
||||
# via openai
|
||||
dnspython==2.6.1
|
||||
# via email-validator
|
||||
# via pymongo
|
||||
docker==7.1.0
|
||||
# via open-webui
|
||||
docx2txt==0.8
|
||||
# via open-webui
|
||||
duckduckgo-search==6.1.5
|
||||
duckduckgo-search==6.2.6
|
||||
# via open-webui
|
||||
easygui==0.98.3
|
||||
# via oletools
|
||||
@@ -141,7 +156,7 @@ fastapi==0.111.0
|
||||
# via open-webui
|
||||
fastapi-cli==0.0.4
|
||||
# via fastapi
|
||||
faster-whisper==1.0.2
|
||||
faster-whisper==1.0.3
|
||||
# via open-webui
|
||||
filelock==3.14.0
|
||||
# via huggingface-hub
|
||||
@@ -166,7 +181,7 @@ frozenlist==1.4.1
|
||||
fsspec==2024.3.1
|
||||
# via huggingface-hub
|
||||
# via torch
|
||||
google-ai-generativelanguage==0.6.4
|
||||
google-ai-generativelanguage==0.6.6
|
||||
# via google-generativeai
|
||||
google-api-core==2.19.0
|
||||
# via google-ai-generativelanguage
|
||||
@@ -183,7 +198,7 @@ google-auth==2.29.0
|
||||
# via kubernetes
|
||||
google-auth-httplib2==0.2.0
|
||||
# via google-api-python-client
|
||||
google-generativeai==0.5.4
|
||||
google-generativeai==0.7.2
|
||||
# via open-webui
|
||||
googleapis-common-protos==1.63.0
|
||||
# via google-api-core
|
||||
@@ -208,8 +223,11 @@ httplib2==0.22.0
|
||||
httptools==0.6.1
|
||||
# via uvicorn
|
||||
httpx==0.27.0
|
||||
# via anthropic
|
||||
# via chromadb
|
||||
# via fastapi
|
||||
# via langfuse
|
||||
# via openai
|
||||
huggingface-hub==0.23.0
|
||||
# via faster-whisper
|
||||
# via sentence-transformers
|
||||
@@ -229,12 +247,16 @@ importlib-metadata==7.0.0
|
||||
# via opentelemetry-api
|
||||
importlib-resources==6.4.0
|
||||
# via chromadb
|
||||
iniconfig==2.0.0
|
||||
# via pytest
|
||||
itsdangerous==2.2.0
|
||||
# via flask
|
||||
jinja2==3.1.4
|
||||
# via fastapi
|
||||
# via flask
|
||||
# via torch
|
||||
jiter==0.5.0
|
||||
# via anthropic
|
||||
jmespath==1.0.1
|
||||
# via boto3
|
||||
# via botocore
|
||||
@@ -249,14 +271,14 @@ jsonpointer==2.4
|
||||
# via jsonpatch
|
||||
kubernetes==29.0.0
|
||||
# via chromadb
|
||||
langchain==0.2.0
|
||||
langchain==0.2.12
|
||||
# via langchain-community
|
||||
# via open-webui
|
||||
langchain-chroma==0.1.1
|
||||
langchain-chroma==0.1.2
|
||||
# via open-webui
|
||||
langchain-community==0.2.0
|
||||
langchain-community==0.2.10
|
||||
# via open-webui
|
||||
langchain-core==0.2.1
|
||||
langchain-core==0.2.28
|
||||
# via langchain
|
||||
# via langchain-chroma
|
||||
# via langchain-community
|
||||
@@ -265,22 +287,26 @@ langchain-text-splitters==0.2.0
|
||||
# via langchain
|
||||
langdetect==1.0.9
|
||||
# via unstructured
|
||||
langfuse==2.33.0
|
||||
langfuse==2.43.3
|
||||
# via open-webui
|
||||
langsmith==0.1.57
|
||||
langsmith==0.1.96
|
||||
# via langchain
|
||||
# via langchain-community
|
||||
# via langchain-core
|
||||
lark==1.1.8
|
||||
# via rtfde
|
||||
lxml==5.2.2
|
||||
# via python-pptx
|
||||
# via unstructured
|
||||
mako==1.3.5
|
||||
# via alembic
|
||||
markdown==3.6
|
||||
# via open-webui
|
||||
markdown-it-py==3.0.0
|
||||
# via rich
|
||||
markupsafe==2.1.5
|
||||
# via jinja2
|
||||
# via mako
|
||||
# via werkzeug
|
||||
marshmallow==3.21.2
|
||||
# via dataclasses-json
|
||||
@@ -339,11 +365,13 @@ onnxruntime==1.17.3
|
||||
# via chromadb
|
||||
# via faster-whisper
|
||||
# via rapidocr-onnxruntime
|
||||
openai==1.38.0
|
||||
# via open-webui
|
||||
opencv-python==4.9.0.80
|
||||
# via rapidocr-onnxruntime
|
||||
opencv-python-headless==4.9.0.80
|
||||
opencv-python-headless==4.10.0.84
|
||||
# via open-webui
|
||||
openpyxl==3.1.2
|
||||
openpyxl==3.1.5
|
||||
# via open-webui
|
||||
opentelemetry-api==1.24.0
|
||||
# via chromadb
|
||||
@@ -380,7 +408,6 @@ ordered-set==4.1.0
|
||||
# via deepdiff
|
||||
orjson==3.10.3
|
||||
# via chromadb
|
||||
# via duckduckgo-search
|
||||
# via fastapi
|
||||
# via langsmith
|
||||
overrides==7.7.0
|
||||
@@ -393,6 +420,7 @@ packaging==23.2
|
||||
# via langfuse
|
||||
# via marshmallow
|
||||
# via onnxruntime
|
||||
# via pytest
|
||||
# via transformers
|
||||
# via unstructured-client
|
||||
pandas==2.2.2
|
||||
@@ -403,19 +431,24 @@ pathspec==0.12.1
|
||||
# via black
|
||||
pcodedmp==1.2.6
|
||||
# via oletools
|
||||
peewee==3.17.5
|
||||
peewee==3.17.6
|
||||
# via open-webui
|
||||
# via peewee-migrate
|
||||
peewee-migrate==1.12.2
|
||||
# via open-webui
|
||||
pillow==10.3.0
|
||||
# via fpdf2
|
||||
# via python-pptx
|
||||
# via rapidocr-onnxruntime
|
||||
# via sentence-transformers
|
||||
platformdirs==4.2.1
|
||||
# via black
|
||||
pluggy==1.5.0
|
||||
# via pytest
|
||||
posthog==3.5.0
|
||||
# via chromadb
|
||||
primp==0.5.5
|
||||
# via duckduckgo-search
|
||||
proto-plus==1.23.0
|
||||
# via google-ai-generativelanguage
|
||||
# via google-api-core
|
||||
@@ -428,6 +461,9 @@ protobuf==4.25.3
|
||||
# via onnxruntime
|
||||
# via opentelemetry-proto
|
||||
# via proto-plus
|
||||
psutil==6.0.0
|
||||
# via open-webui
|
||||
# via unstructured
|
||||
psycopg2-binary==2.9.9
|
||||
# via open-webui
|
||||
pyasn1==0.6.0
|
||||
@@ -440,7 +476,8 @@ pyclipper==1.3.0.post5
|
||||
# via rapidocr-onnxruntime
|
||||
pycparser==2.22
|
||||
# via cffi
|
||||
pydantic==2.7.1
|
||||
pydantic==2.8.2
|
||||
# via anthropic
|
||||
# via chromadb
|
||||
# via fastapi
|
||||
# via google-generativeai
|
||||
@@ -449,30 +486,36 @@ pydantic==2.7.1
|
||||
# via langfuse
|
||||
# via langsmith
|
||||
# via open-webui
|
||||
pydantic-core==2.18.2
|
||||
# via openai
|
||||
pydantic-core==2.20.1
|
||||
# via pydantic
|
||||
pydub==0.25.1
|
||||
# via open-webui
|
||||
pygments==2.18.0
|
||||
# via rich
|
||||
pyjwt==2.8.0
|
||||
pyjwt==2.9.0
|
||||
# via open-webui
|
||||
pymysql==1.1.0
|
||||
pymongo==4.8.0
|
||||
# via open-webui
|
||||
pymysql==1.1.1
|
||||
# via open-webui
|
||||
pypandoc==1.13
|
||||
# via open-webui
|
||||
pyparsing==2.4.7
|
||||
# via httplib2
|
||||
# via oletools
|
||||
pypdf==4.2.0
|
||||
pypdf==4.3.1
|
||||
# via open-webui
|
||||
# via unstructured-client
|
||||
pypika==0.48.9
|
||||
# via chromadb
|
||||
pyproject-hooks==1.1.0
|
||||
# via build
|
||||
pyreqwest-impersonate==0.4.7
|
||||
# via duckduckgo-search
|
||||
pytest==8.2.2
|
||||
# via open-webui
|
||||
# via pytest-docker
|
||||
pytest-docker==3.1.1
|
||||
# via open-webui
|
||||
python-dateutil==2.9.0.post0
|
||||
# via botocore
|
||||
# via kubernetes
|
||||
@@ -492,7 +535,9 @@ python-magic==0.4.27
|
||||
python-multipart==0.0.9
|
||||
# via fastapi
|
||||
# via open-webui
|
||||
python-socketio==5.11.2
|
||||
python-pptx==1.0.0
|
||||
# via open-webui
|
||||
python-socketio==5.11.3
|
||||
# via open-webui
|
||||
pytube==15.0.0
|
||||
# via open-webui
|
||||
@@ -516,15 +561,18 @@ rank-bm25==0.2.2
|
||||
# via open-webui
|
||||
rapidfuzz==3.9.0
|
||||
# via unstructured
|
||||
rapidocr-onnxruntime==1.3.22
|
||||
rapidocr-onnxruntime==1.3.24
|
||||
# via open-webui
|
||||
red-black-tree-mod==1.20
|
||||
# via extract-msg
|
||||
redis==5.0.8
|
||||
# via open-webui
|
||||
regex==2024.5.10
|
||||
# via nltk
|
||||
# via tiktoken
|
||||
# via transformers
|
||||
requests==2.32.2
|
||||
# via chromadb
|
||||
requests==2.32.3
|
||||
# via docker
|
||||
# via google-api-core
|
||||
# via huggingface-hub
|
||||
# via kubernetes
|
||||
@@ -534,6 +582,7 @@ requests==2.32.2
|
||||
# via open-webui
|
||||
# via posthog
|
||||
# via requests-oauthlib
|
||||
# via tiktoken
|
||||
# via transformers
|
||||
# via unstructured
|
||||
# via unstructured-client
|
||||
@@ -556,12 +605,12 @@ scikit-learn==1.4.2
|
||||
scipy==1.13.0
|
||||
# via scikit-learn
|
||||
# via sentence-transformers
|
||||
sentence-transformers==2.7.0
|
||||
sentence-transformers==3.0.1
|
||||
# via open-webui
|
||||
setuptools==69.5.1
|
||||
# via ctranslate2
|
||||
# via opentelemetry-instrumentation
|
||||
shapely==2.0.4
|
||||
shapely==2.0.5
|
||||
# via rapidocr-onnxruntime
|
||||
shellingham==1.5.4
|
||||
# via typer
|
||||
@@ -577,13 +626,17 @@ six==1.16.0
|
||||
# via rapidocr-onnxruntime
|
||||
# via unstructured-client
|
||||
sniffio==1.3.1
|
||||
# via anthropic
|
||||
# via anyio
|
||||
# via httpx
|
||||
# via openai
|
||||
soupsieve==2.5
|
||||
# via beautifulsoup4
|
||||
sqlalchemy==2.0.30
|
||||
sqlalchemy==2.0.32
|
||||
# via alembic
|
||||
# via langchain
|
||||
# via langchain-community
|
||||
# via open-webui
|
||||
starlette==0.37.2
|
||||
# via fastapi
|
||||
sympy==1.12
|
||||
@@ -598,7 +651,10 @@ tenacity==8.3.0
|
||||
# via langchain-core
|
||||
threadpoolctl==3.5.0
|
||||
# via scikit-learn
|
||||
tiktoken==0.7.0
|
||||
# via open-webui
|
||||
tokenizers==0.15.2
|
||||
# via anthropic
|
||||
# via chromadb
|
||||
# via faster-whisper
|
||||
# via transformers
|
||||
@@ -609,21 +665,28 @@ tqdm==4.66.4
|
||||
# via google-generativeai
|
||||
# via huggingface-hub
|
||||
# via nltk
|
||||
# via openai
|
||||
# via sentence-transformers
|
||||
# via transformers
|
||||
# via unstructured
|
||||
transformers==4.39.3
|
||||
# via sentence-transformers
|
||||
typer==0.12.3
|
||||
# via chromadb
|
||||
# via fastapi-cli
|
||||
typing-extensions==4.11.0
|
||||
# via alembic
|
||||
# via anthropic
|
||||
# via chromadb
|
||||
# via fastapi
|
||||
# via google-generativeai
|
||||
# via huggingface-hub
|
||||
# via langchain-core
|
||||
# via openai
|
||||
# via opentelemetry-sdk
|
||||
# via pydantic
|
||||
# via pydantic-core
|
||||
# via python-pptx
|
||||
# via sqlalchemy
|
||||
# via torch
|
||||
# via typer
|
||||
@@ -640,7 +703,7 @@ tzlocal==5.2
|
||||
# via extract-msg
|
||||
ujson==5.10.0
|
||||
# via fastapi
|
||||
unstructured==0.14.0
|
||||
unstructured==0.15.5
|
||||
# via open-webui
|
||||
unstructured-client==0.22.0
|
||||
# via unstructured
|
||||
@@ -648,16 +711,17 @@ uritemplate==4.1.1
|
||||
# via google-api-python-client
|
||||
urllib3==2.2.1
|
||||
# via botocore
|
||||
# via docker
|
||||
# via kubernetes
|
||||
# via requests
|
||||
# via unstructured-client
|
||||
uvicorn==0.22.0
|
||||
uvicorn==0.30.6
|
||||
# via chromadb
|
||||
# via fastapi
|
||||
# via open-webui
|
||||
uvloop==0.19.0
|
||||
# via uvicorn
|
||||
validators==0.28.1
|
||||
validators==0.33.0
|
||||
# via open-webui
|
||||
watchfiles==0.21.0
|
||||
# via uvicorn
|
||||
@@ -676,6 +740,8 @@ wsproto==1.2.0
|
||||
# via simple-websocket
|
||||
xlrd==2.0.1
|
||||
# via open-webui
|
||||
xlsxwriter==3.2.0
|
||||
# via python-pptx
|
||||
yarl==1.9.4
|
||||
# via aiohttp
|
||||
youtube-transcript-api==0.6.2
|
||||
|
||||
+106
-40
@@ -10,16 +10,25 @@
|
||||
# universal: false
|
||||
|
||||
-e file:.
|
||||
aiohttp==3.9.5
|
||||
aiohappyeyeballs==2.3.5
|
||||
# via aiohttp
|
||||
aiohttp==3.10.2
|
||||
# via langchain
|
||||
# via langchain-community
|
||||
# via open-webui
|
||||
aiosignal==1.3.1
|
||||
# via aiohttp
|
||||
alembic==1.13.2
|
||||
# via open-webui
|
||||
annotated-types==0.6.0
|
||||
# via pydantic
|
||||
anyio==4.3.0
|
||||
anthropic==0.32.0
|
||||
# via open-webui
|
||||
anyio==4.4.0
|
||||
# via anthropic
|
||||
# via httpx
|
||||
# via langfuse
|
||||
# via openai
|
||||
# via starlette
|
||||
# via watchfiles
|
||||
apscheduler==3.10.4
|
||||
@@ -32,6 +41,7 @@ asgiref==3.8.1
|
||||
# via opentelemetry-instrumentation-asgi
|
||||
attrs==23.2.0
|
||||
# via aiohttp
|
||||
# via pytest-docker
|
||||
authlib==1.3.1
|
||||
# via open-webui
|
||||
av==11.0.0
|
||||
@@ -40,7 +50,7 @@ backoff==2.2.1
|
||||
# via langfuse
|
||||
# via posthog
|
||||
# via unstructured
|
||||
bcrypt==4.1.3
|
||||
bcrypt==4.2.0
|
||||
# via chromadb
|
||||
# via open-webui
|
||||
# via passlib
|
||||
@@ -49,13 +59,13 @@ beautifulsoup4==4.12.3
|
||||
# via unstructured
|
||||
bidict==0.23.1
|
||||
# via python-socketio
|
||||
black==24.4.2
|
||||
black==24.8.0
|
||||
# via open-webui
|
||||
blinker==1.8.2
|
||||
# via flask
|
||||
boto3==1.34.110
|
||||
boto3==1.35.0
|
||||
# via open-webui
|
||||
botocore==1.34.110
|
||||
botocore==1.35.2
|
||||
# via boto3
|
||||
# via s3transfer
|
||||
build==1.2.1
|
||||
@@ -76,9 +86,9 @@ chardet==5.2.0
|
||||
charset-normalizer==3.3.2
|
||||
# via requests
|
||||
# via unstructured-client
|
||||
chroma-hnswlib==0.7.3
|
||||
chroma-hnswlib==0.7.6
|
||||
# via chromadb
|
||||
chromadb==0.5.0
|
||||
chromadb==0.5.5
|
||||
# via langchain-chroma
|
||||
# via open-webui
|
||||
click==8.1.7
|
||||
@@ -102,7 +112,6 @@ cryptography==42.0.7
|
||||
ctranslate2==4.2.1
|
||||
# via faster-whisper
|
||||
dataclasses-json==0.6.6
|
||||
# via langchain
|
||||
# via langchain-community
|
||||
# via unstructured
|
||||
# via unstructured-client
|
||||
@@ -113,11 +122,17 @@ defusedxml==0.7.1
|
||||
deprecated==1.2.14
|
||||
# via opentelemetry-api
|
||||
# via opentelemetry-exporter-otlp-proto-grpc
|
||||
distro==1.9.0
|
||||
# via anthropic
|
||||
# via openai
|
||||
dnspython==2.6.1
|
||||
# via email-validator
|
||||
# via pymongo
|
||||
docker==7.1.0
|
||||
# via open-webui
|
||||
docx2txt==0.8
|
||||
# via open-webui
|
||||
duckduckgo-search==6.1.5
|
||||
duckduckgo-search==6.2.6
|
||||
# via open-webui
|
||||
easygui==0.98.3
|
||||
# via oletools
|
||||
@@ -141,7 +156,7 @@ fastapi==0.111.0
|
||||
# via open-webui
|
||||
fastapi-cli==0.0.4
|
||||
# via fastapi
|
||||
faster-whisper==1.0.2
|
||||
faster-whisper==1.0.3
|
||||
# via open-webui
|
||||
filelock==3.14.0
|
||||
# via huggingface-hub
|
||||
@@ -166,7 +181,7 @@ frozenlist==1.4.1
|
||||
fsspec==2024.3.1
|
||||
# via huggingface-hub
|
||||
# via torch
|
||||
google-ai-generativelanguage==0.6.4
|
||||
google-ai-generativelanguage==0.6.6
|
||||
# via google-generativeai
|
||||
google-api-core==2.19.0
|
||||
# via google-ai-generativelanguage
|
||||
@@ -183,7 +198,7 @@ google-auth==2.29.0
|
||||
# via kubernetes
|
||||
google-auth-httplib2==0.2.0
|
||||
# via google-api-python-client
|
||||
google-generativeai==0.5.4
|
||||
google-generativeai==0.7.2
|
||||
# via open-webui
|
||||
googleapis-common-protos==1.63.0
|
||||
# via google-api-core
|
||||
@@ -208,8 +223,11 @@ httplib2==0.22.0
|
||||
httptools==0.6.1
|
||||
# via uvicorn
|
||||
httpx==0.27.0
|
||||
# via anthropic
|
||||
# via chromadb
|
||||
# via fastapi
|
||||
# via langfuse
|
||||
# via openai
|
||||
huggingface-hub==0.23.0
|
||||
# via faster-whisper
|
||||
# via sentence-transformers
|
||||
@@ -229,12 +247,16 @@ importlib-metadata==7.0.0
|
||||
# via opentelemetry-api
|
||||
importlib-resources==6.4.0
|
||||
# via chromadb
|
||||
iniconfig==2.0.0
|
||||
# via pytest
|
||||
itsdangerous==2.2.0
|
||||
# via flask
|
||||
jinja2==3.1.4
|
||||
# via fastapi
|
||||
# via flask
|
||||
# via torch
|
||||
jiter==0.5.0
|
||||
# via anthropic
|
||||
jmespath==1.0.1
|
||||
# via boto3
|
||||
# via botocore
|
||||
@@ -249,14 +271,14 @@ jsonpointer==2.4
|
||||
# via jsonpatch
|
||||
kubernetes==29.0.0
|
||||
# via chromadb
|
||||
langchain==0.2.0
|
||||
langchain==0.2.12
|
||||
# via langchain-community
|
||||
# via open-webui
|
||||
langchain-chroma==0.1.1
|
||||
langchain-chroma==0.1.2
|
||||
# via open-webui
|
||||
langchain-community==0.2.0
|
||||
langchain-community==0.2.10
|
||||
# via open-webui
|
||||
langchain-core==0.2.1
|
||||
langchain-core==0.2.28
|
||||
# via langchain
|
||||
# via langchain-chroma
|
||||
# via langchain-community
|
||||
@@ -265,22 +287,26 @@ langchain-text-splitters==0.2.0
|
||||
# via langchain
|
||||
langdetect==1.0.9
|
||||
# via unstructured
|
||||
langfuse==2.33.0
|
||||
langfuse==2.43.3
|
||||
# via open-webui
|
||||
langsmith==0.1.57
|
||||
langsmith==0.1.96
|
||||
# via langchain
|
||||
# via langchain-community
|
||||
# via langchain-core
|
||||
lark==1.1.8
|
||||
# via rtfde
|
||||
lxml==5.2.2
|
||||
# via python-pptx
|
||||
# via unstructured
|
||||
mako==1.3.5
|
||||
# via alembic
|
||||
markdown==3.6
|
||||
# via open-webui
|
||||
markdown-it-py==3.0.0
|
||||
# via rich
|
||||
markupsafe==2.1.5
|
||||
# via jinja2
|
||||
# via mako
|
||||
# via werkzeug
|
||||
marshmallow==3.21.2
|
||||
# via dataclasses-json
|
||||
@@ -339,11 +365,13 @@ onnxruntime==1.17.3
|
||||
# via chromadb
|
||||
# via faster-whisper
|
||||
# via rapidocr-onnxruntime
|
||||
openai==1.38.0
|
||||
# via open-webui
|
||||
opencv-python==4.9.0.80
|
||||
# via rapidocr-onnxruntime
|
||||
opencv-python-headless==4.9.0.80
|
||||
opencv-python-headless==4.10.0.84
|
||||
# via open-webui
|
||||
openpyxl==3.1.2
|
||||
openpyxl==3.1.5
|
||||
# via open-webui
|
||||
opentelemetry-api==1.24.0
|
||||
# via chromadb
|
||||
@@ -380,7 +408,6 @@ ordered-set==4.1.0
|
||||
# via deepdiff
|
||||
orjson==3.10.3
|
||||
# via chromadb
|
||||
# via duckduckgo-search
|
||||
# via fastapi
|
||||
# via langsmith
|
||||
overrides==7.7.0
|
||||
@@ -393,6 +420,7 @@ packaging==23.2
|
||||
# via langfuse
|
||||
# via marshmallow
|
||||
# via onnxruntime
|
||||
# via pytest
|
||||
# via transformers
|
||||
# via unstructured-client
|
||||
pandas==2.2.2
|
||||
@@ -403,19 +431,24 @@ pathspec==0.12.1
|
||||
# via black
|
||||
pcodedmp==1.2.6
|
||||
# via oletools
|
||||
peewee==3.17.5
|
||||
peewee==3.17.6
|
||||
# via open-webui
|
||||
# via peewee-migrate
|
||||
peewee-migrate==1.12.2
|
||||
# via open-webui
|
||||
pillow==10.3.0
|
||||
# via fpdf2
|
||||
# via python-pptx
|
||||
# via rapidocr-onnxruntime
|
||||
# via sentence-transformers
|
||||
platformdirs==4.2.1
|
||||
# via black
|
||||
pluggy==1.5.0
|
||||
# via pytest
|
||||
posthog==3.5.0
|
||||
# via chromadb
|
||||
primp==0.5.5
|
||||
# via duckduckgo-search
|
||||
proto-plus==1.23.0
|
||||
# via google-ai-generativelanguage
|
||||
# via google-api-core
|
||||
@@ -428,6 +461,9 @@ protobuf==4.25.3
|
||||
# via onnxruntime
|
||||
# via opentelemetry-proto
|
||||
# via proto-plus
|
||||
psutil==6.0.0
|
||||
# via open-webui
|
||||
# via unstructured
|
||||
psycopg2-binary==2.9.9
|
||||
# via open-webui
|
||||
pyasn1==0.6.0
|
||||
@@ -440,7 +476,8 @@ pyclipper==1.3.0.post5
|
||||
# via rapidocr-onnxruntime
|
||||
pycparser==2.22
|
||||
# via cffi
|
||||
pydantic==2.7.1
|
||||
pydantic==2.8.2
|
||||
# via anthropic
|
||||
# via chromadb
|
||||
# via fastapi
|
||||
# via google-generativeai
|
||||
@@ -449,30 +486,36 @@ pydantic==2.7.1
|
||||
# via langfuse
|
||||
# via langsmith
|
||||
# via open-webui
|
||||
pydantic-core==2.18.2
|
||||
# via openai
|
||||
pydantic-core==2.20.1
|
||||
# via pydantic
|
||||
pydub==0.25.1
|
||||
# via open-webui
|
||||
pygments==2.18.0
|
||||
# via rich
|
||||
pyjwt==2.8.0
|
||||
pyjwt==2.9.0
|
||||
# via open-webui
|
||||
pymysql==1.1.0
|
||||
pymongo==4.8.0
|
||||
# via open-webui
|
||||
pymysql==1.1.1
|
||||
# via open-webui
|
||||
pypandoc==1.13
|
||||
# via open-webui
|
||||
pyparsing==2.4.7
|
||||
# via httplib2
|
||||
# via oletools
|
||||
pypdf==4.2.0
|
||||
pypdf==4.3.1
|
||||
# via open-webui
|
||||
# via unstructured-client
|
||||
pypika==0.48.9
|
||||
# via chromadb
|
||||
pyproject-hooks==1.1.0
|
||||
# via build
|
||||
pyreqwest-impersonate==0.4.7
|
||||
# via duckduckgo-search
|
||||
pytest==8.2.2
|
||||
# via open-webui
|
||||
# via pytest-docker
|
||||
pytest-docker==3.1.1
|
||||
# via open-webui
|
||||
python-dateutil==2.9.0.post0
|
||||
# via botocore
|
||||
# via kubernetes
|
||||
@@ -492,7 +535,9 @@ python-magic==0.4.27
|
||||
python-multipart==0.0.9
|
||||
# via fastapi
|
||||
# via open-webui
|
||||
python-socketio==5.11.2
|
||||
python-pptx==1.0.0
|
||||
# via open-webui
|
||||
python-socketio==5.11.3
|
||||
# via open-webui
|
||||
pytube==15.0.0
|
||||
# via open-webui
|
||||
@@ -516,15 +561,18 @@ rank-bm25==0.2.2
|
||||
# via open-webui
|
||||
rapidfuzz==3.9.0
|
||||
# via unstructured
|
||||
rapidocr-onnxruntime==1.3.22
|
||||
rapidocr-onnxruntime==1.3.24
|
||||
# via open-webui
|
||||
red-black-tree-mod==1.20
|
||||
# via extract-msg
|
||||
redis==5.0.8
|
||||
# via open-webui
|
||||
regex==2024.5.10
|
||||
# via nltk
|
||||
# via tiktoken
|
||||
# via transformers
|
||||
requests==2.32.2
|
||||
# via chromadb
|
||||
requests==2.32.3
|
||||
# via docker
|
||||
# via google-api-core
|
||||
# via huggingface-hub
|
||||
# via kubernetes
|
||||
@@ -534,6 +582,7 @@ requests==2.32.2
|
||||
# via open-webui
|
||||
# via posthog
|
||||
# via requests-oauthlib
|
||||
# via tiktoken
|
||||
# via transformers
|
||||
# via unstructured
|
||||
# via unstructured-client
|
||||
@@ -556,12 +605,12 @@ scikit-learn==1.4.2
|
||||
scipy==1.13.0
|
||||
# via scikit-learn
|
||||
# via sentence-transformers
|
||||
sentence-transformers==2.7.0
|
||||
sentence-transformers==3.0.1
|
||||
# via open-webui
|
||||
setuptools==69.5.1
|
||||
# via ctranslate2
|
||||
# via opentelemetry-instrumentation
|
||||
shapely==2.0.4
|
||||
shapely==2.0.5
|
||||
# via rapidocr-onnxruntime
|
||||
shellingham==1.5.4
|
||||
# via typer
|
||||
@@ -577,13 +626,17 @@ six==1.16.0
|
||||
# via rapidocr-onnxruntime
|
||||
# via unstructured-client
|
||||
sniffio==1.3.1
|
||||
# via anthropic
|
||||
# via anyio
|
||||
# via httpx
|
||||
# via openai
|
||||
soupsieve==2.5
|
||||
# via beautifulsoup4
|
||||
sqlalchemy==2.0.30
|
||||
sqlalchemy==2.0.32
|
||||
# via alembic
|
||||
# via langchain
|
||||
# via langchain-community
|
||||
# via open-webui
|
||||
starlette==0.37.2
|
||||
# via fastapi
|
||||
sympy==1.12
|
||||
@@ -598,7 +651,10 @@ tenacity==8.3.0
|
||||
# via langchain-core
|
||||
threadpoolctl==3.5.0
|
||||
# via scikit-learn
|
||||
tiktoken==0.7.0
|
||||
# via open-webui
|
||||
tokenizers==0.15.2
|
||||
# via anthropic
|
||||
# via chromadb
|
||||
# via faster-whisper
|
||||
# via transformers
|
||||
@@ -609,21 +665,28 @@ tqdm==4.66.4
|
||||
# via google-generativeai
|
||||
# via huggingface-hub
|
||||
# via nltk
|
||||
# via openai
|
||||
# via sentence-transformers
|
||||
# via transformers
|
||||
# via unstructured
|
||||
transformers==4.39.3
|
||||
# via sentence-transformers
|
||||
typer==0.12.3
|
||||
# via chromadb
|
||||
# via fastapi-cli
|
||||
typing-extensions==4.11.0
|
||||
# via alembic
|
||||
# via anthropic
|
||||
# via chromadb
|
||||
# via fastapi
|
||||
# via google-generativeai
|
||||
# via huggingface-hub
|
||||
# via langchain-core
|
||||
# via openai
|
||||
# via opentelemetry-sdk
|
||||
# via pydantic
|
||||
# via pydantic-core
|
||||
# via python-pptx
|
||||
# via sqlalchemy
|
||||
# via torch
|
||||
# via typer
|
||||
@@ -640,7 +703,7 @@ tzlocal==5.2
|
||||
# via extract-msg
|
||||
ujson==5.10.0
|
||||
# via fastapi
|
||||
unstructured==0.14.0
|
||||
unstructured==0.15.5
|
||||
# via open-webui
|
||||
unstructured-client==0.22.0
|
||||
# via unstructured
|
||||
@@ -648,16 +711,17 @@ uritemplate==4.1.1
|
||||
# via google-api-python-client
|
||||
urllib3==2.2.1
|
||||
# via botocore
|
||||
# via docker
|
||||
# via kubernetes
|
||||
# via requests
|
||||
# via unstructured-client
|
||||
uvicorn==0.22.0
|
||||
uvicorn==0.30.6
|
||||
# via chromadb
|
||||
# via fastapi
|
||||
# via open-webui
|
||||
uvloop==0.19.0
|
||||
# via uvicorn
|
||||
validators==0.28.1
|
||||
validators==0.33.0
|
||||
# via open-webui
|
||||
watchfiles==0.21.0
|
||||
# via uvicorn
|
||||
@@ -676,6 +740,8 @@ wsproto==1.2.0
|
||||
# via simple-websocket
|
||||
xlrd==2.0.1
|
||||
# via open-webui
|
||||
xlsxwriter==3.2.0
|
||||
# via python-pptx
|
||||
yarl==1.9.4
|
||||
# via aiohttp
|
||||
youtube-transcript-api==0.6.2
|
||||
|
||||
+13
@@ -34,6 +34,10 @@ math {
|
||||
@apply rounded-lg;
|
||||
}
|
||||
|
||||
.markdown-prose {
|
||||
@apply prose dark:prose-invert prose-p:my-0 prose-img:my-1 prose-headings:my-1 prose-pre:my-0 prose-table:my-0 prose-blockquote:my-0 prose-ul:-my-0 prose-ol:-my-0 prose-li:-my-0 whitespace-pre-line;
|
||||
}
|
||||
|
||||
.markdown a {
|
||||
@apply underline;
|
||||
}
|
||||
@@ -158,3 +162,12 @@ input[type='number'] {
|
||||
.password {
|
||||
-webkit-text-security: disc;
|
||||
}
|
||||
|
||||
.codespan {
|
||||
color: #eb5757;
|
||||
border-width: 0px;
|
||||
padding: 3px 8px;
|
||||
font-size: 0.8em;
|
||||
font-weight: 600;
|
||||
@apply rounded-md dark:bg-gray-800 bg-gray-100 mx-0.5;
|
||||
}
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
<!DOCTYPE html>
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
|
||||
@@ -32,10 +32,15 @@ export const createNewChat = async (token: string, chat: object) => {
|
||||
return res;
|
||||
};
|
||||
|
||||
export const getChatList = async (token: string = '') => {
|
||||
export const getChatList = async (token: string = '', page: number | null = null) => {
|
||||
let error = null;
|
||||
const searchParams = new URLSearchParams();
|
||||
|
||||
const res = await fetch(`${WEBUI_API_BASE_URL}/chats/`, {
|
||||
if (page !== null) {
|
||||
searchParams.append('page', `${page}`);
|
||||
}
|
||||
|
||||
const res = await fetch(`${WEBUI_API_BASE_URL}/chats/?${searchParams.toString()}`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
|
||||
+10
-252
@@ -1,6 +1,6 @@
|
||||
import { IMAGES_API_BASE_URL } from '$lib/constants';
|
||||
|
||||
export const getImageGenerationConfig = async (token: string = '') => {
|
||||
export const getConfig = async (token: string = '') => {
|
||||
let error = null;
|
||||
|
||||
const res = await fetch(`${IMAGES_API_BASE_URL}/config`, {
|
||||
@@ -32,11 +32,7 @@ export const getImageGenerationConfig = async (token: string = '') => {
|
||||
return res;
|
||||
};
|
||||
|
||||
export const updateImageGenerationConfig = async (
|
||||
token: string = '',
|
||||
engine: string,
|
||||
enabled: boolean
|
||||
) => {
|
||||
export const updateConfig = async (token: string = '', config: object) => {
|
||||
let error = null;
|
||||
|
||||
const res = await fetch(`${IMAGES_API_BASE_URL}/config/update`, {
|
||||
@@ -47,8 +43,7 @@ export const updateImageGenerationConfig = async (
|
||||
...(token && { authorization: `Bearer ${token}` })
|
||||
},
|
||||
body: JSON.stringify({
|
||||
engine,
|
||||
enabled
|
||||
...config
|
||||
})
|
||||
})
|
||||
.then(async (res) => {
|
||||
@@ -72,10 +67,10 @@ export const updateImageGenerationConfig = async (
|
||||
return res;
|
||||
};
|
||||
|
||||
export const getOpenAIConfig = async (token: string = '') => {
|
||||
export const verifyConfigUrl = async (token: string = '') => {
|
||||
let error = null;
|
||||
|
||||
const res = await fetch(`${IMAGES_API_BASE_URL}/openai/config`, {
|
||||
const res = await fetch(`${IMAGES_API_BASE_URL}/config/url/verify`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
@@ -104,46 +99,10 @@ export const getOpenAIConfig = async (token: string = '') => {
|
||||
return res;
|
||||
};
|
||||
|
||||
export const updateOpenAIConfig = async (token: string = '', url: string, key: string) => {
|
||||
export const getImageGenerationConfig = async (token: string = '') => {
|
||||
let error = null;
|
||||
|
||||
const res = await fetch(`${IMAGES_API_BASE_URL}/openai/config/update`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
...(token && { authorization: `Bearer ${token}` })
|
||||
},
|
||||
body: JSON.stringify({
|
||||
url: url,
|
||||
key: key
|
||||
})
|
||||
})
|
||||
.then(async (res) => {
|
||||
if (!res.ok) throw await res.json();
|
||||
return res.json();
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err);
|
||||
if ('detail' in err) {
|
||||
error = err.detail;
|
||||
} else {
|
||||
error = 'Server connection failed';
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return res;
|
||||
};
|
||||
|
||||
export const getImageGenerationEngineUrls = async (token: string = '') => {
|
||||
let error = null;
|
||||
|
||||
const res = await fetch(`${IMAGES_API_BASE_URL}/url`, {
|
||||
const res = await fetch(`${IMAGES_API_BASE_URL}/image/config`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
@@ -172,19 +131,17 @@ export const getImageGenerationEngineUrls = async (token: string = '') => {
|
||||
return res;
|
||||
};
|
||||
|
||||
export const updateImageGenerationEngineUrls = async (token: string = '', urls: object = {}) => {
|
||||
export const updateImageGenerationConfig = async (token: string = '', config: object) => {
|
||||
let error = null;
|
||||
|
||||
const res = await fetch(`${IMAGES_API_BASE_URL}/url/update`, {
|
||||
const res = await fetch(`${IMAGES_API_BASE_URL}/image/config/update`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
...(token && { authorization: `Bearer ${token}` })
|
||||
},
|
||||
body: JSON.stringify({
|
||||
...urls
|
||||
})
|
||||
body: JSON.stringify({ ...config })
|
||||
})
|
||||
.then(async (res) => {
|
||||
if (!res.ok) throw await res.json();
|
||||
@@ -207,138 +164,6 @@ export const updateImageGenerationEngineUrls = async (token: string = '', urls:
|
||||
return res;
|
||||
};
|
||||
|
||||
export const getImageSize = async (token: string = '') => {
|
||||
let error = null;
|
||||
|
||||
const res = await fetch(`${IMAGES_API_BASE_URL}/size`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
...(token && { authorization: `Bearer ${token}` })
|
||||
}
|
||||
})
|
||||
.then(async (res) => {
|
||||
if (!res.ok) throw await res.json();
|
||||
return res.json();
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err);
|
||||
if ('detail' in err) {
|
||||
error = err.detail;
|
||||
} else {
|
||||
error = 'Server connection failed';
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return res.IMAGE_SIZE;
|
||||
};
|
||||
|
||||
export const updateImageSize = async (token: string = '', size: string) => {
|
||||
let error = null;
|
||||
|
||||
const res = await fetch(`${IMAGES_API_BASE_URL}/size/update`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
...(token && { authorization: `Bearer ${token}` })
|
||||
},
|
||||
body: JSON.stringify({
|
||||
size: size
|
||||
})
|
||||
})
|
||||
.then(async (res) => {
|
||||
if (!res.ok) throw await res.json();
|
||||
return res.json();
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err);
|
||||
if ('detail' in err) {
|
||||
error = err.detail;
|
||||
} else {
|
||||
error = 'Server connection failed';
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return res.IMAGE_SIZE;
|
||||
};
|
||||
|
||||
export const getImageSteps = async (token: string = '') => {
|
||||
let error = null;
|
||||
|
||||
const res = await fetch(`${IMAGES_API_BASE_URL}/steps`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
...(token && { authorization: `Bearer ${token}` })
|
||||
}
|
||||
})
|
||||
.then(async (res) => {
|
||||
if (!res.ok) throw await res.json();
|
||||
return res.json();
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err);
|
||||
if ('detail' in err) {
|
||||
error = err.detail;
|
||||
} else {
|
||||
error = 'Server connection failed';
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return res.IMAGE_STEPS;
|
||||
};
|
||||
|
||||
export const updateImageSteps = async (token: string = '', steps: number) => {
|
||||
let error = null;
|
||||
|
||||
const res = await fetch(`${IMAGES_API_BASE_URL}/steps/update`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
...(token && { authorization: `Bearer ${token}` })
|
||||
},
|
||||
body: JSON.stringify({ steps })
|
||||
})
|
||||
.then(async (res) => {
|
||||
if (!res.ok) throw await res.json();
|
||||
return res.json();
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err);
|
||||
if ('detail' in err) {
|
||||
error = err.detail;
|
||||
} else {
|
||||
error = 'Server connection failed';
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return res.IMAGE_STEPS;
|
||||
};
|
||||
|
||||
export const getImageGenerationModels = async (token: string = '') => {
|
||||
let error = null;
|
||||
|
||||
@@ -371,73 +196,6 @@ export const getImageGenerationModels = async (token: string = '') => {
|
||||
return res;
|
||||
};
|
||||
|
||||
export const getDefaultImageGenerationModel = async (token: string = '') => {
|
||||
let error = null;
|
||||
|
||||
const res = await fetch(`${IMAGES_API_BASE_URL}/models/default`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
...(token && { authorization: `Bearer ${token}` })
|
||||
}
|
||||
})
|
||||
.then(async (res) => {
|
||||
if (!res.ok) throw await res.json();
|
||||
return res.json();
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err);
|
||||
if ('detail' in err) {
|
||||
error = err.detail;
|
||||
} else {
|
||||
error = 'Server connection failed';
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return res.model;
|
||||
};
|
||||
|
||||
export const updateDefaultImageGenerationModel = async (token: string = '', model: string) => {
|
||||
let error = null;
|
||||
|
||||
const res = await fetch(`${IMAGES_API_BASE_URL}/models/default/update`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
...(token && { authorization: `Bearer ${token}` })
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model: model
|
||||
})
|
||||
})
|
||||
.then(async (res) => {
|
||||
if (!res.ok) throw await res.json();
|
||||
return res.json();
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err);
|
||||
if ('detail' in err) {
|
||||
error = err.detail;
|
||||
} else {
|
||||
error = 'Server connection failed';
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return res.model;
|
||||
};
|
||||
|
||||
export const imageGenerations = async (token: string = '', prompt: string) => {
|
||||
let error = null;
|
||||
|
||||
|
||||
@@ -69,6 +69,7 @@ type ChatCompletedForm = {
|
||||
model: string;
|
||||
messages: string[];
|
||||
chat_id: string;
|
||||
session_id: string;
|
||||
};
|
||||
|
||||
export const chatCompleted = async (token: string, body: ChatCompletedForm) => {
|
||||
@@ -332,6 +333,42 @@ export const generateSearchQuery = async (
|
||||
return res?.choices[0]?.message?.content.replace(/["']/g, '') ?? prompt;
|
||||
};
|
||||
|
||||
export const generateMoACompletion = async (
|
||||
token: string = '',
|
||||
model: string,
|
||||
prompt: string,
|
||||
responses: string[]
|
||||
) => {
|
||||
const controller = new AbortController();
|
||||
let error = null;
|
||||
|
||||
const res = await fetch(`${WEBUI_BASE_URL}/api/task/moa/completions`, {
|
||||
signal: controller.signal,
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${token}`
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model: model,
|
||||
prompt: prompt,
|
||||
responses: responses,
|
||||
stream: true
|
||||
})
|
||||
}).catch((err) => {
|
||||
console.log(err);
|
||||
error = err;
|
||||
return null;
|
||||
});
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return [res, controller];
|
||||
};
|
||||
|
||||
export const getPipelinesList = async (token: string = '') => {
|
||||
let error = null;
|
||||
|
||||
@@ -628,6 +665,7 @@ export const getBackendConfig = async () => {
|
||||
|
||||
const res = await fetch(`${WEBUI_BASE_URL}/api/config`, {
|
||||
method: 'GET',
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
@@ -912,6 +950,7 @@ export interface ModelConfig {
|
||||
export interface ModelMeta {
|
||||
description?: string;
|
||||
capabilities?: object;
|
||||
profile_image_url?: string;
|
||||
}
|
||||
|
||||
export interface ModelParams {}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { OLLAMA_API_BASE_URL } from '$lib/constants';
|
||||
import { titleGenerationTemplate } from '$lib/utils';
|
||||
|
||||
export const getOllamaConfig = async (token: string = '') => {
|
||||
let error = null;
|
||||
@@ -203,55 +202,6 @@ export const getOllamaModels = async (token: string = '') => {
|
||||
});
|
||||
};
|
||||
|
||||
// TODO: migrate to backend
|
||||
export const generateTitle = async (
|
||||
token: string = '',
|
||||
template: string,
|
||||
model: string,
|
||||
prompt: string
|
||||
) => {
|
||||
let error = null;
|
||||
|
||||
template = titleGenerationTemplate(template, prompt);
|
||||
|
||||
console.log(template);
|
||||
|
||||
const res = await fetch(`${OLLAMA_API_BASE_URL}/api/generate`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${token}`
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model: model,
|
||||
prompt: template,
|
||||
stream: false,
|
||||
options: {
|
||||
// Restrict the number of tokens generated to 50
|
||||
num_predict: 50
|
||||
}
|
||||
})
|
||||
})
|
||||
.then(async (res) => {
|
||||
if (!res.ok) throw await res.json();
|
||||
return res.json();
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err);
|
||||
if ('detail' in err) {
|
||||
error = err.detail;
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return res?.response.replace(/["']/g, '') ?? 'New Chat';
|
||||
};
|
||||
|
||||
export const generatePrompt = async (token: string = '', model: string, conversation: string) => {
|
||||
let error = null;
|
||||
|
||||
@@ -446,7 +396,7 @@ export const deleteModel = async (token: string, tagName: string, urlIdx: string
|
||||
return res;
|
||||
};
|
||||
|
||||
export const pullModel = async (token: string, tagName: string, urlIdx: string | null = null) => {
|
||||
export const pullModel = async (token: string, tagName: string, urlIdx: number | null = null) => {
|
||||
let error = null;
|
||||
const controller = new AbortController();
|
||||
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import { OPENAI_API_BASE_URL } from '$lib/constants';
|
||||
import { titleGenerationTemplate } from '$lib/utils';
|
||||
import { type Model, models, settings } from '$lib/stores';
|
||||
|
||||
export const getOpenAIConfig = async (token: string = '') => {
|
||||
let error = null;
|
||||
@@ -260,7 +258,7 @@ export const getOpenAIModelsDirect = async (
|
||||
throw error;
|
||||
}
|
||||
|
||||
const models = Array.isArray(res) ? res : res?.data ?? null;
|
||||
const models = Array.isArray(res) ? res : (res?.data ?? null);
|
||||
|
||||
return models
|
||||
.map((model) => ({ id: model.id, name: model.name ?? model.id, external: true }))
|
||||
@@ -330,126 +328,3 @@ export const synthesizeOpenAISpeech = async (
|
||||
|
||||
return res;
|
||||
};
|
||||
|
||||
export const generateTitle = async (
|
||||
token: string = '',
|
||||
template: string,
|
||||
model: string,
|
||||
prompt: string,
|
||||
chat_id?: string,
|
||||
url: string = OPENAI_API_BASE_URL
|
||||
) => {
|
||||
let error = null;
|
||||
|
||||
template = titleGenerationTemplate(template, prompt);
|
||||
|
||||
console.log(template);
|
||||
|
||||
const res = await fetch(`${url}/chat/completions`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${token}`
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model: model,
|
||||
messages: [
|
||||
{
|
||||
role: 'user',
|
||||
content: template
|
||||
}
|
||||
],
|
||||
stream: false,
|
||||
// Restricting the max tokens to 50 to avoid long titles
|
||||
max_tokens: 50,
|
||||
...(chat_id && { chat_id: chat_id }),
|
||||
title: true
|
||||
})
|
||||
})
|
||||
.then(async (res) => {
|
||||
if (!res.ok) throw await res.json();
|
||||
return res.json();
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err);
|
||||
if ('detail' in err) {
|
||||
error = err.detail;
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return res?.choices[0]?.message?.content.replace(/["']/g, '') ?? 'New Chat';
|
||||
};
|
||||
|
||||
export const generateSearchQuery = async (
|
||||
token: string = '',
|
||||
model: string,
|
||||
previousMessages: string[],
|
||||
prompt: string,
|
||||
url: string = OPENAI_API_BASE_URL
|
||||
): Promise<string | undefined> => {
|
||||
let error = null;
|
||||
|
||||
// TODO: Allow users to specify the prompt
|
||||
// Get the current date in the format "January 20, 2024"
|
||||
const currentDate = new Intl.DateTimeFormat('en-US', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: '2-digit'
|
||||
}).format(new Date());
|
||||
|
||||
const res = await fetch(`${url}/chat/completions`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${token}`
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model: model,
|
||||
// Few shot prompting
|
||||
messages: [
|
||||
{
|
||||
role: 'assistant',
|
||||
content: `You are tasked with generating web search queries. Give me an appropriate query to answer my question for google search. Answer with only the query. Today is ${currentDate}.`
|
||||
},
|
||||
{
|
||||
role: 'user',
|
||||
content: prompt
|
||||
}
|
||||
// {
|
||||
// role: 'user',
|
||||
// content:
|
||||
// (previousMessages.length > 0
|
||||
// ? `Previous Questions:\n${previousMessages.join('\n')}\n\n`
|
||||
// : '') + `Current Question: ${prompt}`
|
||||
// }
|
||||
],
|
||||
stream: false,
|
||||
// Restricting the max tokens to 30 to avoid long search queries
|
||||
max_tokens: 30
|
||||
})
|
||||
})
|
||||
.then(async (res) => {
|
||||
if (!res.ok) throw await res.json();
|
||||
return res.json();
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err);
|
||||
if ('detail' in err) {
|
||||
error = err.detail;
|
||||
}
|
||||
return undefined;
|
||||
});
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return res?.choices[0]?.message?.content.replace(/["']/g, '') ?? undefined;
|
||||
};
|
||||
|
||||
@@ -75,12 +75,12 @@
|
||||
class="font-semibold uppercase text-xs {section === 'added'
|
||||
? 'text-white bg-blue-600'
|
||||
: section === 'fixed'
|
||||
? 'text-white bg-green-600'
|
||||
: section === 'changed'
|
||||
? 'text-white bg-yellow-600'
|
||||
: section === 'removed'
|
||||
? 'text-white bg-red-600'
|
||||
: ''} w-fit px-3 rounded-full my-2.5"
|
||||
? 'text-white bg-green-600'
|
||||
: section === 'changed'
|
||||
? 'text-white bg-yellow-600'
|
||||
: section === 'removed'
|
||||
? 'text-white bg-red-600'
|
||||
: ''} w-fit px-3 rounded-full my-2.5"
|
||||
>
|
||||
{section}
|
||||
</div>
|
||||
|
||||
@@ -336,8 +336,11 @@
|
||||
<div class="flex-1 mt-3 lg:mt-0 overflow-y-scroll">
|
||||
{#if selectedTab === 'general'}
|
||||
<General
|
||||
saveHandler={() => {
|
||||
saveHandler={async () => {
|
||||
toast.success($i18n.t('Settings saved successfully!'));
|
||||
|
||||
await tick();
|
||||
await config.set(await getBackendConfig());
|
||||
}}
|
||||
/>
|
||||
{:else if selectedTab === 'users'}
|
||||
|
||||
@@ -112,7 +112,7 @@
|
||||
url: OpenAIUrl,
|
||||
batch_size: OpenAIBatchSize
|
||||
}
|
||||
}
|
||||
}
|
||||
: {})
|
||||
}).catch(async (error) => {
|
||||
toast.error(error);
|
||||
|
||||
@@ -1,22 +1,10 @@
|
||||
<script lang="ts">
|
||||
import {
|
||||
getCommunitySharingEnabledStatus,
|
||||
getWebhookUrl,
|
||||
toggleCommunitySharingEnabledStatus,
|
||||
updateWebhookUrl
|
||||
} from '$lib/apis';
|
||||
import {
|
||||
getAdminConfig,
|
||||
getDefaultUserRole,
|
||||
getJWTExpiresDuration,
|
||||
getSignUpEnabledStatus,
|
||||
toggleSignUpEnabledStatus,
|
||||
updateAdminConfig,
|
||||
updateDefaultUserRole,
|
||||
updateJWTExpiresDuration
|
||||
} from '$lib/apis/auths';
|
||||
import { getBackendConfig, getWebhookUrl, updateWebhookUrl } from '$lib/apis';
|
||||
import { getAdminConfig, updateAdminConfig } from '$lib/apis/auths';
|
||||
import Switch from '$lib/components/common/Switch.svelte';
|
||||
import { config } from '$lib/stores';
|
||||
import { onMount, getContext } from 'svelte';
|
||||
import { toast } from 'svelte-sonner';
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
@@ -30,7 +18,7 @@
|
||||
const res = await updateAdminConfig(localStorage.token, adminConfig);
|
||||
|
||||
if (res) {
|
||||
toast.success(i18n.t('Settings updated successfully'));
|
||||
saveHandler();
|
||||
} else {
|
||||
toast.error(i18n.t('Failed to update settings'));
|
||||
}
|
||||
@@ -51,9 +39,8 @@
|
||||
|
||||
<form
|
||||
class="flex flex-col h-full justify-between space-y-3 text-sm"
|
||||
on:submit|preventDefault={() => {
|
||||
on:submit|preventDefault={async () => {
|
||||
updateHandler();
|
||||
saveHandler();
|
||||
}}
|
||||
>
|
||||
<div class=" space-y-3 overflow-y-scroll scrollbar-hidden h-full">
|
||||
@@ -98,6 +85,12 @@
|
||||
<Switch bind:state={adminConfig.ENABLE_COMMUNITY_SHARING} />
|
||||
</div>
|
||||
|
||||
<div class="my-3 flex w-full items-center justify-between pr-2">
|
||||
<div class=" self-center text-xs font-medium">{$i18n.t('Enable Message Rating')}</div>
|
||||
|
||||
<Switch bind:state={adminConfig.ENABLE_MESSAGE_RATING} />
|
||||
</div>
|
||||
|
||||
<hr class=" dark:border-gray-850 my-2" />
|
||||
|
||||
<div class=" w-full justify-between">
|
||||
|
||||
@@ -2,152 +2,172 @@
|
||||
import { toast } from 'svelte-sonner';
|
||||
|
||||
import { createEventDispatcher, onMount, getContext } from 'svelte';
|
||||
import { config, user } from '$lib/stores';
|
||||
import { config as backendConfig, user } from '$lib/stores';
|
||||
|
||||
import { getBackendConfig } from '$lib/apis';
|
||||
import {
|
||||
getImageGenerationModels,
|
||||
getDefaultImageGenerationModel,
|
||||
updateDefaultImageGenerationModel,
|
||||
getImageSize,
|
||||
getImageGenerationConfig,
|
||||
updateImageGenerationConfig,
|
||||
getImageGenerationEngineUrls,
|
||||
updateImageGenerationEngineUrls,
|
||||
updateImageSize,
|
||||
getImageSteps,
|
||||
updateImageSteps,
|
||||
getOpenAIConfig,
|
||||
updateOpenAIConfig
|
||||
getConfig,
|
||||
updateConfig,
|
||||
verifyConfigUrl
|
||||
} from '$lib/apis/images';
|
||||
import { getBackendConfig } from '$lib/apis';
|
||||
import SensitiveInput from '$lib/components/common/SensitiveInput.svelte';
|
||||
import Switch from '$lib/components/common/Switch.svelte';
|
||||
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
let loading = false;
|
||||
|
||||
let imageGenerationEngine = '';
|
||||
let enableImageGeneration = false;
|
||||
let config = null;
|
||||
let imageGenerationConfig = null;
|
||||
|
||||
let AUTOMATIC1111_BASE_URL = '';
|
||||
let AUTOMATIC1111_API_AUTH = '';
|
||||
let COMFYUI_BASE_URL = '';
|
||||
|
||||
let OPENAI_API_BASE_URL = '';
|
||||
let OPENAI_API_KEY = '';
|
||||
|
||||
let selectedModel = '';
|
||||
let models = null;
|
||||
|
||||
let imageSize = '';
|
||||
let steps = 50;
|
||||
let workflowNodes = [
|
||||
{
|
||||
type: 'prompt',
|
||||
key: 'text',
|
||||
node_ids: ''
|
||||
},
|
||||
{
|
||||
type: 'model',
|
||||
key: 'ckpt_name',
|
||||
node_ids: ''
|
||||
},
|
||||
{
|
||||
type: 'width',
|
||||
key: 'width',
|
||||
node_ids: ''
|
||||
},
|
||||
{
|
||||
type: 'height',
|
||||
key: 'height',
|
||||
node_ids: ''
|
||||
},
|
||||
{
|
||||
type: 'steps',
|
||||
key: 'steps',
|
||||
node_ids: ''
|
||||
}
|
||||
];
|
||||
|
||||
const getModels = async () => {
|
||||
models = await getImageGenerationModels(localStorage.token).catch((error) => {
|
||||
toast.error(error);
|
||||
return null;
|
||||
});
|
||||
selectedModel = await getDefaultImageGenerationModel(localStorage.token).catch((error) => {
|
||||
return '';
|
||||
});
|
||||
};
|
||||
|
||||
const updateUrlHandler = async () => {
|
||||
if (imageGenerationEngine === 'comfyui') {
|
||||
const res = await updateImageGenerationEngineUrls(localStorage.token, {
|
||||
COMFYUI_BASE_URL: COMFYUI_BASE_URL
|
||||
}).catch((error) => {
|
||||
toast.error(error);
|
||||
|
||||
console.log(error);
|
||||
return null;
|
||||
});
|
||||
|
||||
if (res) {
|
||||
COMFYUI_BASE_URL = res.COMFYUI_BASE_URL;
|
||||
|
||||
await getModels();
|
||||
|
||||
if (models) {
|
||||
toast.success($i18n.t('Server connection verified'));
|
||||
}
|
||||
} else {
|
||||
({ COMFYUI_BASE_URL } = await getImageGenerationEngineUrls(localStorage.token));
|
||||
}
|
||||
} else {
|
||||
const res = await updateImageGenerationEngineUrls(localStorage.token, {
|
||||
AUTOMATIC1111_BASE_URL: AUTOMATIC1111_BASE_URL,
|
||||
AUTOMATIC1111_API_AUTH: AUTOMATIC1111_API_AUTH
|
||||
}).catch((error) => {
|
||||
toast.error(error);
|
||||
return null;
|
||||
});
|
||||
|
||||
if (res) {
|
||||
AUTOMATIC1111_BASE_URL = res.AUTOMATIC1111_BASE_URL;
|
||||
AUTOMATIC1111_API_AUTH = res.AUTOMATIC1111_API_AUTH;
|
||||
|
||||
await getModels();
|
||||
|
||||
if (models) {
|
||||
toast.success($i18n.t('Server connection verified'));
|
||||
}
|
||||
} else {
|
||||
({ AUTOMATIC1111_BASE_URL, AUTOMATIC1111_API_AUTH } = await getImageGenerationEngineUrls(
|
||||
localStorage.token
|
||||
));
|
||||
}
|
||||
}
|
||||
};
|
||||
const updateImageGeneration = async () => {
|
||||
const res = await updateImageGenerationConfig(
|
||||
localStorage.token,
|
||||
imageGenerationEngine,
|
||||
enableImageGeneration
|
||||
).catch((error) => {
|
||||
const updateConfigHandler = async () => {
|
||||
const res = await updateConfig(localStorage.token, config).catch((error) => {
|
||||
toast.error(error);
|
||||
return null;
|
||||
});
|
||||
|
||||
if (res) {
|
||||
imageGenerationEngine = res.engine;
|
||||
enableImageGeneration = res.enabled;
|
||||
config = res;
|
||||
}
|
||||
|
||||
if (enableImageGeneration) {
|
||||
config.set(await getBackendConfig(localStorage.token));
|
||||
if (config.enabled) {
|
||||
backendConfig.set(await getBackendConfig());
|
||||
getModels();
|
||||
}
|
||||
};
|
||||
|
||||
const validateJSON = (json) => {
|
||||
try {
|
||||
const obj = JSON.parse(json);
|
||||
|
||||
if (obj && typeof obj === 'object') {
|
||||
return true;
|
||||
}
|
||||
} catch (e) {}
|
||||
return false;
|
||||
};
|
||||
|
||||
const saveHandler = async () => {
|
||||
loading = true;
|
||||
|
||||
if (config?.comfyui?.COMFYUI_WORKFLOW) {
|
||||
if (!validateJSON(config.comfyui.COMFYUI_WORKFLOW)) {
|
||||
toast.error('Invalid JSON format for ComfyUI Workflow.');
|
||||
loading = false;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (config?.comfyui?.COMFYUI_WORKFLOW) {
|
||||
config.comfyui.COMFYUI_WORKFLOW_NODES = workflowNodes.map((node) => {
|
||||
return {
|
||||
type: node.type,
|
||||
key: node.key,
|
||||
node_ids: node.node_ids.split(',').map((id) => id.trim())
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
await updateConfig(localStorage.token, config).catch((error) => {
|
||||
toast.error(error);
|
||||
loading = false;
|
||||
return null;
|
||||
});
|
||||
|
||||
await updateImageGenerationConfig(localStorage.token, imageGenerationConfig).catch((error) => {
|
||||
toast.error(error);
|
||||
loading = false;
|
||||
return null;
|
||||
});
|
||||
|
||||
getModels();
|
||||
dispatch('save');
|
||||
loading = false;
|
||||
};
|
||||
|
||||
onMount(async () => {
|
||||
if ($user.role === 'admin') {
|
||||
const res = await getImageGenerationConfig(localStorage.token).catch((error) => {
|
||||
const res = await getConfig(localStorage.token).catch((error) => {
|
||||
toast.error(error);
|
||||
return null;
|
||||
});
|
||||
|
||||
if (res) {
|
||||
imageGenerationEngine = res.engine;
|
||||
enableImageGeneration = res.enabled;
|
||||
config = res;
|
||||
}
|
||||
const URLS = await getImageGenerationEngineUrls(localStorage.token);
|
||||
|
||||
AUTOMATIC1111_BASE_URL = URLS.AUTOMATIC1111_BASE_URL;
|
||||
AUTOMATIC1111_API_AUTH = URLS.AUTOMATIC1111_API_AUTH;
|
||||
COMFYUI_BASE_URL = URLS.COMFYUI_BASE_URL;
|
||||
|
||||
const config = await getOpenAIConfig(localStorage.token);
|
||||
|
||||
OPENAI_API_KEY = config.OPENAI_API_KEY;
|
||||
OPENAI_API_BASE_URL = config.OPENAI_API_BASE_URL;
|
||||
|
||||
imageSize = await getImageSize(localStorage.token);
|
||||
steps = await getImageSteps(localStorage.token);
|
||||
|
||||
if (enableImageGeneration) {
|
||||
if (config.enabled) {
|
||||
getModels();
|
||||
}
|
||||
|
||||
if (config.comfyui.COMFYUI_WORKFLOW) {
|
||||
config.comfyui.COMFYUI_WORKFLOW = JSON.stringify(
|
||||
JSON.parse(config.comfyui.COMFYUI_WORKFLOW),
|
||||
null,
|
||||
2
|
||||
);
|
||||
}
|
||||
|
||||
if ((config?.comfyui?.COMFYUI_WORKFLOW_NODES ?? []).length >= 5) {
|
||||
workflowNodes = config.comfyui.COMFYUI_WORKFLOW_NODES.map((node) => {
|
||||
return {
|
||||
type: node.type,
|
||||
key: node.key,
|
||||
node_ids: node.node_ids.join(',')
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
const imageConfigRes = await getImageGenerationConfig(localStorage.token).catch((error) => {
|
||||
toast.error(error);
|
||||
return null;
|
||||
});
|
||||
|
||||
if (imageConfigRes) {
|
||||
imageGenerationConfig = imageConfigRes;
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@@ -155,263 +175,366 @@
|
||||
<form
|
||||
class="flex flex-col h-full justify-between space-y-3 text-sm"
|
||||
on:submit|preventDefault={async () => {
|
||||
loading = true;
|
||||
|
||||
if (imageGenerationEngine === 'openai') {
|
||||
await updateOpenAIConfig(localStorage.token, OPENAI_API_BASE_URL, OPENAI_API_KEY);
|
||||
}
|
||||
|
||||
await updateDefaultImageGenerationModel(localStorage.token, selectedModel);
|
||||
|
||||
await updateImageSize(localStorage.token, imageSize).catch((error) => {
|
||||
toast.error(error);
|
||||
return null;
|
||||
});
|
||||
await updateImageSteps(localStorage.token, steps).catch((error) => {
|
||||
toast.error(error);
|
||||
return null;
|
||||
});
|
||||
|
||||
dispatch('save');
|
||||
loading = false;
|
||||
saveHandler();
|
||||
}}
|
||||
>
|
||||
<div class=" space-y-3 overflow-y-scroll scrollbar-hidden">
|
||||
<div>
|
||||
<div class=" mb-1 text-sm font-medium">{$i18n.t('Image Settings')}</div>
|
||||
|
||||
<div class=" py-0.5 flex w-full justify-between">
|
||||
<div class=" self-center text-xs font-medium">{$i18n.t('Image Generation Engine')}</div>
|
||||
<div class="flex items-center relative">
|
||||
<select
|
||||
class="w-fit pr-8 rounded px-2 p-1 text-xs bg-transparent outline-none text-right"
|
||||
bind:value={imageGenerationEngine}
|
||||
placeholder={$i18n.t('Select a mode')}
|
||||
on:change={async () => {
|
||||
await updateImageGeneration();
|
||||
}}
|
||||
>
|
||||
<option value="">{$i18n.t('Default (Automatic1111)')}</option>
|
||||
<option value="comfyui">{$i18n.t('ComfyUI')}</option>
|
||||
<option value="openai">{$i18n.t('Open AI (Dall-E)')}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class=" space-y-3 overflow-y-scroll scrollbar-hidden pr-2">
|
||||
{#if config && imageGenerationConfig}
|
||||
<div>
|
||||
<div class=" py-0.5 flex w-full justify-between">
|
||||
<div class=" self-center text-xs font-medium">
|
||||
{$i18n.t('Image Generation (Experimental)')}
|
||||
<div class=" mb-1 text-sm font-medium">{$i18n.t('Image Settings')}</div>
|
||||
|
||||
<div>
|
||||
<div class=" py-0.5 flex w-full justify-between">
|
||||
<div class=" self-center text-xs font-medium">
|
||||
{$i18n.t('Image Generation (Experimental)')}
|
||||
</div>
|
||||
|
||||
<div class="px-1">
|
||||
<Switch
|
||||
bind:state={config.enabled}
|
||||
on:change={(e) => {
|
||||
const enabled = e.detail;
|
||||
|
||||
if (enabled) {
|
||||
if (
|
||||
config.engine === 'automatic1111' &&
|
||||
config.automatic1111.AUTOMATIC1111_BASE_URL === ''
|
||||
) {
|
||||
toast.error($i18n.t('AUTOMATIC1111 Base URL is required.'));
|
||||
config.enabled = false;
|
||||
} else if (
|
||||
config.engine === 'comfyui' &&
|
||||
config.comfyui.COMFYUI_BASE_URL === ''
|
||||
) {
|
||||
toast.error($i18n.t('ComfyUI Base URL is required.'));
|
||||
config.enabled = false;
|
||||
} else if (config.engine === 'openai' && config.openai.OPENAI_API_KEY === '') {
|
||||
toast.error($i18n.t('OpenAI API Key is required.'));
|
||||
config.enabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
updateConfigHandler();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
class="p-1 px-3 text-xs flex rounded transition"
|
||||
on:click={() => {
|
||||
if (imageGenerationEngine === '' && AUTOMATIC1111_BASE_URL === '') {
|
||||
toast.error($i18n.t('AUTOMATIC1111 Base URL is required.'));
|
||||
enableImageGeneration = false;
|
||||
} else if (imageGenerationEngine === 'comfyui' && COMFYUI_BASE_URL === '') {
|
||||
toast.error($i18n.t('ComfyUI Base URL is required.'));
|
||||
enableImageGeneration = false;
|
||||
} else if (imageGenerationEngine === 'openai' && OPENAI_API_KEY === '') {
|
||||
toast.error($i18n.t('OpenAI API Key is required.'));
|
||||
enableImageGeneration = false;
|
||||
} else {
|
||||
enableImageGeneration = !enableImageGeneration;
|
||||
}
|
||||
|
||||
updateImageGeneration();
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
{#if enableImageGeneration === true}
|
||||
<span class="ml-2 self-center">{$i18n.t('On')}</span>
|
||||
{:else}
|
||||
<span class="ml-2 self-center">{$i18n.t('Off')}</span>
|
||||
{/if}
|
||||
</button>
|
||||
<div class=" py-0.5 flex w-full justify-between">
|
||||
<div class=" self-center text-xs font-medium">{$i18n.t('Image Generation Engine')}</div>
|
||||
<div class="flex items-center relative">
|
||||
<select
|
||||
class="w-fit pr-8 rounded px-2 p-1 text-xs bg-transparent outline-none text-right"
|
||||
bind:value={config.engine}
|
||||
placeholder={$i18n.t('Select Engine')}
|
||||
on:change={async () => {
|
||||
updateConfigHandler();
|
||||
}}
|
||||
>
|
||||
<option value="openai">{$i18n.t('Default (Open AI)')}</option>
|
||||
<option value="comfyui">{$i18n.t('ComfyUI')}</option>
|
||||
<option value="automatic1111">{$i18n.t('Automatic1111')}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<hr class=" dark:border-gray-850" />
|
||||
|
||||
{#if imageGenerationEngine === ''}
|
||||
<div class=" mb-2.5 text-sm font-medium">{$i18n.t('AUTOMATIC1111 Base URL')}</div>
|
||||
<div class="flex w-full">
|
||||
<div class="flex-1 mr-2">
|
||||
<input
|
||||
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||
placeholder={$i18n.t('Enter URL (e.g. http://127.0.0.1:7860/)')}
|
||||
bind:value={AUTOMATIC1111_BASE_URL}
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
class="px-2.5 bg-gray-50 hover:bg-gray-100 text-gray-800 dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-gray-100 rounded-lg transition"
|
||||
type="button"
|
||||
on:click={() => {
|
||||
updateUrlHandler();
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
class="w-4 h-4"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M15.312 11.424a5.5 5.5 0 01-9.201 2.466l-.312-.311h2.433a.75.75 0 000-1.5H3.989a.75.75 0 00-.75.75v4.242a.75.75 0 001.5 0v-2.43l.31.31a7 7 0 0011.712-3.138.75.75 0 00-1.449-.39zm1.23-3.723a.75.75 0 00.219-.53V2.929a.75.75 0 00-1.5 0V5.36l-.31-.31A7 7 0 003.239 8.188a.75.75 0 101.448.389A5.5 5.5 0 0113.89 6.11l.311.31h-2.432a.75.75 0 000 1.5h4.243a.75.75 0 00.53-.219z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="mt-2 text-xs text-gray-400 dark:text-gray-500">
|
||||
{$i18n.t('Include `--api` flag when running stable-diffusion-webui')}
|
||||
<a
|
||||
class=" text-gray-300 font-medium"
|
||||
href="https://github.com/AUTOMATIC1111/stable-diffusion-webui/discussions/3734"
|
||||
target="_blank"
|
||||
>
|
||||
{$i18n.t('(e.g. `sh webui.sh --api`)')}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class=" mb-2.5 text-sm font-medium">{$i18n.t('AUTOMATIC1111 Api Auth String')}</div>
|
||||
<SensitiveInput
|
||||
placeholder={$i18n.t('Enter api auth string (e.g. username:password)')}
|
||||
bind:value={AUTOMATIC1111_API_AUTH}
|
||||
required={false}
|
||||
/>
|
||||
|
||||
<div class="mt-2 text-xs text-gray-400 dark:text-gray-500">
|
||||
{$i18n.t('Include `--api-auth` flag when running stable-diffusion-webui')}
|
||||
<a
|
||||
class=" text-gray-300 font-medium"
|
||||
href="https://github.com/AUTOMATIC1111/stable-diffusion-webui/discussions/13993"
|
||||
target="_blank"
|
||||
>
|
||||
{$i18n.t('(e.g. `sh webui.sh --api --api-auth username_password`)').replace('_', ':')}
|
||||
</a>
|
||||
</div>
|
||||
{:else if imageGenerationEngine === 'comfyui'}
|
||||
<div class=" mb-2.5 text-sm font-medium">{$i18n.t('ComfyUI Base URL')}</div>
|
||||
<div class="flex w-full">
|
||||
<div class="flex-1 mr-2">
|
||||
<input
|
||||
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||
placeholder={$i18n.t('Enter URL (e.g. http://127.0.0.1:7860/)')}
|
||||
bind:value={COMFYUI_BASE_URL}
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
class="px-2.5 bg-gray-50 hover:bg-gray-100 text-gray-800 dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-gray-100 rounded-lg transition"
|
||||
type="button"
|
||||
on:click={() => {
|
||||
updateUrlHandler();
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
class="w-4 h-4"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M15.312 11.424a5.5 5.5 0 01-9.201 2.466l-.312-.311h2.433a.75.75 0 000-1.5H3.989a.75.75 0 00-.75.75v4.242a.75.75 0 001.5 0v-2.43l.31.31a7 7 0 0011.712-3.138.75.75 0 00-1.449-.39zm1.23-3.723a.75.75 0 00.219-.53V2.929a.75.75 0 00-1.5 0V5.36l-.31-.31A7 7 0 003.239 8.188a.75.75 0 101.448.389A5.5 5.5 0 0113.89 6.11l.311.31h-2.432a.75.75 0 000 1.5h4.243a.75.75 0 00.53-.219z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
{:else if imageGenerationEngine === 'openai'}
|
||||
<div>
|
||||
<div class=" mb-1.5 text-sm font-medium">{$i18n.t('OpenAI API Config')}</div>
|
||||
|
||||
<div class="flex gap-2 mb-1">
|
||||
<input
|
||||
class="flex-1 w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||
placeholder={$i18n.t('API Base URL')}
|
||||
bind:value={OPENAI_API_BASE_URL}
|
||||
required
|
||||
/>
|
||||
|
||||
<SensitiveInput placeholder={$i18n.t('API Key')} bind:value={OPENAI_API_KEY} />
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if enableImageGeneration}
|
||||
<hr class=" dark:border-gray-850" />
|
||||
|
||||
<div>
|
||||
<div class=" mb-2.5 text-sm font-medium">{$i18n.t('Set Default Model')}</div>
|
||||
<div class="flex w-full">
|
||||
<div class="flex-1 mr-2">
|
||||
{#if imageGenerationEngine === 'openai' && !OPENAI_API_BASE_URL.includes('https://api.openai.com')}
|
||||
<div class="flex flex-col gap-2">
|
||||
{#if (config?.engine ?? 'automatic1111') === 'automatic1111'}
|
||||
<div>
|
||||
<div class=" mb-2 text-sm font-medium">{$i18n.t('AUTOMATIC1111 Base URL')}</div>
|
||||
<div class="flex w-full">
|
||||
<div class="flex-1 mr-2">
|
||||
<input
|
||||
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||
placeholder={$i18n.t('Enter URL (e.g. http://127.0.0.1:7860/)')}
|
||||
bind:value={config.automatic1111.AUTOMATIC1111_BASE_URL}
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
class="px-2.5 bg-gray-50 hover:bg-gray-100 text-gray-800 dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-gray-100 rounded-lg transition"
|
||||
type="button"
|
||||
on:click={async () => {
|
||||
await updateConfigHandler();
|
||||
const res = await verifyConfigUrl(localStorage.token).catch((error) => {
|
||||
toast.error(error);
|
||||
return null;
|
||||
});
|
||||
|
||||
if (res) {
|
||||
toast.success($i18n.t('Server connection verified'));
|
||||
}
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
class="w-4 h-4"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M15.312 11.424a5.5 5.5 0 01-9.201 2.466l-.312-.311h2.433a.75.75 0 000-1.5H3.989a.75.75 0 00-.75.75v4.242a.75.75 0 001.5 0v-2.43l.31.31a7 7 0 0011.712-3.138.75.75 0 00-1.449-.39zm1.23-3.723a.75.75 0 00.219-.53V2.929a.75.75 0 00-1.5 0V5.36l-.31-.31A7 7 0 003.239 8.188a.75.75 0 101.448.389A5.5 5.5 0 0113.89 6.11l.311.31h-2.432a.75.75 0 000 1.5h4.243a.75.75 0 00.53-.219z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="mt-2 text-xs text-gray-400 dark:text-gray-500">
|
||||
{$i18n.t('Include `--api` flag when running stable-diffusion-webui')}
|
||||
<a
|
||||
class=" text-gray-300 font-medium"
|
||||
href="https://github.com/AUTOMATIC1111/stable-diffusion-webui/discussions/3734"
|
||||
target="_blank"
|
||||
>
|
||||
{$i18n.t('(e.g. `sh webui.sh --api`)')}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class=" mb-2 text-sm font-medium">
|
||||
{$i18n.t('AUTOMATIC1111 Api Auth String')}
|
||||
</div>
|
||||
<SensitiveInput
|
||||
placeholder={$i18n.t('Enter api auth string (e.g. username:password)')}
|
||||
bind:value={config.automatic1111.AUTOMATIC1111_API_AUTH}
|
||||
required={false}
|
||||
/>
|
||||
|
||||
<div class="mt-2 text-xs text-gray-400 dark:text-gray-500">
|
||||
{$i18n.t('Include `--api-auth` flag when running stable-diffusion-webui')}
|
||||
<a
|
||||
class=" text-gray-300 font-medium"
|
||||
href="https://github.com/AUTOMATIC1111/stable-diffusion-webui/discussions/13993"
|
||||
target="_blank"
|
||||
>
|
||||
{$i18n
|
||||
.t('(e.g. `sh webui.sh --api --api-auth username_password`)')
|
||||
.replace('_', ':')}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{:else if config?.engine === 'comfyui'}
|
||||
<div class="">
|
||||
<div class=" mb-2 text-sm font-medium">{$i18n.t('ComfyUI Base URL')}</div>
|
||||
<div class="flex w-full">
|
||||
<div class="flex-1 mr-2">
|
||||
<input
|
||||
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||
placeholder={$i18n.t('Enter URL (e.g. http://127.0.0.1:7860/)')}
|
||||
bind:value={config.comfyui.COMFYUI_BASE_URL}
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
class="px-2.5 bg-gray-50 hover:bg-gray-100 text-gray-800 dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-gray-100 rounded-lg transition"
|
||||
type="button"
|
||||
on:click={async () => {
|
||||
await updateConfigHandler();
|
||||
const res = await verifyConfigUrl(localStorage.token).catch((error) => {
|
||||
toast.error(error);
|
||||
return null;
|
||||
});
|
||||
|
||||
if (res) {
|
||||
toast.success($i18n.t('Server connection verified'));
|
||||
}
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
class="w-4 h-4"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M15.312 11.424a5.5 5.5 0 01-9.201 2.466l-.312-.311h2.433a.75.75 0 000-1.5H3.989a.75.75 0 00-.75.75v4.242a.75.75 0 001.5 0v-2.43l.31.31a7 7 0 0011.712-3.138.75.75 0 00-1.449-.39zm1.23-3.723a.75.75 0 00.219-.53V2.929a.75.75 0 00-1.5 0V5.36l-.31-.31A7 7 0 003.239 8.188a.75.75 0 101.448.389A5.5 5.5 0 0113.89 6.11l.311.31h-2.432a.75.75 0 000 1.5h4.243a.75.75 0 00.53-.219z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="">
|
||||
<div class=" mb-2 text-sm font-medium">{$i18n.t('ComfyUI Workflow')}</div>
|
||||
|
||||
{#if config.comfyui.COMFYUI_WORKFLOW}
|
||||
<textarea
|
||||
class="w-full rounded-lg mb-1 py-2 px-4 text-xs bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none disabled:text-gray-600 resize-none"
|
||||
rows="10"
|
||||
bind:value={config.comfyui.COMFYUI_WORKFLOW}
|
||||
required
|
||||
/>
|
||||
{/if}
|
||||
|
||||
<div class="flex w-full">
|
||||
<div class="flex-1">
|
||||
<input
|
||||
id="upload-comfyui-workflow-input"
|
||||
hidden
|
||||
type="file"
|
||||
accept=".json"
|
||||
on:change={(e) => {
|
||||
const file = e.target.files[0];
|
||||
const reader = new FileReader();
|
||||
|
||||
reader.onload = (e) => {
|
||||
config.comfyui.COMFYUI_WORKFLOW = e.target.result;
|
||||
e.target.value = null;
|
||||
};
|
||||
|
||||
reader.readAsText(file);
|
||||
}}
|
||||
/>
|
||||
|
||||
<button
|
||||
class="w-full text-sm font-medium py-2 bg-transparent hover:bg-gray-100 border border-dashed dark:border-gray-800 dark:hover:bg-gray-850 text-center rounded-xl"
|
||||
type="button"
|
||||
on:click={() => {
|
||||
document.getElementById('upload-comfyui-workflow-input')?.click();
|
||||
}}
|
||||
>
|
||||
{$i18n.t('Click here to upload a workflow.json file.')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-2 text-xs text-gray-400 dark:text-gray-500">
|
||||
{$i18n.t('Make sure to export a workflow.json file as API format from ComfyUI.')}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if config.comfyui.COMFYUI_WORKFLOW}
|
||||
<div class="">
|
||||
<div class=" mb-2 text-sm font-medium">{$i18n.t('ComfyUI Workflow Nodes')}</div>
|
||||
|
||||
<div class="text-xs flex flex-col gap-1.5">
|
||||
{#each workflowNodes as node}
|
||||
<div class="flex w-full items-center border dark:border-gray-850 rounded-lg">
|
||||
<div class="flex-shrink-0">
|
||||
<div
|
||||
class=" capitalize line-clamp-1 font-medium px-3 py-1 w-20 text-center rounded-l-lg bg-green-500/10 text-green-700 dark:text-green-200"
|
||||
>
|
||||
{node.type}
|
||||
</div>
|
||||
</div>
|
||||
<div class="">
|
||||
<Tooltip content="Input Key (e.g. text, unet_name, steps)">
|
||||
<input
|
||||
class="py-1 px-3 w-24 text-xs text-center bg-transparent outline-none border-r dark:border-gray-850"
|
||||
placeholder="Key"
|
||||
bind:value={node.key}
|
||||
required
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
<div class="w-full">
|
||||
<Tooltip
|
||||
content="Comma separated Node Ids (e.g. 1 or 1,2)"
|
||||
placement="top-start"
|
||||
>
|
||||
<input
|
||||
class="w-full py-1 px-4 rounded-r-lg text-xs bg-transparent outline-none"
|
||||
placeholder="Node Ids"
|
||||
bind:value={node.node_ids}
|
||||
required
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{:else if config?.engine === 'openai'}
|
||||
<div>
|
||||
<div class=" mb-1.5 text-sm font-medium">{$i18n.t('OpenAI API Config')}</div>
|
||||
|
||||
<div class="flex gap-2 mb-1">
|
||||
<input
|
||||
class="flex-1 w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||
placeholder={$i18n.t('API Base URL')}
|
||||
bind:value={config.openai.OPENAI_API_BASE_URL}
|
||||
required
|
||||
/>
|
||||
|
||||
<SensitiveInput
|
||||
placeholder={$i18n.t('API Key')}
|
||||
bind:value={config.openai.OPENAI_API_KEY}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if config?.enabled}
|
||||
<hr class=" dark:border-gray-850" />
|
||||
|
||||
<div>
|
||||
<div class=" mb-2.5 text-sm font-medium">{$i18n.t('Set Default Model')}</div>
|
||||
<div class="flex w-full">
|
||||
<div class="flex-1 mr-2">
|
||||
<div class="flex w-full">
|
||||
<div class="flex-1">
|
||||
<input
|
||||
list="model-list"
|
||||
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||
bind:value={selectedModel}
|
||||
placeholder="Select a model"
|
||||
/>
|
||||
<Tooltip content={$i18n.t('Enter Model ID')} placement="top-start">
|
||||
<input
|
||||
list="model-list"
|
||||
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||
bind:value={imageGenerationConfig.MODEL}
|
||||
placeholder="Select a model"
|
||||
required
|
||||
/>
|
||||
|
||||
<datalist id="model-list">
|
||||
{#each models ?? [] as model}
|
||||
<option value={model.id}>{model.name}</option>
|
||||
{/each}
|
||||
</datalist>
|
||||
<datalist id="model-list">
|
||||
{#each models ?? [] as model}
|
||||
<option value={model.id}>{model.name}</option>
|
||||
{/each}
|
||||
</datalist>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<select
|
||||
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||
bind:value={selectedModel}
|
||||
placeholder={$i18n.t('Select a model')}
|
||||
required
|
||||
>
|
||||
{#if !selectedModel}
|
||||
<option value="" disabled selected>{$i18n.t('Select a model')}</option>
|
||||
{/if}
|
||||
{#each models ?? [] as model}
|
||||
<option value={model.id} class="bg-gray-100 dark:bg-gray-700">{model.name}</option
|
||||
>
|
||||
{/each}
|
||||
</select>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class=" mb-2.5 text-sm font-medium">{$i18n.t('Set Image Size')}</div>
|
||||
<div class="flex w-full">
|
||||
<div class="flex-1 mr-2">
|
||||
<input
|
||||
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||
placeholder={$i18n.t('Enter Image Size (e.g. 512x512)')}
|
||||
bind:value={imageSize}
|
||||
/>
|
||||
<div>
|
||||
<div class=" mb-2.5 text-sm font-medium">{$i18n.t('Set Image Size')}</div>
|
||||
<div class="flex w-full">
|
||||
<div class="flex-1 mr-2">
|
||||
<Tooltip content={$i18n.t('Enter Image Size (e.g. 512x512)')} placement="top-start">
|
||||
<input
|
||||
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||
placeholder={$i18n.t('Enter Image Size (e.g. 512x512)')}
|
||||
bind:value={imageGenerationConfig.IMAGE_SIZE}
|
||||
required
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class=" mb-2.5 text-sm font-medium">{$i18n.t('Set Steps')}</div>
|
||||
<div class="flex w-full">
|
||||
<div class="flex-1 mr-2">
|
||||
<input
|
||||
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||
placeholder={$i18n.t('Enter Number of Steps (e.g. 50)')}
|
||||
bind:value={steps}
|
||||
/>
|
||||
<div>
|
||||
<div class=" mb-2.5 text-sm font-medium">{$i18n.t('Set Steps')}</div>
|
||||
<div class="flex w-full">
|
||||
<div class="flex-1 mr-2">
|
||||
<Tooltip content={$i18n.t('Enter Number of Steps (e.g. 50)')} placement="top-start">
|
||||
<input
|
||||
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||
placeholder={$i18n.t('Enter Number of Steps (e.g. 50)')}
|
||||
bind:value={imageGenerationConfig.IMAGE_STEPS}
|
||||
required
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
let ollamaEnabled = null;
|
||||
|
||||
let OLLAMA_URLS = [];
|
||||
let selectedOllamaUrlIdx: string | null = null;
|
||||
let selectedOllamaUrlIdx: number | null = null;
|
||||
|
||||
let updateModelId = null;
|
||||
let updateProgress = null;
|
||||
|
||||
@@ -18,7 +18,9 @@
|
||||
let whitelistModels = [''];
|
||||
let permissions = {
|
||||
chat: {
|
||||
deletion: true
|
||||
deletion: true,
|
||||
edit: true,
|
||||
temporary: true
|
||||
}
|
||||
};
|
||||
|
||||
@@ -92,6 +94,88 @@
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class=" flex w-full justify-between">
|
||||
<div class=" self-center text-xs font-medium">{$i18n.t('Allow Chat Editing')}</div>
|
||||
|
||||
<button
|
||||
class="p-1 px-3 text-xs flex rounded transition"
|
||||
on:click={() => {
|
||||
permissions.chat.editing = !(permissions?.chat?.editing ?? true);
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
{#if permissions?.chat?.editing ?? true}
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 16 16"
|
||||
fill="currentColor"
|
||||
class="w-4 h-4"
|
||||
>
|
||||
<path
|
||||
d="M11.5 1A3.5 3.5 0 0 0 8 4.5V7H2.5A1.5 1.5 0 0 0 1 8.5v5A1.5 1.5 0 0 0 2.5 15h7a1.5 1.5 0 0 0 1.5-1.5v-5A1.5 1.5 0 0 0 9.5 7V4.5a2 2 0 1 1 4 0v1.75a.75.75 0 0 0 1.5 0V4.5A3.5 3.5 0 0 0 11.5 1Z"
|
||||
/>
|
||||
</svg>
|
||||
<span class="ml-2 self-center">{$i18n.t('Allow')}</span>
|
||||
{:else}
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 16 16"
|
||||
fill="currentColor"
|
||||
class="w-4 h-4"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M8 1a3.5 3.5 0 0 0-3.5 3.5V7A1.5 1.5 0 0 0 3 8.5v5A1.5 1.5 0 0 0 4.5 15h7a1.5 1.5 0 0 0 1.5-1.5v-5A1.5 1.5 0 0 0 11.5 7V4.5A3.5 3.5 0 0 0 8 1Zm2 6V4.5a2 2 0 1 0-4 0V7h4Z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
|
||||
<span class="ml-2 self-center">{$i18n.t("Don't Allow")}</span>
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class=" flex w-full justify-between">
|
||||
<div class=" self-center text-xs font-medium">{$i18n.t('Allow Temporary Chat')}</div>
|
||||
|
||||
<button
|
||||
class="p-1 px-3 text-xs flex rounded transition"
|
||||
on:click={() => {
|
||||
permissions.chat.temporary = !(permissions?.chat?.temporary ?? true);
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
{#if permissions?.chat?.temporary ?? true}
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 16 16"
|
||||
fill="currentColor"
|
||||
class="w-4 h-4"
|
||||
>
|
||||
<path
|
||||
d="M11.5 1A3.5 3.5 0 0 0 8 4.5V7H2.5A1.5 1.5 0 0 0 1 8.5v5A1.5 1.5 0 0 0 2.5 15h7a1.5 1.5 0 0 0 1.5-1.5v-5A1.5 1.5 0 0 0 9.5 7V4.5a2 2 0 1 1 4 0v1.75a.75.75 0 0 0 1.5 0V4.5A3.5 3.5 0 0 0 11.5 1Z"
|
||||
/>
|
||||
</svg>
|
||||
<span class="ml-2 self-center">{$i18n.t('Allow')}</span>
|
||||
{:else}
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 16 16"
|
||||
fill="currentColor"
|
||||
class="w-4 h-4"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M8 1a3.5 3.5 0 0 0-3.5 3.5V7A1.5 1.5 0 0 0 3 8.5v5A1.5 1.5 0 0 0 4.5 15h7a1.5 1.5 0 0 0 1.5-1.5v-5A1.5 1.5 0 0 0 11.5 7V4.5A3.5 3.5 0 0 0 8 1Zm2 6V4.5a2 2 0 1 0-4 0V7h4Z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
|
||||
<span class="ml-2 self-center">{$i18n.t("Don't Allow")}</span>
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr class=" dark:border-gray-850 my-2" />
|
||||
|
||||
@@ -25,7 +25,9 @@
|
||||
user,
|
||||
socket,
|
||||
showCallOverlay,
|
||||
tools
|
||||
tools,
|
||||
currentChatPage,
|
||||
temporaryChatEnabled
|
||||
} from '$lib/stores';
|
||||
import {
|
||||
convertMessagesToHistory,
|
||||
@@ -52,7 +54,13 @@
|
||||
import { createOpenAITextStream } from '$lib/apis/streaming';
|
||||
import { queryMemory } from '$lib/apis/memories';
|
||||
import { getAndUpdateUserLocation, getUserSettings } from '$lib/apis/users';
|
||||
import { chatCompleted, generateTitle, generateSearchQuery, chatAction } from '$lib/apis';
|
||||
import {
|
||||
chatCompleted,
|
||||
generateTitle,
|
||||
generateSearchQuery,
|
||||
chatAction,
|
||||
generateMoACompletion
|
||||
} from '$lib/apis';
|
||||
|
||||
import Banner from '../common/Banner.svelte';
|
||||
import MessageInput from '$lib/components/chat/MessageInput.svelte';
|
||||
@@ -165,6 +173,14 @@
|
||||
message.content += data.content;
|
||||
} else if (type === 'replace') {
|
||||
message.content = data.content;
|
||||
} else if (type === 'action') {
|
||||
if (data.action === 'continue') {
|
||||
const continueButton = document.getElementById('continue-response-button');
|
||||
|
||||
if (continueButton) {
|
||||
continueButton.click();
|
||||
}
|
||||
}
|
||||
} else if (type === 'confirmation') {
|
||||
eventCallback = cb;
|
||||
|
||||
@@ -237,7 +253,7 @@
|
||||
}
|
||||
});
|
||||
} else {
|
||||
if (!($settings.saveChatHistory ?? true)) {
|
||||
if ($temporaryChatEnabled) {
|
||||
await goto('/');
|
||||
}
|
||||
}
|
||||
@@ -413,7 +429,7 @@
|
||||
}
|
||||
|
||||
if ($chatId == chatId) {
|
||||
if ($settings.saveChatHistory ?? true) {
|
||||
if (!$temporaryChatEnabled) {
|
||||
chat = await updateChatById(localStorage.token, chatId, {
|
||||
models: selectedModels,
|
||||
messages: messages,
|
||||
@@ -421,12 +437,14 @@
|
||||
params: params,
|
||||
files: chatFiles
|
||||
});
|
||||
await chats.set(await getChatList(localStorage.token));
|
||||
|
||||
currentChatPage.set(1);
|
||||
await chats.set(await getChatList(localStorage.token, $currentChatPage));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const chatActionHandler = async (chatId, actionId, modelId, responseMessageId) => {
|
||||
const chatActionHandler = async (chatId, actionId, modelId, responseMessageId, event = null) => {
|
||||
const res = await chatAction(localStorage.token, actionId, {
|
||||
model: modelId,
|
||||
messages: messages.map((m) => ({
|
||||
@@ -436,6 +454,7 @@
|
||||
info: m.info ? m.info : undefined,
|
||||
timestamp: m.timestamp
|
||||
})),
|
||||
...(event ? { event: event } : {}),
|
||||
chat_id: chatId,
|
||||
session_id: $socket?.id,
|
||||
id: responseMessageId
|
||||
@@ -459,7 +478,7 @@
|
||||
}
|
||||
|
||||
if ($chatId == chatId) {
|
||||
if ($settings.saveChatHistory ?? true) {
|
||||
if (!$temporaryChatEnabled) {
|
||||
chat = await updateChatById(localStorage.token, chatId, {
|
||||
models: selectedModels,
|
||||
messages: messages,
|
||||
@@ -467,7 +486,9 @@
|
||||
params: params,
|
||||
files: chatFiles
|
||||
});
|
||||
await chats.set(await getChatList(localStorage.token));
|
||||
|
||||
currentChatPage.set(1);
|
||||
await chats.set(await getChatList(localStorage.token, $currentChatPage));
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -547,7 +568,7 @@
|
||||
content: userPrompt,
|
||||
files: _files.length > 0 ? _files : undefined,
|
||||
timestamp: Math.floor(Date.now() / 1000), // Unix epoch
|
||||
models: selectedModels.filter((m, mIdx) => selectedModels.indexOf(m) === mIdx)
|
||||
models: selectedModels
|
||||
};
|
||||
|
||||
// Add message to history and Set currentId to messageId
|
||||
@@ -567,19 +588,23 @@
|
||||
return _responses;
|
||||
};
|
||||
|
||||
const sendPrompt = async (prompt, parentId, { modelId = null, newChat = false } = {}) => {
|
||||
const sendPrompt = async (
|
||||
prompt,
|
||||
parentId,
|
||||
{ modelId = null, modelIdx = null, newChat = false } = {}
|
||||
) => {
|
||||
let _responses = [];
|
||||
|
||||
// If modelId is provided, use it, else use selected model
|
||||
let selectedModelIds = modelId
|
||||
? [modelId]
|
||||
: atSelectedModel !== undefined
|
||||
? [atSelectedModel.id]
|
||||
: selectedModels;
|
||||
? [atSelectedModel.id]
|
||||
: selectedModels;
|
||||
|
||||
// Create response messages for each selected model
|
||||
const responseMessageIds = {};
|
||||
for (const modelId of selectedModelIds) {
|
||||
for (const [_modelIdx, modelId] of selectedModelIds.entries()) {
|
||||
const model = $models.filter((m) => m.id === modelId).at(0);
|
||||
|
||||
if (model) {
|
||||
@@ -592,6 +617,7 @@
|
||||
content: '',
|
||||
model: model.id,
|
||||
modelName: model.name ?? model.id,
|
||||
modelIdx: modelIdx ? modelIdx : _modelIdx,
|
||||
userContext: null,
|
||||
timestamp: Math.floor(Date.now() / 1000) // Unix epoch
|
||||
};
|
||||
@@ -608,14 +634,14 @@
|
||||
];
|
||||
}
|
||||
|
||||
responseMessageIds[modelId] = responseMessageId;
|
||||
responseMessageIds[`${modelId}-${modelIdx ? modelIdx : _modelIdx}`] = responseMessageId;
|
||||
}
|
||||
}
|
||||
await tick();
|
||||
|
||||
// Create new chat if only one message in messages
|
||||
if (newChat && messages.length == 2) {
|
||||
if ($settings.saveChatHistory ?? true) {
|
||||
if (!$temporaryChatEnabled) {
|
||||
chat = await createNewChat(localStorage.token, {
|
||||
id: $chatId,
|
||||
title: $i18n.t('New Chat'),
|
||||
@@ -627,7 +653,9 @@
|
||||
tags: [],
|
||||
timestamp: Date.now()
|
||||
});
|
||||
await chats.set(await getChatList(localStorage.token));
|
||||
|
||||
currentChatPage.set(1);
|
||||
await chats.set(await getChatList(localStorage.token, $currentChatPage));
|
||||
await chatId.set(chat.id);
|
||||
} else {
|
||||
await chatId.set('local');
|
||||
@@ -638,7 +666,7 @@
|
||||
const _chatId = JSON.parse(JSON.stringify($chatId));
|
||||
|
||||
await Promise.all(
|
||||
selectedModelIds.map(async (modelId) => {
|
||||
selectedModelIds.map(async (modelId, _modelIdx) => {
|
||||
console.log('modelId', modelId);
|
||||
const model = $models.filter((m) => m.id === modelId).at(0);
|
||||
|
||||
@@ -656,7 +684,8 @@
|
||||
);
|
||||
}
|
||||
|
||||
let responseMessageId = responseMessageIds[modelId];
|
||||
let responseMessageId =
|
||||
responseMessageIds[`${modelId}-${modelIdx ? modelIdx : _modelIdx}`];
|
||||
let responseMessage = history.messages[responseMessageId];
|
||||
|
||||
let userContext = null;
|
||||
@@ -703,7 +732,9 @@
|
||||
})
|
||||
);
|
||||
|
||||
await chats.set(await getChatList(localStorage.token));
|
||||
currentChatPage.set(1);
|
||||
await chats.set(await getChatList(localStorage.token, $currentChatPage));
|
||||
|
||||
return _responses;
|
||||
};
|
||||
|
||||
@@ -730,11 +761,11 @@
|
||||
? await getAndUpdateUserLocation(localStorage.token)
|
||||
: undefined
|
||||
)}${
|
||||
responseMessage?.userContext ?? null
|
||||
(responseMessage?.userContext ?? null)
|
||||
? `\n\nUser Context:\n${responseMessage?.userContext ?? ''}`
|
||||
: ''
|
||||
}`
|
||||
}
|
||||
}
|
||||
: undefined,
|
||||
...messages
|
||||
]
|
||||
@@ -802,10 +833,10 @@
|
||||
options: {
|
||||
...(params ?? $settings.params ?? {}),
|
||||
stop:
|
||||
params?.stop ?? $settings?.params?.stop ?? undefined
|
||||
? (params?.stop ?? $settings.params.stop).map((str) =>
|
||||
decodeURIComponent(JSON.parse('"' + str.replace(/\"/g, '\\"') + '"'))
|
||||
)
|
||||
(params?.stop ?? $settings?.params?.stop ?? undefined)
|
||||
? (params?.stop.split(',').map((token) => token.trim()) ?? $settings.params.stop).map(
|
||||
(str) => decodeURIComponent(JSON.parse('"' + str.replace(/\"/g, '\\"') + '"'))
|
||||
)
|
||||
: undefined,
|
||||
num_predict: params?.max_tokens ?? $settings?.params?.max_tokens ?? undefined,
|
||||
repeat_penalty:
|
||||
@@ -868,6 +899,10 @@
|
||||
} else {
|
||||
responseMessage.content += data.message.content;
|
||||
|
||||
if (navigator.vibrate && ($settings?.hapticFeedback ?? false)) {
|
||||
navigator.vibrate(5);
|
||||
}
|
||||
|
||||
const sentences = extractSentencesForAudio(responseMessage.content);
|
||||
sentences.pop();
|
||||
|
||||
@@ -940,18 +975,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
if ($chatId == _chatId) {
|
||||
if ($settings.saveChatHistory ?? true) {
|
||||
chat = await updateChatById(localStorage.token, _chatId, {
|
||||
messages: messages,
|
||||
history: history,
|
||||
models: selectedModels,
|
||||
params: params,
|
||||
files: chatFiles
|
||||
});
|
||||
await chats.set(await getChatList(localStorage.token));
|
||||
}
|
||||
}
|
||||
await saveChatHandler(_chatId);
|
||||
} else {
|
||||
if (res !== null) {
|
||||
const error = await res.json();
|
||||
@@ -1045,10 +1069,10 @@
|
||||
stream: true,
|
||||
model: model.id,
|
||||
stream_options:
|
||||
model.info?.meta?.capabilities?.usage ?? false
|
||||
(model.info?.meta?.capabilities?.usage ?? false)
|
||||
? {
|
||||
include_usage: true
|
||||
}
|
||||
}
|
||||
: undefined,
|
||||
messages: [
|
||||
params?.system || $settings.system || (responseMessage?.userContext ?? null)
|
||||
@@ -1061,11 +1085,11 @@
|
||||
? await getAndUpdateUserLocation(localStorage.token)
|
||||
: undefined
|
||||
)}${
|
||||
responseMessage?.userContext ?? null
|
||||
(responseMessage?.userContext ?? null)
|
||||
? `\n\nUser Context:\n${responseMessage?.userContext ?? ''}`
|
||||
: ''
|
||||
}`
|
||||
}
|
||||
}
|
||||
: undefined,
|
||||
...messages
|
||||
]
|
||||
@@ -1081,7 +1105,7 @@
|
||||
text:
|
||||
arr.length - 1 !== idx
|
||||
? message.content
|
||||
: message?.raContent ?? message.content
|
||||
: (message?.raContent ?? message.content)
|
||||
},
|
||||
...message.files
|
||||
.filter((file) => file.type === 'image')
|
||||
@@ -1092,20 +1116,20 @@
|
||||
}
|
||||
}))
|
||||
]
|
||||
}
|
||||
}
|
||||
: {
|
||||
content:
|
||||
arr.length - 1 !== idx
|
||||
? message.content
|
||||
: message?.raContent ?? message.content
|
||||
})
|
||||
: (message?.raContent ?? message.content)
|
||||
})
|
||||
})),
|
||||
seed: params?.seed ?? $settings?.params?.seed ?? undefined,
|
||||
stop:
|
||||
params?.stop ?? $settings?.params?.stop ?? undefined
|
||||
? (params?.stop ?? $settings.params.stop).map((str) =>
|
||||
decodeURIComponent(JSON.parse('"' + str.replace(/\"/g, '\\"') + '"'))
|
||||
)
|
||||
(params?.stop ?? $settings?.params?.stop ?? undefined)
|
||||
? (params?.stop.split(',').map((token) => token.trim()) ?? $settings.params.stop).map(
|
||||
(str) => decodeURIComponent(JSON.parse('"' + str.replace(/\"/g, '\\"') + '"'))
|
||||
)
|
||||
: undefined,
|
||||
temperature: params?.temperature ?? $settings?.params?.temperature ?? undefined,
|
||||
top_p: params?.top_p ?? $settings?.params?.top_p ?? undefined,
|
||||
@@ -1128,7 +1152,6 @@
|
||||
|
||||
if (res && res.ok && res.body) {
|
||||
const textStream = await createOpenAITextStream(res.body, $settings.splitLargeChunks);
|
||||
let lastUsage = null;
|
||||
|
||||
for await (const update of textStream) {
|
||||
const { value, done, citations, error, usage } = update;
|
||||
@@ -1154,7 +1177,7 @@
|
||||
}
|
||||
|
||||
if (usage) {
|
||||
lastUsage = usage;
|
||||
responseMessage.info = { ...usage, openai: true };
|
||||
}
|
||||
|
||||
if (citations) {
|
||||
@@ -1167,6 +1190,10 @@
|
||||
} else {
|
||||
responseMessage.content += value;
|
||||
|
||||
if (navigator.vibrate && ($settings?.hapticFeedback ?? false)) {
|
||||
navigator.vibrate(5);
|
||||
}
|
||||
|
||||
const sentences = extractSentencesForAudio(responseMessage.content);
|
||||
sentences.pop();
|
||||
|
||||
@@ -1208,12 +1235,8 @@
|
||||
document.getElementById(`speak-button-${responseMessage.id}`)?.click();
|
||||
}
|
||||
|
||||
if (lastUsage) {
|
||||
responseMessage.info = { ...lastUsage, openai: true };
|
||||
}
|
||||
|
||||
if ($chatId == _chatId) {
|
||||
if ($settings.saveChatHistory ?? true) {
|
||||
if (!$temporaryChatEnabled) {
|
||||
chat = await updateChatById(localStorage.token, _chatId, {
|
||||
models: selectedModels,
|
||||
messages: messages,
|
||||
@@ -1221,7 +1244,9 @@
|
||||
params: params,
|
||||
files: chatFiles
|
||||
});
|
||||
await chats.set(await getChatList(localStorage.token));
|
||||
|
||||
currentChatPage.set(1);
|
||||
await chats.set(await getChatList(localStorage.token, $currentChatPage));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -1324,7 +1349,10 @@
|
||||
} else {
|
||||
// If there are multiple models selected, use the model of the response message for regeneration
|
||||
// e.g. many model chat
|
||||
await sendPrompt(userPrompt, userMessage.id, { modelId: message.model });
|
||||
await sendPrompt(userPrompt, userMessage.id, {
|
||||
modelId: message.model,
|
||||
modelIdx: message.modelIdx
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -1384,9 +1412,11 @@
|
||||
title = _title;
|
||||
}
|
||||
|
||||
if ($settings.saveChatHistory ?? true) {
|
||||
if (!$temporaryChatEnabled) {
|
||||
chat = await updateChatById(localStorage.token, _chatId, { title: _title });
|
||||
await chats.set(await getChatList(localStorage.token));
|
||||
|
||||
currentChatPage.set(1);
|
||||
await chats.set(await getChatList(localStorage.token, $currentChatPage));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1474,6 +1504,69 @@
|
||||
return [];
|
||||
});
|
||||
};
|
||||
|
||||
const saveChatHandler = async (_chatId) => {
|
||||
if ($chatId == _chatId) {
|
||||
if (!$temporaryChatEnabled) {
|
||||
chat = await updateChatById(localStorage.token, _chatId, {
|
||||
messages: messages,
|
||||
history: history,
|
||||
models: selectedModels,
|
||||
params: params,
|
||||
files: chatFiles
|
||||
});
|
||||
|
||||
currentChatPage.set(1);
|
||||
await chats.set(await getChatList(localStorage.token, $currentChatPage));
|
||||
}
|
||||
}
|
||||
};
|
||||
const mergeResponses = async (messageId, responses, _chatId) => {
|
||||
console.log('mergeResponses', messageId, responses);
|
||||
const message = history.messages[messageId];
|
||||
const mergedResponse = {
|
||||
status: true,
|
||||
content: ''
|
||||
};
|
||||
message.merged = mergedResponse;
|
||||
messages = messages;
|
||||
|
||||
try {
|
||||
const [res, controller] = await generateMoACompletion(
|
||||
localStorage.token,
|
||||
message.model,
|
||||
history.messages[message.parentId].content,
|
||||
responses
|
||||
);
|
||||
|
||||
if (res && res.ok && res.body) {
|
||||
const textStream = await createOpenAITextStream(res.body, $settings.splitLargeChunks);
|
||||
for await (const update of textStream) {
|
||||
const { value, done, citations, error, usage } = update;
|
||||
if (error || done) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (mergedResponse.content == '' && value == '\n') {
|
||||
continue;
|
||||
} else {
|
||||
mergedResponse.content += value;
|
||||
messages = messages;
|
||||
}
|
||||
|
||||
if (autoScroll) {
|
||||
scrollToBottom();
|
||||
}
|
||||
}
|
||||
|
||||
await saveChatHandler(_chatId);
|
||||
} else {
|
||||
console.error(res);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
@@ -1600,6 +1693,7 @@
|
||||
{sendPrompt}
|
||||
{continueGeneration}
|
||||
{regenerateResponse}
|
||||
{mergeResponses}
|
||||
{chatActionHandler}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -9,6 +9,8 @@
|
||||
import FileItem from '$lib/components/common/FileItem.svelte';
|
||||
import Collapsible from '$lib/components/common/Collapsible.svelte';
|
||||
|
||||
import { user } from '$lib/stores';
|
||||
|
||||
export let models = [];
|
||||
|
||||
export let chatFiles = [];
|
||||
@@ -78,7 +80,7 @@
|
||||
<Collapsible title={$i18n.t('Advanced Params')} open={true}>
|
||||
<div class="text-sm mt-1.5" slot="content">
|
||||
<div>
|
||||
<AdvancedParams bind:params />
|
||||
<AdvancedParams admin={$user?.role === 'admin'} bind:params />
|
||||
</div>
|
||||
</div>
|
||||
</Collapsible>
|
||||
|
||||
@@ -384,7 +384,7 @@
|
||||
|
||||
{#if atSelectedModel !== undefined}
|
||||
<div
|
||||
class="px-3 py-2.5 text-left w-full flex justify-between items-center absolute bottom-0 left-0 right-0 bg-gradient-to-t from-50% from-white dark:from-gray-900"
|
||||
class="px-3 py-2.5 text-left w-full flex justify-between items-center absolute bottom-0 left-0 right-0 bg-gradient-to-t from-50% from-white dark:from-gray-900 z-50"
|
||||
>
|
||||
<div class="flex items-center gap-2 text-sm dark:text-gray-500">
|
||||
<img
|
||||
|
||||
@@ -609,10 +609,10 @@
|
||||
style="font-size:{rmsLevel * 100 > 4
|
||||
? '4.5'
|
||||
: rmsLevel * 100 > 2
|
||||
? '4.25'
|
||||
: rmsLevel * 100 > 1
|
||||
? '3.75'
|
||||
: '3.5'}rem;width: 100%; text-align:center;"
|
||||
? '4.25'
|
||||
: rmsLevel * 100 > 1
|
||||
? '3.75'
|
||||
: '3.5'}rem;width: 100%; text-align:center;"
|
||||
>
|
||||
{emoji}
|
||||
</div>
|
||||
@@ -658,10 +658,10 @@
|
||||
class=" {rmsLevel * 100 > 4
|
||||
? ' size-[4.5rem]'
|
||||
: rmsLevel * 100 > 2
|
||||
? ' size-16'
|
||||
: rmsLevel * 100 > 1
|
||||
? 'size-14'
|
||||
: 'size-12'} transition-all rounded-full {(model?.info?.meta
|
||||
? ' size-16'
|
||||
: rmsLevel * 100 > 1
|
||||
? 'size-14'
|
||||
: 'size-12'} transition-all rounded-full {(model?.info?.meta
|
||||
?.profile_image_url ?? '/static/favicon.png') !== '/static/favicon.png'
|
||||
? ' bg-cover bg-center bg-no-repeat'
|
||||
: 'bg-black dark:bg-white'} bg-black dark:bg-white"
|
||||
@@ -691,10 +691,10 @@
|
||||
style="font-size:{rmsLevel * 100 > 4
|
||||
? '13'
|
||||
: rmsLevel * 100 > 2
|
||||
? '12'
|
||||
: rmsLevel * 100 > 1
|
||||
? '11.5'
|
||||
: '11'}rem;width:100%;text-align:center;"
|
||||
? '12'
|
||||
: rmsLevel * 100 > 1
|
||||
? '11.5'
|
||||
: '11'}rem;width:100%;text-align:center;"
|
||||
>
|
||||
{emoji}
|
||||
</div>
|
||||
@@ -740,10 +740,10 @@
|
||||
class=" {rmsLevel * 100 > 4
|
||||
? ' size-52'
|
||||
: rmsLevel * 100 > 2
|
||||
? 'size-48'
|
||||
: rmsLevel * 100 > 1
|
||||
? 'size-[11.5rem]'
|
||||
: 'size-44'} transition-all rounded-full {(model?.info?.meta
|
||||
? 'size-48'
|
||||
: rmsLevel * 100 > 1
|
||||
? 'size-[11.5rem]'
|
||||
: 'size-44'} transition-all rounded-full {(model?.info?.meta
|
||||
?.profile_image_url ?? '/static/favicon.png') !== '/static/favicon.png'
|
||||
? ' bg-cover bg-center bg-no-repeat'
|
||||
: 'bg-black dark:bg-white'} "
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
title: $i18n.t('All Documents'),
|
||||
collection_names: $documents.map((doc) => doc.collection_name)
|
||||
}
|
||||
]
|
||||
]
|
||||
: []),
|
||||
...$documents
|
||||
.reduce((a, e, i, arr) => {
|
||||
|
||||
@@ -147,8 +147,8 @@
|
||||
<div class="m-1 overflow-y-auto p-1 rounded-r-lg space-y-0.5 scrollbar-hidden">
|
||||
{#each filteredModels as model, modelIdx}
|
||||
<button
|
||||
class=" px-3 py-1.5 rounded-xl w-full text-left {modelIdx === selectedIdx
|
||||
? ' bg-gray-50 dark:bg-gray-850 selected-command-option-button'
|
||||
class="px-3 py-1.5 rounded-xl w-full text-left {modelIdx === selectedIdx
|
||||
? 'bg-gray-50 dark:bg-gray-850 selected-command-option-button'
|
||||
: ''}"
|
||||
type="button"
|
||||
on:click={() => {
|
||||
@@ -159,13 +159,18 @@
|
||||
}}
|
||||
on:focus={() => {}}
|
||||
>
|
||||
<div class=" font-medium text-black dark:text-gray-100 line-clamp-1">
|
||||
<div class="flex font-medium text-black dark:text-gray-100 line-clamp-1">
|
||||
<img
|
||||
src={model?.info?.meta?.profile_image_url ?? '/static/favicon.png'}
|
||||
alt={model?.name ?? model.id}
|
||||
class="rounded-full size-6 items-center mr-2"
|
||||
/>
|
||||
{model.name}
|
||||
</div>
|
||||
|
||||
<!-- <div class=" text-xs text-gray-600 line-clamp-1">
|
||||
{doc.title}
|
||||
</div> -->
|
||||
</div> -->
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
@@ -1,19 +1,16 @@
|
||||
<script lang="ts">
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { chats, config, settings, user as _user, mobile } from '$lib/stores';
|
||||
import { chats, config, settings, user as _user, mobile, currentChatPage } from '$lib/stores';
|
||||
import { tick, getContext, onMount } from 'svelte';
|
||||
|
||||
import { toast } from 'svelte-sonner';
|
||||
import { getChatList, updateChatById } from '$lib/apis/chats';
|
||||
import { copyToClipboard, findWordIndices } from '$lib/utils';
|
||||
|
||||
import UserMessage from './Messages/UserMessage.svelte';
|
||||
import ResponseMessage from './Messages/ResponseMessage.svelte';
|
||||
import Placeholder from './Messages/Placeholder.svelte';
|
||||
import Spinner from '../common/Spinner.svelte';
|
||||
import { imageGenerations } from '$lib/apis/images';
|
||||
import { copyToClipboard, findWordIndices } from '$lib/utils';
|
||||
import CompareMessages from './Messages/CompareMessages.svelte';
|
||||
import { stringify } from 'postcss';
|
||||
import MultiResponseMessages from './Messages/MultiResponseMessages.svelte';
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
@@ -22,6 +19,7 @@
|
||||
export let sendPrompt: Function;
|
||||
export let continueGeneration: Function;
|
||||
export let regenerateResponse: Function;
|
||||
export let mergeResponses: Function;
|
||||
export let chatActionHandler: Function;
|
||||
|
||||
export let user = $_user;
|
||||
@@ -53,34 +51,43 @@
|
||||
}
|
||||
};
|
||||
|
||||
const confirmEditMessage = async (messageId, content) => {
|
||||
let userPrompt = content;
|
||||
let userMessageId = uuidv4();
|
||||
const confirmEditMessage = async (messageId, content, submit = true) => {
|
||||
if (submit) {
|
||||
let userPrompt = content;
|
||||
let userMessageId = uuidv4();
|
||||
|
||||
let userMessage = {
|
||||
id: userMessageId,
|
||||
parentId: history.messages[messageId].parentId,
|
||||
childrenIds: [],
|
||||
role: 'user',
|
||||
content: userPrompt,
|
||||
...(history.messages[messageId].files && { files: history.messages[messageId].files }),
|
||||
models: selectedModels.filter((m, mIdx) => selectedModels.indexOf(m) === mIdx)
|
||||
};
|
||||
let userMessage = {
|
||||
id: userMessageId,
|
||||
parentId: history.messages[messageId].parentId,
|
||||
childrenIds: [],
|
||||
role: 'user',
|
||||
content: userPrompt,
|
||||
...(history.messages[messageId].files && { files: history.messages[messageId].files }),
|
||||
models: selectedModels
|
||||
};
|
||||
|
||||
let messageParentId = history.messages[messageId].parentId;
|
||||
let messageParentId = history.messages[messageId].parentId;
|
||||
|
||||
if (messageParentId !== null) {
|
||||
history.messages[messageParentId].childrenIds = [
|
||||
...history.messages[messageParentId].childrenIds,
|
||||
userMessageId
|
||||
];
|
||||
if (messageParentId !== null) {
|
||||
history.messages[messageParentId].childrenIds = [
|
||||
...history.messages[messageParentId].childrenIds,
|
||||
userMessageId
|
||||
];
|
||||
}
|
||||
|
||||
history.messages[userMessageId] = userMessage;
|
||||
history.currentId = userMessageId;
|
||||
|
||||
await tick();
|
||||
await sendPrompt(userPrompt, userMessageId);
|
||||
} else {
|
||||
history.messages[messageId].content = content;
|
||||
await tick();
|
||||
await updateChatById(localStorage.token, chatId, {
|
||||
messages: messages,
|
||||
history: history
|
||||
});
|
||||
}
|
||||
|
||||
history.messages[userMessageId] = userMessage;
|
||||
history.currentId = userMessageId;
|
||||
|
||||
await tick();
|
||||
await sendPrompt(userPrompt, userMessageId);
|
||||
};
|
||||
|
||||
const updateChatMessages = async () => {
|
||||
@@ -90,7 +97,8 @@
|
||||
history: history
|
||||
});
|
||||
|
||||
await chats.set(await getChatList(localStorage.token));
|
||||
currentChatPage.set(1);
|
||||
await chats.set(await getChatList(localStorage.token, $currentChatPage));
|
||||
};
|
||||
|
||||
const confirmEditResponseMessage = async (messageId, content) => {
|
||||
@@ -146,12 +154,14 @@
|
||||
|
||||
await tick();
|
||||
|
||||
const element = document.getElementById('messages-container');
|
||||
autoScroll = element.scrollHeight - element.scrollTop <= element.clientHeight + 50;
|
||||
if ($settings?.scrollOnBranchChange ?? true) {
|
||||
const element = document.getElementById('messages-container');
|
||||
autoScroll = element.scrollHeight - element.scrollTop <= element.clientHeight + 50;
|
||||
|
||||
setTimeout(() => {
|
||||
scrollToBottom();
|
||||
}, 100);
|
||||
setTimeout(() => {
|
||||
scrollToBottom();
|
||||
}, 100);
|
||||
}
|
||||
};
|
||||
|
||||
const showNextMessage = async (message) => {
|
||||
@@ -195,12 +205,14 @@
|
||||
|
||||
await tick();
|
||||
|
||||
const element = document.getElementById('messages-container');
|
||||
autoScroll = element.scrollHeight - element.scrollTop <= element.clientHeight + 50;
|
||||
if ($settings?.scrollOnBranchChange ?? true) {
|
||||
const element = document.getElementById('messages-container');
|
||||
autoScroll = element.scrollHeight - element.scrollTop <= element.clientHeight + 50;
|
||||
|
||||
setTimeout(() => {
|
||||
scrollToBottom();
|
||||
}, 100);
|
||||
setTimeout(() => {
|
||||
scrollToBottom();
|
||||
}, 100);
|
||||
}
|
||||
};
|
||||
|
||||
const deleteMessageHandler = async (messageId) => {
|
||||
@@ -300,7 +312,7 @@
|
||||
{#each messages as message, messageIdx}
|
||||
<div class=" w-full {messageIdx === messages.length - 1 ? ' pb-12' : ''}">
|
||||
<div
|
||||
class="flex flex-col justify-between px-5 mb-3 {$settings?.widescreenMode ?? null
|
||||
class="flex flex-col justify-between px-5 mb-3 {($settings?.widescreenMode ?? null)
|
||||
? 'max-w-full'
|
||||
: 'max-w-5xl'} mx-auto rounded-lg group"
|
||||
>
|
||||
@@ -312,16 +324,16 @@
|
||||
{message}
|
||||
isFirstMessage={messageIdx === 0}
|
||||
siblings={message.parentId !== null
|
||||
? history.messages[message.parentId]?.childrenIds ?? []
|
||||
: Object.values(history.messages)
|
||||
? (history.messages[message.parentId]?.childrenIds ?? [])
|
||||
: (Object.values(history.messages)
|
||||
.filter((message) => message.parentId === null)
|
||||
.map((message) => message.id) ?? []}
|
||||
.map((message) => message.id) ?? [])}
|
||||
{confirmEditMessage}
|
||||
{showPreviousMessage}
|
||||
{showNextMessage}
|
||||
copyToClipboard={copyToClipboardWithToast}
|
||||
/>
|
||||
{:else if $mobile || (history.messages[message.parentId]?.models?.length ?? 1) === 1}
|
||||
{:else if (history.messages[message.parentId]?.models?.length ?? 1) === 1}
|
||||
{#key message.id && history.currentId}
|
||||
<ResponseMessage
|
||||
{message}
|
||||
@@ -337,7 +349,13 @@
|
||||
{continueGeneration}
|
||||
{regenerateResponse}
|
||||
on:action={async (e) => {
|
||||
await chatActionHandler(chatId, e.detail, message.model, message.id);
|
||||
console.log('action', e);
|
||||
if (typeof e.detail === 'string') {
|
||||
await chatActionHandler(chatId, e.detail, message.model, message.id);
|
||||
} else {
|
||||
const { id, event } = e.detail;
|
||||
await chatActionHandler(chatId, id, message.model, message.id, event);
|
||||
}
|
||||
}}
|
||||
on:save={async (e) => {
|
||||
console.log('save', e);
|
||||
@@ -353,8 +371,9 @@
|
||||
{/key}
|
||||
{:else}
|
||||
{#key message.parentId}
|
||||
<CompareMessages
|
||||
<MultiResponseMessages
|
||||
bind:history
|
||||
isLastMessage={messageIdx + 1 === messages.length}
|
||||
{messages}
|
||||
{readOnly}
|
||||
{chatId}
|
||||
@@ -365,6 +384,7 @@
|
||||
{rateMessage}
|
||||
copyToClipboard={copyToClipboardWithToast}
|
||||
{continueGeneration}
|
||||
{mergeResponses}
|
||||
{regenerateResponse}
|
||||
on:change={async () => {
|
||||
await updateChatById(localStorage.token, chatId, {
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
<script lang="ts">
|
||||
import CitationsModal from './CitationsModal.svelte';
|
||||
|
||||
export let citations = [];
|
||||
|
||||
let showCitationModal = false;
|
||||
let selectedCitation = null;
|
||||
</script>
|
||||
|
||||
<CitationsModal bind:show={showCitationModal} citation={selectedCitation} />
|
||||
|
||||
<div class="mt-1 mb-2 w-full flex gap-1 items-center flex-wrap">
|
||||
{#each citations.reduce((acc, citation) => {
|
||||
citation.document.forEach((document, index) => {
|
||||
const metadata = citation.metadata?.[index];
|
||||
const id = metadata?.source ?? 'N/A';
|
||||
let source = citation?.source;
|
||||
|
||||
if (metadata?.name) {
|
||||
source = { ...source, name: metadata.name };
|
||||
}
|
||||
|
||||
// Check if ID looks like a URL
|
||||
if (id.startsWith('http://') || id.startsWith('https://')) {
|
||||
source = { name: id };
|
||||
}
|
||||
|
||||
const existingSource = acc.find((item) => item.id === id);
|
||||
|
||||
if (existingSource) {
|
||||
existingSource.document.push(document);
|
||||
existingSource.metadata.push(metadata);
|
||||
} else {
|
||||
acc.push( { id: id, source: source, document: [document], metadata: metadata ? [metadata] : [] } );
|
||||
}
|
||||
});
|
||||
return acc;
|
||||
}, []) as citation, idx}
|
||||
<div class="flex gap-1 text-xs font-semibold">
|
||||
<button
|
||||
class="flex dark:text-gray-300 py-1 px-1 bg-gray-50 hover:bg-gray-100 dark:bg-gray-850 dark:hover:bg-gray-800 transition rounded-xl"
|
||||
on:click={() => {
|
||||
showCitationModal = true;
|
||||
selectedCitation = citation;
|
||||
}}
|
||||
>
|
||||
<div class="bg-white dark:bg-gray-700 rounded-full size-4">
|
||||
{idx + 1}
|
||||
</div>
|
||||
<div class="flex-1 mx-2 line-clamp-1">
|
||||
{citation.source.name}
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
@@ -60,8 +60,8 @@
|
||||
href={document?.metadata?.file_id
|
||||
? `/api/v1/files/${document?.metadata?.file_id}/content`
|
||||
: document.source.name.includes('http')
|
||||
? document.source.name
|
||||
: `#`}
|
||||
? document.source.name
|
||||
: `#`}
|
||||
target="_blank"
|
||||
>
|
||||
{document?.metadata?.name ?? document.source.name}
|
||||
|
||||
@@ -1,17 +1,27 @@
|
||||
<script lang="ts">
|
||||
import Spinner from '$lib/components/common/Spinner.svelte';
|
||||
import { copyToClipboard } from '$lib/utils';
|
||||
import hljs from 'highlight.js';
|
||||
import 'highlight.js/styles/github-dark.min.css';
|
||||
import { loadPyodide } from 'pyodide';
|
||||
import { onMount, tick } from 'svelte';
|
||||
import mermaid from 'mermaid';
|
||||
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import { getContext, getAllContexts, onMount } from 'svelte';
|
||||
import { copyToClipboard } from '$lib/utils';
|
||||
|
||||
import 'highlight.js/styles/github-dark.min.css';
|
||||
|
||||
import PyodideWorker from '$lib/workers/pyodide.worker?worker';
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
export let id = '';
|
||||
|
||||
export let token;
|
||||
export let lang = '';
|
||||
export let code = '';
|
||||
|
||||
let mermaidHtml = null;
|
||||
|
||||
let highlightedCode = null;
|
||||
let executing = false;
|
||||
|
||||
@@ -204,70 +214,109 @@ __builtins__.input = input`);
|
||||
};
|
||||
|
||||
let debounceTimeout;
|
||||
$: if (code) {
|
||||
// Function to perform the code highlighting
|
||||
const highlightCode = () => {
|
||||
highlightedCode = hljs.highlightAuto(code, hljs.getLanguage(lang)?.aliases).value || code;
|
||||
};
|
||||
|
||||
// Clear the previous timeout if it exists
|
||||
clearTimeout(debounceTimeout);
|
||||
const drawMermaidDiagram = async () => {
|
||||
try {
|
||||
const { svg } = await mermaid.render(`mermaid-${uuidv4()}`, code);
|
||||
mermaidHtml = svg;
|
||||
} catch (error) {
|
||||
console.log('Error:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// Set a new timeout to debounce the code highlighting
|
||||
debounceTimeout = setTimeout(highlightCode, 10);
|
||||
$: if (token.raw) {
|
||||
if (lang === 'mermaid' && (token?.raw ?? '').slice(-4).includes('```')) {
|
||||
(async () => {
|
||||
await drawMermaidDiagram();
|
||||
})();
|
||||
} else {
|
||||
// Function to perform the code highlighting
|
||||
const highlightCode = () => {
|
||||
highlightedCode = hljs.highlightAuto(code, hljs.getLanguage(lang)?.aliases).value || code;
|
||||
};
|
||||
|
||||
// Clear the previous timeout if it exists
|
||||
clearTimeout(debounceTimeout);
|
||||
// Set a new timeout to debounce the code highlighting
|
||||
debounceTimeout = setTimeout(highlightCode, 10);
|
||||
}
|
||||
}
|
||||
|
||||
onMount(async () => {
|
||||
if (document.documentElement.classList.contains('dark')) {
|
||||
mermaid.initialize({
|
||||
startOnLoad: true,
|
||||
theme: 'dark',
|
||||
securityLevel: 'loose'
|
||||
});
|
||||
} else {
|
||||
mermaid.initialize({
|
||||
startOnLoad: true,
|
||||
theme: 'default',
|
||||
securityLevel: 'loose'
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="mb-4" dir="ltr">
|
||||
<div
|
||||
class="flex justify-between bg-[#202123] text-white text-xs px-4 pt-1 pb-0.5 rounded-t-lg overflow-x-auto"
|
||||
>
|
||||
<div class="p-1">{@html lang}</div>
|
||||
<div class="my-2" dir="ltr">
|
||||
{#if lang === 'mermaid'}
|
||||
{#if mermaidHtml}
|
||||
{@html `${mermaidHtml}`}
|
||||
{:else}
|
||||
<pre class="mermaid">{code}</pre>
|
||||
{/if}
|
||||
{:else}
|
||||
<div
|
||||
class="flex justify-between bg-[#202123] text-white text-xs px-4 pt-1 pb-0.5 rounded-t-lg overflow-x-auto"
|
||||
>
|
||||
<div class="p-1">{lang}</div>
|
||||
|
||||
<div class="flex items-center">
|
||||
{#if lang.toLowerCase() === 'python' || lang.toLowerCase() === 'py' || (lang === '' && checkPythonCode(code))}
|
||||
{#if executing}
|
||||
<div class="copy-code-button bg-none border-none p-1 cursor-not-allowed">Running</div>
|
||||
{:else}
|
||||
<button
|
||||
class="copy-code-button bg-none border-none p-1"
|
||||
on:click={() => {
|
||||
executePython(code);
|
||||
}}>Run</button
|
||||
>
|
||||
<div class="flex items-center">
|
||||
{#if lang.toLowerCase() === 'python' || lang.toLowerCase() === 'py' || (lang === '' && checkPythonCode(code))}
|
||||
{#if executing}
|
||||
<div class="copy-code-button bg-none border-none p-1 cursor-not-allowed">Running</div>
|
||||
{:else}
|
||||
<button
|
||||
class="copy-code-button bg-none border-none p-1"
|
||||
on:click={() => {
|
||||
executePython(code);
|
||||
}}>{$i18n.t('Run')}</button
|
||||
>
|
||||
{/if}
|
||||
{/if}
|
||||
{/if}
|
||||
<button class="copy-code-button bg-none border-none p-1" on:click={copyCode}
|
||||
>{copied ? 'Copied' : 'Copy Code'}</button
|
||||
>
|
||||
<button class="copy-code-button bg-none border-none p-1" on:click={copyCode}
|
||||
>{copied ? $i18n.t('Copied') : $i18n.t('Copy Code')}</button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<pre
|
||||
class=" hljs p-4 px-5 overflow-x-auto"
|
||||
style="border-top-left-radius: 0px; border-top-right-radius: 0px; {(executing ||
|
||||
stdout ||
|
||||
stderr ||
|
||||
result) &&
|
||||
'border-bottom-left-radius: 0px; border-bottom-right-radius: 0px;'}"><code
|
||||
class="language-{lang} rounded-t-none whitespace-pre"
|
||||
>{#if highlightedCode}{@html highlightedCode}{:else}{code}{/if}</code
|
||||
></pre>
|
||||
<pre
|
||||
class=" hljs p-4 px-5 overflow-x-auto"
|
||||
style="border-top-left-radius: 0px; border-top-right-radius: 0px; {(executing ||
|
||||
stdout ||
|
||||
stderr ||
|
||||
result) &&
|
||||
'border-bottom-left-radius: 0px; border-bottom-right-radius: 0px;'}"><code
|
||||
class="language-{lang} rounded-t-none whitespace-pre"
|
||||
>{#if highlightedCode}{@html highlightedCode}{:else}{code}{/if}</code
|
||||
></pre>
|
||||
|
||||
<div
|
||||
id="plt-canvas-{id}"
|
||||
class="bg-[#202123] text-white max-w-full overflow-x-auto scrollbar-hidden"
|
||||
/>
|
||||
<div
|
||||
id="plt-canvas-{id}"
|
||||
class="bg-[#202123] text-white max-w-full overflow-x-auto scrollbar-hidden"
|
||||
/>
|
||||
|
||||
{#if executing}
|
||||
<div class="bg-[#202123] text-white px-4 py-4 rounded-b-lg">
|
||||
<div class=" text-gray-500 text-xs mb-1">STDOUT/STDERR</div>
|
||||
<div class="text-sm">Running...</div>
|
||||
</div>
|
||||
{:else if stdout || stderr || result}
|
||||
<div class="bg-[#202123] text-white px-4 py-4 rounded-b-lg">
|
||||
<div class=" text-gray-500 text-xs mb-1">STDOUT/STDERR</div>
|
||||
<div class="text-sm">{stdout || stderr || result}</div>
|
||||
</div>
|
||||
{#if executing}
|
||||
<div class="bg-[#202123] text-white px-4 py-4 rounded-b-lg">
|
||||
<div class=" text-gray-500 text-xs mb-1">STDOUT/STDERR</div>
|
||||
<div class="text-sm">Running...</div>
|
||||
</div>
|
||||
{:else if stdout || stderr || result}
|
||||
<div class="bg-[#202123] text-white px-4 py-4 rounded-b-lg">
|
||||
<div class=" text-gray-500 text-xs mb-1">STDOUT/STDERR</div>
|
||||
<div class="text-sm">{stdout || stderr || result}</div>
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -1,167 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
|
||||
import { updateChatById } from '$lib/apis/chats';
|
||||
import { onMount, tick } from 'svelte';
|
||||
import ResponseMessage from './ResponseMessage.svelte';
|
||||
|
||||
export let chatId;
|
||||
|
||||
export let history;
|
||||
export let messages = [];
|
||||
export let messageIdx;
|
||||
|
||||
export let parentMessage;
|
||||
|
||||
export let readOnly = false;
|
||||
|
||||
export let updateChatMessages: Function;
|
||||
export let confirmEditResponseMessage: Function;
|
||||
export let rateMessage: Function;
|
||||
|
||||
export let copyToClipboard: Function;
|
||||
export let continueGeneration: Function;
|
||||
export let regenerateResponse: Function;
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
let currentMessageId;
|
||||
|
||||
let groupedMessagesIdx = {};
|
||||
let groupedMessages = {};
|
||||
|
||||
$: groupedMessages = parentMessage?.models.reduce((a, model) => {
|
||||
const modelMessages = parentMessage?.childrenIds
|
||||
.map((id) => history.messages[id])
|
||||
.filter((m) => m.model === model);
|
||||
|
||||
return {
|
||||
...a,
|
||||
[model]: { messages: modelMessages }
|
||||
};
|
||||
}, {});
|
||||
|
||||
const showPreviousMessage = (model) => {
|
||||
groupedMessagesIdx[model] = Math.max(0, groupedMessagesIdx[model] - 1);
|
||||
let messageId = groupedMessages[model].messages[groupedMessagesIdx[model]].id;
|
||||
|
||||
console.log(messageId);
|
||||
let messageChildrenIds = history.messages[messageId].childrenIds;
|
||||
|
||||
while (messageChildrenIds.length !== 0) {
|
||||
messageId = messageChildrenIds.at(-1);
|
||||
messageChildrenIds = history.messages[messageId].childrenIds;
|
||||
}
|
||||
|
||||
history.currentId = messageId;
|
||||
|
||||
dispatch('change');
|
||||
};
|
||||
|
||||
const showNextMessage = (model) => {
|
||||
groupedMessagesIdx[model] = Math.min(
|
||||
groupedMessages[model].messages.length - 1,
|
||||
groupedMessagesIdx[model] + 1
|
||||
);
|
||||
|
||||
let messageId = groupedMessages[model].messages[groupedMessagesIdx[model]].id;
|
||||
console.log(messageId);
|
||||
|
||||
let messageChildrenIds = history.messages[messageId].childrenIds;
|
||||
|
||||
while (messageChildrenIds.length !== 0) {
|
||||
messageId = messageChildrenIds.at(-1);
|
||||
messageChildrenIds = history.messages[messageId].childrenIds;
|
||||
}
|
||||
|
||||
history.currentId = messageId;
|
||||
|
||||
dispatch('change');
|
||||
};
|
||||
|
||||
onMount(async () => {
|
||||
await tick();
|
||||
currentMessageId = messages[messageIdx].id;
|
||||
|
||||
for (const model of parentMessage?.models) {
|
||||
const idx = groupedMessages[model].messages.findIndex((m) => m.id === currentMessageId);
|
||||
|
||||
if (idx !== -1) {
|
||||
groupedMessagesIdx[model] = idx;
|
||||
} else {
|
||||
groupedMessagesIdx[model] = 0;
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<div>
|
||||
<div
|
||||
class="flex snap-x snap-mandatory overflow-x-auto scrollbar-hidden"
|
||||
id="responses-container-{parentMessage.id}"
|
||||
>
|
||||
{#key currentMessageId}
|
||||
{#each Object.keys(groupedMessages) as model}
|
||||
{#if groupedMessagesIdx[model] !== undefined && groupedMessages[model].messages.length > 0}
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
{@const message = groupedMessages[model].messages[groupedMessagesIdx[model]]}
|
||||
|
||||
<div
|
||||
class=" snap-center min-w-80 w-full max-w-full m-1 border {history.messages[
|
||||
currentMessageId
|
||||
].model === model
|
||||
? 'border-gray-100 dark:border-gray-800 border-[1.5px]'
|
||||
: 'border-gray-50 dark:border-gray-850 '} transition p-5 rounded-3xl"
|
||||
on:click={() => {
|
||||
if (currentMessageId != message.id) {
|
||||
currentMessageId = message.id;
|
||||
let messageId = message.id;
|
||||
console.log(messageId);
|
||||
|
||||
//
|
||||
let messageChildrenIds = history.messages[messageId].childrenIds;
|
||||
while (messageChildrenIds.length !== 0) {
|
||||
messageId = messageChildrenIds.at(-1);
|
||||
messageChildrenIds = history.messages[messageId].childrenIds;
|
||||
}
|
||||
|
||||
history.currentId = messageId;
|
||||
dispatch('change');
|
||||
}
|
||||
}}
|
||||
>
|
||||
<ResponseMessage
|
||||
message={groupedMessages[model].messages[groupedMessagesIdx[model]]}
|
||||
siblings={groupedMessages[model].messages.map((m) => m.id)}
|
||||
isLastMessage={true}
|
||||
{updateChatMessages}
|
||||
{confirmEditResponseMessage}
|
||||
showPreviousMessage={() => showPreviousMessage(model)}
|
||||
showNextMessage={() => showNextMessage(model)}
|
||||
{readOnly}
|
||||
{rateMessage}
|
||||
{copyToClipboard}
|
||||
{continueGeneration}
|
||||
regenerateResponse={async (message) => {
|
||||
regenerateResponse(message);
|
||||
await tick();
|
||||
groupedMessagesIdx[model] = groupedMessages[model].messages.length - 1;
|
||||
}}
|
||||
on:save={async (e) => {
|
||||
console.log('save', e);
|
||||
|
||||
const message = e.detail;
|
||||
history.messages[message.id] = message;
|
||||
await updateChatById(localStorage.token, chatId, {
|
||||
messages: messages,
|
||||
history: history
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
{/each}
|
||||
{/key}
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,26 @@
|
||||
<script lang="ts">
|
||||
export let content = '';
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="flex mt-2 mb-4 space-x-2 border px-4 py-3 border-red-800 bg-red-800/30 font-medium rounded-lg"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
class="w-5 h-5 self-center"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M12 9v3.75m9-.75a9 9 0 11-18 0 9 9 0 0118 0zm-9 3.75h.008v.008H12v-.008z"
|
||||
/>
|
||||
</svg>
|
||||
|
||||
<div class=" self-center">
|
||||
{content}
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,32 @@
|
||||
<script>
|
||||
import { marked } from 'marked';
|
||||
import markedKatex from '$lib/utils/marked/katex-extension';
|
||||
import { replaceTokens, processResponseContent } from '$lib/utils';
|
||||
import { user } from '$lib/stores';
|
||||
|
||||
import MarkdownTokens from './Markdown/MarkdownTokens.svelte';
|
||||
|
||||
export let id;
|
||||
export let content;
|
||||
export let model = null;
|
||||
|
||||
let tokens = [];
|
||||
|
||||
const options = {
|
||||
throwOnError: false
|
||||
};
|
||||
|
||||
marked.use(markedKatex(options));
|
||||
|
||||
$: (async () => {
|
||||
if (content) {
|
||||
tokens = marked.lexer(
|
||||
replaceTokens(processResponseContent(content), model?.name, $user?.name)
|
||||
);
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
|
||||
{#key id}
|
||||
<MarkdownTokens {tokens} {id} />
|
||||
{/key}
|
||||
@@ -0,0 +1,9 @@
|
||||
<script lang="ts">
|
||||
import katex from 'katex';
|
||||
import 'katex/contrib/mhchem';
|
||||
|
||||
export let content: string;
|
||||
export let displayMode: boolean = false;
|
||||
</script>
|
||||
|
||||
{@html katex.renderToString(content, { displayMode, throwOnError: false })}
|
||||
@@ -0,0 +1,62 @@
|
||||
<script lang="ts">
|
||||
import DOMPurify from 'dompurify';
|
||||
import type { Token } from 'marked';
|
||||
import { revertSanitizedResponseContent, unescapeHtml } from '$lib/utils';
|
||||
import { onMount } from 'svelte';
|
||||
import Image from '$lib/components/common/Image.svelte';
|
||||
|
||||
import KatexRenderer from './KatexRenderer.svelte';
|
||||
import { WEBUI_BASE_URL } from '$lib/constants';
|
||||
|
||||
export let id: string;
|
||||
export let tokens: Token[];
|
||||
</script>
|
||||
|
||||
{#each tokens as token}
|
||||
{#if token.type === 'escape'}
|
||||
{unescapeHtml(token.text)}
|
||||
{:else if token.type === 'html'}
|
||||
{@const html = DOMPurify.sanitize(token.text)}
|
||||
{#if html && html.includes('<video')}
|
||||
{@html html}
|
||||
{:else if token.text.includes(`<iframe src="${WEBUI_BASE_URL}/api/v1/files/`)}
|
||||
{@html `${token.text}`}
|
||||
{:else}
|
||||
{token.text}
|
||||
{/if}
|
||||
{:else if token.type === 'link'}
|
||||
<a href={token.href} target="_blank" rel="nofollow" title={token.title}>{token.text}</a>
|
||||
{:else if token.type === 'image'}
|
||||
<Image src={token.href} alt={token.text} />
|
||||
{:else if token.type === 'strong'}
|
||||
<strong>
|
||||
<svelte:self id={`${id}-strong`} tokens={token.tokens} />
|
||||
</strong>
|
||||
{:else if token.type === 'em'}
|
||||
<em>
|
||||
<svelte:self id={`${id}-em`} tokens={token.tokens} />
|
||||
</em>
|
||||
{:else if token.type === 'codespan'}
|
||||
<code class="codespan">{unescapeHtml(token.text)}</code>
|
||||
{:else if token.type === 'br'}
|
||||
<br />
|
||||
{:else if token.type === 'del'}
|
||||
<del>
|
||||
<svelte:self id={`${id}-del`} tokens={token.tokens} />
|
||||
</del>
|
||||
{:else if token.type === 'inlineKatex'}
|
||||
{#if token.text}
|
||||
<KatexRenderer content={revertSanitizedResponseContent(token.text)} displayMode={false} />
|
||||
{/if}
|
||||
{:else if token.type === 'iframe'}
|
||||
<iframe
|
||||
src="{WEBUI_BASE_URL}/api/v1/files/{token.fileId}/content"
|
||||
title={token.fileId}
|
||||
width="100%"
|
||||
frameborder="0"
|
||||
onload="this.style.height=(this.contentWindow.document.body.scrollHeight+20)+'px';"
|
||||
></iframe>
|
||||
{:else if token.type === 'text'}
|
||||
{token.raw}
|
||||
{/if}
|
||||
{/each}
|
||||
@@ -0,0 +1,149 @@
|
||||
<script lang="ts">
|
||||
import DOMPurify from 'dompurify';
|
||||
import { onMount } from 'svelte';
|
||||
import { marked, type Token } from 'marked';
|
||||
import { revertSanitizedResponseContent, unescapeHtml } from '$lib/utils';
|
||||
|
||||
import CodeBlock from '$lib/components/chat/Messages/CodeBlock.svelte';
|
||||
import MarkdownInlineTokens from '$lib/components/chat/Messages/Markdown/MarkdownInlineTokens.svelte';
|
||||
import KatexRenderer from './KatexRenderer.svelte';
|
||||
import { WEBUI_BASE_URL } from '$lib/constants';
|
||||
|
||||
export let id: string;
|
||||
export let tokens: Token[];
|
||||
export let top = true;
|
||||
|
||||
const headerComponent = (depth: number) => {
|
||||
return 'h' + depth;
|
||||
};
|
||||
</script>
|
||||
|
||||
<!-- {JSON.stringify(tokens)} -->
|
||||
{#each tokens as token, tokenIdx}
|
||||
{#if token.type === 'hr'}
|
||||
<hr />
|
||||
{:else if token.type === 'heading'}
|
||||
<svelte:element this={headerComponent(token.depth)}>
|
||||
<MarkdownInlineTokens id={`${id}-${tokenIdx}-h`} tokens={token.tokens} />
|
||||
</svelte:element>
|
||||
{:else if token.type === 'code'}
|
||||
<CodeBlock
|
||||
id={`${id}-${tokenIdx}`}
|
||||
{token}
|
||||
lang={token?.lang ?? ''}
|
||||
code={revertSanitizedResponseContent(token?.text ?? '')}
|
||||
/>
|
||||
{:else if token.type === 'table'}
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
{#each token.header as header, headerIdx}
|
||||
<th style={token.align[headerIdx] ? '' : `text-align: ${token.align[headerIdx]}`}>
|
||||
<MarkdownInlineTokens
|
||||
id={`${id}-${tokenIdx}-header-${headerIdx}`}
|
||||
tokens={header.tokens}
|
||||
/>
|
||||
</th>
|
||||
{/each}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each token.rows as row, rowIdx}
|
||||
<tr>
|
||||
{#each row ?? [] as cell, cellIdx}
|
||||
<td style={token.align[cellIdx] ? '' : `text-align: ${token.align[cellIdx]}`}>
|
||||
<MarkdownInlineTokens
|
||||
id={`${id}-${tokenIdx}-row-${rowIdx}-${cellIdx}`}
|
||||
tokens={cell.tokens}
|
||||
/>
|
||||
</td>
|
||||
{/each}
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
{:else if token.type === 'blockquote'}
|
||||
<blockquote>
|
||||
<svelte:self id={`${id}-${tokenIdx}`} tokens={token.tokens} />
|
||||
</blockquote>
|
||||
{:else if token.type === 'list'}
|
||||
{#if token.ordered}
|
||||
<ol start={token.start || 1}>
|
||||
{#each token.items as item, itemIdx}
|
||||
<li>
|
||||
<svelte:self
|
||||
id={`${id}-${tokenIdx}-${itemIdx}`}
|
||||
tokens={item.tokens}
|
||||
top={token.loose}
|
||||
/>
|
||||
</li>
|
||||
{/each}
|
||||
</ol>
|
||||
{:else}
|
||||
<ul>
|
||||
{#each token.items as item, itemIdx}
|
||||
<li>
|
||||
<svelte:self
|
||||
id={`${id}-${tokenIdx}-${itemIdx}`}
|
||||
tokens={item.tokens}
|
||||
top={token.loose}
|
||||
/>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
{/if}
|
||||
{:else if token.type === 'html'}
|
||||
{@const html = DOMPurify.sanitize(token.text)}
|
||||
{#if html && html.includes('<video')}
|
||||
{@html html}
|
||||
{:else if token.text.includes(`<iframe src="${WEBUI_BASE_URL}/api/v1/files/`)}
|
||||
{@html `${token.text}`}
|
||||
{:else}
|
||||
{token.text}
|
||||
{/if}
|
||||
{:else if token.type === 'iframe'}
|
||||
<iframe
|
||||
src="{WEBUI_BASE_URL}/api/v1/files/{token.fileId}/content"
|
||||
title={token.fileId}
|
||||
width="100%"
|
||||
frameborder="0"
|
||||
onload="this.style.height=(this.contentWindow.document.body.scrollHeight+20)+'px';"
|
||||
></iframe>
|
||||
{:else if token.type === 'paragraph'}
|
||||
<p>
|
||||
<MarkdownInlineTokens id={`${id}-${tokenIdx}-p`} tokens={token.tokens ?? []} />
|
||||
</p>
|
||||
{:else if token.type === 'text'}
|
||||
{#if top}
|
||||
<p>
|
||||
{#if token.tokens}
|
||||
<MarkdownInlineTokens id={`${id}-${tokenIdx}-t`} tokens={token.tokens} />
|
||||
{:else}
|
||||
{unescapeHtml(token.text)}
|
||||
{/if}
|
||||
</p>
|
||||
{:else if token.tokens}
|
||||
<MarkdownInlineTokens id={`${id}-${tokenIdx}-p`} tokens={token.tokens ?? []} />
|
||||
{:else}
|
||||
{unescapeHtml(token.text)}
|
||||
{/if}
|
||||
{:else if token.type === 'inlineKatex'}
|
||||
{#if token.text}
|
||||
<KatexRenderer
|
||||
content={revertSanitizedResponseContent(token.text)}
|
||||
displayMode={token?.displayMode ?? false}
|
||||
/>
|
||||
{/if}
|
||||
{:else if token.type === 'blockKatex'}
|
||||
{#if token.text}
|
||||
<KatexRenderer
|
||||
content={revertSanitizedResponseContent(token.text)}
|
||||
displayMode={token?.displayMode ?? false}
|
||||
/>
|
||||
{/if}
|
||||
{:else if token.type === 'space'}
|
||||
{''}
|
||||
{:else}
|
||||
{console.log('Unknown token', token)}
|
||||
{/if}
|
||||
{/each}
|
||||
@@ -0,0 +1,261 @@
|
||||
<script lang="ts">
|
||||
import dayjs from 'dayjs';
|
||||
import { onMount, tick, getContext } from 'svelte';
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
|
||||
import { mobile, settings } from '$lib/stores';
|
||||
|
||||
import { generateMoACompletion } from '$lib/apis';
|
||||
import { updateChatById } from '$lib/apis/chats';
|
||||
import { createOpenAITextStream } from '$lib/apis/streaming';
|
||||
|
||||
import ResponseMessage from './ResponseMessage.svelte';
|
||||
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
||||
import Merge from '$lib/components/icons/Merge.svelte';
|
||||
|
||||
import Markdown from './Markdown.svelte';
|
||||
import Name from './Name.svelte';
|
||||
import Skeleton from './Skeleton.svelte';
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
export let chatId;
|
||||
|
||||
export let history;
|
||||
export let messages = [];
|
||||
export let messageIdx;
|
||||
|
||||
export let parentMessage;
|
||||
export let isLastMessage;
|
||||
|
||||
export let readOnly = false;
|
||||
|
||||
export let updateChatMessages: Function;
|
||||
export let confirmEditResponseMessage: Function;
|
||||
export let rateMessage: Function;
|
||||
|
||||
export let copyToClipboard: Function;
|
||||
export let continueGeneration: Function;
|
||||
export let mergeResponses: Function;
|
||||
export let regenerateResponse: Function;
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
let currentMessageId;
|
||||
let groupedMessages = {};
|
||||
let groupedMessagesIdx = {};
|
||||
|
||||
$: if (parentMessage) {
|
||||
initHandler();
|
||||
}
|
||||
|
||||
const showPreviousMessage = (modelIdx) => {
|
||||
groupedMessagesIdx[modelIdx] = Math.max(0, groupedMessagesIdx[modelIdx] - 1);
|
||||
let messageId = groupedMessages[modelIdx].messages[groupedMessagesIdx[modelIdx]].id;
|
||||
|
||||
console.log(messageId);
|
||||
let messageChildrenIds = history.messages[messageId].childrenIds;
|
||||
|
||||
while (messageChildrenIds.length !== 0) {
|
||||
messageId = messageChildrenIds.at(-1);
|
||||
messageChildrenIds = history.messages[messageId].childrenIds;
|
||||
}
|
||||
|
||||
history.currentId = messageId;
|
||||
dispatch('change');
|
||||
};
|
||||
|
||||
const showNextMessage = (modelIdx) => {
|
||||
groupedMessagesIdx[modelIdx] = Math.min(
|
||||
groupedMessages[modelIdx].messages.length - 1,
|
||||
groupedMessagesIdx[modelIdx] + 1
|
||||
);
|
||||
|
||||
let messageId = groupedMessages[modelIdx].messages[groupedMessagesIdx[modelIdx]].id;
|
||||
console.log(messageId);
|
||||
|
||||
let messageChildrenIds = history.messages[messageId].childrenIds;
|
||||
|
||||
while (messageChildrenIds.length !== 0) {
|
||||
messageId = messageChildrenIds.at(-1);
|
||||
messageChildrenIds = history.messages[messageId].childrenIds;
|
||||
}
|
||||
|
||||
history.currentId = messageId;
|
||||
dispatch('change');
|
||||
};
|
||||
|
||||
const initHandler = async () => {
|
||||
await tick();
|
||||
currentMessageId = messages[messageIdx].id;
|
||||
|
||||
groupedMessages = parentMessage?.models.reduce((a, model, modelIdx) => {
|
||||
// Find all messages that are children of the parent message and have the same model
|
||||
const modelMessages = parentMessage?.childrenIds
|
||||
.map((id) => history.messages[id])
|
||||
.filter((m) => m.modelIdx === modelIdx);
|
||||
|
||||
return {
|
||||
...a,
|
||||
[modelIdx]: { messages: modelMessages }
|
||||
};
|
||||
}, {});
|
||||
|
||||
groupedMessagesIdx = parentMessage?.models.reduce((a, model, modelIdx) => {
|
||||
const idx = groupedMessages[modelIdx].messages.findIndex((m) => m.id === currentMessageId);
|
||||
if (idx !== -1) {
|
||||
return {
|
||||
...a,
|
||||
[modelIdx]: idx
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
...a,
|
||||
[modelIdx]: 0
|
||||
};
|
||||
}
|
||||
}, {});
|
||||
};
|
||||
|
||||
const mergeResponsesHandler = async () => {
|
||||
const responses = Object.keys(groupedMessages).map((modelIdx) => {
|
||||
const { messages } = groupedMessages[modelIdx];
|
||||
return messages[groupedMessagesIdx[modelIdx]].content;
|
||||
});
|
||||
mergeResponses(currentMessageId, responses, chatId);
|
||||
};
|
||||
|
||||
onMount(async () => {
|
||||
initHandler();
|
||||
});
|
||||
</script>
|
||||
|
||||
<div>
|
||||
<div
|
||||
class="flex snap-x snap-mandatory overflow-x-auto scrollbar-hidden"
|
||||
id="responses-container-{chatId}-{parentMessage.id}"
|
||||
>
|
||||
{#key currentMessageId}
|
||||
{#each Object.keys(groupedMessages) as modelIdx}
|
||||
{#if groupedMessagesIdx[modelIdx] !== undefined && groupedMessages[modelIdx].messages.length > 0}
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
{@const message = groupedMessages[modelIdx].messages[groupedMessagesIdx[modelIdx]]}
|
||||
|
||||
<div
|
||||
class=" snap-center w-full max-w-full m-1 border {history.messages[currentMessageId]
|
||||
?.modelIdx == modelIdx
|
||||
? `border-gray-100 dark:border-gray-800 border-[1.5px] ${
|
||||
$mobile ? 'min-w-full' : 'min-w-[32rem]'
|
||||
}`
|
||||
: `border-gray-50 dark:border-gray-850 border-dashed ${
|
||||
$mobile ? 'min-w-full' : 'min-w-80'
|
||||
}`} transition-all p-5 rounded-2xl"
|
||||
on:click={() => {
|
||||
if (currentMessageId != message.id) {
|
||||
currentMessageId = message.id;
|
||||
let messageId = message.id;
|
||||
console.log(messageId);
|
||||
//
|
||||
let messageChildrenIds = history.messages[messageId].childrenIds;
|
||||
while (messageChildrenIds.length !== 0) {
|
||||
messageId = messageChildrenIds.at(-1);
|
||||
messageChildrenIds = history.messages[messageId].childrenIds;
|
||||
}
|
||||
history.currentId = messageId;
|
||||
dispatch('change');
|
||||
}
|
||||
}}
|
||||
>
|
||||
{#key history.currentId}
|
||||
{#if message}
|
||||
<ResponseMessage
|
||||
{message}
|
||||
siblings={groupedMessages[modelIdx].messages.map((m) => m.id)}
|
||||
isLastMessage={true}
|
||||
{updateChatMessages}
|
||||
{confirmEditResponseMessage}
|
||||
showPreviousMessage={() => showPreviousMessage(modelIdx)}
|
||||
showNextMessage={() => showNextMessage(modelIdx)}
|
||||
{readOnly}
|
||||
{rateMessage}
|
||||
{copyToClipboard}
|
||||
{continueGeneration}
|
||||
regenerateResponse={async (message) => {
|
||||
regenerateResponse(message);
|
||||
await tick();
|
||||
groupedMessagesIdx[modelIdx] = groupedMessages[modelIdx].messages.length - 1;
|
||||
}}
|
||||
on:save={async (e) => {
|
||||
console.log('save', e);
|
||||
|
||||
const message = e.detail;
|
||||
history.messages[message.id] = message;
|
||||
await updateChatById(localStorage.token, chatId, {
|
||||
messages: messages,
|
||||
history: history
|
||||
});
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
{/key}
|
||||
</div>
|
||||
{/if}
|
||||
{/each}
|
||||
{/key}
|
||||
</div>
|
||||
|
||||
{#if !readOnly && isLastMessage}
|
||||
{#if !Object.keys(groupedMessages).find((modelIdx) => {
|
||||
const { messages } = groupedMessages[modelIdx];
|
||||
return !messages[groupedMessagesIdx[modelIdx]].done;
|
||||
})}
|
||||
<div class="flex justify-end">
|
||||
<div class="w-full">
|
||||
{#if history.messages[currentMessageId]?.merged?.status}
|
||||
{@const message = history.messages[currentMessageId]?.merged}
|
||||
|
||||
<div class="w-full rounded-xl pl-5 pr-2 py-2">
|
||||
<Name>
|
||||
Merged Response
|
||||
|
||||
{#if message.timestamp}
|
||||
<span
|
||||
class=" self-center invisible group-hover:visible text-gray-400 text-xs font-medium uppercase ml-0.5 -mt-0.5"
|
||||
>
|
||||
{dayjs(message.timestamp * 1000).format($i18n.t('h:mm a'))}
|
||||
</span>
|
||||
{/if}
|
||||
</Name>
|
||||
|
||||
<div class="mt-1 markdown-prose w-full min-w-full">
|
||||
{#if (message?.content ?? '') === ''}
|
||||
<Skeleton />
|
||||
{:else}
|
||||
<Markdown id={`merged`} content={message.content ?? ''} />
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class=" flex-shrink-0 text-gray-600 dark:text-gray-500 mt-1">
|
||||
<Tooltip content={$i18n.t('Merge Responses')} placement="bottom">
|
||||
<button
|
||||
type="button"
|
||||
id="merge-response-button"
|
||||
class="{true
|
||||
? 'visible'
|
||||
: 'invisible group-hover:visible'} p-1 hover:bg-black/5 dark:hover:bg-white/5 rounded-lg dark:hover:text-white hover:text-black transition regenerate-response-button"
|
||||
on:click={() => {
|
||||
mergeResponsesHandler();
|
||||
}}
|
||||
>
|
||||
<Merge className=" size-5 " />
|
||||
</button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
@@ -2,7 +2,7 @@
|
||||
import { WEBUI_BASE_URL } from '$lib/constants';
|
||||
import { marked } from 'marked';
|
||||
|
||||
import { config, user, models as _models } from '$lib/stores';
|
||||
import { config, user, models as _models, temporaryChatEnabled } from '$lib/stores';
|
||||
import { onMount, getContext } from 'svelte';
|
||||
|
||||
import { blur, fade } from 'svelte/transition';
|
||||
@@ -10,6 +10,7 @@
|
||||
import Suggestions from '../MessageInput/Suggestions.svelte';
|
||||
import { sanitizeResponseContent } from '$lib/utils';
|
||||
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
||||
import EyeSlash from '$lib/components/icons/EyeSlash.svelte';
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
@@ -64,6 +65,18 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if $temporaryChatEnabled}
|
||||
<Tooltip
|
||||
content="This chat won't appear in history and your messages will not be saved."
|
||||
className="w-fit"
|
||||
placement="top-start"
|
||||
>
|
||||
<div class="flex items-center gap-2 text-gray-500 font-medium text-lg my-2 w-fit">
|
||||
<EyeSlash strokeWidth="2.5" className="size-5" /> Temporary Chat
|
||||
</div>
|
||||
</Tooltip>
|
||||
{/if}
|
||||
|
||||
<div
|
||||
class=" mt-2 mb-4 text-3xl text-gray-800 dark:text-gray-100 font-semibold text-left flex items-center gap-4 font-primary"
|
||||
>
|
||||
@@ -113,7 +126,8 @@
|
||||
<div class=" w-full font-primary" in:fade={{ duration: 200, delay: 300 }}>
|
||||
<Suggestions
|
||||
suggestionPrompts={models[selectedModelIdx]?.info?.meta?.suggestion_prompts ??
|
||||
$config.default_prompt_suggestions}
|
||||
$config?.default_prompt_suggestions ??
|
||||
[]}
|
||||
{submitPrompt}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -57,7 +57,10 @@
|
||||
message.annotation.reason = selectedReason;
|
||||
message.annotation.comment = comment;
|
||||
|
||||
dispatch('submit');
|
||||
dispatch('submit', {
|
||||
reason: selectedReason,
|
||||
comment: comment
|
||||
});
|
||||
|
||||
toast.success($i18n.t('Thanks for your feedback!'));
|
||||
show = false;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user